@bsv/overlay 0.1.12 → 0.1.14

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.
Files changed (37) hide show
  1. package/dist/cjs/package.json +2 -2
  2. package/dist/cjs/src/Engine.js +82 -411
  3. package/dist/cjs/src/Engine.js.map +1 -1
  4. package/dist/cjs/src/GASP/OverlayGASPRemote.js +6 -4
  5. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  6. package/dist/cjs/src/GASP/OverlayGASPStorage.js +152 -77
  7. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/Engine.js +82 -411
  10. package/dist/esm/src/Engine.js.map +1 -1
  11. package/dist/esm/src/GASP/OverlayGASPRemote.js +7 -4
  12. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  13. package/dist/esm/src/GASP/OverlayGASPStorage.js +151 -77
  14. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/Engine.d.ts +14 -115
  17. package/dist/types/src/Engine.d.ts.map +1 -1
  18. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +3 -2
  19. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  20. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +29 -3
  21. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/src/Engine.ts +68 -9
  25. package/src/__tests/Engine.test.ts +95 -1
  26. package/dist/cjs/src/SHIPAdvertisement.js +0 -3
  27. package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
  28. package/dist/cjs/src/SLAPAdvertisement.js +0 -3
  29. package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
  30. package/dist/esm/src/SHIPAdvertisement.js +0 -2
  31. package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
  32. package/dist/esm/src/SLAPAdvertisement.js +0 -2
  33. package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
  34. package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
  35. package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
  36. package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
  37. package/dist/types/src/SLAPAdvertisement.d.ts.map +0 -1
package/src/Engine.ts CHANGED
@@ -15,6 +15,7 @@ import { SyncConfiguration } from './SyncConfiguration.js'
15
15
  import { Advertisement } from './Advertisement.js'
16
16
  import { OverlayGASPRemote } from './GASP/OverlayGASPRemote.js'
17
17
  import { OverlayGASPStorage } from './GASP/OverlayGASPStorage.js'
18
+ import { URL } from "url"
18
19
 
19
20
  /**
20
21
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
@@ -34,6 +35,7 @@ export class Engine {
34
35
  * @param {SyncConfiguration} syncConfiguration — Configuration object describing historical synchronization of topics.
35
36
  * @param {boolean} logTime - Enables / disables the timing logs for various operations in the Overlay submit route.
36
37
  * @param {string} logPrefix - Supports overriding the log prefix with a custom string.
38
+ * @param {boolean} throwOnBroadcastFailure - Enables / disables throwing an error when a transaction broadcast failure is detected.
37
39
  */
38
40
  constructor(
39
41
  public managers: { [key: string]: TopicManager },
@@ -47,7 +49,8 @@ export class Engine {
47
49
  public advertiser?: Advertiser,
48
50
  public syncConfiguration?: SyncConfiguration,
49
51
  public logTime = false,
50
- public logPrefix = '[OVERLAY_ENGINE] '
52
+ public logPrefix = '[OVERLAY_ENGINE] ',
53
+ public throwOnBroadcastFailure = false
51
54
  ) {
52
55
  // To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all overlay topics, except for 'tm_ship' and 'tm_slap'.
53
56
  // For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
@@ -122,7 +125,7 @@ export class Engine {
122
125
  this.endTime(`chainTracker_${txid.substring(0, 10)}`)
123
126
 
124
127
  const steak: STEAK = {}
125
- let admissableOutputs: AdmittanceInstructions = { outputsToAdmit: [], coinsToRetain: [] }
128
+ let admissibleOutputs: AdmittanceInstructions = { outputsToAdmit: [], coinsToRetain: [] }
126
129
  const previousCoins: number[] = []
127
130
 
128
131
  // Parallelize the topic processing
@@ -179,7 +182,7 @@ export class Engine {
179
182
  const admissibleOutputsPromise = (async () => {
180
183
  try {
181
184
  this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
182
- admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
185
+ admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
183
186
  this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
184
187
  } catch (_) {
185
188
  steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
@@ -189,7 +192,7 @@ export class Engine {
189
192
  // Wait for both tasks to complete
190
193
  await Promise.all([markSpentPromises, admissibleOutputsPromise])
191
194
  // Keep track of what outputs were admitted for what topic
192
- steak[topic] = admissableOutputs
195
+ steak[topic] = admissibleOutputs
193
196
  })
194
197
 
195
198
  await Promise.all(topicPromises)
@@ -198,8 +201,10 @@ export class Engine {
198
201
  this.startTime(`broadcast_${txid.substring(0, 10)}`)
199
202
  if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
200
203
  const response = await this.broadcaster.broadcast(tx)
201
- if (isBroadcastFailure(response)) {
202
- throw new Error(`Failed to broadcast transaction! Error: ${response.description}`)
204
+ if (isBroadcastFailure(response) && this.throwOnBroadcastFailure) {
205
+ const e = new Error(`Failed to broadcast transaction! Error: ${response.description}`);
206
+ (e as any).more = response.more
207
+ throw e
203
208
  }
204
209
  }
205
210
  this.endTime(`broadcast_${txid.substring(0, 10)}`)
@@ -212,7 +217,7 @@ export class Engine {
212
217
 
213
218
  for (const topic of taggedBEEF.topics) {
214
219
  // Keep track of which outputs to admit, mark as stale, or retain
215
- const outputsToAdmit: number[] = admissableOutputs.outputsToAdmit
220
+ const outputsToAdmit: number[] = admissibleOutputs.outputsToAdmit
216
221
  const staleCoins: Array<{
217
222
  txid: string
218
223
  outputIndex: number
@@ -227,7 +232,7 @@ export class Engine {
227
232
  for (const inputIndex of previousCoins) {
228
233
  const previousTXID = tx.inputs[inputIndex].sourceTXID || tx.inputs[inputIndex].sourceTransaction?.id('hex') as string
229
234
  const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
230
- if (!admissableOutputs.coinsToRetain.includes(inputIndex)) {
235
+ if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
231
236
  staleCoins.push({
232
237
  txid: previousTXID,
233
238
  outputIndex: previousOutputIndex
@@ -464,7 +469,7 @@ export class Engine {
464
469
  * @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
465
470
  */
466
471
  async syncAdvertisements(): Promise<void> {
467
- if (this.advertiser === undefined) {
472
+ if (this.advertiser === undefined || !this.hostingURL || !this.isValidUrl(this.hostingURL)) {
468
473
  return
469
474
  }
470
475
  const advertiser = this.advertiser
@@ -945,6 +950,60 @@ export class Engine {
945
950
  const documentation = await this.lookupServices[provider]?.getDocumentation?.()
946
951
  return documentation !== undefined ? documentation : 'No documentation found!'
947
952
  }
953
+
954
+ /**
955
+ * Validates a URL to ensure it does not match disallowed patterns:
956
+ * - Contains "http:" protocol
957
+ * - Contains "localhost" (with or without a port)
958
+ * - Internal or non-routable IP addresses (e.g., 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x)
959
+ * - Non-routable IPs like 127.x.x.x, 0.0.0.0, or IPv6 loopback (::1)
960
+ *
961
+ * @param url - The URL string to validate
962
+ * @returns {boolean} - Returns `false` if the URL violates any of the conditions `true` otherwise
963
+ */
964
+ private isValidUrl(url: string): boolean {
965
+ try {
966
+ const parsedUrl = new URL(url)
967
+
968
+ // Disallow http:
969
+ if (parsedUrl.protocol === "http:") {
970
+ return false
971
+ }
972
+
973
+ // Disallow localhost with or without a port
974
+ if (/^localhost(:\d+)?$/i.test(parsedUrl.hostname)) {
975
+ return false
976
+ }
977
+
978
+ // Disallow internal and non-routable IP addresses
979
+ const ipAddress = parsedUrl.hostname
980
+
981
+ // Regex for non-routable IPv4 IPs
982
+ const nonRoutableIpv4Patterns = [
983
+ /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Loopback IPs
984
+ /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 10.x.x.x private IPs
985
+ /^192\.168\.\d{1,3}\.\d{1,3}$/, // 192.168.x.x private IPs
986
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.x.x to 172.31.x.x private IPs
987
+ /^0\.0\.0\.0$/ // Non-routable address
988
+ ]
989
+
990
+ // Check for IPv4 matches
991
+ if (nonRoutableIpv4Patterns.some((pattern) => pattern.test(ipAddress))) {
992
+ return false
993
+ }
994
+
995
+ // Check for non-routable IPv6 addresses explicitly
996
+ if (ipAddress === "[::1]") {
997
+ return false
998
+ }
999
+
1000
+ // If none of the disallowed conditions matched, the URL is valid
1001
+ return true
1002
+ } catch (error) {
1003
+ // If the input is not a valid URL, return false
1004
+ return false
1005
+ }
1006
+ }
948
1007
  }
949
1008
 
950
1009
  //////////
@@ -7,6 +7,8 @@ import { Storage } from '../storage/Storage'
7
7
  import { Transaction, Utils } from '@bsv/sdk'
8
8
  import { Output } from '../Output'
9
9
  import { SyncConfiguration } from '../SyncConfiguration'
10
+ import { TaggedBEEF } from '../TaggedBEEF.js'
11
+ import { Advertiser } from '../Advertiser.js'
10
12
 
11
13
  const mockChainTracker = {
12
14
  isValidRootForHeight: jest.fn(async () => true)
@@ -24,13 +26,37 @@ const mockOutput: Output = {
24
26
  outputIndex: 0,
25
27
  outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
26
28
  topic: 'hello',
27
- satoshis: exampleTX.outputs[0].satoshis,
29
+ satoshis: exampleTX.outputs[0].satoshis as number,
28
30
  beef: exampleBeef,
29
31
  spent: false,
30
32
  outputsConsumed: [],
31
33
  consumedBy: []
32
34
  }
33
35
 
36
+ const invalidHostingUrls = [
37
+ "http://example.com", // Invalid: http
38
+ "https://localhost:3000", // Invalid: localhost
39
+ "https://192.168.1.1", // Invalid: internal private IP
40
+ "https://127.0.0.1", // Invalid: loopback IP
41
+ "https://0.0.0.0", // Invalid: non-routable IP
42
+ "http://172.16.0.1", // Invalid: private IP
43
+ "[::1]"
44
+ ]
45
+
46
+
47
+ const validHostingUrls = [
48
+ "https://example.com", // Valid: public URL
49
+ "https://8.8.8.8", // Valid: public routable IP
50
+ "https://255.255.255.255", // Valid: public routable IP
51
+ ]
52
+
53
+ const mockAdvertiser: Advertiser = {
54
+ createAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
55
+ revokeAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
56
+ findAllAdvertisements: jest.fn(async () => []),
57
+ parseAdvertisement: jest.fn()
58
+ }
59
+
34
60
  describe('BSV Overlay Services Engine', () => {
35
61
  beforeEach(() => {
36
62
  jest.clearAllMocks()
@@ -66,6 +92,74 @@ describe('BSV Overlay Services Engine', () => {
66
92
  findUTXOsForTopic: jest.fn()
67
93
  }
68
94
  })
95
+
96
+ it('engine.syncAdvertisements should return void when invalid hostingURL is provided', async () => {
97
+ for (const url of invalidHostingUrls) {
98
+ const engine = new Engine(
99
+ { tm_helloworld: mockTopicManager },
100
+ { ls_helloworld: mockLookupService },
101
+ mockStorageEngine,
102
+ mockChainTracker,
103
+ url, // Invalid hostingURL
104
+ ['tracker1'], // shipTrackers
105
+ ['tracker2'], // slapTrackers
106
+ undefined,
107
+ mockAdvertiser,
108
+ undefined
109
+ )
110
+
111
+ engine.submit = jest.fn(async (taggedBEEF: TaggedBEEF, onSteakReady: any, mode?: string) => ({ outputsToAdmit: [] }))
112
+
113
+ // Call the method that would normally trigger syncAdvertisements
114
+ const result = await engine.syncAdvertisements()
115
+
116
+ expect(result).toBeUndefined()
117
+
118
+ // Verify that Advertiser methods are NOT called
119
+ expect(mockAdvertiser.createAdvertisements).not.toHaveBeenCalled()
120
+ expect(mockAdvertiser.findAllAdvertisements).not.toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
121
+
122
+ console.log('Got past', url)
123
+ }
124
+ })
125
+
126
+ it('should allow engine.syncAdvertisements to proceed with valid hostingURLs', async () => {
127
+ const mockAdvertiser: Advertiser = {
128
+ createAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_TAG', data: Buffer.from('mock data') }),
129
+ findAllAdvertisements: jest.fn().mockResolvedValue([]),
130
+ revokeAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_REVOKE_TAG', data: Buffer.from('revocation data') }),
131
+ parseAdvertisement: jest.fn().mockReturnValue({
132
+ protocol: 'SHIP',
133
+ topicOrServiceName: 'mock-topic',
134
+ timestamp: Date.now(),
135
+ }),
136
+ }
137
+
138
+ for (const url of validHostingUrls) {
139
+ const engine = new Engine(
140
+ { tm_helloworld: mockTopicManager },
141
+ { ls_helloworld: mockLookupService },
142
+ mockStorageEngine,
143
+ mockChainTracker,
144
+ url, // Valid hostingURL
145
+ ['tracker1'], // shipTrackers
146
+ ['tracker2'], // slapTrackers
147
+ undefined,
148
+ mockAdvertiser, // Pass the mock Advertiser
149
+ undefined
150
+ )
151
+
152
+ // Call the method that would normally trigger syncAdvertisements
153
+ engine.submit = jest.fn(async (taggedBEEF: TaggedBEEF, onSteakReady: any, mode?: string) => ({ outputsToAdmit: [] }))
154
+ const result = await engine.syncAdvertisements()
155
+ expect(engine.submit).toHaveBeenCalled()
156
+
157
+ // Verify that Advertiser methods are called
158
+ expect(mockAdvertiser.createAdvertisements).toHaveBeenCalled()
159
+ expect(mockAdvertiser.findAllAdvertisements).toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
160
+ }
161
+ })
162
+
69
163
  it('Uses SHIP sync configuration by default if no syncConfiguration was provided', () => {
70
164
  const engine = new Engine(
71
165
  { tm_helloworld: mockTopicManager },
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=SHIPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=SLAPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=SHIPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=SLAPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,9 +0,0 @@
1
- export interface SHIPAdvertisement {
2
- protocol: 'SHIP';
3
- identityKey: string;
4
- domain: string;
5
- topic: string;
6
- beef?: number[];
7
- outputIndex?: number;
8
- }
9
- //# sourceMappingURL=SHIPAdvertisement.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SHIPAdvertisement.d.ts","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB"}
@@ -1,9 +0,0 @@
1
- export interface SLAPAdvertisement {
2
- protocol: 'SLAP';
3
- identityKey: string;
4
- domain: string;
5
- service: string;
6
- beef?: number[];
7
- outputIndex?: number;
8
- }
9
- //# sourceMappingURL=SLAPAdvertisement.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SLAPAdvertisement.d.ts","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB"}