@bsv/message-box-client

中文文档
2.7.0发布于 2026/10/28SEE LICENSE IN LICENSE.txtTypeScriptESM + CJS构建来源已验证近 30 天下载 6682

A client for P2P messaging and payments

原始使用文档
文档目录16 节
  1. Install
  2. Basic messaging
  3. Live messages
  4. What the live socket does on its own
  5. Knowing what the socket is doing
  6. Socket options
  7. Host selection and public-service access
  8. Permissions and delivery quotes
  9. Peer payments
  10. Token settlement
  11. HTTP and WebSocket contract
  12. Public exports
  13. Development
  14. License
  15. Payment acceptance and acknowledgment ordering
  16. SDK4 hotfix peer requirement

@bsv/message-box-client

Patch candidate 2.6.1 updates packed first-party runtime or development dependency ranges. This metadata-only patch preserves public APIs, peer ranges, wire formats and defaults. No additional consumer or data migration is required; earlier migration guidance still applies.

Authenticated, optionally encrypted store-and-forward messaging for BSV applications. The package supports browser and Node.js consumers, HTTP polling, live authenticated WebSockets, overlay-based host discovery, peer payments, token-settlement adapters, permissions, quotes, and push-device registration.

Install

npm install @bsv/message-box-client @bsv/sdk

@bsv/sdk is a required peer dependency. The package supports Node.js 22 and newer and publishes module-correct ESM and CommonJS entry points.

Basic messaging

import { MessageBoxClient } from '@bsv/message-box-client'
import { WalletClient } from '@bsv/sdk'

const wallet = new WalletClient()
const messages = new MessageBoxClient({
  walletClient: wallet,
  host: 'https://message-box-us-1.bsvb.tech'
})

const recipient = '025706528f0f6894b2ba505007267ccff1133e004452a1f6b72ac716f246216366'

await messages.sendMessage({
  recipient,
  messageBox: 'general_inbox',
  body: { text: 'Hello' }
})

const pending = await messages.listMessages({
  messageBox: 'general_inbox'
})

await messages.acknowledgeMessage({
  messageIds: pending.map(message => message.messageId)
})

TerraTestNet clients select the isolated overlay network and provide the dedicated TTN Message Box deployment explicitly:

const ttnMessages = new MessageBoxClient({
  walletClient: wallet,
  host: 'https://messagebox.ttn.example',
  networkPreset: 'teratestnet'
})

The explicit host is required until the dedicated TTN Message Box service has been deployed. This prevents TTN clients from silently using a testnet service.

listMessages() preserves its historical fetch-all behavior by following bounded server pages. Limit aggregate client memory when appropriate:

const firstTwoPages = await messages.listMessages({
  messageBox: 'general_inbox',
  pageSize: 250,
  maxPages: 2
})

const nextThousand = await messages.listMessages({
  messageBox: 'general_inbox',
  offset: 1000,
  limit: 1000
})

The client uses AuthFetch for BRC-105 challenges. It intentionally does not add a second Message Box-specific cost-approval mechanism because BRC-100 wallet permissions already govern payment authorization.

The UMD bundle includes SDK 2.5.0 support for the optional x-bsv-payment-known-txids response header; module consumers can enable it by using SDK 2.5.0 or later. This SDK extension lets recipients advertise already validated payment ancestors so compatible wallets can omit them from payment BEEF. It is not a standardized BRC-105 header. Existing compatible SDK peers remain supported, and services that omit the header retain existing payment behavior. No consumer migration is required.

Explicit init() is optional. Public methods initialize the wallet identity when needed:

await messages.init()

Client diagnostics, including errors, are disabled by default. Set enableLogging: true only when fixed lifecycle events are useful to a trusted local operator. Diagnostics intentionally omit message bodies and IDs, identity keys, hosts and query parameters, wallet exceptions, settlement artifacts, transactions, payment amounts, device tokens, and server response objects. Applications should record their own correlation identifier when they need to connect a fixed client event to an application request; do not reintroduce wallet or message data into console logs.

Encryption is enabled by default through the wallet protocol. Plaintext is an explicit interoperability choice:

await messages.sendMessage({
  recipient,
  messageBox: 'public_payloads',
  body: 'This payload is intentionally plaintext.',
  skipEncryption: true
})

The server authenticates and routes ciphertext but does not hold the wallet keys required to decrypt it.

Live messages

await messages.listenForLiveMessages({
  messageBox: 'general_inbox',
  onMessage: message => {
    console.log(message.sender, message.body)
  }
})

await messages.sendLiveMessage({
  recipient,
  messageBox: 'general_inbox',
  body: 'Live when possible, store-and-forward when needed.'
})

sendLiveMessage() uses an authenticated WebSocket and falls back to the HTTP send route when the socket is unavailable or does not acknowledge delivery. A conflicting WebSocket identity pin is a misconfiguration, not an unavailable socket, so it still throws. It captures and validates the same immutable recipient, message-box, body, message-ID, permission, and payment-ceiling authority as sendMessage() before opening or joining a socket. Room names are exact, control-free strings rather than values the client or server silently trims. Call disconnectWebSocket() when a long-lived client shuts down.

What the live socket does on its own

A dropped socket is kept while Socket.IO reconnects it, and the rooms you asked for are joined again once the new connection authenticates — membership belongs to the server-side socket that joined, so a replacement has to ask for itself. A server-forced disconnect is one Socket.IO will not retry, so the client rebuilds the socket itself: immediately the first time, then doubling from one second to a thirty-second cap until a socket holds its authentication.

Joins are paced: at most 16 await the server's answer on a socket, and the rest go out as answers arrive, or after five seconds without one. The server drops a socket with more than 32 signed messages in process, so a client on more than 32 boxes that sent every join at once never kept its socket.

A room counts as joined on the current socket from the moment it is requested, before the server answers, until the server refuses it with joinFailed or you leave it. getJoinedRooms() and the live status's rooms both mean that.

Two things follow for a caller:

  • Polling is required for correctness, not as a fallback. A live message reaches only the sockets held by the process that served the send, and a paid message is never pushed at all. Keep calling listMessages(); it is also the only path that internalizes a recipient payment.
  • getJoinedRooms() reports; it is not a lever. It returns the rooms joined on the current socket, requested and not refused. Mutating the returned set does not cancel a rejoin — call leaveRoom(), which releases the room whether or not the socket is up.

Knowing what the socket is doing

const stop = messages.onLiveStatus(({ state, rooms, reason }) => {
  // 'connecting' | 'live' | 'reconnecting' | 'closed'
  poller.setInterval(state === 'live' ? 60_000 : 5_000)
})

The listener is called once immediately with the current status and on every transition after that; onLiveStatus returns a function that removes it, and messages.liveStatus reads the same value without subscribing.

reconnecting means something will restore the socket — Socket.IO's own retry, or a rebuild the client has scheduled. closed means nothing will: a deliberate disconnectWebSocket(), a drop under managerOptions.reconnection: false, or a rebuild that failed. The next call that needs a socket still builds one.

rooms lists the rooms joined on the current socket, as defined above, and is empty unless the state is live. A room the server refuses is removed from it with a new report. A listener that throws is logged and does not stop the others.

The status describes this socket. It is not a delivery guarantee: keep polling listMessages() whatever it says.

Socket options

socketOptions is forwarded to the underlying AuthSocketClient when the live socket is created. Use it to select Socket.IO transports when a deployment's fronting infrastructure does not carry Engine.IO HTTP polling:

const messages = new MessageBoxClient({
  walletClient: wallet,
  host: 'https://messagebox.example',
  socketOptions: { managerOptions: { transports: ['websocket'] } }
})

await messages.listenForLiveMessages({
  messageBox: 'general_inbox',
  onMessage: message => {
    console.log(message.sender, message.body)
  }
})

It also carries requestedCertificates, sessionManager, maxPendingAuthMessages, and onError.

Four fields are excluded from the type. wallet and originator are owned by the client, which always uses its own values. managerOptions.autoConnect is excluded because the socket connects when it is created and AuthSocketClient exposes no way to start one later, so disabling auto-connect could never connect; the constructor throws if it is passed as false.

managerOptions.retries is also excluded: AuthSocket does not send the raw Socket.IO acknowledgements that message retries require, so enabling retries would block subsequent authentication and application messages. The constructor rejects nonzero retries. Connection reconnection settings such as reconnection and reconnectionAttempts remain supported.

managerOptions.reconnection: false is read and honoured: a drop disposes the socket rather than waiting for a retry that will never come. Nothing rebuilds it on its own under that setting. The next initializeConnection(), joinRoom(), listenForLiveMessages() or sendLiveMessage() builds a new socket and restores the rooms and subscriptions, so a client that sends recovers on its next send — but one that only subscribes and then waits never does. Leave reconnection enabled unless the host is driving reconnection itself.

socketOptions applies only to the live socket path — initializeConnection(), listenForLiveMessages(), and sendLiveMessage(). It has no effect on sendMessage(), listMessages(), or acknowledgeMessage(), which use authenticated HTTP.

Host selection and public-service access

An explicitly configured host must use HTTPS except on loopback (localhost, *.localhost, 127.0.0.0/8, or [::1]). It must be an exact absolute URL without surrounding whitespace, credentials, a query, a fragment, or control characters. Route prefixes are supported:

const messages = new MessageBoxClient({
  walletClient: wallet,
  host: 'https://messaging.example.com/api'
})

Every Message Box HTTP response must be the result of successful BRC-103 mutual authentication. The client rejects AuthFetch's ordinary-HTTP compatibility fallback, requires a canonical curve-valid server identity, and keeps the first authenticated identity for each origin authoritative for that client instance. For durable independent trust, pin expected server identities by base URL:

const messages = new MessageBoxClient({
  walletClient: wallet,
  host: 'https://messaging.example.com/api',
  serverIdentityKeysByHost: {
    'https://messaging.example.com': '02…'
  }
})

Pins are per URL origin, so two route prefixes on the same origin cannot name different identities. Different overlay hosts may authenticate with different identities. A server-key change requires creating a new client with an updated, independently validated pin; it is never accepted silently within an instance.

Overlay advertisements are untrusted network input. Discovered destinations must use public HTTPS addresses; loopback, private, link-local, reserved, and documentation-only hosts are rejected before a request is made. If no valid advertisement exists, the configured host is used. Each returned transaction, output index, canonical PushDrop envelope, advertised identity, and signature is bounded and independently verified against the requested identity. The lookup provider therefore supplies candidates, not routing authority.

Message Box is a public protocol service. Compatible servers should remain browser-accessible by default with credential-free wildcard CORS, including opaque Origin: null callers such as mobile webviews. Operators may opt into an exact-origin allowlist or disable browser CORS. CSP governs served documents and is not API authorization. BRC-103 authentication, recipient ownership, permissions, payments, quotas, and message encryption remain the security boundaries.

Permissions and delivery quotes

await messages.setMessageBoxPermission({
  messageBox: 'notifications',
  sender: recipient,
  recipientFee: 10
})

const quote = await messages.getMessageBoxQuote({
  recipient,
  messageBox: 'notifications'
})

Permission fees use these values:

  • -1: blocked
  • 0: allowed without a recipient fee
  • positive integer: required satoshi amount

Set checkPermissions: true on sendMessage() when the client should quote and construct the required payment before sending. The mutually authenticated Message Box is authoritative for its own price, while the wallet remains the final spending authority. Applications can add an independent ceiling with maximumPayment; the client rejects a larger quote before requesting a wallet action:

await messages.sendMessage({
  recipient,
  messageBox: 'notifications',
  body: { title: 'Bounded-cost alert' },
  checkPermissions: true,
  maximumPayment: 25
})

Quote rows must bind every requested recipient and message-box name exactly once. Fees are bounded integers, blocked-recipient and status fields must agree, and aggregate arithmetic must stay in the safe-integer domain. Before a payment is transmitted, the client independently parses the wallet's Atomic BEEF and requires every delivery and recipient output to retain its requested script, amount, and remittance index. A custom wallet response is never treated as payment evidence merely because it returned transaction bytes.

Outgoing recipients are canonical compressed public keys. Message-box names, caller-supplied message IDs, and serialized bodies are exact and bounded; body objects and request records must contain only plain own data. The client captures the complete send authority before its first asynchronous operation, so later caller mutation cannot change the recipient, message box, body, flags, or payment ceiling. Server send results are accepted only when their recipient and message ID match the submitted message. Permission and device responses are also treated as untrusted protocol input: records, canonical identity keys, fees, timestamps, masked tokens, platform values, identifiers, and pagination arguments must match their bounded schema.

Peer payments

import { PeerPayClient } from '@bsv/message-box-client'

const payments = new PeerPayClient({ walletClient: wallet })

await payments.sendPayment({
  recipient,
  amount: 50_000
})

const incoming = await payments.listIncomingPayments()
for (const payment of incoming) {
  await payments.acceptPayment(payment)
}

PeerPayClient uses BRC-29 wallet-payment derivation and the same authenticated Message Box transport. It also supports live delivery, payment requests, responses, cancellations, and explicit rejection/refund flows. Its SDK peer accepts both historical number[] and binary Wallet Wire Uint8Array transaction results, while payment messages retain a portable JSON byte-array representation. Receipt remains compatible with pending messages whose typed-array bytes were already serialized as contiguous numeric keys. The same compatibility contract covers paid-message fees, batch delivery, token settlements, live-message fallback, and generic remittance transport. In a multi-recipient send, the quoted server delivery fee applies to every allowed recipient. The client checks the aggregate against maximumPayment and places it in the batch's single server remittance output. Malformed, sparse, or out-of-range byte records are rejected before wallet internalization and the source message remains available for retry or recovery. Incoming-payment reads are capped at 1,000 messages and validate canonical sender keys, transaction bytes, derivation metadata, positive integer amounts, output indexes, and message IDs. Live callbacks apply the same validation. acceptPayment and rejectPayment use the caller's message ID only, reload the authenticated inbox, require one unique match, and use the fresh record for all wallet, acknowledgement, and refund decisions. Payment-request lists and live callbacks likewise require a canonical body/envelope identity match, bounded fields, an unexpired request, and an affirmative 32-byte HMAC proof. Replayed request IDs from one sender fail closed. Fulfill and decline calls use only the selected message ID, reload the authenticated inbox, and reject concurrent processing of that ID by the same client instance. Response reads are bounded and preserve the authenticated sender and messageId; always bind both requestId and sender to the original request recipient.

Token settlement

PeerTokenClient routes token transfers and requests through Message Box while delegating token-standard-specific transaction work to registered TokenSettlementAdapter implementations:

import { PeerTokenClient } from '@bsv/message-box-client'

const tokens = new PeerTokenClient({
  walletClient: wallet,
  adapters: [myTokenAdapter]
})

The library does not assume a token-standard wire format. Adapters own the standard-specific construction, acceptance, termination, and receipt data for their protocol, while the client enforces common authority boundaries: canonical peer keys, bounded transaction/message fields, canonical positive integer amounts, exact adapter action verdicts, and unchanged requested protocol/asset/amount fields. Incoming reads are capped at 1,000 records. acceptToken() uses only the supplied message ID, reloads the authenticated inbox, requires one unique fresh match, and acknowledges only after the adapter returns the literal accept action. An acknowledgement outage after accepted custody is recoverable and does not turn the result into a truthy error string. Token requests apply the same bounded authenticated-envelope model. Live and listed requests require a canonical body/envelope sender match, an unexpired bounded schema, and an affirmative 32-byte HMAC proof. Fulfillment and decline operations reload the inbox by message ID and use only the unique fresh request; fulfillment also requires the selected source's protocol and asset to match it. Outgoing requests, cancellations, and response listings validate and bound all common fields before wallet or transport work. One client instance rejects concurrent fulfill/decline processing for the same token-request message.

RemittanceAdapter exposes Message Box as an SDK remittance communication layer.

HTTP and WebSocket contract

The client uses these authenticated HTTP routes:

MethodRoutePurpose
POST/sendMessageSend to one or up to 100 recipients
POST/listMessagesRead a named box owned by the authenticated identity
POST/acknowledgeMessageDelete acknowledged messages owned by the identity
POST/registerDeviceRegister a Firebase push token
GET/devicesList the identity's registered devices
POST/permissions/setSet a sender-specific or box-wide permission
GET/permissions/getRead a permission
GET/permissions/listList permissions
GET/permissions/quoteQuote one or up to 100 recipients

The corresponding OpenAPI source is specs/messaging/message-box-http.yaml. Live delivery uses authenticated Socket.IO events and identity-owned rooms.

Public exports

  • MessageBoxClient
  • PeerPayClient
  • PeerTokenClient
  • RemittanceAdapter
  • TokenSettlementAdapter and its supporting types
  • messaging, payment, token, device, permission, quote, and batch-send types
  • standard message-box name constants

Development

From the repository root:

pnpm --filter @bsv/message-box-client typecheck
pnpm --filter @bsv/message-box-client lint
pnpm --filter @bsv/message-box-client test
pnpm --filter @bsv/message-box-client test:coverage
pnpm --filter @bsv/message-box-client pack:check
pnpm --filter @bsv/message-box-client test:browser

The unit suite is deterministic and does not contact a deployed service. test:integration is explicitly opt-in because it requires configured wallet, Message Box, database, WebSocket, and overlay services:

MESSAGE_BOX_RUN_INTEGRATION=true \
MESSAGE_BOX_INTEGRATION_HOST=http://127.0.0.1:8080 \
MESSAGE_BOX_WALLET_ORIGINATOR=localhost \
pnpm --filter @bsv/message-box-client test:integration

Targets under *.bsvb.tech require an additional explicit MESSAGE_BOX_ALLOW_PRODUCTION_INTEGRATION=true acknowledgement because the suite creates and acknowledges real messages.

The release tarball contains compiled ESM/CommonJS JavaScript, matching declarations and source maps, the UMD browser bundle, this README, and the license. It does not publish TypeScript source, tests, coverage, editor files, or package-manager locks.

License

TS Stack first-party material is under the Open BSV License Version 6. The UMD bundle incorporates separately licensed SDK material; keep THIRD_PARTY_NOTICES.md and LICENSES/ with the bundle.

Payment acceptance and acknowledgment ordering

acknowledgeNotification checks the original notification envelope, internalizes a present recipient payment with the configured originator, and acknowledges only after the wallet returns accepted: true. A notification without a payment can be acknowledged immediately. Failures, incomplete envelopes and payments with no wallet-payment outputs remain queued. Its boolean return contract is unchanged: false can mean no payment or a retained failed payment.

Incoming payment envelopes accept Atomic BEEF up to 32 MiB and at most 101 payment outputs, matching the existing 100-recipient batch plus delivery-fee output contract. The client snapshots those fields independently so a valid large transaction is not rejected by the smaller generic JSON-graph limit; sparse arrays, accessors, non-byte values, and oversized fields still fail closed before wallet work.

acceptPayment also requires affirmative wallet acceptance before acknowledgment; validation and internalization failures reject instead of returning a truthy error string. An acknowledgement failure after accepted custody is recoverable and does not change the successful result. For refundable amounts, rejectPayment internalizes first, sends the refund, and then acknowledges. A failed internalization prevents both refund and acknowledgment; a failed refund send leaves the message queued. The existing small-payment policy is unchanged.

This is the initial ordering remediation for issue #503. It does not provide a durable refund journal or exactly-once delivery. Reconcile uncertain refund-send outcomes before retrying, since a send may have completed before its response was lost. Payment-envelope retention and outcome reporting through listMessages/listMessagesLite, basket-insertion policy, and resumable refund semantics remain open. Use the original envelope for notification payment processing; a plain acknowledgment is not evidence that a payment was accepted.

Payment- and token-request fulfillment also cannot provide cross-process or cross-crash exactly-once settlement with the current protocol. If settlement succeeds but its response fails or is lost, the request remains queued and an automatic retry can pay twice. Do not retry an uncertain fulfillment until the wallet and peer have been reconciled. Persist each authenticated request ID and sender together with every attempted or completed settlement transaction ID. Before fulfilling or retrying, reject a transaction ID already associated with a different request and reject a different transaction ID for a request whose outcome has not been reconciled. Apply the same durable correlation in every service process; in-memory serialization is not cross-process replay protection.

SDK4 hotfix peer requirement

The SDK4 peer alternative starts at @bsv/sdk ^4.0.1, which restores numeric wallet results and legacy wire compatibility. Existing SDK2/3 alternatives and lower bounds remain unchanged. SDK4 users should install the 4.0.1 hotfix; no application-wallet wire or data migration is required.

版本与发布记录

@bsv/message-box-client 共发布 49 个版本,首次发布于 2025/06/27;过去 12 个月发布了 29 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.7.0latest2026/10/2811 天前
2.6.12026/10/2415 天前
2.6.02026/10/031 个月前
2.5.42026/09/251 个月前
2.5.32026/09/241 个月前
2.5.22026/09/231 个月前

关于 @bsv/message-box-client 的常见问题

@bsv/message-box-client 是什么?

A client for P2P messaging and payments

如何安装 @bsv/message-box-client?

在项目目录运行 npm install @bsv/message-box-client,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@bsv/sdk(^2.8.6 || ^3.0.0 || ^4.0.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@bsv/message-box-client 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bsv/message-box-client 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@bsv/message-box-client 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bsv/message-box-client 可以商用吗?

@bsv/message-box-client 使用 SEE LICENSE IN LICENSE.txt 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@bsv/message-box-client 的下载量有多少?

根据 npm 官方下载统计,@bsv/message-box-client 在 2026/10/09 – 2026/11/07 这 30 天内被下载 6,682 次,日均约 223 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bsv/message-box-client 还在维护吗?

最新版本 2.7.0 发布于 2026/10/28(11 天前);至今共发布 49 个版本,其中过去 12 个月发布了 29 个;首次发布于 2025/06/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。