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