@bsv/sdk 1.8.7 → 1.8.9

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 (43) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js +7 -2
  3. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js.map +1 -1
  4. package/dist/cjs/src/kvstore/GlobalKVStore.js +26 -4
  5. package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -1
  6. package/dist/cjs/src/kvstore/kvStoreInterpreter.js +7 -3
  7. package/dist/cjs/src/kvstore/kvStoreInterpreter.js.map +1 -1
  8. package/dist/cjs/src/kvstore/types.js +2 -1
  9. package/dist/cjs/src/kvstore/types.js.map +1 -1
  10. package/dist/cjs/src/overlay-tools/LookupResolver.js +7 -2
  11. package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js +7 -2
  14. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js.map +1 -1
  15. package/dist/esm/src/kvstore/GlobalKVStore.js +26 -4
  16. package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -1
  17. package/dist/esm/src/kvstore/kvStoreInterpreter.js +7 -3
  18. package/dist/esm/src/kvstore/kvStoreInterpreter.js.map +1 -1
  19. package/dist/esm/src/kvstore/types.js +2 -1
  20. package/dist/esm/src/kvstore/types.js.map +1 -1
  21. package/dist/esm/src/overlay-tools/LookupResolver.js +7 -2
  22. package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts +1 -1
  25. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts.map +1 -1
  26. package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -1
  27. package/dist/types/src/kvstore/kvStoreInterpreter.d.ts +2 -1
  28. package/dist/types/src/kvstore/kvStoreInterpreter.d.ts.map +1 -1
  29. package/dist/types/src/kvstore/types.d.ts +10 -0
  30. package/dist/types/src/kvstore/types.d.ts.map +1 -1
  31. package/dist/types/src/overlay-tools/LookupResolver.d.ts +1 -1
  32. package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
  33. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  34. package/dist/umd/bundle.js +1 -1
  35. package/dist/umd/bundle.js.map +1 -1
  36. package/docs/reference/kvstore.md +20 -2
  37. package/package.json +1 -1
  38. package/src/auth/transports/SimplifiedFetchTransport.ts +10 -2
  39. package/src/kvstore/GlobalKVStore.ts +33 -8
  40. package/src/kvstore/__tests/GlobalKVStore.test.ts +129 -0
  41. package/src/kvstore/kvStoreInterpreter.ts +8 -3
  42. package/src/kvstore/types.ts +11 -1
  43. package/src/overlay-tools/LookupResolver.ts +10 -2
@@ -169,6 +169,7 @@ export interface KVStoreEntry {
169
169
  value: string;
170
170
  controller: PubKeyHex;
171
171
  protocolID: WalletProtocol;
172
+ tags?: string[];
172
173
  token?: KVStoreToken;
173
174
  history?: string[];
174
175
  }
@@ -246,6 +247,8 @@ export interface KVStoreQuery {
246
247
  key?: string;
247
248
  controller?: PubKeyHex;
248
249
  protocolID?: WalletProtocol;
250
+ tags?: string[];
251
+ tagQueryMode?: "all" | "any";
249
252
  limit?: number;
250
253
  skip?: number;
251
254
  sortOrder?: "asc" | "desc";
@@ -254,6 +257,16 @@ export interface KVStoreQuery {
254
257
 
255
258
  See also: [PubKeyHex](./wallet.md#type-pubkeyhex), [WalletProtocol](./wallet.md#type-walletprotocol)
256
259
 
260
+ #### Property tagQueryMode
261
+
262
+ Controls tag matching behavior when tags are specified.
263
+ - 'all': Requires all specified tags to be present (default)
264
+ - 'any': Requires at least one of the specified tags to be present
265
+
266
+ ```ts
267
+ tagQueryMode?: "all" | "any"
268
+ ```
269
+
257
270
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
258
271
 
259
272
  ---
@@ -279,6 +292,7 @@ export interface KVStoreSetOptions {
279
292
  tokenSetDescription?: string;
280
293
  tokenUpdateDescription?: string;
281
294
  tokenAmount?: number;
295
+ tags?: string[];
282
296
  }
283
297
  ```
284
298
 
@@ -577,7 +591,8 @@ kvProtocol = {
577
591
  key: 1,
578
592
  value: 2,
579
593
  controller: 3,
580
- signature: 4
594
+ tags: 4,
595
+ signature: 5
581
596
  }
582
597
  ```
583
598
 
@@ -595,7 +610,10 @@ kvStoreInterpreter: InterpreterFunction<string, KVContext> = async (transaction:
595
610
  if (ctx == null || ctx.key == null)
596
611
  return undefined;
597
612
  const decoded = PushDrop.decode(output.lockingScript);
598
- if (decoded.fields.length !== Object.keys(kvProtocol).length)
613
+ const expectedFieldCount = Object.keys(kvProtocol).length;
614
+ const hasTagsField = decoded.fields.length === expectedFieldCount;
615
+ const isOldFormat = decoded.fields.length === expectedFieldCount - 1;
616
+ if (!isOldFormat && !hasTagsField)
599
617
  return undefined;
600
618
  const key = Utils.toUTF8(decoded.fields[kvProtocol.key]);
601
619
  const protocolID = Utils.toUTF8(decoded.fields[kvProtocol.protocolID]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -3,8 +3,10 @@
3
3
  import { AuthMessage, RequestedCertificateSet, Transport } from '../types.js'
4
4
  import * as Utils from '../../primitives/utils.js'
5
5
 
6
- // Only bind window.fetch in the browser
7
- const defaultFetch = typeof window !== 'undefined' ? fetch.bind(window) : fetch
6
+ const defaultFetch: typeof fetch =
7
+ typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function'
8
+ ? globalThis.fetch.bind(globalThis)
9
+ : fetch
8
10
 
9
11
  /**
10
12
  * Implements an HTTP-specific transport for handling Peer mutual authentication messages.
@@ -21,6 +23,12 @@ export class SimplifiedFetchTransport implements Transport {
21
23
  * @param fetchClient - A fetch implementation to use for HTTP requests (default: global fetch).
22
24
  */
23
25
  constructor (baseUrl: string, fetchClient = defaultFetch) {
26
+ if (typeof fetchClient !== 'function') {
27
+ throw new Error(
28
+ 'SimplifiedFetchTransport requires a fetch implementation. ' +
29
+ 'In environments without fetch, provide a polyfill or custom implementation.'
30
+ )
31
+ }
24
32
  this.fetchClient = fetchClient
25
33
  this.baseUrl = baseUrl
26
34
  }
@@ -138,6 +138,7 @@ export class GlobalKVStore {
138
138
  const tokenSetDescription = (options.tokenSetDescription != null && options.tokenSetDescription !== '') ? options.tokenSetDescription : `Create KVStore value for ${key}`
139
139
  const tokenUpdateDescription = (options.tokenUpdateDescription != null && options.tokenUpdateDescription !== '') ? options.tokenUpdateDescription : `Update KVStore value for ${key}`
140
140
  const tokenAmount = options.tokenAmount ?? this.config.tokenAmount
141
+ const tags = options.tags ?? []
141
142
 
142
143
  try {
143
144
  // Check for existing token to spend
@@ -146,13 +147,20 @@ export class GlobalKVStore {
146
147
 
147
148
  // Create PushDrop locking script
148
149
  const pushdrop = new PushDrop(this.wallet, this.config.originator)
150
+ const lockingScriptFields = [
151
+ Utils.toArray(JSON.stringify(protocolID), 'utf8'),
152
+ Utils.toArray(key, 'utf8'),
153
+ Utils.toArray(value, 'utf8'),
154
+ Utils.toArray(controller, 'hex')
155
+ ]
156
+
157
+ // Add tags as optional 5th field for backwards compatibility
158
+ if (tags.length > 0) {
159
+ lockingScriptFields.push(Utils.toArray(JSON.stringify(tags), 'utf8'))
160
+ }
161
+
149
162
  const lockingScript = await pushdrop.lock(
150
- [
151
- Utils.toArray(JSON.stringify(protocolID), 'utf8'),
152
- Utils.toArray(key, 'utf8'),
153
- Utils.toArray(value, 'utf8'),
154
- Utils.toArray(controller, 'hex')
155
- ],
163
+ lockingScriptFields,
156
164
  protocolID ?? this.config.protocolID as WalletProtocol,
157
165
  Utils.toUTF8(Utils.toArray(key, 'utf8')),
158
166
  'anyone',
@@ -409,7 +417,12 @@ export class GlobalKVStore {
409
417
  const output = tx.outputs[result.outputIndex]
410
418
  const decoded = PushDrop.decode(output.lockingScript)
411
419
 
412
- if (decoded.fields.length !== 5) {
420
+ // Support backwards compatibility: old format without tags, new format with tags
421
+ const expectedFieldCount = Object.keys(kvProtocol).length
422
+ const hasTagsField = decoded.fields.length === expectedFieldCount
423
+ const isOldFormat = decoded.fields.length === expectedFieldCount - 1
424
+
425
+ if (!isOldFormat && !hasTagsField) {
413
426
  continue
414
427
  }
415
428
 
@@ -429,11 +442,23 @@ export class GlobalKVStore {
429
442
  continue
430
443
  }
431
444
 
445
+ // Extract tags if present (backwards compatible)
446
+ let tags: string[] | undefined
447
+ if (hasTagsField && decoded.fields[kvProtocol.tags] != null) {
448
+ try {
449
+ tags = JSON.parse(Utils.toUTF8(decoded.fields[kvProtocol.tags]))
450
+ } catch (e) {
451
+ // If tags parsing fails, continue without tags
452
+ tags = undefined
453
+ }
454
+ }
455
+
432
456
  const entry: KVStoreEntry = {
433
457
  key: Utils.toUTF8(decoded.fields[kvProtocol.key]),
434
458
  value: Utils.toUTF8(decoded.fields[kvProtocol.value]),
435
459
  controller: Utils.toHex(decoded.fields[kvProtocol.controller]),
436
- protocolID: JSON.parse(Utils.toUTF8(decoded.fields[kvProtocol.protocolID]))
460
+ protocolID: JSON.parse(Utils.toUTF8(decoded.fields[kvProtocol.protocolID])),
461
+ tags
437
462
  }
438
463
 
439
464
  if (options.includeToken === true) {
@@ -287,6 +287,46 @@ describe('GlobalKVStore', () => {
287
287
  })
288
288
  })
289
289
 
290
+ it('returns entry with tags when token includes tags field', async () => {
291
+ primeResolverWithOneOutput(mockResolver)
292
+
293
+ const originalDecode = (MockPushDrop as any).decode
294
+ ;(MockPushDrop as any).decode = jest.fn().mockReturnValue({
295
+ fields: [
296
+ Array.from(Buffer.from(JSON.stringify([1, 'kvstore']))), // protocolID
297
+ Array.from(Buffer.from(TEST_KEY)), // key
298
+ Array.from(Buffer.from(TEST_VALUE)), // value
299
+ Array.from(Buffer.from(TEST_CONTROLLER, 'hex')), // controller
300
+ // tags field as JSON string so Utils.toUTF8 returns it directly
301
+ '["alpha","beta"]',
302
+ Array.from(Buffer.from('signature')) // signature
303
+ ]
304
+ })
305
+
306
+ const result = await kvStore.get({ key: TEST_KEY })
307
+
308
+ expect(Array.isArray(result)).toBe(true)
309
+ expect(result).toHaveLength(1)
310
+ if (Array.isArray(result) && result.length > 0) {
311
+ expect(result[0].tags).toEqual(['alpha', 'beta'])
312
+ }
313
+
314
+ ;(MockPushDrop as any).decode = originalDecode
315
+ })
316
+
317
+ it('omits tags when token is in old-format (no tags field)', async () => {
318
+ primeResolverWithOneOutput(mockResolver)
319
+
320
+ // primePushDropDecodeToValidValue() already sets old-format (no tags)
321
+ const result = await kvStore.get({ key: TEST_KEY })
322
+
323
+ expect(Array.isArray(result)).toBe(true)
324
+ expect(result).toHaveLength(1)
325
+ if (Array.isArray(result) && result.length > 0) {
326
+ expect(result[0].tags).toBeUndefined()
327
+ }
328
+ })
329
+
290
330
  it('returns entry with history when history=true', async () => {
291
331
  primeResolverWithOneOutput(mockResolver)
292
332
  mockHistorian.buildHistory.mockResolvedValue(['oldValue', TEST_VALUE])
@@ -320,6 +360,67 @@ describe('GlobalKVStore', () => {
320
360
  })
321
361
  })
322
362
 
363
+ it('forwards tags-only queries to the resolver', async () => {
364
+ primeResolverEmpty(mockResolver)
365
+
366
+ const tags = ['group:music', 'env:prod']
367
+ const result = await kvStore.get({ tags })
368
+
369
+ expect(Array.isArray(result)).toBe(true)
370
+ expect(mockResolver.query).toHaveBeenCalledWith({
371
+ service: 'ls_kvstore',
372
+ query: expect.objectContaining({ tags })
373
+ })
374
+ })
375
+
376
+ it('forwards tagQueryMode "all" to the resolver (default)', async () => {
377
+ primeResolverEmpty(mockResolver)
378
+
379
+ const tags = ['music', 'rock']
380
+ const result = await kvStore.get({ tags, tagQueryMode: 'all' })
381
+
382
+ expect(Array.isArray(result)).toBe(true)
383
+ expect(mockResolver.query).toHaveBeenCalledWith({
384
+ service: 'ls_kvstore',
385
+ query: expect.objectContaining({
386
+ tags,
387
+ tagQueryMode: 'all'
388
+ })
389
+ })
390
+ })
391
+
392
+ it('forwards tagQueryMode "any" to the resolver', async () => {
393
+ primeResolverEmpty(mockResolver)
394
+
395
+ const tags = ['music', 'jazz']
396
+ const result = await kvStore.get({ tags, tagQueryMode: 'any' })
397
+
398
+ expect(Array.isArray(result)).toBe(true)
399
+ expect(mockResolver.query).toHaveBeenCalledWith({
400
+ service: 'ls_kvstore',
401
+ query: expect.objectContaining({
402
+ tags,
403
+ tagQueryMode: 'any'
404
+ })
405
+ })
406
+ })
407
+
408
+ it('defaults to tagQueryMode "all" when not specified', async () => {
409
+ primeResolverEmpty(mockResolver)
410
+
411
+ const tags = ['category:news']
412
+ const result = await kvStore.get({ tags })
413
+
414
+ expect(Array.isArray(result)).toBe(true)
415
+ expect(mockResolver.query).toHaveBeenCalledWith({
416
+ service: 'ls_kvstore',
417
+ query: expect.objectContaining({ tags })
418
+ })
419
+ // Verify tagQueryMode is not explicitly set (will default to 'all' on server side)
420
+ const call = (mockResolver.query as jest.Mock).mock.calls[0][0]
421
+ expect(call.query.tagQueryMode).toBeUndefined()
422
+ })
423
+
323
424
  it('includes token data when includeToken=true for key queries', async () => {
324
425
  primeResolverWithOneOutput(mockResolver)
325
426
 
@@ -644,6 +745,34 @@ describe('GlobalKVStore', () => {
644
745
  expect(mockBroadcaster.broadcast).toHaveBeenCalled()
645
746
  })
646
747
 
748
+ it('includes tags field in locking script when options.tags provided', async () => {
749
+ primeResolverEmpty(mockResolver)
750
+
751
+ // Override PushDrop to capture the instance used within set()
752
+ const originalImpl = (MockPushDrop as any).mockImplementation
753
+ const mockLockingScript = { toHex: () => 'mockLockingScriptHex' }
754
+ const localPushDrop = {
755
+ lock: jest.fn().mockResolvedValue(mockLockingScript),
756
+ unlock: jest.fn().mockReturnValue({
757
+ sign: jest.fn().mockResolvedValue({ toHex: () => 'mockUnlockingScript' })
758
+ })
759
+ }
760
+ ;(MockPushDrop as any).mockImplementation(() => localPushDrop as any)
761
+
762
+ const providedTags = ['primary', 'news']
763
+ await kvStore.set(TEST_KEY, TEST_VALUE, { tags: providedTags })
764
+
765
+ // Validate PushDrop.lock was called with 5 fields (protocolID, key, value, controller, tags)
766
+ expect(localPushDrop.lock).toHaveBeenCalled()
767
+ const lockArgs = (localPushDrop.lock as jest.Mock).mock.calls[0]
768
+ const fields = lockArgs[0]
769
+ expect(Array.isArray(fields)).toBe(true)
770
+ expect(fields.length).toBe(5)
771
+
772
+ // Restore original implementation
773
+ ;(MockPushDrop as any).mockImplementation = originalImpl
774
+ })
775
+
647
776
  it('updates existing token when one exists', async () => {
648
777
  // Mock the queryOverlay to return an entry with a token
649
778
  const mockQueryOverlay = jest.spyOn(kvStore as any, 'queryOverlay')
@@ -10,7 +10,8 @@ export interface KVContext { key: string, protocolID: WalletProtocol }
10
10
  /**
11
11
  * KVStore interpreter used by Historian.
12
12
  *
13
- * Validates the KVStore PushDrop tokens: [protocolID, key, value, controller, signature].
13
+ * Validates the KVStore PushDrop tokens: [protocolID, key, value, controller, signature] (old format)
14
+ * or [protocolID, key, value, controller, tags, signature] (new format).
14
15
  * Filters outputs by the provided key in the interpreter context.
15
16
  * Produces the plaintext value for matching outputs; returns undefined otherwise.
16
17
  *
@@ -30,8 +31,12 @@ export const kvStoreInterpreter: InterpreterFunction<string, KVContext> = async
30
31
  // Decode the KVStore token
31
32
  const decoded = PushDrop.decode(output.lockingScript)
32
33
 
33
- // Validate KVStore token format (must have 5 fields: [protocolID, key, value, controller, signature])
34
- if (decoded.fields.length !== Object.keys(kvProtocol).length) return undefined
34
+ // Support backwards compatibility: old format without tags, new format with tags
35
+ const expectedFieldCount = Object.keys(kvProtocol).length
36
+ const hasTagsField = decoded.fields.length === expectedFieldCount
37
+ const isOldFormat = decoded.fields.length === expectedFieldCount - 1
38
+
39
+ if (!isOldFormat && !hasTagsField) return undefined
35
40
 
36
41
  // Only return values for the given key and protocolID
37
42
  const key = Utils.toUTF8(decoded.fields[kvProtocol.key])
@@ -41,6 +41,13 @@ export interface KVStoreQuery {
41
41
  key?: string
42
42
  controller?: PubKeyHex
43
43
  protocolID?: WalletProtocol
44
+ tags?: string[]
45
+ /**
46
+ * Controls tag matching behavior when tags are specified.
47
+ * - 'all': Requires all specified tags to be present (default)
48
+ * - 'any': Requires at least one of the specified tags to be present
49
+ */
50
+ tagQueryMode?: 'all' | 'any'
44
51
  limit?: number
45
52
  skip?: number
46
53
  sortOrder?: 'asc' | 'desc'
@@ -63,6 +70,7 @@ export interface KVStoreSetOptions {
63
70
  tokenSetDescription?: string
64
71
  tokenUpdateDescription?: string
65
72
  tokenAmount?: number
73
+ tags?: string[]
66
74
  }
67
75
 
68
76
  export interface KVStoreRemoveOptions {
@@ -78,6 +86,7 @@ export interface KVStoreEntry {
78
86
  value: string
79
87
  controller: PubKeyHex
80
88
  protocolID: WalletProtocol
89
+ tags?: string[]
81
90
  token?: KVStoreToken
82
91
  history?: string[]
83
92
  }
@@ -110,5 +119,6 @@ export const kvProtocol = {
110
119
  key: 1,
111
120
  value: 2,
112
121
  controller: 3,
113
- signature: 4
122
+ tags: 4,
123
+ signature: 5 // Note: signature moves to position 5 when tags are present
114
124
  }
@@ -2,8 +2,10 @@ import { Transaction } from '../transaction/index.js'
2
2
  import OverlayAdminTokenTemplate from './OverlayAdminTokenTemplate.js'
3
3
  import * as Utils from '../primitives/utils.js'
4
4
 
5
- // Only bind window.fetch in the browser
6
- const defaultFetch = typeof window !== 'undefined' ? fetch.bind(window) : fetch
5
+ const defaultFetch: typeof fetch =
6
+ typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function'
7
+ ? globalThis.fetch.bind(globalThis)
8
+ : fetch
7
9
 
8
10
  /**
9
11
  * The question asked to the Overlay Services Engine when a consumer of state wishes to look up information.
@@ -113,6 +115,12 @@ export class HTTPSOverlayLookupFacilitator implements OverlayLookupFacilitator {
113
115
  allowHTTP: boolean
114
116
 
115
117
  constructor (httpClient = defaultFetch, allowHTTP: boolean = false) {
118
+ if (typeof httpClient !== 'function') {
119
+ throw new Error(
120
+ 'HTTPSOverlayLookupFacilitator requires a fetch implementation. ' +
121
+ 'In environments without fetch, provide a polyfill or custom implementation.'
122
+ )
123
+ }
116
124
  this.fetchClient = httpClient
117
125
  this.allowHTTP = allowHTTP
118
126
  }