@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.
package/dist/nostr.mjs CHANGED
@@ -1,4 +1,6 @@
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/nostr-pubkey.ts
4
6
  var HEX_64_RE = /^[0-9a-f]{64}$/;
@@ -71,8 +73,216 @@ function createLogger(namespace) {
71
73
  };
72
74
  }
73
75
 
76
+ // src/utils/time.ts
77
+ var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
78
+ function rfc3339Ms(value) {
79
+ if (!value || !RFC3339.test(value)) return null;
80
+ const ms = Date.parse(value);
81
+ return Number.isNaN(ms) ? null : ms;
82
+ }
83
+
84
+ // src/canonical.ts
85
+ var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
86
+ var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
87
+ function assertSafeIdentity({ protocol, identifier }) {
88
+ if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
89
+ throw new Error(
90
+ `invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
91
+ );
92
+ }
93
+ if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
94
+ throw new Error(
95
+ `invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
96
+ );
97
+ }
98
+ }
99
+ function parseIdentities(identitiesStr) {
100
+ if (!identitiesStr || identitiesStr.trim() === "") {
101
+ return [];
102
+ }
103
+ if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
104
+ throw new Error("identities field contains forbidden characters (newline)");
105
+ }
106
+ const bindings = identitiesStr.split(",").map((binding) => {
107
+ const colonIndex = binding.indexOf(":");
108
+ if (colonIndex === -1) {
109
+ throw new Error(`Invalid identity binding format: ${binding}`);
110
+ }
111
+ return {
112
+ protocol: binding.substring(0, colonIndex).trim(),
113
+ identifier: binding.substring(colonIndex + 1).trim()
114
+ };
115
+ });
116
+ for (const b of bindings) {
117
+ if (!b.protocol || !b.identifier) {
118
+ throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
119
+ }
120
+ assertSafeIdentity(b);
121
+ }
122
+ return bindings;
123
+ }
124
+
125
+ // src/verify.ts
126
+ var log = createLogger("ocp/verify");
127
+ function looksLikeHex(s) {
128
+ return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
129
+ }
130
+ function toBase64Maybe(sig) {
131
+ if (looksLikeHex(sig)) {
132
+ return Buffer.from(sig, "hex").toString("base64");
133
+ }
134
+ return sig;
135
+ }
136
+ function detectSignatureScheme(addr, sig) {
137
+ const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
138
+ if (isSegWit) {
139
+ return "bip322";
140
+ }
141
+ const sigBase64 = toBase64Maybe(sig);
142
+ const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
143
+ if (looksLikeLegacy) {
144
+ return "legacy";
145
+ }
146
+ return "bip322";
147
+ }
148
+ async function verifySignature({
149
+ addr,
150
+ msg,
151
+ sig,
152
+ scheme
153
+ }) {
154
+ const detectedScheme = scheme || detectSignatureScheme(addr, sig);
155
+ log.debug(
156
+ { addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
157
+ "Verifying signature"
158
+ );
159
+ if (detectedScheme === "bip322") {
160
+ const sigBase64 = toBase64Maybe(sig);
161
+ log.debug(
162
+ { sigBase64: sigBase64.substring(0, 50) + "..." },
163
+ "Signature converted to base64"
164
+ );
165
+ log.debug(
166
+ {
167
+ msgFull: msg,
168
+ msgLines: msg.split("\n").length,
169
+ msgFirstLine: msg.split("\n")[0],
170
+ msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
171
+ },
172
+ "Message being verified"
173
+ );
174
+ const ok = verifySignature$1(addr, msg, sigBase64);
175
+ log.info({ addr, ok }, "BIP-322 verification result");
176
+ if (!ok) {
177
+ const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
178
+ if (isSegWit && sigBase64.length === 88) {
179
+ log.warn(
180
+ { addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
181
+ "BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
182
+ );
183
+ } else {
184
+ log.error(
185
+ {
186
+ addr,
187
+ sigLength: sigBase64.length,
188
+ msgLength: msg.length,
189
+ msgPreview: msg.substring(0, 200)
190
+ },
191
+ "BIP-322 verification failed - signature does not match message"
192
+ );
193
+ }
194
+ }
195
+ return ok ? "sig_ok_bip322" : "sig_invalid";
196
+ }
197
+ if (detectedScheme === "legacy") {
198
+ const sigBase64 = toBase64Maybe(sig);
199
+ if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
200
+ log.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
201
+ return "sig_unsupported_script";
202
+ }
203
+ log.debug(
204
+ {
205
+ addr,
206
+ sigBase64: sigBase64.substring(0, 50) + "...",
207
+ msgPreview: msg.substring(0, 100) + "...",
208
+ msgLength: msg.length
209
+ },
210
+ "Verifying legacy signature"
211
+ );
212
+ const ok = verifySignature$1(addr, msg, sigBase64);
213
+ log.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
214
+ if (!ok) {
215
+ log.error(
216
+ {
217
+ addr,
218
+ msgFirstLine: msg.split("\n")[0],
219
+ sigFormat: sigBase64.substring(0, 10)
220
+ },
221
+ "Legacy signature verification failed - check message format and signature"
222
+ );
223
+ }
224
+ return ok ? "sig_ok_legacy" : "sig_invalid";
225
+ }
226
+ return "invalid_scheme";
227
+ }
228
+ async function signatureIsValid(input) {
229
+ try {
230
+ const code = await verifySignature(input);
231
+ return code === "sig_ok_bip322" || code === "sig_ok_legacy";
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+ function parseCanonicalMessage(msg) {
237
+ const lines = msg.split("\n").map((l) => l.trim());
238
+ const getVal = (prefix) => {
239
+ const needle = prefix + ":";
240
+ const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
241
+ if (!hit) return void 0;
242
+ const idx = hit.indexOf(":");
243
+ return hit.slice(idx + 1).trim();
244
+ };
245
+ const address = getVal("address") || "";
246
+ const identitiesStr = getVal("identities") || "";
247
+ const issued_at = getVal("issued_at") || "";
248
+ const expires = getVal("expires");
249
+ let identities = [];
250
+ try {
251
+ identities = parseIdentities(identitiesStr);
252
+ } catch (err) {
253
+ log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
254
+ }
255
+ const coreKeys = /* @__PURE__ */ new Set([
256
+ "orangecheck",
257
+ "identities",
258
+ "address",
259
+ "purpose",
260
+ "nonce",
261
+ "issued_at",
262
+ "ack"
263
+ ]);
264
+ const extensions = {};
265
+ for (const line of lines) {
266
+ const idx = line.indexOf(":");
267
+ if (idx === -1) continue;
268
+ const key = line.slice(0, idx).trim().toLowerCase();
269
+ const val = line.slice(idx + 1).trim();
270
+ if (!coreKeys.has(key) && val) extensions[key] = val;
271
+ }
272
+ const network = extensions["network"] || "mainnet";
273
+ const bondStr = extensions["bond"];
274
+ const bond = bondStr ? parseInt(bondStr, 10) : void 0;
275
+ return {
276
+ core: { address, identities, issued_at },
277
+ expires,
278
+ network,
279
+ extensions,
280
+ bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
281
+ };
282
+ }
283
+
74
284
  // src/nostr.ts
75
- var log = createLogger("ocp/nostr");
285
+ var log2 = createLogger("ocp/nostr");
76
286
  var ATTESTATION_EVENT_KIND = 30078;
77
287
  var DEFAULT_RELAYS = [
78
288
  "wss://relay.damus.io",
@@ -148,7 +358,7 @@ function getAddressFromEvent(event) {
148
358
  async function publishToRelays(event, relays = DEFAULT_RELAYS) {
149
359
  const success = [];
150
360
  const failed = [];
151
- log.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
361
+ log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
152
362
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
153
363
  const publishPromises = relays.map(async (relayUrl) => {
154
364
  let ws = null;
@@ -187,7 +397,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
187
397
  cleanup();
188
398
  resolve();
189
399
  } else {
190
- log.warn(
400
+ log2.warn(
191
401
  { relay: relayUrl, reason: data[3] },
192
402
  "Relay rejected event"
193
403
  );
@@ -197,7 +407,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
197
407
  }
198
408
  }
199
409
  } catch (err) {
200
- log.error(
410
+ log2.error(
201
411
  { relay: relayUrl, error: err },
202
412
  "Failed to parse relay response"
203
413
  );
@@ -207,7 +417,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
207
417
  }
208
418
  };
209
419
  wsRef.onerror = (err) => {
210
- log.error({ relay: relayUrl, error: err }, "WebSocket error");
420
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error");
211
421
  recordFailure();
212
422
  cleanup();
213
423
  reject(err);
@@ -217,7 +427,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
217
427
  };
218
428
  });
219
429
  } catch (err) {
220
- log.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
430
+ log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
221
431
  recordFailure();
222
432
  } finally {
223
433
  if (ws && ws.readyState !== WebSocket.CLOSED) {
@@ -226,12 +436,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
226
436
  }
227
437
  });
228
438
  await Promise.allSettled(publishPromises);
229
- log.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
439
+ log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
230
440
  return { success, failed };
231
441
  }
232
442
  async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
233
443
  const events = [];
234
- log.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
444
+ log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
235
445
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
236
446
  const queryPromises = relays.map(async (relayUrl) => {
237
447
  let ws = null;
@@ -271,11 +481,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
271
481
  resolve();
272
482
  }
273
483
  } catch (err) {
274
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
484
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
275
485
  }
276
486
  };
277
487
  wsRef.onerror = (err) => {
278
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
488
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
279
489
  cleanup();
280
490
  reject(err);
281
491
  };
@@ -284,7 +494,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
284
494
  };
285
495
  });
286
496
  } catch (err) {
287
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
497
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
288
498
  } finally {
289
499
  if (ws && ws.readyState !== WebSocket.CLOSED) {
290
500
  ws.close();
@@ -292,12 +502,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
292
502
  }
293
503
  });
294
504
  await Promise.allSettled(queryPromises);
295
- log.info({ eventCount: events.length }, "Query complete");
505
+ log2.info({ eventCount: events.length }, "Query complete");
296
506
  return events;
297
507
  }
298
508
  async function queryByAddress(address, relays = DEFAULT_RELAYS) {
299
509
  const events = [];
300
- log.info({ address, relayCount: relays.length }, "Querying relays for address");
510
+ log2.info({ address, relayCount: relays.length }, "Querying relays for address");
301
511
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
302
512
  const queryPromises = relays.map(async (relayUrl) => {
303
513
  try {
@@ -339,28 +549,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
339
549
  resolve();
340
550
  }
341
551
  } catch (err) {
342
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
552
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
343
553
  }
344
554
  };
345
555
  ws.onerror = (err) => {
346
556
  clearTimeout(timeout);
347
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
557
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
348
558
  reject(err);
349
559
  };
350
560
  });
351
561
  } catch (err) {
352
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
562
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
353
563
  }
354
564
  });
355
565
  await Promise.allSettled(queryPromises);
356
- log.info({ eventCount: events.length }, "Query complete");
566
+ log2.info({ eventCount: events.length }, "Query complete");
357
567
  return events;
358
568
  }
359
569
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
360
570
  const events = [];
361
571
  const identityTag = `${protocol}:${identifier}`;
362
572
  const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
363
- log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
573
+ log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
364
574
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
365
575
  const queryPromises = relays.map(async (relayUrl) => {
366
576
  try {
@@ -395,26 +605,26 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
395
605
  resolve();
396
606
  }
397
607
  } catch (err) {
398
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
608
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
399
609
  }
400
610
  };
401
611
  ws.onerror = (err) => {
402
612
  clearTimeout(timeout);
403
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
613
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
404
614
  reject(err);
405
615
  };
406
616
  });
407
617
  } catch (err) {
408
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
618
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
409
619
  }
410
620
  });
411
621
  await Promise.allSettled(queryPromises);
412
- log.info({ eventCount: events.length }, "Query complete");
622
+ log2.info({ eventCount: events.length }, "Query complete");
413
623
  return events;
414
624
  }
415
625
  async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
416
626
  const events = [];
417
- log.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
627
+ log2.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
418
628
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
419
629
  const queryPromises = relays.map(async (relayUrl) => {
420
630
  try {
@@ -450,30 +660,22 @@ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
450
660
  resolve();
451
661
  }
452
662
  } catch (err) {
453
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
663
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
454
664
  }
455
665
  };
456
666
  ws.onerror = (err) => {
457
667
  clearTimeout(timeout);
458
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
668
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
459
669
  reject(err);
460
670
  };
461
671
  });
462
672
  } catch (err) {
463
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
673
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
464
674
  }
465
675
  });
466
676
  await Promise.allSettled(queryPromises);
467
- const seen = /* @__PURE__ */ new Set();
468
- const envelopes = [];
469
- for (const ev of events) {
470
- const envelope = parseAttestationFromEvent(ev);
471
- if (!envelope || seen.has(envelope.attestation_id)) continue;
472
- if (!await verifyAttestationEnvelope(envelope)) continue;
473
- seen.add(envelope.attestation_id);
474
- envelopes.push(envelope);
475
- }
476
- log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
677
+ const envelopes = await collectSignedEnvelopes(events);
678
+ log2.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
477
679
  return envelopes;
478
680
  }
479
681
  async function verifyAttestationEnvelope(envelope) {
@@ -485,20 +687,72 @@ async function verifyAttestationEnvelope(envelope) {
485
687
  const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
486
688
  return computedId === envelope.attestation_id;
487
689
  } catch (err) {
488
- log.error({ error: err }, "Failed to verify attestation envelope");
690
+ log2.error({ error: err }, "Failed to verify attestation envelope");
489
691
  return false;
490
692
  }
491
693
  }
694
+ function base64Url(text) {
695
+ const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
696
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
697
+ }
698
+ function signedEnvelope(envelope) {
699
+ let parsed;
700
+ try {
701
+ parsed = parseCanonicalMessage(envelope.message);
702
+ } catch {
703
+ return null;
704
+ }
705
+ if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
706
+ const { expires_at: _copy, ...rest } = envelope;
707
+ return {
708
+ ...rest,
709
+ address: parsed.core.address,
710
+ identities: parsed.core.identities,
711
+ issued_at: parsed.core.issued_at,
712
+ ...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
713
+ message_b64url: base64Url(envelope.message)
714
+ };
715
+ }
716
+ async function collectSignedEnvelopes(events, accept = () => true) {
717
+ const copies = /* @__PURE__ */ new Map();
718
+ for (const event of events) {
719
+ const raw = parseAttestationFromEvent(event);
720
+ if (!raw || !await verifyAttestationEnvelope(raw)) continue;
721
+ const envelope = signedEnvelope(raw);
722
+ if (!envelope || !accept(event, envelope)) continue;
723
+ const same = copies.get(envelope.attestation_id) ?? [];
724
+ if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
725
+ same.push(envelope);
726
+ }
727
+ copies.set(envelope.attestation_id, same);
728
+ }
729
+ const kept = [];
730
+ for (const same of copies.values()) {
731
+ if (same.length === 1) {
732
+ kept.push(same[0]);
733
+ continue;
734
+ }
735
+ let valid;
736
+ for (const e of same) {
737
+ if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
738
+ valid = e;
739
+ break;
740
+ }
741
+ }
742
+ kept.push(valid ?? same[0]);
743
+ }
744
+ return kept;
745
+ }
492
746
  function parseAttestationFromEvent(event) {
493
747
  try {
494
748
  const envelope = JSON.parse(event.content);
495
749
  return envelope;
496
750
  } catch (err) {
497
- log.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
751
+ log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
498
752
  return null;
499
753
  }
500
754
  }
501
755
 
502
- export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, verifyAttestationEnvelope };
756
+ export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, collectSignedEnvelopes, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope, verifyAttestationEnvelope };
503
757
  //# sourceMappingURL=nostr.mjs.map
504
758
  //# sourceMappingURL=nostr.mjs.map