@bsv/message-box-client 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/message-box-client",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -64,6 +64,6 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@bsv/authsocket-client": "^1.0.12",
67
- "@bsv/sdk": "^1.8.2"
67
+ "@bsv/sdk": "^1.8.8"
68
68
  }
69
69
  }
@@ -903,6 +903,143 @@ class MessageBoxClient {
903
903
  throw new Error(`Failed to send message: ${errorMessage}`);
904
904
  }
905
905
  }
906
+ /**
907
+ * Multi-recipient sender. Uses the multi-quote route to:
908
+ * - identify blocked recipients
909
+ * - compute per-recipient payment
910
+ * Then sends to the allowed recipients with payment attached.
911
+ */
912
+ async sendMesagetoRecepients(params, overrideHost) {
913
+ var _a, _b, _c;
914
+ await this.assertInitialized();
915
+ const { recipients, messageBox, body, skipEncryption } = params;
916
+ if (!Array.isArray(recipients) || recipients.length === 0) {
917
+ throw new Error('You must provide at least one recipient!');
918
+ }
919
+ if (!messageBox || messageBox.trim() === '') {
920
+ throw new Error('You must provide a messageBox to send this message into!');
921
+ }
922
+ if (body == null || (typeof body === 'string' && body.trim().length === 0)) {
923
+ throw new Error('Every message must have a body!');
924
+ }
925
+ // 1) Multi-quote for all recipients
926
+ const quoteResponse = await this.getMessageBoxQuote({
927
+ recipient: recipients,
928
+ messageBox
929
+ }, overrideHost);
930
+ const quotesByRecipient = Array.isArray(quoteResponse === null || quoteResponse === void 0 ? void 0 : quoteResponse.quotesByRecipient)
931
+ ? quoteResponse.quotesByRecipient : [];
932
+ const blocked = ((_a = quoteResponse === null || quoteResponse === void 0 ? void 0 : quoteResponse.blockedRecipients) !== null && _a !== void 0 ? _a : []);
933
+ const totals = quoteResponse === null || quoteResponse === void 0 ? void 0 : quoteResponse.totals;
934
+ // 2) Filter allowed recipients
935
+ const allowedRecipients = recipients.filter(r => !blocked.includes(r));
936
+ if (allowedRecipients.length === 0) {
937
+ return {
938
+ status: 'error',
939
+ description: `All ${recipients.length} recipients are blocked.`,
940
+ sent: [],
941
+ blocked,
942
+ failed: recipients.map(r => ({ recipient: r, error: 'blocked' })),
943
+ totals
944
+ };
945
+ }
946
+ // 3) Map recipient -> fees
947
+ const perRecipientQuotes = new Map();
948
+ for (const q of quotesByRecipient) {
949
+ perRecipientQuotes.set(q.recipient, { recipientFee: q.recipientFee, deliveryFee: q.deliveryFee });
950
+ }
951
+ // 4) One delivery agent only (batch goes to one server)
952
+ const { deliveryAgentIdentityKeyByHost } = quoteResponse;
953
+ if (!deliveryAgentIdentityKeyByHost || Object.keys(deliveryAgentIdentityKeyByHost).length === 0) {
954
+ throw new Error('Missing delivery agent identity keys in quote response.');
955
+ }
956
+ if (Object.keys(deliveryAgentIdentityKeyByHost).length > 1 && !overrideHost) {
957
+ // To keep the single-POST invariant, we require all recipients to share a host
958
+ throw new Error('Recipients resolve to multiple hosts. Use overrideHost to force a single server or split by host.');
959
+ }
960
+ // pick the host to POST to
961
+ const finalHost = (overrideHost !== null && overrideHost !== void 0 ? overrideHost : await this.resolveHostForRecipient(allowedRecipients[0])).replace(/\/+$/, '');
962
+ const singleDeliveryKey = (_b = deliveryAgentIdentityKeyByHost[finalHost]) !== null && _b !== void 0 ? _b : Object.values(deliveryAgentIdentityKeyByHost)[0];
963
+ if (!singleDeliveryKey) {
964
+ throw new Error('Could not determine server delivery agent identity key.');
965
+ }
966
+ // 5) Identity key (sender)
967
+ if (!this.myIdentityKey) {
968
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, this.originator);
969
+ this.myIdentityKey = keyResult.publicKey;
970
+ }
971
+ // 6) Build per-recipient messageIds (HMAC), same order as allowedRecipients
972
+ const messageIds = [];
973
+ for (const r of allowedRecipients) {
974
+ const hmac = await this.walletClient.createHmac({
975
+ data: Array.from(new TextEncoder().encode(JSON.stringify(body))),
976
+ protocolID: [1, 'messagebox'],
977
+ keyID: '1',
978
+ counterparty: r
979
+ }, this.originator);
980
+ const mid = Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
981
+ messageIds.push(mid);
982
+ }
983
+ // 7) Body: for batch route the server expects a single shared body
984
+ // NOTE: If you need per-recipient encryption, we must change the server payload shape.
985
+ let finalBody;
986
+ if (skipEncryption === true) {
987
+ finalBody = typeof body === 'string' ? body : JSON.stringify(body);
988
+ }
989
+ else {
990
+ // safest for now: send plaintext; the recipients can decrypt payload fields client-side if needed
991
+ finalBody = typeof body === 'string' ? body : JSON.stringify(body);
992
+ }
993
+ // 8) ONE batch payment with server output at index 0
994
+ const paymentData = await this.createMessagePaymentBatch(allowedRecipients, perRecipientQuotes, singleDeliveryKey);
995
+ // 9) Single POST to /sendMessage with recipients[] + messageId[]
996
+ const requestBody = {
997
+ message: {
998
+ recipients: allowedRecipients,
999
+ messageBox,
1000
+ messageId: messageIds, // aligned by index with recipients
1001
+ body: finalBody
1002
+ },
1003
+ payment: paymentData
1004
+ };
1005
+ Logger.log('[MB CLIENT] Sending HTTP request to:', `${finalHost}/sendMessage`);
1006
+ Logger.log('[MB CLIENT] Request Body (batch):', JSON.stringify({ ...requestBody, payment: { ...paymentData, tx: '<omitted>' } }, null, 2));
1007
+ try {
1008
+ const response = await this.authFetch.fetch(`${finalHost}/sendMessage`, {
1009
+ method: 'POST',
1010
+ headers: { 'Content-Type': 'application/json' },
1011
+ body: JSON.stringify(requestBody)
1012
+ });
1013
+ const parsed = await response.json().catch(() => ({}));
1014
+ if (!response.ok || parsed.status !== 'success') {
1015
+ const msg = !response.ok ? `HTTP ${response.status} - ${response.statusText}` : ((_c = parsed.description) !== null && _c !== void 0 ? _c : 'Unknown server error');
1016
+ throw new Error(msg);
1017
+ }
1018
+ // server returns { results: [{ recipient, messageId }] }
1019
+ const sent = Array.isArray(parsed.results) ? parsed.results : [];
1020
+ const failed = []; // handled server-side now
1021
+ const status = sent.length === allowedRecipients.length ? 'success'
1022
+ : sent.length > 0 ? 'partial'
1023
+ : 'error';
1024
+ const description = status === 'success'
1025
+ ? `Sent to ${sent.length} recipients.`
1026
+ : status === 'partial'
1027
+ ? `Sent to ${sent.length} recipients; ${allowedRecipients.length - sent.length} failed; ${blocked.length} blocked.`
1028
+ : `Failed to send to ${allowedRecipients.length} allowed recipients. ${blocked.length} blocked.`;
1029
+ return { status, description, sent, blocked, failed, totals };
1030
+ }
1031
+ catch (err) {
1032
+ const msg = err instanceof Error ? err.message : 'Unknown error';
1033
+ return {
1034
+ status: 'error',
1035
+ description: `Batch send failed: ${msg}`,
1036
+ sent: [],
1037
+ blocked,
1038
+ failed: allowedRecipients.map(r => ({ recipient: r, error: msg })),
1039
+ totals
1040
+ };
1041
+ }
1042
+ }
906
1043
  /**
907
1044
  * @method anointHost
908
1045
  * @async
@@ -1561,31 +1698,145 @@ class MessageBoxClient {
1561
1698
  */
1562
1699
  async getMessageBoxQuote(params, overrideHost) {
1563
1700
  var _a;
1564
- const finalHost = overrideHost !== null && overrideHost !== void 0 ? overrideHost : await this.resolveHostForRecipient(params.recipient);
1565
- const queryParams = new URLSearchParams({
1566
- recipient: params.recipient,
1567
- messageBox: params.messageBox
1568
- });
1569
- Logger.log('[MB CLIENT] Getting messageBox quote...');
1570
- const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
1571
- method: 'GET'
1572
- });
1573
- if (!response.ok) {
1574
- const errorData = await response.json().catch(() => ({}));
1575
- throw new Error(`Failed to get quote: HTTP ${response.status} - ${(_a = String(errorData.description)) !== null && _a !== void 0 ? _a : response.statusText}`);
1576
- }
1577
- const { status, description, quote } = await response.json();
1578
- if (status === 'error') {
1579
- throw new Error(description !== null && description !== void 0 ? description : 'Failed to get quote');
1580
- }
1581
- const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key');
1582
- if (deliveryAgentIdentityKey == null) {
1583
- throw new Error('Failed to get quote: Delivery agent did not provide their identity key');
1701
+ // ---------- SINGLE RECIPIENT (back-compat) ----------
1702
+ if (!Array.isArray(params.recipient)) {
1703
+ const finalHost = overrideHost !== null && overrideHost !== void 0 ? overrideHost : await this.resolveHostForRecipient(params.recipient);
1704
+ const queryParams = new URLSearchParams({
1705
+ recipient: params.recipient,
1706
+ messageBox: params.messageBox
1707
+ });
1708
+ Logger.log('[MB CLIENT] Getting messageBox quote (single)...');
1709
+ console.log("HELP IM QUOTING", `${finalHost}/permissions/quote?${queryParams.toString()}`);
1710
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, { method: 'GET' });
1711
+ console.log("server response from getquote]", response);
1712
+ if (!response.ok) {
1713
+ const errorData = await response.json().catch(() => ({}));
1714
+ throw new Error(`Failed to get quote: HTTP ${response.status} - ${(_a = String(errorData.description)) !== null && _a !== void 0 ? _a : response.statusText}`);
1715
+ }
1716
+ const { status, description, quote } = await response.json();
1717
+ if (status === 'error') {
1718
+ throw new Error(description !== null && description !== void 0 ? description : 'Failed to get quote');
1719
+ }
1720
+ const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key');
1721
+ console.log("deliveryAgentIdentityKey", deliveryAgentIdentityKey);
1722
+ if (deliveryAgentIdentityKey == null) {
1723
+ throw new Error('Failed to get quote: Delivery agent did not provide their identity key');
1724
+ }
1725
+ return {
1726
+ recipientFee: quote.recipientFee,
1727
+ deliveryFee: quote.deliveryFee,
1728
+ deliveryAgentIdentityKey
1729
+ };
1584
1730
  }
1731
+ // ---------- MULTI RECIPIENTS ----------
1732
+ const recipients = params.recipient;
1733
+ if (recipients.length === 0) {
1734
+ throw new Error('At least one recipient is required.');
1735
+ }
1736
+ Logger.log('[MB CLIENT] Getting messageBox quotes (multi)...');
1737
+ console.log("[MB CLIENT] Getting messageBox quotes (multi)...");
1738
+ // Resolve host per recipient (unless caller forces overrideHost)
1739
+ // Group recipients by host so we call each overlay once.
1740
+ const hostGroups = new Map();
1741
+ for (const r of recipients) {
1742
+ const host = overrideHost !== null && overrideHost !== void 0 ? overrideHost : await this.resolveHostForRecipient(r);
1743
+ const list = hostGroups.get(host);
1744
+ if (list)
1745
+ list.push(r);
1746
+ else
1747
+ hostGroups.set(host, [r]);
1748
+ }
1749
+ const deliveryAgentIdentityKeyByHost = {};
1750
+ const quotesByRecipient = [];
1751
+ const blockedRecipients = [];
1752
+ let totalDeliveryFees = 0;
1753
+ let totalRecipientFees = 0;
1754
+ // Helper to fetch one host group
1755
+ const fetchGroup = async (host, groupRecipients) => {
1756
+ var _a;
1757
+ const qp = new URLSearchParams();
1758
+ for (const r of groupRecipients)
1759
+ qp.append('recipient', r);
1760
+ qp.set('messageBox', params.messageBox);
1761
+ const url = `${host}/permissions/quote?${qp.toString()}`;
1762
+ Logger.log('[MB CLIENT] Multi-quote GET:', url);
1763
+ const resp = await this.authFetch.fetch(url, { method: 'GET' });
1764
+ if (!resp.ok) {
1765
+ const errorData = await resp.json().catch(() => ({}));
1766
+ throw new Error(`Failed to get quote (host ${host}): HTTP ${resp.status} - ${(_a = String(errorData.description)) !== null && _a !== void 0 ? _a : resp.statusText}`);
1767
+ }
1768
+ const deliveryAgentKey = resp.headers.get('x-bsv-auth-identity-key');
1769
+ if (!deliveryAgentKey) {
1770
+ throw new Error(`Failed to get quote (host ${host}): missing delivery agent identity key`);
1771
+ }
1772
+ deliveryAgentIdentityKeyByHost[host] = deliveryAgentKey;
1773
+ const payload = await resp.json();
1774
+ // Server supports both shapes. For multi we expect:
1775
+ // { quotesByRecipient, totals, blockedRecipients }
1776
+ if (Array.isArray(payload === null || payload === void 0 ? void 0 : payload.quotesByRecipient)) {
1777
+ // merge quotes
1778
+ for (const q of payload.quotesByRecipient) {
1779
+ quotesByRecipient.push({
1780
+ recipient: q.recipient,
1781
+ messageBox: q.messageBox,
1782
+ deliveryFee: q.deliveryFee,
1783
+ recipientFee: q.recipientFee,
1784
+ status: q.status
1785
+ });
1786
+ // aggregate client-side totals as well (in case we hit multiple hosts)
1787
+ totalDeliveryFees += q.deliveryFee;
1788
+ if (q.recipientFee === -1) {
1789
+ if (!blockedRecipients.includes(q.recipient))
1790
+ blockedRecipients.push(q.recipient);
1791
+ }
1792
+ else {
1793
+ totalRecipientFees += q.recipientFee;
1794
+ }
1795
+ }
1796
+ // Also merge server totals if present (they are per-host); we already aggregated above,
1797
+ // so we don’t need to use payload.totals except for sanity/logging.
1798
+ if (Array.isArray(payload === null || payload === void 0 ? void 0 : payload.blockedRecipients)) {
1799
+ for (const br of payload.blockedRecipients) {
1800
+ if (!blockedRecipients.includes(br))
1801
+ blockedRecipients.push(br);
1802
+ }
1803
+ }
1804
+ }
1805
+ else if (payload === null || payload === void 0 ? void 0 : payload.quote) {
1806
+ // Defensive: if an overlay still returns single-quote shape for multi (shouldn’t),
1807
+ // we map it to each recipient in the group uniformly.
1808
+ for (const r of groupRecipients) {
1809
+ const { deliveryFee, recipientFee } = payload.quote;
1810
+ const status = recipientFee === -1 ? 'blocked' : recipientFee === 0 ? 'always_allow' : 'payment_required';
1811
+ quotesByRecipient.push({
1812
+ recipient: r,
1813
+ messageBox: params.messageBox,
1814
+ deliveryFee,
1815
+ recipientFee,
1816
+ status
1817
+ });
1818
+ totalDeliveryFees += deliveryFee;
1819
+ if (recipientFee === -1)
1820
+ blockedRecipients.push(r);
1821
+ else
1822
+ totalRecipientFees += recipientFee;
1823
+ }
1824
+ }
1825
+ else {
1826
+ throw new Error(`Unexpected quote response shape from host ${host}`);
1827
+ }
1828
+ };
1829
+ // Run all host groups (in parallel, but you can limit if needed)
1830
+ await Promise.all(Array.from(hostGroups.entries()).map(([host, group]) => fetchGroup(host, group)));
1585
1831
  return {
1586
- recipientFee: quote.recipientFee,
1587
- deliveryFee: quote.deliveryFee,
1588
- deliveryAgentIdentityKey
1832
+ quotesByRecipient,
1833
+ totals: {
1834
+ deliveryFees: totalDeliveryFees,
1835
+ recipientFees: totalRecipientFees,
1836
+ totalForPayableRecipients: totalDeliveryFees + totalRecipientFees
1837
+ },
1838
+ blockedRecipients,
1839
+ deliveryAgentIdentityKeyByHost
1589
1840
  };
1590
1841
  }
1591
1842
  /**
@@ -1742,13 +1993,20 @@ class MessageBoxClient {
1742
1993
  */
1743
1994
  async sendNotification(recipient, body, overrideHost) {
1744
1995
  await this.assertInitialized();
1745
- // Use sendMessage with permission checking enabled
1746
- // This eliminates duplication of quote fetching and payment logic
1747
- return await this.sendMessage({
1748
- recipient,
1996
+ // Single recipient keep original flow
1997
+ if (!Array.isArray(recipient)) {
1998
+ return await this.sendMessage({
1999
+ recipient,
2000
+ messageBox: 'notifications',
2001
+ body,
2002
+ checkPermissions: true
2003
+ }, overrideHost);
2004
+ }
2005
+ // Multiple recipients → new flow
2006
+ return await this.sendMesagetoRecepients({
2007
+ recipients: recipient,
1749
2008
  messageBox: 'notifications',
1750
- body,
1751
- checkPermissions: true
2009
+ body
1752
2010
  }, overrideHost);
1753
2011
  }
1754
2012
  /**
@@ -1895,6 +2153,7 @@ class MessageBoxClient {
1895
2153
  const derivationPrefix = sdk_1.Utils.toBase64((0, sdk_1.Random)(32));
1896
2154
  const derivationSuffix = sdk_1.Utils.toBase64((0, sdk_1.Random)(32));
1897
2155
  // Get host's derived public key
2156
+ console.log('delivery agent:', quote.deliveryAgentIdentityKey);
1898
2157
  const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
1899
2158
  protocolID: [2, '3241645161d8'],
1900
2159
  keyID: `${derivationPrefix} ${derivationSuffix}`,
@@ -1975,6 +2234,89 @@ class MessageBoxClient {
1975
2234
  // labels
1976
2235
  };
1977
2236
  }
2237
+ async createMessagePaymentBatch(recipients, perRecipientQuotes,
2238
+ // server (delivery agent) identity key to pay the delivery fee to
2239
+ serverIdentityKey, description = 'MessageBox delivery payment (batch)') {
2240
+ var _a;
2241
+ const outputs = [];
2242
+ const createActionOutputs = [];
2243
+ // figure out the per-request delivery fee (take it from any quoted recipient)
2244
+ const deliveryFeeOnce = (_a = recipients.reduce((acc, r) => {
2245
+ const q = perRecipientQuotes.get(r);
2246
+ return q ? (acc !== null && acc !== void 0 ? acc : q.deliveryFee) : acc;
2247
+ }, undefined)) !== null && _a !== void 0 ? _a : 0;
2248
+ const senderIdentityKey = await this.getIdentityKey();
2249
+ let outputIndex = 0;
2250
+ // index 0: server delivery fee (if any)
2251
+ if (deliveryFeeOnce > 0) {
2252
+ const derivationPrefix = sdk_1.Utils.toBase64((0, sdk_1.Random)(32));
2253
+ const derivationSuffix = sdk_1.Utils.toBase64((0, sdk_1.Random)(32));
2254
+ const { publicKey: agentDerived } = await this.walletClient.getPublicKey({
2255
+ protocolID: [2, '3241645161d8'],
2256
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
2257
+ counterparty: serverIdentityKey
2258
+ }, this.originator);
2259
+ const lockingScript = new sdk_1.P2PKH().lock(sdk_1.PublicKey.fromString(agentDerived).toAddress()).toHex();
2260
+ createActionOutputs.push({
2261
+ satoshis: deliveryFeeOnce,
2262
+ lockingScript,
2263
+ outputDescription: 'MessageBox server delivery fee (batch)',
2264
+ customInstructions: JSON.stringify({
2265
+ derivationPrefix,
2266
+ derivationSuffix,
2267
+ recipientIdentityKey: serverIdentityKey
2268
+ })
2269
+ });
2270
+ outputs.push({
2271
+ outputIndex: outputIndex++,
2272
+ protocol: 'wallet payment',
2273
+ paymentRemittance: { derivationPrefix, derivationSuffix, senderIdentityKey }
2274
+ });
2275
+ }
2276
+ // recipient outputs start at index 1 (or 0 if no delivery fee)
2277
+ const anyoneWallet = new sdk_1.ProtoWallet('anyone');
2278
+ const anyoneIdKey = (await anyoneWallet.getPublicKey({ identityKey: true })).publicKey;
2279
+ for (const r of recipients) {
2280
+ const q = perRecipientQuotes.get(r);
2281
+ if (!q || q.recipientFee <= 0)
2282
+ continue;
2283
+ const derivationPrefix = sdk_1.Utils.toBase64((0, sdk_1.Random)(32));
2284
+ const derivationSuffix = sdk_1.Utils.toBase64((0, sdk_1.Random)(32));
2285
+ const { publicKey: recipientDerived } = await anyoneWallet.getPublicKey({
2286
+ protocolID: [2, '3241645161d8'],
2287
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
2288
+ counterparty: r
2289
+ });
2290
+ const lockingScript = new sdk_1.P2PKH().lock(sdk_1.PublicKey.fromString(recipientDerived).toAddress()).toHex();
2291
+ createActionOutputs.push({
2292
+ satoshis: q.recipientFee,
2293
+ lockingScript,
2294
+ outputDescription: `Recipient message fee (${r.slice(0, 8)}…)`,
2295
+ customInstructions: JSON.stringify({
2296
+ derivationPrefix,
2297
+ derivationSuffix,
2298
+ recipientIdentityKey: r
2299
+ })
2300
+ });
2301
+ outputs.push({
2302
+ outputIndex: outputIndex++,
2303
+ protocol: 'wallet payment',
2304
+ paymentRemittance: {
2305
+ derivationPrefix,
2306
+ derivationSuffix,
2307
+ senderIdentityKey: anyoneIdKey
2308
+ }
2309
+ });
2310
+ }
2311
+ const { tx } = await this.walletClient.createAction({
2312
+ description,
2313
+ outputs: createActionOutputs,
2314
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
2315
+ }, this.originator);
2316
+ if (!tx)
2317
+ throw new Error('Failed to create payment transaction');
2318
+ return { tx, outputs, description };
2319
+ }
1978
2320
  }
1979
2321
  exports.MessageBoxClient = MessageBoxClient;
1980
2322
  //# sourceMappingURL=MessageBoxClient.js.map