@bsv/sdk 1.9.31 → 1.10.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.
Files changed (51) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/Peer.js +68 -48
  3. package/dist/cjs/src/auth/Peer.js.map +1 -1
  4. package/dist/cjs/src/identity/IdentityClient.js +124 -20
  5. package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
  6. package/dist/cjs/src/primitives/BigNumber.js +28 -54
  7. package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
  8. package/dist/cjs/src/primitives/ECDSA.js +36 -1
  9. package/dist/cjs/src/primitives/ECDSA.js.map +1 -1
  10. package/dist/cjs/src/primitives/ReductionContext.js +35 -46
  11. package/dist/cjs/src/primitives/ReductionContext.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/auth/Peer.js +68 -48
  14. package/dist/esm/src/auth/Peer.js.map +1 -1
  15. package/dist/esm/src/identity/IdentityClient.js +124 -20
  16. package/dist/esm/src/identity/IdentityClient.js.map +1 -1
  17. package/dist/esm/src/primitives/BigNumber.js +28 -54
  18. package/dist/esm/src/primitives/BigNumber.js.map +1 -1
  19. package/dist/esm/src/primitives/ECDSA.js +36 -1
  20. package/dist/esm/src/primitives/ECDSA.js.map +1 -1
  21. package/dist/esm/src/primitives/ReductionContext.js +35 -46
  22. package/dist/esm/src/primitives/ReductionContext.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  25. package/dist/types/src/auth/types.d.ts +2 -0
  26. package/dist/types/src/auth/types.d.ts.map +1 -1
  27. package/dist/types/src/identity/IdentityClient.d.ts +8 -0
  28. package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
  29. package/dist/types/src/primitives/BigNumber.d.ts +8 -0
  30. package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
  31. package/dist/types/src/primitives/ECDSA.d.ts +24 -0
  32. package/dist/types/src/primitives/ECDSA.d.ts.map +1 -1
  33. package/dist/types/src/primitives/ReductionContext.d.ts +9 -0
  34. package/dist/types/src/primitives/ReductionContext.d.ts.map +1 -1
  35. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  36. package/dist/umd/bundle.js +3 -3
  37. package/dist/umd/bundle.js.map +1 -1
  38. package/docs/index.md +15 -1
  39. package/docs/reference/auth.md +2 -0
  40. package/docs/reference/messages.md +0 -24
  41. package/docs/reference/primitives.md +91 -31
  42. package/package.json +1 -1
  43. package/src/auth/Peer.ts +122 -57
  44. package/src/auth/__tests/Peer.test.ts +166 -257
  45. package/src/auth/types.ts +2 -0
  46. package/src/identity/IdentityClient.ts +153 -29
  47. package/src/identity/__tests/IdentityClient.test.ts +289 -1
  48. package/src/primitives/BigNumber.ts +27 -31
  49. package/src/primitives/ECDSA.ts +41 -2
  50. package/src/primitives/ReductionContext.ts +44 -48
  51. package/src/primitives/__tests/ECDSA.test.ts +16 -0
@@ -13,11 +13,16 @@ import {
13
13
  } from '../wallet/index.js'
14
14
  import { BroadcastFailure, BroadcastResponse, Transaction } from '../transaction/index.js'
15
15
  import Certificate from '../auth/certificates/Certificate.js'
16
+ import { VerifiableCertificate } from '../auth/certificates/VerifiableCertificate.js'
16
17
  import { PushDrop } from '../script/index.js'
17
18
  import { PrivateKey, Utils } from '../primitives/index.js'
19
+ import ProtoWallet from '../wallet/ProtoWallet.js'
18
20
  import { LookupResolver, SHIPBroadcaster, TopicBroadcaster, withDoubleSpendRetry } from '../overlay-tools/index.js'
19
21
  import { ContactsManager, Contact } from './ContactsManager.js'
20
22
 
23
+ // Default SocialCert certifier for fallback resolution when no wallet is available
24
+ const DEFAULT_CERTIFIER = '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'
25
+
21
26
  /**
22
27
  * IdentityClient lets you discover who others are, and let the world know who you are.
23
28
  */
@@ -30,7 +35,7 @@ export class IdentityClient {
30
35
  private readonly originator?: OriginatorDomainNameStringUnder250Bytes
31
36
  ) {
32
37
  this.originator = originator
33
- this.wallet = wallet ?? new WalletClient()
38
+ this.wallet = wallet ?? new WalletClient('auto', this.originator)
34
39
  this.contactsManager = new ContactsManager(this.wallet, this.originator)
35
40
  }
36
41
 
@@ -140,19 +145,33 @@ export class IdentityClient {
140
145
  ): Promise<DisplayableIdentity[]> {
141
146
  if (overrideWithContacts) {
142
147
  // Override results with personal contacts if available
143
- const contacts = await this.contactsManager.getContacts(args.identityKey)
144
- if (contacts.length > 0) {
145
- return contacts
148
+ try {
149
+ const contacts = await this.contactsManager.getContacts(args.identityKey)
150
+ if (contacts.length > 0) {
151
+ return contacts
152
+ }
153
+ } catch (e) {
154
+ // Contacts require wallet - continue to try discovery
146
155
  }
147
156
  }
148
157
 
149
- const { certificates } = await this.wallet.discoverByIdentityKey(
150
- args,
151
- this.originator
152
- )
153
- return certificates.map((cert) => {
154
- return IdentityClient.parseIdentity(cert)
155
- })
158
+ try {
159
+ const { certificates } = await this.wallet.discoverByIdentityKey(
160
+ args,
161
+ this.originator
162
+ )
163
+ return certificates.map((cert) => {
164
+ return IdentityClient.parseIdentity(cert)
165
+ })
166
+ } catch (error: unknown) {
167
+ // Fallback to LookupResolver if no wallet is available
168
+ const errorMessage = error instanceof Error ? error.message : ''
169
+ if (errorMessage.includes('No wallet available')) {
170
+ const certificates = await this.resolveViaLookup({ identityKey: args.identityKey })
171
+ return certificates.map((cert) => IdentityClient.parseIdentity(cert))
172
+ }
173
+ throw error
174
+ }
156
175
  }
157
176
 
158
177
  /**
@@ -167,26 +186,131 @@ export class IdentityClient {
167
186
  overrideWithContacts = true
168
187
  ): Promise<DisplayableIdentity[]> {
169
188
  // Run both queries in parallel for better performance
170
- const [contacts, certificatesResult] = await Promise.all([
171
- overrideWithContacts
172
- ? this.contactsManager.getContacts()
173
- : Promise.resolve([]),
174
- this.wallet.discoverByAttributes(args, this.originator)
175
- ])
176
-
177
- // Fast lookup by identityKey
178
- const contactByKey = new Map<PubKeyHex, Contact>(
179
- contacts.map((contact) => [contact.identityKey, contact] as const)
180
- )
189
+ const contactsPromise = overrideWithContacts
190
+ ? this.contactsManager.getContacts().catch(() => [] as Contact[])
191
+ : Promise.resolve([] as Contact[])
181
192
 
182
- // Guard if certificates might be absent
183
- const certs = certificatesResult?.certificates ?? []
193
+ try {
194
+ const [contacts, certificatesResult] = await Promise.all([
195
+ contactsPromise,
196
+ this.wallet.discoverByAttributes(args, this.originator)
197
+ ])
198
+
199
+ // Fast lookup by identityKey
200
+ const contactByKey = new Map<PubKeyHex, Contact>(
201
+ contacts.map((contact) => [contact.identityKey, contact] as const)
202
+ )
184
203
 
185
- // Parse certificates and substitute with contacts where available
186
- return certs.map(
187
- (cert) =>
188
- contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert)
189
- )
204
+ // Guard if certificates might be absent
205
+ const certs = certificatesResult?.certificates ?? []
206
+
207
+ // Parse certificates and substitute with contacts where available
208
+ return certs.map(
209
+ (cert) =>
210
+ contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert)
211
+ )
212
+ } catch (error: unknown) {
213
+ // Fallback to LookupResolver if no wallet is available
214
+ const errorMessage = error instanceof Error ? error.message : ''
215
+ if (errorMessage.includes('No wallet available')) {
216
+ const certificates = await this.resolveViaLookup({ attributes: args.attributes })
217
+ return certificates.map((cert) => IdentityClient.parseIdentity(cert))
218
+ }
219
+ throw error
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Fallback method to resolve identity certificates via LookupResolver when no wallet is available.
225
+ * Uses the ls_identity overlay with a default SocialCert certifier.
226
+ *
227
+ * @param query - Query parameters: either identityKey or attributes
228
+ * @returns {Promise<IdentityCertificate[]>} Array of identity certificates
229
+ */
230
+ private async resolveViaLookup (
231
+ query: { identityKey?: PubKeyHex, attributes?: Record<string, string> }
232
+ ): Promise<IdentityCertificate[]> {
233
+ const resolver = new LookupResolver({ networkPreset: 'mainnet' })
234
+
235
+ // Build the lookup query based on what was provided
236
+ const lookupQuery: Record<string, any> = {
237
+ certifiers: [DEFAULT_CERTIFIER]
238
+ }
239
+
240
+ if (query.identityKey !== undefined) {
241
+ lookupQuery.identityKey = query.identityKey
242
+ } else if (query.attributes !== undefined) {
243
+ // For attribute search, use the 'any' matcher for flexible matching
244
+ const attributeValues = Object.values(query.attributes)
245
+ if (attributeValues.length === 1) {
246
+ lookupQuery.attributes = { any: attributeValues[0] }
247
+ } else {
248
+ lookupQuery.attributes = query.attributes
249
+ }
250
+ }
251
+
252
+ const lookupResult = await resolver.query({
253
+ service: 'ls_identity',
254
+ query: lookupQuery
255
+ })
256
+
257
+ if (lookupResult.type !== 'output-list' || lookupResult.outputs.length === 0) {
258
+ return []
259
+ }
260
+
261
+ const certificates: IdentityCertificate[] = []
262
+ const anyoneWallet = new ProtoWallet('anyone')
263
+
264
+ for (const output of lookupResult.outputs) {
265
+ try {
266
+ const tx = Transaction.fromBEEF(output.beef)
267
+ const decodedOutput = PushDrop.decode(tx.outputs[output.outputIndex].lockingScript)
268
+ const certData = JSON.parse(Utils.toUTF8(decodedOutput.fields[0]))
269
+
270
+ const verifiableCert = new VerifiableCertificate(
271
+ certData.type,
272
+ certData.serialNumber,
273
+ certData.subject,
274
+ certData.certifier,
275
+ certData.revocationOutpoint,
276
+ certData.fields,
277
+ certData.keyring,
278
+ certData.signature
279
+ )
280
+
281
+ // Decrypt fields using 'anyone' wallet for publicly revealed certificates
282
+ const decryptedFields = await verifiableCert.decryptFields(anyoneWallet)
283
+
284
+ // Verify the certificate is valid
285
+ await verifiableCert.verify()
286
+
287
+ // Construct IdentityCertificate with default certifier info
288
+ const identityCert: IdentityCertificate = {
289
+ type: certData.type,
290
+ serialNumber: certData.serialNumber,
291
+ subject: certData.subject,
292
+ certifier: certData.certifier,
293
+ revocationOutpoint: certData.revocationOutpoint,
294
+ fields: certData.fields,
295
+ signature: certData.signature,
296
+ certifierInfo: {
297
+ name: 'SocialCert',
298
+ iconUrl: 'https://socialcert.net/favicon.ico',
299
+ description: 'Social identity verification',
300
+ trust: 5
301
+ },
302
+ publiclyRevealedKeyring: certData.keyring,
303
+ decryptedFields
304
+ }
305
+
306
+ certificates.push(identityCert)
307
+ } catch (e) {
308
+ // Skip invalid certificates and continue processing
309
+ continue
310
+ }
311
+ }
312
+
313
+ return certificates
190
314
  }
191
315
 
192
316
  /**
@@ -15,14 +15,41 @@ jest.mock('../../script', () => {
15
15
  }
16
16
  })
17
17
 
18
+ // Store mock functions for LookupResolver so tests can configure them
19
+ const mockLookupResolverQuery = jest.fn()
20
+
18
21
  jest.mock('../../overlay-tools/index.js', () => {
19
22
  return {
20
23
  TopicBroadcaster: jest.fn().mockImplementation(() => ({
21
24
  broadcast: jest.fn().mockResolvedValue('broadcastResult')
25
+ })),
26
+ LookupResolver: jest.fn().mockImplementation(() => ({
27
+ query: mockLookupResolverQuery
28
+ }))
29
+ }
30
+ })
31
+
32
+ // Mock VerifiableCertificate for resolveViaLookup tests
33
+ const mockDecryptFields = jest.fn()
34
+ const mockVerify = jest.fn()
35
+
36
+ jest.mock('../../auth/certificates/VerifiableCertificate.js', () => {
37
+ return {
38
+ VerifiableCertificate: jest.fn().mockImplementation(() => ({
39
+ decryptFields: mockDecryptFields,
40
+ verify: mockVerify
22
41
  }))
23
42
  }
24
43
  })
25
44
 
45
+ // Mock ProtoWallet for resolveViaLookup tests
46
+ jest.mock('../../wallet/ProtoWallet.js', () => {
47
+ return {
48
+ __esModule: true,
49
+ default: jest.fn().mockImplementation(() => ({}))
50
+ }
51
+ })
52
+
26
53
  jest.mock('../../transaction/index.js', () => {
27
54
  return {
28
55
  Transaction: {
@@ -267,6 +294,187 @@ describe('IdentityClient', () => {
267
294
  // Wallet method should not be called when contact is found
268
295
  expect(walletMock.discoverByIdentityKey).not.toHaveBeenCalled()
269
296
  })
297
+
298
+ it('should fallback to LookupResolver when no wallet is available', async () => {
299
+ // Mock wallet methods to throw "No wallet available" error
300
+ walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
301
+ new Error('No wallet available over any communication substrate. Install a BSV wallet today!')
302
+ )
303
+
304
+ // Mock ContactsManager to also throw (since it requires wallet)
305
+ const mockContactsManager = identityClient['contactsManager']
306
+ mockContactsManager.getContacts = jest.fn().mockRejectedValue(
307
+ new Error('No wallet available')
308
+ )
309
+
310
+ // Setup mock certificate data that would come from lookup
311
+ const certData = {
312
+ type: KNOWN_IDENTITY_TYPES.xCert,
313
+ serialNumber: 'serial123',
314
+ subject: 'test-identity-key',
315
+ certifier: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17',
316
+ revocationOutpoint: 'outpoint',
317
+ fields: { userName: 'encrypted', profilePhoto: 'encrypted' },
318
+ keyring: { fieldKey: 'keyringValue' },
319
+ signature: 'sig123'
320
+ }
321
+
322
+ // Mock LookupResolver to return output-list with certificate data
323
+ mockLookupResolverQuery.mockResolvedValue({
324
+ type: 'output-list',
325
+ outputs: [{
326
+ beef: new Uint8Array([1, 2, 3]),
327
+ outputIndex: 0
328
+ }]
329
+ })
330
+
331
+ // Mock PushDrop.decode to return certificate JSON
332
+ const { PushDrop } = require('../../script')
333
+ PushDrop.decode.mockReturnValue({
334
+ fields: [new TextEncoder().encode(JSON.stringify(certData))]
335
+ })
336
+
337
+ // Mock VerifiableCertificate methods
338
+ mockDecryptFields.mockResolvedValue({
339
+ userName: 'TestUser',
340
+ profilePhoto: 'test-photo-url'
341
+ })
342
+ mockVerify.mockResolvedValue(true)
343
+
344
+ const identities = await identityClient.resolveByIdentityKey(
345
+ { identityKey: 'test-identity-key' },
346
+ false // Don't override with contacts
347
+ )
348
+
349
+ // Verify wallet was called and threw
350
+ expect(walletMock.discoverByIdentityKey).toHaveBeenCalled()
351
+
352
+ // Verify LookupResolver was queried
353
+ expect(mockLookupResolverQuery).toHaveBeenCalledWith({
354
+ service: 'ls_identity',
355
+ query: {
356
+ certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
357
+ identityKey: 'test-identity-key'
358
+ }
359
+ })
360
+
361
+ // Verify results were returned from fallback
362
+ expect(identities).toHaveLength(1)
363
+ expect(identities[0].name).toBe('TestUser')
364
+ expect(identities[0].identityKey).toBe('test-identity-key')
365
+ })
366
+
367
+ it('should return empty array when LookupResolver returns no results', async () => {
368
+ walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
369
+ new Error('No wallet available')
370
+ )
371
+
372
+ const mockContactsManager = identityClient['contactsManager']
373
+ mockContactsManager.getContacts = jest.fn().mockRejectedValue(
374
+ new Error('No wallet available')
375
+ )
376
+
377
+ // Mock LookupResolver to return empty results
378
+ mockLookupResolverQuery.mockResolvedValue({
379
+ type: 'output-list',
380
+ outputs: []
381
+ })
382
+
383
+ const identities = await identityClient.resolveByIdentityKey(
384
+ { identityKey: 'nonexistent-key' },
385
+ false
386
+ )
387
+
388
+ expect(identities).toHaveLength(0)
389
+ })
390
+
391
+ it('should return empty array when LookupResolver returns non-output-list type', async () => {
392
+ walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
393
+ new Error('No wallet available')
394
+ )
395
+
396
+ const mockContactsManager = identityClient['contactsManager']
397
+ mockContactsManager.getContacts = jest.fn().mockRejectedValue(
398
+ new Error('No wallet available')
399
+ )
400
+
401
+ // Mock LookupResolver to return different type
402
+ mockLookupResolverQuery.mockResolvedValue({
403
+ type: 'freeform',
404
+ result: {}
405
+ })
406
+
407
+ const identities = await identityClient.resolveByIdentityKey(
408
+ { identityKey: 'some-key' },
409
+ false
410
+ )
411
+
412
+ expect(identities).toHaveLength(0)
413
+ })
414
+
415
+ it('should skip invalid certificates and continue processing', async () => {
416
+ walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
417
+ new Error('No wallet available')
418
+ )
419
+
420
+ const mockContactsManager = identityClient['contactsManager']
421
+ mockContactsManager.getContacts = jest.fn().mockRejectedValue(
422
+ new Error('No wallet available')
423
+ )
424
+
425
+ // Mock LookupResolver to return multiple outputs
426
+ mockLookupResolverQuery.mockResolvedValue({
427
+ type: 'output-list',
428
+ outputs: [
429
+ { beef: new Uint8Array([1]), outputIndex: 0 },
430
+ { beef: new Uint8Array([2]), outputIndex: 0 }
431
+ ]
432
+ })
433
+
434
+ // First call throws (invalid cert), second succeeds
435
+ const { PushDrop } = require('../../script')
436
+ PushDrop.decode
437
+ .mockImplementationOnce(() => { throw new Error('Invalid script') })
438
+ .mockImplementationOnce(() => ({
439
+ fields: [new TextEncoder().encode(JSON.stringify({
440
+ type: KNOWN_IDENTITY_TYPES.xCert,
441
+ serialNumber: 'serial',
442
+ subject: 'valid-key',
443
+ certifier: 'certifier',
444
+ revocationOutpoint: 'outpoint',
445
+ fields: {},
446
+ keyring: {},
447
+ signature: 'sig'
448
+ }))]
449
+ }))
450
+
451
+ mockDecryptFields.mockResolvedValue({ userName: 'ValidUser' })
452
+ mockVerify.mockResolvedValue(true)
453
+
454
+ const identities = await identityClient.resolveByIdentityKey(
455
+ { identityKey: 'some-key' },
456
+ false
457
+ )
458
+
459
+ // Should have 1 identity (the valid one)
460
+ expect(identities).toHaveLength(1)
461
+ expect(identities[0].name).toBe('ValidUser')
462
+ })
463
+
464
+ it('should re-throw non-wallet errors', async () => {
465
+ walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
466
+ new Error('Network timeout')
467
+ )
468
+
469
+ const mockContactsManager = identityClient['contactsManager']
470
+ mockContactsManager.getContacts = jest.fn().mockRejectedValue(
471
+ new Error('No wallet available')
472
+ )
473
+
474
+ await expect(
475
+ identityClient.resolveByIdentityKey({ identityKey: 'test-key' }, false)
476
+ ).rejects.toThrow('Network timeout')
477
+ })
270
478
  })
271
479
 
272
480
  it('should throw if createAction returns no tx', async () => {
@@ -367,7 +575,11 @@ describe('IdentityClient', () => {
367
575
  {
368
576
  name: 'Alice Smith',
369
577
  identityKey: 'alice-key',
370
- avatarURL: '', abbreviatedKey: 'alice-i...', badgeIconURL: '', badgeLabel: '', badgeClickURL: ''
578
+ avatarURL: '',
579
+ abbreviatedKey: 'alice-i...',
580
+ badgeIconURL: '',
581
+ badgeLabel: '',
582
+ badgeClickURL: ''
371
583
  }
372
584
  ]
373
585
 
@@ -416,6 +628,82 @@ describe('IdentityClient', () => {
416
628
  expect(identities[0].name).toBe('alice@example.com') // Should be discovered identity, not contact
417
629
  expect(mockContactsManager.getContacts).not.toHaveBeenCalled() // Should not fetch contacts
418
630
  })
631
+
632
+ it('should fallback to LookupResolver when no wallet is available for attribute search', async () => {
633
+ walletMock.discoverByAttributes = jest.fn().mockRejectedValue(
634
+ new Error('No wallet available')
635
+ )
636
+
637
+ // Setup mock certificate data
638
+ const certData = {
639
+ type: KNOWN_IDENTITY_TYPES.emailCert,
640
+ serialNumber: 'serial123',
641
+ subject: 'found-identity-key',
642
+ certifier: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17',
643
+ revocationOutpoint: 'outpoint',
644
+ fields: { email: 'encrypted' },
645
+ keyring: { fieldKey: 'keyringValue' },
646
+ signature: 'sig123'
647
+ }
648
+
649
+ mockLookupResolverQuery.mockResolvedValue({
650
+ type: 'output-list',
651
+ outputs: [{
652
+ beef: new Uint8Array([1, 2, 3]),
653
+ outputIndex: 0
654
+ }]
655
+ })
656
+
657
+ const { PushDrop } = require('../../script')
658
+ PushDrop.decode.mockReturnValue({
659
+ fields: [new TextEncoder().encode(JSON.stringify(certData))]
660
+ })
661
+
662
+ mockDecryptFields.mockResolvedValue({ email: 'found@example.com' })
663
+ mockVerify.mockResolvedValue(true)
664
+
665
+ const identities = await identityClient.resolveByAttributes(
666
+ { attributes: { email: 'found@example.com' } },
667
+ false
668
+ )
669
+
670
+ // Verify LookupResolver was queried with attributes
671
+ expect(mockLookupResolverQuery).toHaveBeenCalledWith({
672
+ service: 'ls_identity',
673
+ query: {
674
+ certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
675
+ attributes: { any: 'found@example.com' }
676
+ }
677
+ })
678
+
679
+ expect(identities).toHaveLength(1)
680
+ expect(identities[0].name).toBe('found@example.com')
681
+ })
682
+
683
+ it('should use multi-attribute query when multiple attributes provided', async () => {
684
+ walletMock.discoverByAttributes = jest.fn().mockRejectedValue(
685
+ new Error('No wallet available')
686
+ )
687
+
688
+ mockLookupResolverQuery.mockResolvedValue({
689
+ type: 'output-list',
690
+ outputs: []
691
+ })
692
+
693
+ await identityClient.resolveByAttributes(
694
+ { attributes: { firstName: 'John', lastName: 'Doe' } },
695
+ false
696
+ )
697
+
698
+ // Verify query uses full attributes object when more than one
699
+ expect(mockLookupResolverQuery).toHaveBeenCalledWith({
700
+ service: 'ls_identity',
701
+ query: {
702
+ certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
703
+ attributes: { firstName: 'John', lastName: 'Doe' }
704
+ }
705
+ })
706
+ })
419
707
  })
420
708
 
421
709
  describe('parseIdentity', () => {
@@ -1305,47 +1305,43 @@ export default class BigNumber {
1305
1305
  * @param p - The `BigNumber` specifying the modulus field.
1306
1306
  * @returns The multiplicative inverse `BigNumber` in the modulus field specified by `p`.
1307
1307
  */
1308
+ /**
1309
+ * SECURITY NOTE:
1310
+ * This implementation avoids variable-time extended Euclidean algorithms
1311
+ * to reduce timing side-channel leakage. However, JavaScript BigInt arithmetic
1312
+ * does not provide constant-time guarantees. This implementation is suitable
1313
+ * for browser and single-tenant environments but is not hardened against
1314
+ * high-resolution timing attacks in shared CPU contexts.
1315
+ */
1308
1316
  _invmp (p: BigNumber): BigNumber {
1309
1317
  this.assert(p._sign === 0, 'p must not be negative for _invmp')
1310
1318
  this.assert(!p.isZero(), 'p must not be zero for _invmp')
1311
1319
 
1312
- const aBN: BigNumber = this.umod(p)
1313
-
1314
- let aVal = aBN._magnitude
1315
- let bVal = p._magnitude
1316
- let x1Val = 1n
1317
- let x2Val = 0n
1318
- const modulus = p._magnitude
1320
+ // Fermat inversion: a^(p-2) mod p
1321
+ // NOTE: This assumes p is prime (true for all cryptographic use cases here).
1322
+ // This avoids variable-time EEA loops but BigInt arithmetic itself
1323
+ // is not constant-time (documented limitation).
1319
1324
 
1320
- while (aVal > 1n && bVal > 1n) {
1321
- let i = 0; while (((aVal >> BigInt(i)) & 1n) === 0n) i++
1322
- if (i > 0) {
1323
- aVal >>= BigInt(i)
1324
- for (let k = 0; k < i; ++k) { if ((x1Val & 1n) !== 0n) x1Val += modulus; x1Val >>= 1n }
1325
- }
1325
+ const a = this.umod(p)
1326
+ const exp = p.subn(2)
1326
1327
 
1327
- let j = 0; while (((bVal >> BigInt(j)) & 1n) === 0n) j++
1328
- if (j > 0) {
1329
- bVal >>= BigInt(j)
1330
- for (let k = 0; k < j; ++k) { if ((x2Val & 1n) !== 0n) x2Val += modulus; x2Val >>= 1n }
1331
- }
1332
-
1333
- if (aVal >= bVal) { aVal -= bVal; x1Val -= x2Val } else { bVal -= aVal; x2Val -= x1Val }
1328
+ // Use modular exponentiation via ReductionContext if available
1329
+ if (a.red !== null) {
1330
+ return a.redPow(exp)
1334
1331
  }
1335
1332
 
1336
- let resultVal: bigint
1337
- if (aVal === 1n) resultVal = x1Val
1338
- else if (bVal === 1n) resultVal = x2Val
1339
- else if (aVal === 0n && bVal === 1n) resultVal = x2Val
1340
- else if (bVal === 0n && aVal === 1n) resultVal = x1Val
1341
- else throw new Error('_invmp: GCD is not 1, inverse does not exist. aVal=' + aVal + ', bVal=' + bVal)
1333
+ // Fallback: non-reduction context modular exponentiation
1334
+ let result = new BigNumber(1n)
1335
+ let base = a.clone()
1336
+ const e = exp.clone()
1342
1337
 
1343
- resultVal %= modulus
1344
- if (resultVal < 0n) resultVal += modulus
1338
+ while (!e.isZero()) {
1339
+ if (e.isOdd()) result = result.mul(base).umod(p)
1340
+ base = base.sqr().umod(p)
1341
+ e.iushrn(1)
1342
+ }
1345
1343
 
1346
- const resultBN = new BigNumber(0n)
1347
- resultBN._initializeState(resultVal, 0)
1348
- return resultBN
1344
+ return result
1349
1345
  }
1350
1346
 
1351
1347
  /**