@orangecheck/sdk 1.14.1 → 1.15.0

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,11 +1,52 @@
1
1
  'use strict';
2
2
 
3
3
  var base = require('@scure/base');
4
+ var buffer = require('buffer');
5
+ var bip322 = require('@orangecheck/bip322');
4
6
 
5
7
  // src/canonical.ts
6
8
  function toLowerHex(bytes) {
7
9
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
8
10
  }
11
+ var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
12
+ var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
13
+ function assertSafeIdentity({ protocol, identifier }) {
14
+ if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
15
+ throw new Error(
16
+ `invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
17
+ );
18
+ }
19
+ if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
20
+ throw new Error(
21
+ `invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
22
+ );
23
+ }
24
+ }
25
+ function parseIdentities(identitiesStr) {
26
+ if (!identitiesStr || identitiesStr.trim() === "") {
27
+ return [];
28
+ }
29
+ if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
30
+ throw new Error("identities field contains forbidden characters (newline)");
31
+ }
32
+ const bindings = identitiesStr.split(",").map((binding) => {
33
+ const colonIndex = binding.indexOf(":");
34
+ if (colonIndex === -1) {
35
+ throw new Error(`Invalid identity binding format: ${binding}`);
36
+ }
37
+ return {
38
+ protocol: binding.substring(0, colonIndex).trim(),
39
+ identifier: binding.substring(colonIndex + 1).trim()
40
+ };
41
+ });
42
+ for (const b of bindings) {
43
+ if (!b.protocol || !b.identifier) {
44
+ throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
45
+ }
46
+ assertSafeIdentity(b);
47
+ }
48
+ return bindings;
49
+ }
9
50
  async function generateAttestationId(message) {
10
51
  const encoder = new TextEncoder();
11
52
  const data = encoder.encode(message);
@@ -109,8 +150,173 @@ function createLogger(namespace) {
109
150
  };
110
151
  }
111
152
 
153
+ // src/utils/time.ts
154
+ var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
155
+ function rfc3339Ms(value) {
156
+ if (!value || !RFC3339.test(value)) return null;
157
+ const ms = Date.parse(value);
158
+ return Number.isNaN(ms) ? null : ms;
159
+ }
160
+ var log = createLogger("ocp/verify");
161
+ function looksLikeHex(s) {
162
+ return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
163
+ }
164
+ function toBase64Maybe(sig) {
165
+ if (looksLikeHex(sig)) {
166
+ return buffer.Buffer.from(sig, "hex").toString("base64");
167
+ }
168
+ return sig;
169
+ }
170
+ function detectSignatureScheme(addr, sig) {
171
+ const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
172
+ if (isSegWit) {
173
+ return "bip322";
174
+ }
175
+ const sigBase64 = toBase64Maybe(sig);
176
+ const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
177
+ if (looksLikeLegacy) {
178
+ return "legacy";
179
+ }
180
+ return "bip322";
181
+ }
182
+ async function verifySignature({
183
+ addr,
184
+ msg,
185
+ sig,
186
+ scheme
187
+ }) {
188
+ const detectedScheme = scheme || detectSignatureScheme(addr, sig);
189
+ log.debug(
190
+ { addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
191
+ "Verifying signature"
192
+ );
193
+ if (detectedScheme === "bip322") {
194
+ const sigBase64 = toBase64Maybe(sig);
195
+ log.debug(
196
+ { sigBase64: sigBase64.substring(0, 50) + "..." },
197
+ "Signature converted to base64"
198
+ );
199
+ log.debug(
200
+ {
201
+ msgFull: msg,
202
+ msgLines: msg.split("\n").length,
203
+ msgFirstLine: msg.split("\n")[0],
204
+ msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
205
+ },
206
+ "Message being verified"
207
+ );
208
+ const ok = bip322.verifySignature(addr, msg, sigBase64);
209
+ log.info({ addr, ok }, "BIP-322 verification result");
210
+ if (!ok) {
211
+ const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
212
+ if (isSegWit && sigBase64.length === 88) {
213
+ log.warn(
214
+ { addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
215
+ "BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
216
+ );
217
+ } else {
218
+ log.error(
219
+ {
220
+ addr,
221
+ sigLength: sigBase64.length,
222
+ msgLength: msg.length,
223
+ msgPreview: msg.substring(0, 200)
224
+ },
225
+ "BIP-322 verification failed - signature does not match message"
226
+ );
227
+ }
228
+ }
229
+ return ok ? "sig_ok_bip322" : "sig_invalid";
230
+ }
231
+ if (detectedScheme === "legacy") {
232
+ const sigBase64 = toBase64Maybe(sig);
233
+ if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
234
+ log.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
235
+ return "sig_unsupported_script";
236
+ }
237
+ log.debug(
238
+ {
239
+ addr,
240
+ sigBase64: sigBase64.substring(0, 50) + "...",
241
+ msgPreview: msg.substring(0, 100) + "...",
242
+ msgLength: msg.length
243
+ },
244
+ "Verifying legacy signature"
245
+ );
246
+ const ok = bip322.verifySignature(addr, msg, sigBase64);
247
+ log.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
248
+ if (!ok) {
249
+ log.error(
250
+ {
251
+ addr,
252
+ msgFirstLine: msg.split("\n")[0],
253
+ sigFormat: sigBase64.substring(0, 10)
254
+ },
255
+ "Legacy signature verification failed - check message format and signature"
256
+ );
257
+ }
258
+ return ok ? "sig_ok_legacy" : "sig_invalid";
259
+ }
260
+ return "invalid_scheme";
261
+ }
262
+ async function signatureIsValid(input) {
263
+ try {
264
+ const code = await verifySignature(input);
265
+ return code === "sig_ok_bip322" || code === "sig_ok_legacy";
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ function parseCanonicalMessage(msg) {
271
+ const lines = msg.split("\n").map((l) => l.trim());
272
+ const getVal = (prefix) => {
273
+ const needle = prefix + ":";
274
+ const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
275
+ if (!hit) return void 0;
276
+ const idx = hit.indexOf(":");
277
+ return hit.slice(idx + 1).trim();
278
+ };
279
+ const address = getVal("address") || "";
280
+ const identitiesStr = getVal("identities") || "";
281
+ const issued_at = getVal("issued_at") || "";
282
+ const expires = getVal("expires");
283
+ let identities = [];
284
+ try {
285
+ identities = parseIdentities(identitiesStr);
286
+ } catch (err) {
287
+ log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
288
+ }
289
+ const coreKeys = /* @__PURE__ */ new Set([
290
+ "orangecheck",
291
+ "identities",
292
+ "address",
293
+ "purpose",
294
+ "nonce",
295
+ "issued_at",
296
+ "ack"
297
+ ]);
298
+ const extensions = {};
299
+ for (const line of lines) {
300
+ const idx = line.indexOf(":");
301
+ if (idx === -1) continue;
302
+ const key = line.slice(0, idx).trim().toLowerCase();
303
+ const val = line.slice(idx + 1).trim();
304
+ if (!coreKeys.has(key) && val) extensions[key] = val;
305
+ }
306
+ const network = extensions["network"] || "mainnet";
307
+ const bondStr = extensions["bond"];
308
+ const bond = bondStr ? parseInt(bondStr, 10) : void 0;
309
+ return {
310
+ core: { address, identities, issued_at },
311
+ expires,
312
+ network,
313
+ extensions,
314
+ bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
315
+ };
316
+ }
317
+
112
318
  // src/nostr.ts
113
- var log = createLogger("ocp/nostr");
319
+ var log2 = createLogger("ocp/nostr");
114
320
  var ATTESTATION_EVENT_KIND = 30078;
115
321
  var DEFAULT_RELAYS = [
116
322
  "wss://relay.damus.io",
@@ -163,7 +369,7 @@ function createAttestationEvent(envelope, pubkey) {
163
369
  async function publishToRelays(event, relays = DEFAULT_RELAYS) {
164
370
  const success = [];
165
371
  const failed = [];
166
- log.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
372
+ log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
167
373
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
168
374
  const publishPromises = relays.map(async (relayUrl) => {
169
375
  let ws = null;
@@ -202,7 +408,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
202
408
  cleanup();
203
409
  resolve();
204
410
  } else {
205
- log.warn(
411
+ log2.warn(
206
412
  { relay: relayUrl, reason: data[3] },
207
413
  "Relay rejected event"
208
414
  );
@@ -212,7 +418,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
212
418
  }
213
419
  }
214
420
  } catch (err) {
215
- log.error(
421
+ log2.error(
216
422
  { relay: relayUrl, error: err },
217
423
  "Failed to parse relay response"
218
424
  );
@@ -222,7 +428,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
222
428
  }
223
429
  };
224
430
  wsRef.onerror = (err) => {
225
- log.error({ relay: relayUrl, error: err }, "WebSocket error");
431
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error");
226
432
  recordFailure();
227
433
  cleanup();
228
434
  reject(err);
@@ -232,7 +438,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
232
438
  };
233
439
  });
234
440
  } catch (err) {
235
- log.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
441
+ log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
236
442
  recordFailure();
237
443
  } finally {
238
444
  if (ws && ws.readyState !== WebSocket.CLOSED) {
@@ -241,12 +447,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
241
447
  }
242
448
  });
243
449
  await Promise.allSettled(publishPromises);
244
- log.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
450
+ log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
245
451
  return { success, failed };
246
452
  }
247
453
  async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
248
454
  const events = [];
249
- log.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
455
+ log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
250
456
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
251
457
  const queryPromises = relays.map(async (relayUrl) => {
252
458
  let ws = null;
@@ -286,11 +492,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
286
492
  resolve();
287
493
  }
288
494
  } catch (err) {
289
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
495
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
290
496
  }
291
497
  };
292
498
  wsRef.onerror = (err) => {
293
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
499
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
294
500
  cleanup();
295
501
  reject(err);
296
502
  };
@@ -299,7 +505,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
299
505
  };
300
506
  });
301
507
  } catch (err) {
302
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
508
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
303
509
  } finally {
304
510
  if (ws && ws.readyState !== WebSocket.CLOSED) {
305
511
  ws.close();
@@ -307,12 +513,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
307
513
  }
308
514
  });
309
515
  await Promise.allSettled(queryPromises);
310
- log.info({ eventCount: events.length }, "Query complete");
516
+ log2.info({ eventCount: events.length }, "Query complete");
311
517
  return events;
312
518
  }
313
519
  async function queryByAddress(address, relays = DEFAULT_RELAYS) {
314
520
  const events = [];
315
- log.info({ address, relayCount: relays.length }, "Querying relays for address");
521
+ log2.info({ address, relayCount: relays.length }, "Querying relays for address");
316
522
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
317
523
  const queryPromises = relays.map(async (relayUrl) => {
318
524
  try {
@@ -354,28 +560,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
354
560
  resolve();
355
561
  }
356
562
  } catch (err) {
357
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
563
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
358
564
  }
359
565
  };
360
566
  ws.onerror = (err) => {
361
567
  clearTimeout(timeout);
362
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
568
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
363
569
  reject(err);
364
570
  };
365
571
  });
366
572
  } catch (err) {
367
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
573
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
368
574
  }
369
575
  });
370
576
  await Promise.allSettled(queryPromises);
371
- log.info({ eventCount: events.length }, "Query complete");
577
+ log2.info({ eventCount: events.length }, "Query complete");
372
578
  return events;
373
579
  }
374
580
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
375
581
  const events = [];
376
582
  const identityTag = `${protocol}:${identifier}`;
377
583
  const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
378
- log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
584
+ log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
379
585
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
380
586
  const queryPromises = relays.map(async (relayUrl) => {
381
587
  try {
@@ -410,21 +616,21 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
410
616
  resolve();
411
617
  }
412
618
  } catch (err) {
413
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
619
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
414
620
  }
415
621
  };
416
622
  ws.onerror = (err) => {
417
623
  clearTimeout(timeout);
418
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
624
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
419
625
  reject(err);
420
626
  };
421
627
  });
422
628
  } catch (err) {
423
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
629
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
424
630
  }
425
631
  });
426
632
  await Promise.allSettled(queryPromises);
427
- log.info({ eventCount: events.length }, "Query complete");
633
+ log2.info({ eventCount: events.length }, "Query complete");
428
634
  return events;
429
635
  }
430
636
  async function verifyAttestationEnvelope(envelope) {
@@ -436,22 +642,74 @@ async function verifyAttestationEnvelope(envelope) {
436
642
  const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
437
643
  return computedId === envelope.attestation_id;
438
644
  } catch (err) {
439
- log.error({ error: err }, "Failed to verify attestation envelope");
645
+ log2.error({ error: err }, "Failed to verify attestation envelope");
440
646
  return false;
441
647
  }
442
648
  }
649
+ function base64Url(text) {
650
+ const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
651
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
652
+ }
653
+ function signedEnvelope(envelope) {
654
+ let parsed;
655
+ try {
656
+ parsed = parseCanonicalMessage(envelope.message);
657
+ } catch {
658
+ return null;
659
+ }
660
+ if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
661
+ const { expires_at: _copy, ...rest } = envelope;
662
+ return {
663
+ ...rest,
664
+ address: parsed.core.address,
665
+ identities: parsed.core.identities,
666
+ issued_at: parsed.core.issued_at,
667
+ ...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
668
+ message_b64url: base64Url(envelope.message)
669
+ };
670
+ }
671
+ async function collectSignedEnvelopes(events, accept = () => true) {
672
+ const copies = /* @__PURE__ */ new Map();
673
+ for (const event of events) {
674
+ const raw = parseAttestationFromEvent(event);
675
+ if (!raw || !await verifyAttestationEnvelope(raw)) continue;
676
+ const envelope = signedEnvelope(raw);
677
+ if (!envelope || !accept(event, envelope)) continue;
678
+ const same = copies.get(envelope.attestation_id) ?? [];
679
+ if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
680
+ same.push(envelope);
681
+ }
682
+ copies.set(envelope.attestation_id, same);
683
+ }
684
+ const kept = [];
685
+ for (const same of copies.values()) {
686
+ if (same.length === 1) {
687
+ kept.push(same[0]);
688
+ continue;
689
+ }
690
+ let valid;
691
+ for (const e of same) {
692
+ if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
693
+ valid = e;
694
+ break;
695
+ }
696
+ }
697
+ kept.push(valid ?? same[0]);
698
+ }
699
+ return kept;
700
+ }
443
701
  function parseAttestationFromEvent(event) {
444
702
  try {
445
703
  const envelope = JSON.parse(event.content);
446
704
  return envelope;
447
705
  } catch (err) {
448
- log.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
706
+ log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
449
707
  return null;
450
708
  }
451
709
  }
452
710
 
453
711
  // src/nostr-crypto.ts
454
- var log2 = createLogger("ocp/nostr-crypto");
712
+ var log3 = createLogger("ocp/nostr-crypto");
455
713
  function isNip07Available() {
456
714
  if (typeof window === "undefined") {
457
715
  return false;
@@ -460,22 +718,22 @@ function isNip07Available() {
460
718
  }
461
719
  async function signNostrEvent(event) {
462
720
  if (!isNip07Available()) {
463
- log2.warn("NIP-07 extension not available");
721
+ log3.warn("NIP-07 extension not available");
464
722
  return null;
465
723
  }
466
724
  try {
467
725
  const nostr = window.nostr;
468
726
  const signedEvent = await nostr.signEvent(event);
469
- log2.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
727
+ log3.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
470
728
  return signedEvent;
471
729
  } catch (err) {
472
- log2.error({ error: err }, "Failed to sign Nostr event");
730
+ log3.error({ error: err }, "Failed to sign Nostr event");
473
731
  return null;
474
732
  }
475
733
  }
476
734
 
477
735
  // src/attestation.ts
478
- var log3 = createLogger("ocp/attestation");
736
+ var log4 = createLogger("ocp/attestation");
479
737
  var AttestationError = class extends Error {
480
738
  constructor(message, code) {
481
739
  super(message);
@@ -485,7 +743,7 @@ var AttestationError = class extends Error {
485
743
  };
486
744
  async function createAttestation(options) {
487
745
  const { message, signature, scheme, address, identities = [], extensions = {} } = options;
488
- log3.info({ address, identityCount: identities.length }, "Creating attestation");
746
+ log4.info({ address, identityCount: identities.length }, "Creating attestation");
489
747
  const envelope = await createAttestationEnvelope(
490
748
  message,
491
749
  signature,
@@ -501,29 +759,29 @@ async function createAttestation(options) {
501
759
  "INTEGRITY_CHECK_FAILED"
502
760
  );
503
761
  }
504
- log3.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
762
+ log4.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
505
763
  return envelope;
506
764
  }
507
765
  async function publishAttestation(options) {
508
766
  const { envelope, npub, relays = DEFAULT_RELAYS } = options;
509
- log3.info(
767
+ log4.info(
510
768
  { attestationId: envelope.attestation_id, relayCount: relays.length },
511
769
  "Publishing attestation"
512
770
  );
513
771
  const unsignedEvent = createAttestationEvent(envelope, npub);
514
772
  if (!isNip07Available()) {
515
- log3.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
773
+ log4.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
516
774
  throw new Error(
517
775
  "nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish"
518
776
  );
519
777
  }
520
- log3.info("Using NIP-07 to sign event");
778
+ log4.info("Using NIP-07 to sign event");
521
779
  const signedEvent = await signNostrEvent(unsignedEvent);
522
780
  if (!signedEvent) {
523
781
  throw new Error("nostr_signing_failed: NIP-07 extension rejected or errored");
524
782
  }
525
783
  const result = await publishToRelays(signedEvent, relays);
526
- log3.info(
784
+ log4.info(
527
785
  {
528
786
  attestationId: envelope.attestation_id,
529
787
  successCount: result.success.length,
@@ -543,49 +801,34 @@ async function discoverAttestations(options) {
543
801
  const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
544
802
  let events = [];
545
803
  if (attestationId) {
546
- log3.info({ attestationId }, "Discovering by attestation ID");
804
+ log4.info({ attestationId }, "Discovering by attestation ID");
547
805
  events = await queryByAttestationId(attestationId, relays);
548
806
  } else if (address) {
549
- log3.info({ address }, "Discovering by Bitcoin address");
807
+ log4.info({ address }, "Discovering by Bitcoin address");
550
808
  events = await queryByAddress(address, relays);
551
809
  } else if (identity) {
552
- log3.info({ identity }, "Discovering by identity");
810
+ log4.info({ identity }, "Discovering by identity");
553
811
  events = await queryByIdentity(identity.protocol, identity.identifier, relays);
554
812
  } else {
555
813
  throw new Error("Must provide attestationId, address, or identity");
556
814
  }
557
- const envelopes = [];
558
- const kept = /* @__PURE__ */ new Set();
559
- for (const event of events) {
560
- const envelope = parseAttestationFromEvent(event);
561
- if (envelope && kept.has(envelope.attestation_id)) continue;
562
- if (envelope && !eventIndexesEnvelope(event, envelope)) {
563
- log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
564
- } else if (envelope) {
565
- const isValid = await verifyAttestationEnvelope(envelope);
566
- if (isValid) {
567
- kept.add(envelope.attestation_id);
568
- envelopes.push(envelope);
569
- } else {
570
- log3.warn(
571
- { attestationId: envelope.attestation_id },
572
- "Skipping invalid attestation envelope"
573
- );
574
- }
575
- }
576
- }
577
- log3.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
815
+ const envelopes = await collectSignedEnvelopes(events, (event, envelope) => {
816
+ if (eventIndexesEnvelope(event, envelope)) return true;
817
+ log4.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
818
+ return false;
819
+ });
820
+ log4.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
578
821
  return envelopes;
579
822
  }
580
823
  async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
581
- log3.info({ attestationId }, "Verifying attestation by ID");
824
+ log4.info({ attestationId }, "Verifying attestation by ID");
582
825
  const envelopes = await discoverAttestations({ attestationId, relays });
583
826
  if (envelopes.length === 0) {
584
- log3.warn({ attestationId }, "Attestation not found on any relay");
827
+ log4.warn({ attestationId }, "Attestation not found on any relay");
585
828
  return null;
586
829
  }
587
830
  if (envelopes.length > 1) {
588
- log3.warn(
831
+ log4.warn(
589
832
  { attestationId, count: envelopes.length },
590
833
  "Multiple attestations found with same ID - using first"
591
834
  );
@@ -593,11 +836,11 @@ async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
593
836
  return envelopes[0] || null;
594
837
  }
595
838
  async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
596
- log3.info({ address }, "Getting all attestations for address");
839
+ log4.info({ address }, "Getting all attestations for address");
597
840
  return discoverAttestations({ address, relays });
598
841
  }
599
842
  async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
600
- log3.info({ protocol, identifier }, "Getting all attestations for identity");
843
+ log4.info({ protocol, identifier }, "Getting all attestations for identity");
601
844
  return discoverAttestations({ identity: { protocol, identifier }, relays });
602
845
  }
603
846
  function formatIdentitiesForDisplay(identities) {