@bsv/sdk 2.1.1 → 2.1.3
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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/Peer.js +21 -18
- package/dist/cjs/src/auth/Peer.js.map +1 -1
- package/dist/cjs/src/auth/SessionManager.js.map +1 -1
- package/dist/cjs/src/auth/clients/AuthFetch.js +4 -1
- package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -1
- package/dist/cjs/src/identity/ContactsManager.js +44 -6
- package/dist/cjs/src/identity/ContactsManager.js.map +1 -1
- package/dist/cjs/src/identity/IdentityClient.js +106 -37
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/src/overlay-tools/LookupResolver.js +180 -82
- package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/cjs/src/primitives/Hash.js +173 -50
- package/dist/cjs/src/primitives/Hash.js.map +1 -1
- package/dist/cjs/src/primitives/SymmetricKey.js +123 -1
- package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/Peer.js +28 -18
- package/dist/esm/src/auth/Peer.js.map +1 -1
- package/dist/esm/src/auth/SessionManager.js.map +1 -1
- package/dist/esm/src/auth/clients/AuthFetch.js +4 -1
- package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -1
- package/dist/esm/src/identity/ContactsManager.js +44 -6
- package/dist/esm/src/identity/ContactsManager.js.map +1 -1
- package/dist/esm/src/identity/IdentityClient.js +106 -37
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/src/overlay-tools/LookupResolver.js +180 -82
- package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/esm/src/primitives/Hash.js +177 -50
- package/dist/esm/src/primitives/Hash.js.map +1 -1
- package/dist/esm/src/primitives/SymmetricKey.js +123 -1
- package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/Peer.d.ts +3 -3
- package/dist/types/src/auth/Peer.d.ts.map +1 -1
- package/dist/types/src/auth/SessionManager.d.ts +21 -0
- package/dist/types/src/auth/SessionManager.d.ts.map +1 -1
- package/dist/types/src/auth/clients/AuthFetch.d.ts +2 -2
- package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -1
- package/dist/types/src/identity/ContactsManager.d.ts +13 -2
- package/dist/types/src/identity/ContactsManager.d.ts.map +1 -1
- package/dist/types/src/identity/IdentityClient.d.ts +50 -24
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/src/overlay-tools/LookupResolver.d.ts +15 -1
- package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
- package/dist/types/src/primitives/Hash.d.ts +21 -16
- package/dist/types/src/primitives/Hash.d.ts.map +1 -1
- package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
- package/dist/types/src/wallet/Wallet.interfaces.d.ts +16 -1
- package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
- package/dist/types/src/wallet/WalletClient.d.ts +1 -1
- package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/window.CWI.d.ts +1 -1
- package/dist/types/src/wallet/substrates/window.CWI.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +4 -4
- package/package.json +1 -1
- package/src/auth/Peer.ts +30 -20
- package/src/auth/SessionManager.ts +22 -0
- package/src/auth/__tests/Peer.test.ts +47 -1
- package/src/auth/clients/AuthFetch.ts +6 -3
- package/src/identity/ContactsManager.ts +47 -6
- package/src/identity/IdentityClient.ts +137 -53
- package/src/identity/__tests/IdentityClient.additional.test.ts +150 -1
- package/src/identity/__tests/IdentityClient.test.ts +4 -4
- package/src/overlay-tools/LookupResolver.ts +191 -77
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +90 -0
- package/src/primitives/Hash.ts +232 -96
- package/src/primitives/SymmetricKey.ts +145 -1
- package/src/primitives/__tests/Hash.additional.test.ts +65 -0
- package/src/primitives/__tests/Hash.test.ts +6 -1
- package/src/script/__tests/Spend.test.ts +45 -4
- package/src/transaction/MerklePath.ts +1 -1
- package/src/transaction/__tests/Transaction.test.ts +17 -0
- package/src/wallet/Wallet.interfaces.ts +16 -1
- package/src/wallet/WalletClient.ts +1 -1
- package/src/wallet/substrates/window.CWI.ts +1 -1
|
@@ -18,30 +18,74 @@ import { PrivateKey, Utils } from '../primitives/index.js'
|
|
|
18
18
|
import { LookupResolver, SHIPBroadcaster, TopicBroadcaster, withDoubleSpendRetry } from '../overlay-tools/index.js'
|
|
19
19
|
import { ContactsManager, Contact } from './ContactsManager.js'
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of identity certificates to parse synchronously before yielding to the
|
|
23
|
+
* event loop. Keeps the main thread responsive when an overlay query returns many results
|
|
24
|
+
* (e.g. a bulk enrichment of N identityKeys).
|
|
25
|
+
*/
|
|
26
|
+
const PARSE_BATCH_SIZE = 32
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Yield control to the event loop so queued microtasks / timers can run. Uses
|
|
30
|
+
* `scheduler.yield()` when available (Chromium) or a 0ms macrotask fallback.
|
|
31
|
+
*/
|
|
32
|
+
async function yieldToEventLoop (): Promise<void> {
|
|
33
|
+
const sched = (globalThis as any).scheduler
|
|
34
|
+
if (sched != null && typeof sched.yield === 'function') {
|
|
35
|
+
return await sched.yield()
|
|
36
|
+
}
|
|
37
|
+
return await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
/** Options for {@link IdentityClient.resolveByIdentityKey}. */
|
|
22
41
|
export interface ResolveByIdentityKeyOptions {
|
|
23
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Opt-in to consulting personal contacts before/alongside the overlay. Default `false`.
|
|
44
|
+
*
|
|
45
|
+
* Most callers (including any client without a populated contacts basket) pay no benefit
|
|
46
|
+
* from the contacts path and incur its setup cost. Set `true` only in UI contexts where
|
|
47
|
+
* the user has likely saved contacts and a local cache hit is preferable to a fresh overlay
|
|
48
|
+
* answer.
|
|
49
|
+
*/
|
|
50
|
+
useContacts?: boolean
|
|
51
|
+
/**
|
|
52
|
+
* Legacy alias for {@link useContacts}. When provided, takes precedence over the new flag.
|
|
53
|
+
* Kept for binary compatibility — new code should use `useContacts`.
|
|
54
|
+
*/
|
|
24
55
|
overrideWithContacts?: boolean
|
|
25
56
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* answer alongside
|
|
57
|
+
* When `true` (and {@link useContacts} is also true), fire contacts and overlay in parallel
|
|
58
|
+
* rather than short-circuiting on a contacts hit. Use only when callers specifically need a
|
|
59
|
+
* fresh overlay answer alongside any cached contact record.
|
|
29
60
|
*/
|
|
30
61
|
parallel?: boolean
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
/** Options for {@link IdentityClient.resolveByAttributes}. */
|
|
34
65
|
export interface ResolveByAttributesOptions {
|
|
35
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Opt-in to consulting personal contacts before/alongside the overlay. Default `false`.
|
|
68
|
+
* See {@link ResolveByIdentityKeyOptions.useContacts}.
|
|
69
|
+
*/
|
|
70
|
+
useContacts?: boolean
|
|
71
|
+
/** Legacy alias for {@link useContacts}. Takes precedence when provided. */
|
|
36
72
|
overrideWithContacts?: boolean
|
|
37
73
|
/**
|
|
38
|
-
*
|
|
39
|
-
* contacts-first path tries to match the supplied attributes against the locally-stored
|
|
40
|
-
* contacts; if any contact matches every attribute, the overlay is skipped.
|
|
74
|
+
* When `true` (and {@link useContacts} is also true), fire contacts and overlay in parallel.
|
|
41
75
|
*/
|
|
42
76
|
parallel?: boolean
|
|
43
77
|
}
|
|
44
78
|
|
|
79
|
+
/** Normalize either legacy boolean / new options object into a canonical { useContacts, parallel }. */
|
|
80
|
+
function normalizeOpts (
|
|
81
|
+
raw: boolean | ResolveByIdentityKeyOptions | ResolveByAttributesOptions | undefined
|
|
82
|
+
): { useContacts: boolean, parallel: boolean } {
|
|
83
|
+
if (raw === undefined) return { useContacts: false, parallel: false }
|
|
84
|
+
if (typeof raw === 'boolean') return { useContacts: raw, parallel: false }
|
|
85
|
+
const useContacts = raw.overrideWithContacts ?? raw.useContacts ?? false
|
|
86
|
+
return { useContacts, parallel: raw.parallel === true }
|
|
87
|
+
}
|
|
88
|
+
|
|
45
89
|
/**
|
|
46
90
|
* IdentityClient lets you discover who others are, and let the world know who you are.
|
|
47
91
|
*/
|
|
@@ -153,99 +197,100 @@ export class IdentityClient {
|
|
|
153
197
|
}
|
|
154
198
|
|
|
155
199
|
/**
|
|
156
|
-
* Resolves displayable identity certificates
|
|
200
|
+
* Resolves displayable identity certificates issued to a given identity key.
|
|
201
|
+
*
|
|
202
|
+
* **Default behavior (changed): contacts are NOT consulted.** Most clients have no
|
|
203
|
+
* contacts saved locally, so the previous "contacts-first" default paid setup cost for no
|
|
204
|
+
* gain. Pass `{ useContacts: true }` to opt in — appropriate when you know the user has
|
|
205
|
+
* saved contacts and prefers a local hit over a fresh overlay answer.
|
|
157
206
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
207
|
+
* When `useContacts: true`:
|
|
208
|
+
* - Default short-circuits: if a contact matches, the overlay is skipped entirely.
|
|
209
|
+
* - `{ parallel: true }` fires contacts and overlay in parallel; contact wins on hit.
|
|
161
210
|
*
|
|
162
211
|
* @param args - Arguments for requesting the discovery based on the identity key.
|
|
163
|
-
* @param
|
|
164
|
-
* contacts are skipped entirely and the overlay is queried directly. Boolean kept for legacy
|
|
165
|
-
* call sites; new code should prefer the options-object form.
|
|
166
|
-
* @returns The promise resolves to displayable identities.
|
|
212
|
+
* @param opts - Boolean (legacy) or options object. Boolean `true` ≡ `{ useContacts: true }`.
|
|
167
213
|
*/
|
|
168
214
|
async resolveByIdentityKey (
|
|
169
215
|
args: DiscoverByIdentityKeyArgs,
|
|
170
|
-
|
|
216
|
+
opts: boolean | ResolveByIdentityKeyOptions = false
|
|
171
217
|
): Promise<DisplayableIdentity[]> {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
218
|
+
const { useContacts, parallel } = normalizeOpts(opts)
|
|
219
|
+
|
|
220
|
+
// Fast path: skip contacts entirely. Default — straight overlay query,
|
|
221
|
+
// no listOutputs / decrypt / cache churn.
|
|
222
|
+
if (!useContacts) {
|
|
223
|
+
const certificatesResult = await this.wallet.discoverByIdentityKey(args, this.originator)
|
|
224
|
+
const certs = certificatesResult?.certificates ?? []
|
|
225
|
+
return await IdentityClient.parseIdentities(certs)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!parallel) {
|
|
180
229
|
const contacts = await this.contactsManager.getContacts(args.identityKey)
|
|
181
230
|
if (contacts.length > 0) return contacts
|
|
182
231
|
|
|
183
232
|
const certificatesResult = await this.wallet.discoverByIdentityKey(args, this.originator)
|
|
184
233
|
const certs = certificatesResult?.certificates ?? []
|
|
185
|
-
return
|
|
234
|
+
return await IdentityClient.parseIdentities(certs)
|
|
186
235
|
}
|
|
187
236
|
|
|
188
237
|
const [contacts, certificatesResult] = await Promise.all([
|
|
189
|
-
|
|
190
|
-
? this.contactsManager.getContacts(args.identityKey)
|
|
191
|
-
: Promise.resolve([] as Contact[]),
|
|
238
|
+
this.contactsManager.getContacts(args.identityKey),
|
|
192
239
|
this.wallet.discoverByIdentityKey(args, this.originator)
|
|
193
240
|
])
|
|
194
241
|
|
|
195
242
|
if (contacts.length > 0) return contacts
|
|
196
243
|
const certs = certificatesResult?.certificates ?? []
|
|
197
|
-
return
|
|
244
|
+
return await IdentityClient.parseIdentities(certs)
|
|
198
245
|
}
|
|
199
246
|
|
|
200
247
|
/**
|
|
201
|
-
* Resolves displayable identity certificates by specific identity attributes
|
|
248
|
+
* Resolves displayable identity certificates by specific identity attributes.
|
|
202
249
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
250
|
+
* **Default behavior (changed): contacts are NOT consulted.** See
|
|
251
|
+
* {@link resolveByIdentityKey} for the reasoning. Pass `{ useContacts: true }` to opt in.
|
|
205
252
|
*
|
|
206
253
|
* @param args - Attributes and optional parameters used to discover certificates.
|
|
207
|
-
* @param
|
|
208
|
-
* for legacy call sites; new code should prefer the options-object form.
|
|
209
|
-
* @returns The promise resolves to displayable identities.
|
|
254
|
+
* @param opts - Boolean (legacy) or options object. Boolean `true` ≡ `{ useContacts: true }`.
|
|
210
255
|
*/
|
|
211
256
|
async resolveByAttributes (
|
|
212
257
|
args: DiscoverByAttributesArgs,
|
|
213
|
-
|
|
258
|
+
opts: boolean | ResolveByAttributesOptions = false
|
|
214
259
|
): Promise<DisplayableIdentity[]> {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
const { useContacts, parallel } = normalizeOpts(opts)
|
|
261
|
+
|
|
262
|
+
// Fast path: skip contacts entirely.
|
|
263
|
+
if (!useContacts) {
|
|
264
|
+
const certificatesResult = await this.wallet.discoverByAttributes(args, this.originator)
|
|
265
|
+
const certs = certificatesResult?.certificates ?? []
|
|
266
|
+
return await IdentityClient.parseIdentities(certs)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!parallel) {
|
|
223
270
|
const contacts = await this.contactsManager.getContacts()
|
|
224
271
|
const matches = this.matchContactsByAttributes(contacts, args)
|
|
225
272
|
if (matches.length > 0) return matches
|
|
226
273
|
|
|
227
274
|
const certificatesResult = await this.wallet.discoverByAttributes(args, this.originator)
|
|
228
275
|
const certs = certificatesResult?.certificates ?? []
|
|
276
|
+
if (contacts.length === 0) return await IdentityClient.parseIdentities(certs)
|
|
229
277
|
const contactByKey = new Map<PubKeyHex, Contact>(
|
|
230
278
|
contacts.map((contact) => [contact.identityKey, contact] as const)
|
|
231
279
|
)
|
|
232
|
-
return
|
|
233
|
-
(cert) => contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert)
|
|
234
|
-
)
|
|
280
|
+
return await IdentityClient.parseIdentitiesWithOverrides(certs, contactByKey)
|
|
235
281
|
}
|
|
236
282
|
|
|
237
283
|
const [contacts, certificatesResult] = await Promise.all([
|
|
238
|
-
|
|
284
|
+
this.contactsManager.getContacts(),
|
|
239
285
|
this.wallet.discoverByAttributes(args, this.originator)
|
|
240
286
|
])
|
|
241
287
|
|
|
288
|
+
const certs = certificatesResult?.certificates ?? []
|
|
289
|
+
if (contacts.length === 0) return await IdentityClient.parseIdentities(certs)
|
|
242
290
|
const contactByKey = new Map<PubKeyHex, Contact>(
|
|
243
291
|
contacts.map((contact) => [contact.identityKey, contact] as const)
|
|
244
292
|
)
|
|
245
|
-
|
|
246
|
-
return certs.map(
|
|
247
|
-
(cert) => contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert)
|
|
248
|
-
)
|
|
293
|
+
return await IdentityClient.parseIdentitiesWithOverrides(certs, contactByKey)
|
|
249
294
|
}
|
|
250
295
|
|
|
251
296
|
/**
|
|
@@ -410,6 +455,45 @@ export class IdentityClient {
|
|
|
410
455
|
return await this.contactsManager.removeContact(identityKey)
|
|
411
456
|
}
|
|
412
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Parse an array of certificates into DisplayableIdentity records, yielding to the
|
|
460
|
+
* event loop every {@link PARSE_BATCH_SIZE} entries so large result sets don't hog
|
|
461
|
+
* the main thread. Equivalent to `certs.map(parseIdentity)` for small inputs.
|
|
462
|
+
*/
|
|
463
|
+
static async parseIdentities (certs: IdentityCertificate[]): Promise<DisplayableIdentity[]> {
|
|
464
|
+
const n = certs.length
|
|
465
|
+
if (n <= PARSE_BATCH_SIZE) {
|
|
466
|
+
return certs.map((c) => IdentityClient.parseIdentity(c))
|
|
467
|
+
}
|
|
468
|
+
const out: DisplayableIdentity[] = new Array(n)
|
|
469
|
+
for (let i = 0; i < n; i++) {
|
|
470
|
+
out[i] = IdentityClient.parseIdentity(certs[i])
|
|
471
|
+
if ((i + 1) % PARSE_BATCH_SIZE === 0) await yieldToEventLoop()
|
|
472
|
+
}
|
|
473
|
+
return out
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Same as {@link parseIdentities} but consults a contact override map keyed by subject
|
|
478
|
+
* identity key. Used by `resolveByAttributes` when contacts are loaded.
|
|
479
|
+
*/
|
|
480
|
+
static async parseIdentitiesWithOverrides (
|
|
481
|
+
certs: IdentityCertificate[],
|
|
482
|
+
contactByKey: Map<PubKeyHex, Contact>
|
|
483
|
+
): Promise<DisplayableIdentity[]> {
|
|
484
|
+
const n = certs.length
|
|
485
|
+
if (n <= PARSE_BATCH_SIZE) {
|
|
486
|
+
return certs.map((cert) => contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert))
|
|
487
|
+
}
|
|
488
|
+
const out: DisplayableIdentity[] = new Array(n)
|
|
489
|
+
for (let i = 0; i < n; i++) {
|
|
490
|
+
const cert = certs[i]
|
|
491
|
+
out[i] = contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert)
|
|
492
|
+
if ((i + 1) % PARSE_BATCH_SIZE === 0) await yieldToEventLoop()
|
|
493
|
+
}
|
|
494
|
+
return out
|
|
495
|
+
}
|
|
496
|
+
|
|
413
497
|
/**
|
|
414
498
|
* Parse out identity and certifier attributes to display from an IdentityCertificate
|
|
415
499
|
* @param identityToParse - The Identity Certificate to parse
|
|
@@ -571,7 +571,7 @@ describe('IdentityClient (additional coverage)', () => {
|
|
|
571
571
|
mockContactsManager.getContacts = jest.fn().mockResolvedValue([contact])
|
|
572
572
|
walletMock.discoverByAttributes = jest.fn().mockResolvedValue({ certificates: [discoveredCertificate] })
|
|
573
573
|
|
|
574
|
-
const result = await identityClient.resolveByAttributes({ attributes: { email: 'alice@example.com' } })
|
|
574
|
+
const result = await identityClient.resolveByAttributes({ attributes: { email: 'alice@example.com' } }, { useContacts: true })
|
|
575
575
|
expect(result[0].name).toBe('Alice From Contact')
|
|
576
576
|
})
|
|
577
577
|
|
|
@@ -764,4 +764,153 @@ describe('IdentityClient (additional coverage)', () => {
|
|
|
764
764
|
expect(mockContactsManager.removeContact).toHaveBeenCalledWith('key-to-remove')
|
|
765
765
|
})
|
|
766
766
|
})
|
|
767
|
+
|
|
768
|
+
// ─── useContacts branches in resolveByIdentityKey / resolveByAttributes ─────
|
|
769
|
+
|
|
770
|
+
// Shared helpers — extracted to keep new tests DRY (avoid Sonar duplication gate).
|
|
771
|
+
const xCert = (subject: string, userName: string): any => ({
|
|
772
|
+
type: KNOWN_IDENTITY_TYPES.xCert,
|
|
773
|
+
subject,
|
|
774
|
+
decryptedFields: { userName, profilePhoto: '' },
|
|
775
|
+
certifierInfo: { name: 'CX', iconUrl: '' }
|
|
776
|
+
})
|
|
777
|
+
const emailCertOf = (subject: string, email: string): any => ({
|
|
778
|
+
type: KNOWN_IDENTITY_TYPES.emailCert,
|
|
779
|
+
subject,
|
|
780
|
+
decryptedFields: { email },
|
|
781
|
+
certifierInfo: { name: 'EC', iconUrl: '' }
|
|
782
|
+
})
|
|
783
|
+
const contactOf = (name: string, identityKey: string): any => ({
|
|
784
|
+
name, identityKey, avatarURL: '', abbreviatedKey: '', badgeIconURL: '', badgeLabel: '', badgeClickURL: ''
|
|
785
|
+
})
|
|
786
|
+
const stubDiscoveryByKey = (contacts: any[], certificates: any[]): void => {
|
|
787
|
+
identityClient['contactsManager'].getContacts = jest.fn().mockResolvedValue(contacts)
|
|
788
|
+
walletMock.discoverByIdentityKey = jest.fn().mockResolvedValue({ certificates })
|
|
789
|
+
}
|
|
790
|
+
const stubDiscoveryByAttr = (contacts: any[], certificates: any[]): void => {
|
|
791
|
+
identityClient['contactsManager'].getContacts = jest.fn().mockResolvedValue(contacts)
|
|
792
|
+
walletMock.discoverByAttributes = jest.fn().mockResolvedValue({ certificates })
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
describe('resolveByIdentityKey with useContacts opt-in', () => {
|
|
796
|
+
it('contacts miss falls through to overlay (sequential)', async () => {
|
|
797
|
+
stubDiscoveryByKey([], [xCert('k1', 'XUser')])
|
|
798
|
+
const result = await identityClient.resolveByIdentityKey({ identityKey: 'k1' }, { useContacts: true })
|
|
799
|
+
expect(walletMock.discoverByIdentityKey).toHaveBeenCalled()
|
|
800
|
+
expect(result[0].name).toBe('XUser')
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('parallel mode returns contact on hit even though overlay runs', async () => {
|
|
804
|
+
const contact = contactOf('Cached Alice', 'k2')
|
|
805
|
+
stubDiscoveryByKey([contact], [])
|
|
806
|
+
const result = await identityClient.resolveByIdentityKey({ identityKey: 'k2' }, { useContacts: true, parallel: true })
|
|
807
|
+
expect(walletMock.discoverByIdentityKey).toHaveBeenCalled()
|
|
808
|
+
expect(result).toEqual([contact])
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('parallel mode contacts miss returns parsed overlay results', async () => {
|
|
812
|
+
stubDiscoveryByKey([], [xCert('k3', 'XOnly')])
|
|
813
|
+
const result = await identityClient.resolveByIdentityKey({ identityKey: 'k3' }, { useContacts: true, parallel: true })
|
|
814
|
+
expect(result[0].name).toBe('XOnly')
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('legacy boolean opt-in (true) consults contacts', async () => {
|
|
818
|
+
stubDiscoveryByKey([contactOf('Legacy True', 'k4')], [])
|
|
819
|
+
const result = await identityClient.resolveByIdentityKey({ identityKey: 'k4' }, true)
|
|
820
|
+
expect(result[0].name).toBe('Legacy True')
|
|
821
|
+
expect(walletMock.discoverByIdentityKey).not.toHaveBeenCalled()
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
it('overrideWithContacts legacy alias takes precedence over useContacts', async () => {
|
|
825
|
+
stubDiscoveryByKey([contactOf('Override Wins', 'k5')], [])
|
|
826
|
+
const result = await identityClient.resolveByIdentityKey(
|
|
827
|
+
{ identityKey: 'k5' },
|
|
828
|
+
{ useContacts: false, overrideWithContacts: true }
|
|
829
|
+
)
|
|
830
|
+
expect(result[0].name).toBe('Override Wins')
|
|
831
|
+
})
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
describe('resolveByAttributes with useContacts opt-in', () => {
|
|
835
|
+
it('contacts no-match falls through to overlay with contact overrides applied', async () => {
|
|
836
|
+
stubDiscoveryByAttr([contactOf('Override Alice', 'k-over')], [emailCertOf('k-over', 'alice@example.com')])
|
|
837
|
+
const result = await identityClient.resolveByAttributes(
|
|
838
|
+
{ attributes: { email: 'alice@example.com' } },
|
|
839
|
+
{ useContacts: true }
|
|
840
|
+
)
|
|
841
|
+
expect(result[0].name).toBe('Override Alice')
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
it('contacts empty + overlay miss returns empty', async () => {
|
|
845
|
+
stubDiscoveryByAttr([], [])
|
|
846
|
+
const result = await identityClient.resolveByAttributes(
|
|
847
|
+
{ attributes: { email: 'nobody@example.com' } },
|
|
848
|
+
{ useContacts: true }
|
|
849
|
+
)
|
|
850
|
+
expect(result).toEqual([])
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('parallel mode with no contacts parses overlay only', async () => {
|
|
854
|
+
stubDiscoveryByAttr([], [emailCertOf('no-contact-key', 'lone@example.com')])
|
|
855
|
+
const result = await identityClient.resolveByAttributes(
|
|
856
|
+
{ attributes: { email: 'lone@example.com' } },
|
|
857
|
+
{ useContacts: true, parallel: true }
|
|
858
|
+
)
|
|
859
|
+
expect(result[0].name).toBe('lone@example.com')
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
it('parallel mode with contacts applies overrides on overlay results', async () => {
|
|
863
|
+
stubDiscoveryByAttr([contactOf('Parallel Contact', 'pk')], [emailCertOf('pk', 'p@example.com')])
|
|
864
|
+
const result = await identityClient.resolveByAttributes(
|
|
865
|
+
{ attributes: { email: 'p@example.com' } },
|
|
866
|
+
{ useContacts: true, parallel: true }
|
|
867
|
+
)
|
|
868
|
+
expect(result[0].name).toBe('Parallel Contact')
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('matchContactsByAttributes ignores non-string attribute values', async () => {
|
|
872
|
+
stubDiscoveryByAttr([contactOf('X', 'kkkk')], [])
|
|
873
|
+
const result = await identityClient.resolveByAttributes(
|
|
874
|
+
{ attributes: { count: 5 as unknown as string } },
|
|
875
|
+
{ useContacts: true }
|
|
876
|
+
)
|
|
877
|
+
// No string-valued attrs → matchContactsByAttributes returns [] → overlay path
|
|
878
|
+
expect(walletMock.discoverByAttributes).toHaveBeenCalled()
|
|
879
|
+
expect(result).toEqual([])
|
|
880
|
+
})
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
describe('parseIdentities batched path', () => {
|
|
884
|
+
it('yields to event loop when batch > PARSE_BATCH_SIZE', async () => {
|
|
885
|
+
const certs = Array.from({ length: 64 }, (_, i) => xCert(`subject-${i}`, `user-${i}`))
|
|
886
|
+
const result = await IdentityClient.parseIdentities(certs)
|
|
887
|
+
expect(result).toHaveLength(64)
|
|
888
|
+
expect(result[63].name).toBe('user-63')
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
it('parseIdentitiesWithOverrides batches with overrides applied', async () => {
|
|
892
|
+
const certs = Array.from({ length: 50 }, (_, i) => xCert(`subject-${i}`, `user-${i}`))
|
|
893
|
+
const overrideMap = new Map<string, any>([
|
|
894
|
+
['subject-5', contactOf('Override 5', 'subject-5')],
|
|
895
|
+
['subject-40', contactOf('Override 40', 'subject-40')]
|
|
896
|
+
])
|
|
897
|
+
const result = await IdentityClient.parseIdentitiesWithOverrides(certs, overrideMap)
|
|
898
|
+
expect(result[5].name).toBe('Override 5')
|
|
899
|
+
expect(result[40].name).toBe('Override 40')
|
|
900
|
+
expect(result[6].name).toBe('user-6')
|
|
901
|
+
})
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
describe('yieldToEventLoop scheduler.yield path', () => {
|
|
905
|
+
const origScheduler = (globalThis as any).scheduler
|
|
906
|
+
afterEach(() => { (globalThis as any).scheduler = origScheduler })
|
|
907
|
+
|
|
908
|
+
it('uses scheduler.yield when available', async () => {
|
|
909
|
+
const yieldFn = jest.fn().mockResolvedValue(undefined)
|
|
910
|
+
;(globalThis as any).scheduler = { yield: yieldFn }
|
|
911
|
+
const certs = Array.from({ length: 64 }, (_, i) => xCert(`s-${i}`, `u-${i}`))
|
|
912
|
+
await IdentityClient.parseIdentities(certs)
|
|
913
|
+
expect(yieldFn).toHaveBeenCalled()
|
|
914
|
+
})
|
|
915
|
+
})
|
|
767
916
|
})
|
|
@@ -260,11 +260,11 @@ describe('IdentityClient', () => {
|
|
|
260
260
|
mockContactsManager.getContacts = jest.fn().mockResolvedValue([contact])
|
|
261
261
|
walletMock.discoverByIdentityKey = jest.fn().mockResolvedValue({ certificates: [discoveredCertificate] })
|
|
262
262
|
|
|
263
|
-
const identities = await identityClient.resolveByIdentityKey({ identityKey: 'alice-identity-key' })
|
|
263
|
+
const identities = await identityClient.resolveByIdentityKey({ identityKey: 'alice-identity-key' }, { useContacts: true })
|
|
264
264
|
|
|
265
265
|
expect(identities).toHaveLength(1)
|
|
266
266
|
expect(identities[0].name).toBe('Alice Smith (Personal Contact)') // Contact should be returned, not discovered identity
|
|
267
|
-
//
|
|
267
|
+
// With useContacts opt-in, contacts hit short-circuits the overlay query entirely.
|
|
268
268
|
expect(walletMock.discoverByIdentityKey).not.toHaveBeenCalled()
|
|
269
269
|
})
|
|
270
270
|
|
|
@@ -284,7 +284,7 @@ describe('IdentityClient', () => {
|
|
|
284
284
|
|
|
285
285
|
const identities = await identityClient.resolveByIdentityKey(
|
|
286
286
|
{ identityKey: 'alice-identity-key' },
|
|
287
|
-
{ parallel: true }
|
|
287
|
+
{ useContacts: true, parallel: true }
|
|
288
288
|
)
|
|
289
289
|
|
|
290
290
|
expect(identities).toHaveLength(1)
|
|
@@ -380,7 +380,7 @@ describe('IdentityClient', () => {
|
|
|
380
380
|
mockContactsManager.getContacts = jest.fn().mockResolvedValue([contact])
|
|
381
381
|
walletMock.discoverByAttributes = jest.fn().mockResolvedValue({ certificates: [discoveredCertificate] })
|
|
382
382
|
|
|
383
|
-
const identities = await identityClient.resolveByAttributes({ attributes: { name: 'Alice' } })
|
|
383
|
+
const identities = await identityClient.resolveByAttributes({ attributes: { name: 'Alice' } }, { useContacts: true })
|
|
384
384
|
|
|
385
385
|
expect(identities).toHaveLength(1)
|
|
386
386
|
expect(identities[0].name).toBe('Alice Smith (Personal)') // Contact should be returned, not discovered identity
|