@orangecheck/sdk 1.14.1 → 1.16.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 { verifySignature as verifySignature$1 } from '@orangecheck/bip322';
3
+ import { Buffer } from 'buffer';
2
4
 
3
5
  // src/nostr-pubkey.ts
4
6
  var HEX_64_RE = /^[0-9a-f]{64}$/;
@@ -71,8 +73,135 @@ 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
+ function toBase64Maybe(sig) {
125
+ if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {
126
+ return Buffer.from(sig, "hex").toString("base64");
127
+ }
128
+ return sig;
129
+ }
130
+ function signatureScheme(sigBase64) {
131
+ if (/^(ful|pof)/.test(sigBase64)) return null;
132
+ if (sigBase64.startsWith("smp")) return "bip322";
133
+ return Buffer.from(sigBase64, "base64").length === 65 ? "legacy" : "bip322";
134
+ }
135
+
136
+ // src/verify.ts
137
+ var log = createLogger("ocp/verify");
138
+ async function verifySignature({ addr, msg, sig }) {
139
+ const sigBase64 = toBase64Maybe(sig);
140
+ const scheme = signatureScheme(sigBase64);
141
+ if (!scheme) return "sig_unsupported_script";
142
+ const ok = verifySignature$1(addr, msg, sigBase64);
143
+ log.info({ addr, ok, scheme, msgLength: msg.length }, "Signature verification result");
144
+ if (!ok) return "sig_invalid";
145
+ return scheme === "legacy" ? "sig_ok_legacy" : "sig_ok_bip322";
146
+ }
147
+ async function signatureIsValid(input) {
148
+ try {
149
+ const code = await verifySignature(input);
150
+ return code === "sig_ok_bip322" || code === "sig_ok_legacy";
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+ function parseCanonicalMessage(msg) {
156
+ const lines = msg.split("\n").map((l) => l.trim());
157
+ const getVal = (prefix) => {
158
+ const needle = prefix + ":";
159
+ const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
160
+ if (!hit) return void 0;
161
+ const idx = hit.indexOf(":");
162
+ return hit.slice(idx + 1).trim();
163
+ };
164
+ const address = getVal("address") || "";
165
+ const identitiesStr = getVal("identities") || "";
166
+ const issued_at = getVal("issued_at") || "";
167
+ const expires = getVal("expires");
168
+ let identities = [];
169
+ try {
170
+ identities = parseIdentities(identitiesStr);
171
+ } catch (err) {
172
+ log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
173
+ }
174
+ const coreKeys = /* @__PURE__ */ new Set([
175
+ "orangecheck",
176
+ "identities",
177
+ "address",
178
+ "purpose",
179
+ "nonce",
180
+ "issued_at",
181
+ "ack"
182
+ ]);
183
+ const extensions = {};
184
+ for (const line of lines) {
185
+ const idx = line.indexOf(":");
186
+ if (idx === -1) continue;
187
+ const key = line.slice(0, idx).trim().toLowerCase();
188
+ const val = line.slice(idx + 1).trim();
189
+ if (!coreKeys.has(key) && val) extensions[key] = val;
190
+ }
191
+ const network = extensions["network"] || "mainnet";
192
+ const bondStr = extensions["bond"];
193
+ const bond = bondStr ? parseInt(bondStr, 10) : void 0;
194
+ return {
195
+ core: { address, identities, issued_at },
196
+ expires,
197
+ network,
198
+ extensions,
199
+ bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
200
+ };
201
+ }
202
+
74
203
  // src/nostr.ts
75
- var log = createLogger("ocp/nostr");
204
+ var log2 = createLogger("ocp/nostr");
76
205
  var ATTESTATION_EVENT_KIND = 30078;
77
206
  var DEFAULT_RELAYS = [
78
207
  "wss://relay.damus.io",
@@ -148,7 +277,7 @@ function getAddressFromEvent(event) {
148
277
  async function publishToRelays(event, relays = DEFAULT_RELAYS) {
149
278
  const success = [];
150
279
  const failed = [];
151
- log.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
280
+ log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
152
281
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
153
282
  const publishPromises = relays.map(async (relayUrl) => {
154
283
  let ws = null;
@@ -187,7 +316,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
187
316
  cleanup();
188
317
  resolve();
189
318
  } else {
190
- log.warn(
319
+ log2.warn(
191
320
  { relay: relayUrl, reason: data[3] },
192
321
  "Relay rejected event"
193
322
  );
@@ -197,7 +326,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
197
326
  }
198
327
  }
199
328
  } catch (err) {
200
- log.error(
329
+ log2.error(
201
330
  { relay: relayUrl, error: err },
202
331
  "Failed to parse relay response"
203
332
  );
@@ -207,7 +336,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
207
336
  }
208
337
  };
209
338
  wsRef.onerror = (err) => {
210
- log.error({ relay: relayUrl, error: err }, "WebSocket error");
339
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error");
211
340
  recordFailure();
212
341
  cleanup();
213
342
  reject(err);
@@ -217,7 +346,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
217
346
  };
218
347
  });
219
348
  } catch (err) {
220
- log.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
349
+ log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
221
350
  recordFailure();
222
351
  } finally {
223
352
  if (ws && ws.readyState !== WebSocket.CLOSED) {
@@ -226,12 +355,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
226
355
  }
227
356
  });
228
357
  await Promise.allSettled(publishPromises);
229
- log.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
358
+ log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
230
359
  return { success, failed };
231
360
  }
232
361
  async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
233
362
  const events = [];
234
- log.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
363
+ log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
235
364
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
236
365
  const queryPromises = relays.map(async (relayUrl) => {
237
366
  let ws = null;
@@ -271,11 +400,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
271
400
  resolve();
272
401
  }
273
402
  } catch (err) {
274
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
403
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
275
404
  }
276
405
  };
277
406
  wsRef.onerror = (err) => {
278
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
407
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
279
408
  cleanup();
280
409
  reject(err);
281
410
  };
@@ -284,7 +413,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
284
413
  };
285
414
  });
286
415
  } catch (err) {
287
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
416
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
288
417
  } finally {
289
418
  if (ws && ws.readyState !== WebSocket.CLOSED) {
290
419
  ws.close();
@@ -292,12 +421,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
292
421
  }
293
422
  });
294
423
  await Promise.allSettled(queryPromises);
295
- log.info({ eventCount: events.length }, "Query complete");
424
+ log2.info({ eventCount: events.length }, "Query complete");
296
425
  return events;
297
426
  }
298
427
  async function queryByAddress(address, relays = DEFAULT_RELAYS) {
299
428
  const events = [];
300
- log.info({ address, relayCount: relays.length }, "Querying relays for address");
429
+ log2.info({ address, relayCount: relays.length }, "Querying relays for address");
301
430
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
302
431
  const queryPromises = relays.map(async (relayUrl) => {
303
432
  try {
@@ -339,28 +468,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
339
468
  resolve();
340
469
  }
341
470
  } catch (err) {
342
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
471
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
343
472
  }
344
473
  };
345
474
  ws.onerror = (err) => {
346
475
  clearTimeout(timeout);
347
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
476
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
348
477
  reject(err);
349
478
  };
350
479
  });
351
480
  } catch (err) {
352
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
481
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
353
482
  }
354
483
  });
355
484
  await Promise.allSettled(queryPromises);
356
- log.info({ eventCount: events.length }, "Query complete");
485
+ log2.info({ eventCount: events.length }, "Query complete");
357
486
  return events;
358
487
  }
359
488
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
360
489
  const events = [];
361
490
  const identityTag = `${protocol}:${identifier}`;
362
491
  const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
363
- log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
492
+ log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
364
493
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
365
494
  const queryPromises = relays.map(async (relayUrl) => {
366
495
  try {
@@ -395,26 +524,26 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
395
524
  resolve();
396
525
  }
397
526
  } catch (err) {
398
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
527
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
399
528
  }
400
529
  };
401
530
  ws.onerror = (err) => {
402
531
  clearTimeout(timeout);
403
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
532
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
404
533
  reject(err);
405
534
  };
406
535
  });
407
536
  } catch (err) {
408
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
537
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
409
538
  }
410
539
  });
411
540
  await Promise.allSettled(queryPromises);
412
- log.info({ eventCount: events.length }, "Query complete");
541
+ log2.info({ eventCount: events.length }, "Query complete");
413
542
  return events;
414
543
  }
415
544
  async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
416
545
  const events = [];
417
- log.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
546
+ log2.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
418
547
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
419
548
  const queryPromises = relays.map(async (relayUrl) => {
420
549
  try {
@@ -450,30 +579,22 @@ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
450
579
  resolve();
451
580
  }
452
581
  } catch (err) {
453
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
582
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
454
583
  }
455
584
  };
456
585
  ws.onerror = (err) => {
457
586
  clearTimeout(timeout);
458
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
587
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
459
588
  reject(err);
460
589
  };
461
590
  });
462
591
  } catch (err) {
463
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
592
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
464
593
  }
465
594
  });
466
595
  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");
596
+ const envelopes = await collectSignedEnvelopes(events);
597
+ log2.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
477
598
  return envelopes;
478
599
  }
479
600
  async function verifyAttestationEnvelope(envelope) {
@@ -485,20 +606,72 @@ async function verifyAttestationEnvelope(envelope) {
485
606
  const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
486
607
  return computedId === envelope.attestation_id;
487
608
  } catch (err) {
488
- log.error({ error: err }, "Failed to verify attestation envelope");
609
+ log2.error({ error: err }, "Failed to verify attestation envelope");
489
610
  return false;
490
611
  }
491
612
  }
613
+ function base64Url(text) {
614
+ const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
615
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
616
+ }
617
+ function signedEnvelope(envelope) {
618
+ let parsed;
619
+ try {
620
+ parsed = parseCanonicalMessage(envelope.message);
621
+ } catch {
622
+ return null;
623
+ }
624
+ if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
625
+ const { expires_at: _copy, ...rest } = envelope;
626
+ return {
627
+ ...rest,
628
+ address: parsed.core.address,
629
+ identities: parsed.core.identities,
630
+ issued_at: parsed.core.issued_at,
631
+ ...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
632
+ message_b64url: base64Url(envelope.message)
633
+ };
634
+ }
635
+ async function collectSignedEnvelopes(events, accept = () => true) {
636
+ const copies = /* @__PURE__ */ new Map();
637
+ for (const event of events) {
638
+ const raw = parseAttestationFromEvent(event);
639
+ if (!raw || !await verifyAttestationEnvelope(raw)) continue;
640
+ const envelope = signedEnvelope(raw);
641
+ if (!envelope || !accept(event, envelope)) continue;
642
+ const same = copies.get(envelope.attestation_id) ?? [];
643
+ if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
644
+ same.push(envelope);
645
+ }
646
+ copies.set(envelope.attestation_id, same);
647
+ }
648
+ const kept = [];
649
+ for (const same of copies.values()) {
650
+ if (same.length === 1) {
651
+ kept.push(same[0]);
652
+ continue;
653
+ }
654
+ let valid;
655
+ for (const e of same) {
656
+ if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
657
+ valid = e;
658
+ break;
659
+ }
660
+ }
661
+ kept.push(valid ?? same[0]);
662
+ }
663
+ return kept;
664
+ }
492
665
  function parseAttestationFromEvent(event) {
493
666
  try {
494
667
  const envelope = JSON.parse(event.content);
495
668
  return envelope;
496
669
  } catch (err) {
497
- log.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
670
+ log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
498
671
  return null;
499
672
  }
500
673
  }
501
674
 
502
- export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, verifyAttestationEnvelope };
675
+ export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, collectSignedEnvelopes, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope, verifyAttestationEnvelope };
503
676
  //# sourceMappingURL=nostr.mjs.map
504
677
  //# sourceMappingURL=nostr.mjs.map