From f26703837229314571a27602006984feae8d0acf Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Wed, 23 Sep 2026 19:11:17 -0700 Subject: [PATCH 1/2] fix(sdk): preserve HTTP wallet discovery caller and browser fetch --- docs/packages/sdk/bsv-sdk.md | 16 +++-- docs/reference/package-api-migrations.md | 6 +- docs/reference/stack-facts.md | 2 +- governance/package-release-notes.json | 6 +- governance/repository-health/baselines.json | 2 +- packages/sdk/CHANGELOG.md | 11 +++ packages/sdk/README.md | 14 ++-- packages/sdk/package.json | 2 +- packages/sdk/src/wallet/WalletClient.ts | 24 +++---- .../__tests/WalletClient.discovery.test.ts | 18 +++++ .../WalletClient.http-discovery.test.ts | 68 +++++++++++++++++++ .../src/wallet/substrates/HTTPWalletJSON.ts | 2 +- 12 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 packages/sdk/src/wallet/__tests/WalletClient.http-discovery.test.ts diff --git a/docs/packages/sdk/bsv-sdk.md b/docs/packages/sdk/bsv-sdk.md index 8a346051f9..3a6238de7e 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.2' +version: '2.8.3' npm: '@bsv/sdk' last_updated: '2026-09-23' last_verified: '2026-09-23' @@ -15,6 +15,13 @@ repo: 'https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk' # @bsv/sdk +The 2.8.3 candidate repairs HTTP wallet discovery with an explicit BRC100 +originator and binds the default JSON transport fetch receiver for browsers. +Apps affected by these client defects can update the SDK without changing calls. +Wallets retain the existing BRC100 contract; an ecosystem-wide application +migration is not required. The candidate is not published until the protected +release workflow completes. + Version 2.8.1 repairs portable decryption of authenticated empty AES-GCM plaintext. Browser/mobile and native Node envelopes now interoperate for empty encrypted fields. Encryption bytes and the full authentication tag @@ -395,7 +402,7 @@ 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 +### Wallet discovery deadlines since 2.8.2 Automatic React Native and XDM probes retain their short discovery deadlines and remove listeners when unavailable. A successful probe creates a separate @@ -403,6 +410,5 @@ 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. +is needed. Applications affected by the timeout defect can update their bundled +SDK; a wallet release alone cannot replace code served by a web application. diff --git a/docs/reference/package-api-migrations.md b/docs/reference/package-api-migrations.md index e7d9669400..fd1b693c56 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.2` | `2.8.2` | none | [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/sdk` | `2.8.2` | `2.8.3` | patch | [API and usage](../packages/sdk/bsv-sdk.md) | No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. This repairs SDK client defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it. | | `@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: 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. SDK 2.8.2 was published by protected release 35912232040 from reviewed main cc1256c4d81b0f64de89a0df7a5af6efbe3b3983; registry bytes and provenance match the verified candidate. -- 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. +- Release note: Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged. +- Migration: No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. This repairs SDK client defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it. | Public subpath | Runtime target(s) | Declaration target(s) | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | diff --git a/docs/reference/stack-facts.md b/docs/reference/stack-facts.md index 073d967db1..035968df67 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.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/sdk` | `2.8.3` | 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 82980cbd89..f8a9f9fba9 100644 --- a/governance/package-release-notes.json +++ b/governance/package-release-notes.json @@ -167,9 +167,9 @@ { "name": "@bsv/sdk", "publishedVersion": "2.8.2", - "releaseType": "none", - "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. SDK 2.8.2 was published by protected release 35912232040 from reviewed main cc1256c4d81b0f64de89a0df7a5af6efbe3b3983; registry bytes and provenance match the verified candidate.", - "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." + "releaseType": "patch", + "summary": "Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged.", + "migration": "No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. This repairs SDK client defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it." }, { "name": "@bsv/simple", diff --git a/governance/repository-health/baselines.json b/governance/repository-health/baselines.json index 0597883dd3..d5c86c5715 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.2", + "@bsv/sdk": "2.8.3", "@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 e4b27cc6e0..ccca091afb 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -214,6 +214,17 @@ All notable changes to this project will be documented in this file. The format ## [Unreleased] +### 2.8.3 candidate — HTTP wallet discovery compatibility + +- Pass the configured caller originator through automatic discovery probes so + HTTP WalletWire keeps its existing caller binding without rejecting discovery. +- Bind the default HTTP JSON fetch receiver for native browser implementations; + preserve supplied HTTP clients, discovery ordering, deadlines and validation. +- No BRC100 API, wire or account-data migration. Applications affected by these + client defects can update the SDK without changing their calls; wallet releases + do not require every application to upgrade in coordination. +- Publication remains a separate protected release workflow. + ### 2.8.2 candidate — wallet discovery timeout lifecycle - Keep React Native and XDM discovery bounded without carrying the short probe diff --git a/packages/sdk/README.md b/packages/sdk/README.md index f4b7385b44..a9787bc2c1 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -49,14 +49,18 @@ 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. +SDK 2.8.2 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. + +The 2.8.3 candidate preserves the configured BRC100 originator in automatic +HTTP WalletWire discovery and binds the browser's default JSON `fetch` receiver. +Applications affected by these client defects can update their bundled SDK +without changing calls. Wallet upgrades continue to support the existing BRC100 +contract; an ecosystem-wide application migration is not required. No API, wire +or account-data migration is required. Source 2.8.3 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 f2289c31f0..e8fb50931c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bsv/sdk", - "version": "2.8.2", + "version": "2.8.3", "sideEffects": false, "engines": { "node": ">=22" diff --git a/packages/sdk/src/wallet/WalletClient.ts b/packages/sdk/src/wallet/WalletClient.ts index c9159ccc24..c20a0a4884 100644 --- a/packages/sdk/src/wallet/WalletClient.ts +++ b/packages/sdk/src/wallet/WalletClient.ts @@ -125,26 +125,22 @@ 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() let result let timeoutHandle: ReturnType | undefined - if (typeof timeout === 'number') { - try { - result = await Promise.race([ - sub.getVersion({}), - new Promise((_resolve, reject) => { - timeoutHandle = setTimeout(() => reject(new Error('Timed out.')), timeout) - }) - ]) - } finally { - if (timeoutHandle !== undefined) clearTimeout(timeoutHandle) - } - } else { - result = await sub.getVersion({}) + try { + result = await Promise.race([ + sub.getVersion({}, this.originator), + new Promise((_resolve, reject) => { + timeoutHandle = setTimeout(() => reject(new Error('Timed out.')), timeout) + }) + ]) + } finally { + clearTimeout(timeoutHandle) } validateWalletResult('getVersion', result) // Probe deadlines bound discovery, not later calls that may await user approval. diff --git a/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts b/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts index e9c2da71c0..e5b566c379 100644 --- a/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts +++ b/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts @@ -82,6 +82,24 @@ describe('WalletClient discovery timeout lifecycle', () => { expect(jest.getTimerCount()).toBe(0) }) + it('falls back cleanly when a CWI probe throws synchronously', async () => { + const { listeners } = walletHost('react-native') + Object.defineProperty(window, 'CWI', { + value: { + getVersion: () => { + throw new Error('Provider closed during discovery') + } + } + }) + const client = new WalletClient('auto', 'app.example') + const connected = client.connectToSubstrate() + await jest.advanceTimersByTimeAsync(10) + await connected + expect(client.substrate).toBeInstanceOf(ReactNativeWebView) + expect(listeners.size).toBe(0) + expect(jest.getTimerCount()).toBe(0) + }) + it.each(['react-native', 'xdm'])( 'does not impose the %s discovery deadline on subsequent user approval', async transport => { diff --git a/packages/sdk/src/wallet/__tests/WalletClient.http-discovery.test.ts b/packages/sdk/src/wallet/__tests/WalletClient.http-discovery.test.ts new file mode 100644 index 0000000000..2e411e86f9 --- /dev/null +++ b/packages/sdk/src/wallet/__tests/WalletClient.http-discovery.test.ts @@ -0,0 +1,68 @@ +import { createServer } from 'node:http' +import type { AddressInfo } from 'node:net' +import WalletClient from '../WalletClient' +import WalletWireTransceiver from '../substrates/WalletWireTransceiver' +import HTTPWalletJSON from '../substrates/HTTPWalletJSON' + +describe('WalletClient automatic HTTP discovery', () => { + const originalFetch = globalThis.fetch + + afterEach(() => jest.restoreAllMocks()) + + it.each([ + ['wire', 'http://localhost:3301'], + ['json', 'http://localhost:3321'], + ['secure-json', 'https://localhost:2121'] + ])( + 'preserves an explicit caller through %s discovery and later calls', + async (kind, endpoint) => { + const requests: Array<{ path: string; origin: string | undefined }> = [] + const server = createServer((request, response) => { + requests.push({ path: request.url!, origin: request.headers.origin }) + request.resume() + response.setHeader( + 'Content-Type', + kind === 'wire' ? 'application/octet-stream' : 'application/json' + ) + response.end( + kind === 'wire' + ? Buffer.concat([Buffer.from([0]), Buffer.from('1.0.0.0')]) + : JSON.stringify({ version: '1.0.0.0' }) + ) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() as AddressInfo + // Use real HTTP without occupying a developer's installed wallet ports. + jest.spyOn(globalThis, 'fetch').mockImplementation(async function ( + this: unknown, + input, + init + ) { + // Browser fetch enforces its receiver even though Node's fetch does not. + if (this !== globalThis) throw new TypeError('Illegal invocation') + const url = new URL(String(input)) + if (url.origin !== endpoint) throw new Error('This wallet transport is unavailable') + return await originalFetch(`http://127.0.0.1:${address.port}${url.pathname}`, init) + }) + try { + const client = new WalletClient('auto', 'app.example') + await expect(client.getVersion()).resolves.toEqual({ version: '1.0.0.0' }) + expect(client.substrate).toBeInstanceOf( + kind === 'wire' ? WalletWireTransceiver : HTTPWalletJSON + ) + await expect(client.getVersion()).resolves.toEqual({ version: '1.0.0.0' }) + expect(requests).toEqual( + Array.from({ length: 3 }, () => ({ + path: '/getVersion', + origin: kind === 'wire' ? 'app.example' : 'http://app.example' + })) + ) + } finally { + server.closeAllConnections() + await new Promise((resolve, reject) => + server.close(error => (error ? reject(error) : resolve())) + ) + } + } + ) +}) diff --git a/packages/sdk/src/wallet/substrates/HTTPWalletJSON.ts b/packages/sdk/src/wallet/substrates/HTTPWalletJSON.ts index 070791cefa..2a30abf710 100644 --- a/packages/sdk/src/wallet/substrates/HTTPWalletJSON.ts +++ b/packages/sdk/src/wallet/substrates/HTTPWalletJSON.ts @@ -275,7 +275,7 @@ export default class HTTPWalletJSON implements WalletInterface { constructor( originator: OriginatorDomainNameStringUnder250Bytes | undefined, baseUrl: string = 'http://localhost:3321', - httpClient = fetch + httpClient = globalThis.fetch.bind(globalThis) ) { this.baseUrl = normalizeWalletHttpBaseUrl(baseUrl) this.originator = validateOriginator(originator) From fbb1ca135fc57d4d263ee4183580f73cb0098ba6 Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Wed, 23 Sep 2026 19:37:32 -0700 Subject: [PATCH 2/2] fix(sdk): preserve signed action history on legacy wallet wire --- docs/packages/sdk/bsv-sdk.md | 12 +++ docs/reference/package-api-migrations.md | 6 +- governance/package-release-notes.json | 4 +- packages/sdk/CHANGELOG.md | 6 +- packages/sdk/README.md | 3 + .../wallet/substrates/WalletWireProcessor.ts | 2 +- .../substrates/WalletWireTransceiver.ts | 25 +++-- .../__tests/WalletWire.security.test.ts | 92 +++++++++++++++---- 8 files changed, 118 insertions(+), 32 deletions(-) diff --git a/docs/packages/sdk/bsv-sdk.md b/docs/packages/sdk/bsv-sdk.md index 3a6238de7e..062f6cbb79 100644 --- a/docs/packages/sdk/bsv-sdk.md +++ b/docs/packages/sdk/bsv-sdk.md @@ -17,11 +17,23 @@ repo: 'https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk' The 2.8.3 candidate repairs HTTP wallet discovery with an explicit BRC100 originator and binds the default JSON transport fetch receiver for browsers. +It restores signed `listActions` net amounts with the historical wire encoding, +matching JSON validation; count, length and individual output bounds stay intact. Apps affected by these client defects can update the SDK without changing calls. Wallets retain the existing BRC100 contract; an ecosystem-wide application migration is not required. The candidate is not published until the protected release workflow completes. +The action-history compatibility regression affects signed `listActions` values +in the 2.8.x binary processor and client. Updating a wallet repairs responses for +older compatible clients; an application already bundling the affected 2.8.x +binary client also needs the SDK patch. No call or persisted-data change is +needed. Synthetic cross-version checks cover SDK 2.4.0, 2.5.0, 2.7.1, 2.8.1 and +2.8.2 at eight signed/unsigned boundaries: all 40 candidate cases pass, all 32 +previously valid cases retain exact response bytes, and the eight known negative +2.8.1/2.8.2 failures are recorded separately. These checks are bounded fixture +evidence, not a claim that every ecosystem application has been tested. + Version 2.8.1 repairs portable decryption of authenticated empty AES-GCM plaintext. Browser/mobile and native Node envelopes now interoperate for empty encrypted fields. Encryption bytes and the full authentication tag diff --git a/docs/reference/package-api-migrations.md b/docs/reference/package-api-migrations.md index fd1b693c56..0fa5cf68b3 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.2` | `2.8.3` | patch | [API and usage](../packages/sdk/bsv-sdk.md) | No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. This repairs SDK client defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it. | +| `@bsv/sdk` | `2.8.2` | `2.8.3` | patch | [API and usage](../packages/sdk/bsv-sdk.md) | No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. Wallets and affected 2.8.x binary clients should include this patch to restore signed action history. Existing older clients retain their original calls and wire bytes. This repairs SDK client and wire compatibility defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it. | | `@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: Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged. -- Migration: No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. This repairs SDK client defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it. +- Release note: Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged. Restores signed listActions net amounts using their historical int64 wire encoding, matching existing JSON validation, while retaining canonical unsigned counts/lengths and nonnegative individual output values. +- Migration: No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. Wallets and affected 2.8.x binary clients should include this patch to restore signed action history. Existing older clients retain their original calls and wire bytes. This repairs SDK client and wire compatibility defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it. | Public subpath | Runtime target(s) | Declaration target(s) | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | diff --git a/governance/package-release-notes.json b/governance/package-release-notes.json index f8a9f9fba9..cf48889f83 100644 --- a/governance/package-release-notes.json +++ b/governance/package-release-notes.json @@ -168,8 +168,8 @@ "name": "@bsv/sdk", "publishedVersion": "2.8.2", "releaseType": "patch", - "summary": "Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged.", - "migration": "No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. This repairs SDK client defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it." + "summary": "Preserves the configured BRC100 caller originator during automatic wallet discovery, so the HTTP WalletWire probe satisfies the existing transport caller binding. Binds the default HTTP JSON fetch receiver for browsers while retaining custom HTTP clients. Keeps transport preference, bounded discovery, operation timeout behavior, validation and origin checks unchanged. Restores signed listActions net amounts using their historical int64 wire encoding, matching existing JSON validation, while retaining canonical unsigned counts/lengths and nonnegative individual output values.", + "migration": "No application API, wire, account-data or wallet protocol migration is required. Applications affected by explicit-originator HTTP discovery or the browser JSON fallback can update their bundled SDK to 2.8.3 without changing calls. Wallets and affected 2.8.x binary clients should include this patch to restore signed action history. Existing older clients retain their original calls and wire bytes. This repairs SDK client and wire compatibility defects; wallet upgrades must continue supporting existing applications and do not require an ecosystem-wide SDK migration. Source 2.8.3 remains a candidate until the protected release workflow publishes it." }, { "name": "@bsv/simple", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index ccca091afb..87d39bfa68 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -214,12 +214,16 @@ All notable changes to this project will be documented in this file. The format ## [Unreleased] -### 2.8.3 candidate — HTTP wallet discovery compatibility +### 2.8.3 candidate — BRC100 discovery and action-history compatibility - Pass the configured caller originator through automatic discovery probes so HTTP WalletWire keeps its existing caller binding without rejecting discovery. - Bind the default HTTP JSON fetch receiver for native browser implementations; preserve supplied HTTP clients, discovery ordering, deadlines and validation. +- Restore signed `listActions` net amounts with the existing int64 wire encoding, + matching established SDK behavior and JSON validation. Reject out-of-range, + unsafe, noncanonical and truncated values without relaxing counts, lengths or + individual output values. - No BRC100 API, wire or account-data migration. Applications affected by these client defects can update the SDK without changing their calls; wallet releases do not require every application to upgrade in coordination. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index a9787bc2c1..e6f19988b1 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -56,6 +56,9 @@ probe deadline. Explicit substrate `responseTimeout` values remain enforced. The 2.8.3 candidate preserves the configured BRC100 originator in automatic HTTP WalletWire discovery and binds the browser's default JSON `fetch` receiver. +It also restores negative `listActions` net amounts using their historical signed +int64 wire bytes, matching existing JSON validation. Counts, lengths and individual +output values remain unsigned and bounded. Applications affected by these client defects can update their bundled SDK without changing calls. Wallet upgrades continue to support the existing BRC100 contract; an ecosystem-wide application migration is not required. No API, wire diff --git a/packages/sdk/src/wallet/substrates/WalletWireProcessor.ts b/packages/sdk/src/wallet/substrates/WalletWireProcessor.ts index 82aacf2b2d..6a74422579 100644 --- a/packages/sdk/src/wallet/substrates/WalletWireProcessor.ts +++ b/packages/sdk/src/wallet/substrates/WalletWireProcessor.ts @@ -902,7 +902,7 @@ export default class WalletWireProcessor implements WalletWire { // satoshis resultWriter.writeVarIntNum( - this.#requireInteger(action.satoshis, 'listActions satoshis', 0, 21e14) + this.#requireInteger(action.satoshis, 'listActions satoshis', -21e14, 21e14) ) // status diff --git a/packages/sdk/src/wallet/substrates/WalletWireTransceiver.ts b/packages/sdk/src/wallet/substrates/WalletWireTransceiver.ts index 18cdf8a08c..6dd50af789 100644 --- a/packages/sdk/src/wallet/substrates/WalletWireTransceiver.ts +++ b/packages/sdk/src/wallet/substrates/WalletWireTransceiver.ts @@ -398,14 +398,27 @@ export default class WalletWireTransceiver implements WalletInterface { return status } + #readActionSatoshis(reader: ReaderUint8Array): number { + // Action history is a signed net amount. Legacy wallet wire encodes negative + // values as 0xff followed by signed little-endian int64. This exception is + // field-specific: counts, lengths and individual output values stay unsigned. + let value: number + if ( + reader.remaining() >= 9 && + reader.bin[reader.pos] === 0xff && + reader.bin[reader.pos + 8] >= 0x80 + ) { + reader.skip(1) + value = reader.readInt64LEBn().toNumber() + } else { + value = reader.readVarIntNumStrict(false) + } + return this.#requireInteger(value, 'listActions satoshis', -21e14, 21e14) + } + #parseAction(reader: ReaderUint8Array): ListActionsResult['actions'][number] { const txid = toHex(reader.read(32)) - const satoshis = this.#requireInteger( - reader.readVarIntNumStrict(false), - 'listActions satoshis', - 0, - 21e14 - ) + const satoshis = this.#readActionSatoshis(reader) const status = this.#parseActionStatus(reader.readInt8()) const isOutgoing = this.#readBooleanFlag(reader, 'listActions isOutgoing') const description = toUTF8Strict(reader.read(reader.readVarIntNumStrict(false))) diff --git a/packages/sdk/src/wallet/substrates/__tests/WalletWire.security.test.ts b/packages/sdk/src/wallet/substrates/__tests/WalletWire.security.test.ts index 155757d337..e0c5dd7f55 100644 --- a/packages/sdk/src/wallet/substrates/__tests/WalletWire.security.test.ts +++ b/packages/sdk/src/wallet/substrates/__tests/WalletWire.security.test.ts @@ -10,6 +10,13 @@ import { MAX_WALLET_WIRE_FRAME_BYTES } from '../WalletWire.js' const GENERATOR_PUBLIC_KEY = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' const VALID_SIGNATURE_HEX = '3006020101020101' +// Frozen field layout, independent of the processor under test: success, one +// action, zero txid, amount, completed/outgoing, "Entry", labels/version/locktime, +// inputs/outputs. Negative amounts use the historical signed-int64 representation. +function historyResponse(amount: number[]): number[] { + return [0, 1, ...Array(32).fill(0), ...amount, 1, 1, 5, 69, 110, 116, 114, 121, 0, 1, 0, 0, 0] +} + describe('WalletWire strict binary framing', () => { it.each([ [ @@ -191,27 +198,74 @@ describe('WalletWire strict binary framing', () => { expect(result.outputs[0].satoshis).toBe(1) }) - it('fails closed rather than changing a negative action-history amount on legacy wire', async () => { - const transceiver = new WalletWireTransceiver( - new WalletWireProcessor({ - listActions: async () => ({ - totalActions: 1, - actions: [ - { - txid: '00'.repeat(32), - satoshis: -1, - status: 'completed', - isOutgoing: true, - description: 'Outgoing history action', - version: 1, - lockTime: 0 - } - ] - }) - } as unknown as WalletInterface) + it.each([-21e14, -65536, -222, -1, 0, 253, 65536, 21e14])( + 'preserves signed action-history amount %s on legacy wire', + async satoshis => { + const transceiver = new WalletWireTransceiver( + new WalletWireProcessor({ + listActions: async () => ({ + totalActions: 1, + actions: [ + { + txid: '00'.repeat(32), + satoshis, + status: 'completed', + isOutgoing: true, + description: 'Outgoing history action', + version: 1, + lockTime: 0 + } + ] + }) + } as unknown as WalletInterface) + ) + + await expect(transceiver.listActions({ labels: [] })).resolves.toMatchObject({ + actions: [{ satoshis }] + }) + } + ) + + it('decodes the published signed history representation without a new wire format', async () => { + const wire: WalletWire = { + transmitToWallet: async () => historyResponse([255, 34, 255, 255, 255, 255, 255, 255, 255]) + } + await expect( + new WalletWireTransceiver(wire).listActions({ labels: [] }) + ).resolves.toMatchObject({ + actions: [{ satoshis: -222, isOutgoing: true, status: 'completed' }] + }) + }) + + it.each([ + ['negative outside supply', Utils.Writer.varIntNum(-21e14 - 1)], + ['positive outside supply', Utils.Writer.varIntNum(21e14 + 1)], + ['unsafe signed integer', [255, 0, 0, 0, 0, 0, 0, 0, 128]], + ['noncanonical uint16', [253, 1, 0]], + ['noncanonical uint32', [254, 253, 0, 0, 0]], + ['noncanonical uint64', [255, 1, 0, 0, 0, 0, 0, 0, 0]] + ])('rejects %s action-history amounts', async (_name, amount) => { + const wire: WalletWire = { transmitToWallet: async () => historyResponse(amount as number[]) } + await expect(new WalletWireTransceiver(wire).listActions({ labels: [] })).rejects.toThrow() + }) + + it('rejects truncated signed history amounts', async () => { + const wire: WalletWire = { + transmitToWallet: async () => [0, 1, ...Array(32).fill(0), 255, 255] + } + await expect(new WalletWireTransceiver(wire).listActions({ labels: [] })).rejects.toThrow( + 'available data' ) + }) - await expect(transceiver.listActions({ labels: [] })).rejects.toThrow('listActions satoshis') + it('still rejects negative individual output values in action history', async () => { + const prefix = historyResponse([0]).slice(0, -1) + const wire: WalletWire = { + transmitToWallet: async () => [...prefix, 1, 0, ...Utils.Writer.varIntNum(-1)] + } + await expect(new WalletWireTransceiver(wire).listActions({ labels: [] })).rejects.toThrow( + 'number too large' + ) }) it('rejects a direct binary request that bypasses transceiver argument validation', async () => {