@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.d.mts CHANGED
@@ -16,6 +16,8 @@ declare function queryByAddress(address: string, relays?: string[]): Promise<Nos
16
16
  declare function queryByIdentity(protocol: string, identifier: string, relays?: string[]): Promise<NostrEvent[]>;
17
17
  declare function queryRecent(relays?: string[], limit?: number): Promise<AttestationEnvelope[]>;
18
18
  declare function verifyAttestationEnvelope(envelope: AttestationEnvelope): Promise<boolean>;
19
+ declare function signedEnvelope(envelope: AttestationEnvelope): AttestationEnvelope | null;
20
+ declare function collectSignedEnvelopes(events: NostrEvent[], accept?: (event: NostrEvent, envelope: AttestationEnvelope) => boolean): Promise<AttestationEnvelope[]>;
19
21
  declare function parseAttestationFromEvent(event: NostrEvent): AttestationEnvelope | null;
20
22
 
21
- export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, verifyAttestationEnvelope };
23
+ export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, collectSignedEnvelopes, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope, verifyAttestationEnvelope };
package/dist/nostr.d.ts CHANGED
@@ -16,6 +16,8 @@ declare function queryByAddress(address: string, relays?: string[]): Promise<Nos
16
16
  declare function queryByIdentity(protocol: string, identifier: string, relays?: string[]): Promise<NostrEvent[]>;
17
17
  declare function queryRecent(relays?: string[], limit?: number): Promise<AttestationEnvelope[]>;
18
18
  declare function verifyAttestationEnvelope(envelope: AttestationEnvelope): Promise<boolean>;
19
+ declare function signedEnvelope(envelope: AttestationEnvelope): AttestationEnvelope | null;
20
+ declare function collectSignedEnvelopes(events: NostrEvent[], accept?: (event: NostrEvent, envelope: AttestationEnvelope) => boolean): Promise<AttestationEnvelope[]>;
19
21
  declare function parseAttestationFromEvent(event: NostrEvent): AttestationEnvelope | null;
20
22
 
21
- export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, verifyAttestationEnvelope };
23
+ export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, collectSignedEnvelopes, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope, verifyAttestationEnvelope };
package/dist/nostr.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var base = require('@scure/base');
4
+ var bip322 = require('@orangecheck/bip322');
5
+ var buffer = require('buffer');
4
6
 
5
7
  // src/nostr-pubkey.ts
6
8
  var HEX_64_RE = /^[0-9a-f]{64}$/;
@@ -73,8 +75,135 @@ function createLogger(namespace) {
73
75
  };
74
76
  }
75
77
 
78
+ // src/utils/time.ts
79
+ var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
80
+ function rfc3339Ms(value) {
81
+ if (!value || !RFC3339.test(value)) return null;
82
+ const ms = Date.parse(value);
83
+ return Number.isNaN(ms) ? null : ms;
84
+ }
85
+
86
+ // src/canonical.ts
87
+ var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
88
+ var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
89
+ function assertSafeIdentity({ protocol, identifier }) {
90
+ if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
91
+ throw new Error(
92
+ `invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
93
+ );
94
+ }
95
+ if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
96
+ throw new Error(
97
+ `invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
98
+ );
99
+ }
100
+ }
101
+ function parseIdentities(identitiesStr) {
102
+ if (!identitiesStr || identitiesStr.trim() === "") {
103
+ return [];
104
+ }
105
+ if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
106
+ throw new Error("identities field contains forbidden characters (newline)");
107
+ }
108
+ const bindings = identitiesStr.split(",").map((binding) => {
109
+ const colonIndex = binding.indexOf(":");
110
+ if (colonIndex === -1) {
111
+ throw new Error(`Invalid identity binding format: ${binding}`);
112
+ }
113
+ return {
114
+ protocol: binding.substring(0, colonIndex).trim(),
115
+ identifier: binding.substring(colonIndex + 1).trim()
116
+ };
117
+ });
118
+ for (const b of bindings) {
119
+ if (!b.protocol || !b.identifier) {
120
+ throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
121
+ }
122
+ assertSafeIdentity(b);
123
+ }
124
+ return bindings;
125
+ }
126
+ function toBase64Maybe(sig) {
127
+ if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {
128
+ return buffer.Buffer.from(sig, "hex").toString("base64");
129
+ }
130
+ return sig;
131
+ }
132
+ function signatureScheme(sigBase64) {
133
+ if (/^(ful|pof)/.test(sigBase64)) return null;
134
+ if (sigBase64.startsWith("smp")) return "bip322";
135
+ return buffer.Buffer.from(sigBase64, "base64").length === 65 ? "legacy" : "bip322";
136
+ }
137
+
138
+ // src/verify.ts
139
+ var log = createLogger("ocp/verify");
140
+ async function verifySignature({ addr, msg, sig }) {
141
+ const sigBase64 = toBase64Maybe(sig);
142
+ const scheme = signatureScheme(sigBase64);
143
+ if (!scheme) return "sig_unsupported_script";
144
+ const ok = bip322.verifySignature(addr, msg, sigBase64);
145
+ log.info({ addr, ok, scheme, msgLength: msg.length }, "Signature verification result");
146
+ if (!ok) return "sig_invalid";
147
+ return scheme === "legacy" ? "sig_ok_legacy" : "sig_ok_bip322";
148
+ }
149
+ async function signatureIsValid(input) {
150
+ try {
151
+ const code = await verifySignature(input);
152
+ return code === "sig_ok_bip322" || code === "sig_ok_legacy";
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ function parseCanonicalMessage(msg) {
158
+ const lines = msg.split("\n").map((l) => l.trim());
159
+ const getVal = (prefix) => {
160
+ const needle = prefix + ":";
161
+ const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
162
+ if (!hit) return void 0;
163
+ const idx = hit.indexOf(":");
164
+ return hit.slice(idx + 1).trim();
165
+ };
166
+ const address = getVal("address") || "";
167
+ const identitiesStr = getVal("identities") || "";
168
+ const issued_at = getVal("issued_at") || "";
169
+ const expires = getVal("expires");
170
+ let identities = [];
171
+ try {
172
+ identities = parseIdentities(identitiesStr);
173
+ } catch (err) {
174
+ log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
175
+ }
176
+ const coreKeys = /* @__PURE__ */ new Set([
177
+ "orangecheck",
178
+ "identities",
179
+ "address",
180
+ "purpose",
181
+ "nonce",
182
+ "issued_at",
183
+ "ack"
184
+ ]);
185
+ const extensions = {};
186
+ for (const line of lines) {
187
+ const idx = line.indexOf(":");
188
+ if (idx === -1) continue;
189
+ const key = line.slice(0, idx).trim().toLowerCase();
190
+ const val = line.slice(idx + 1).trim();
191
+ if (!coreKeys.has(key) && val) extensions[key] = val;
192
+ }
193
+ const network = extensions["network"] || "mainnet";
194
+ const bondStr = extensions["bond"];
195
+ const bond = bondStr ? parseInt(bondStr, 10) : void 0;
196
+ return {
197
+ core: { address, identities, issued_at },
198
+ expires,
199
+ network,
200
+ extensions,
201
+ bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
202
+ };
203
+ }
204
+
76
205
  // src/nostr.ts
77
- var log = createLogger("ocp/nostr");
206
+ var log2 = createLogger("ocp/nostr");
78
207
  var ATTESTATION_EVENT_KIND = 30078;
79
208
  var DEFAULT_RELAYS = [
80
209
  "wss://relay.damus.io",
@@ -150,7 +279,7 @@ function getAddressFromEvent(event) {
150
279
  async function publishToRelays(event, relays = DEFAULT_RELAYS) {
151
280
  const success = [];
152
281
  const failed = [];
153
- log.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
282
+ log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
154
283
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
155
284
  const publishPromises = relays.map(async (relayUrl) => {
156
285
  let ws = null;
@@ -189,7 +318,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
189
318
  cleanup();
190
319
  resolve();
191
320
  } else {
192
- log.warn(
321
+ log2.warn(
193
322
  { relay: relayUrl, reason: data[3] },
194
323
  "Relay rejected event"
195
324
  );
@@ -199,7 +328,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
199
328
  }
200
329
  }
201
330
  } catch (err) {
202
- log.error(
331
+ log2.error(
203
332
  { relay: relayUrl, error: err },
204
333
  "Failed to parse relay response"
205
334
  );
@@ -209,7 +338,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
209
338
  }
210
339
  };
211
340
  wsRef.onerror = (err) => {
212
- log.error({ relay: relayUrl, error: err }, "WebSocket error");
341
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error");
213
342
  recordFailure();
214
343
  cleanup();
215
344
  reject(err);
@@ -219,7 +348,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
219
348
  };
220
349
  });
221
350
  } catch (err) {
222
- log.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
351
+ log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
223
352
  recordFailure();
224
353
  } finally {
225
354
  if (ws && ws.readyState !== WebSocket.CLOSED) {
@@ -228,12 +357,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
228
357
  }
229
358
  });
230
359
  await Promise.allSettled(publishPromises);
231
- log.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
360
+ log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
232
361
  return { success, failed };
233
362
  }
234
363
  async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
235
364
  const events = [];
236
- log.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
365
+ log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
237
366
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
238
367
  const queryPromises = relays.map(async (relayUrl) => {
239
368
  let ws = null;
@@ -273,11 +402,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
273
402
  resolve();
274
403
  }
275
404
  } catch (err) {
276
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
405
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
277
406
  }
278
407
  };
279
408
  wsRef.onerror = (err) => {
280
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
409
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
281
410
  cleanup();
282
411
  reject(err);
283
412
  };
@@ -286,7 +415,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
286
415
  };
287
416
  });
288
417
  } catch (err) {
289
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
418
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
290
419
  } finally {
291
420
  if (ws && ws.readyState !== WebSocket.CLOSED) {
292
421
  ws.close();
@@ -294,12 +423,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
294
423
  }
295
424
  });
296
425
  await Promise.allSettled(queryPromises);
297
- log.info({ eventCount: events.length }, "Query complete");
426
+ log2.info({ eventCount: events.length }, "Query complete");
298
427
  return events;
299
428
  }
300
429
  async function queryByAddress(address, relays = DEFAULT_RELAYS) {
301
430
  const events = [];
302
- log.info({ address, relayCount: relays.length }, "Querying relays for address");
431
+ log2.info({ address, relayCount: relays.length }, "Querying relays for address");
303
432
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
304
433
  const queryPromises = relays.map(async (relayUrl) => {
305
434
  try {
@@ -341,28 +470,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
341
470
  resolve();
342
471
  }
343
472
  } catch (err) {
344
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
473
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
345
474
  }
346
475
  };
347
476
  ws.onerror = (err) => {
348
477
  clearTimeout(timeout);
349
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
478
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
350
479
  reject(err);
351
480
  };
352
481
  });
353
482
  } catch (err) {
354
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
483
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
355
484
  }
356
485
  });
357
486
  await Promise.allSettled(queryPromises);
358
- log.info({ eventCount: events.length }, "Query complete");
487
+ log2.info({ eventCount: events.length }, "Query complete");
359
488
  return events;
360
489
  }
361
490
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
362
491
  const events = [];
363
492
  const identityTag = `${protocol}:${identifier}`;
364
493
  const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
365
- log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
494
+ log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
366
495
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
367
496
  const queryPromises = relays.map(async (relayUrl) => {
368
497
  try {
@@ -397,26 +526,26 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
397
526
  resolve();
398
527
  }
399
528
  } catch (err) {
400
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
529
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
401
530
  }
402
531
  };
403
532
  ws.onerror = (err) => {
404
533
  clearTimeout(timeout);
405
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
534
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
406
535
  reject(err);
407
536
  };
408
537
  });
409
538
  } catch (err) {
410
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
539
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
411
540
  }
412
541
  });
413
542
  await Promise.allSettled(queryPromises);
414
- log.info({ eventCount: events.length }, "Query complete");
543
+ log2.info({ eventCount: events.length }, "Query complete");
415
544
  return events;
416
545
  }
417
546
  async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
418
547
  const events = [];
419
- log.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
548
+ log2.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
420
549
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
421
550
  const queryPromises = relays.map(async (relayUrl) => {
422
551
  try {
@@ -452,30 +581,22 @@ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
452
581
  resolve();
453
582
  }
454
583
  } catch (err) {
455
- log.error({ relay: relayUrl, error: err }, "Failed to parse event");
584
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
456
585
  }
457
586
  };
458
587
  ws.onerror = (err) => {
459
588
  clearTimeout(timeout);
460
- log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
589
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
461
590
  reject(err);
462
591
  };
463
592
  });
464
593
  } catch (err) {
465
- log.error({ relay: relayUrl, error: err }, "Failed to query relay");
594
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
466
595
  }
467
596
  });
468
597
  await Promise.allSettled(queryPromises);
469
- const seen = /* @__PURE__ */ new Set();
470
- const envelopes = [];
471
- for (const ev of events) {
472
- const envelope = parseAttestationFromEvent(ev);
473
- if (!envelope || seen.has(envelope.attestation_id)) continue;
474
- if (!await verifyAttestationEnvelope(envelope)) continue;
475
- seen.add(envelope.attestation_id);
476
- envelopes.push(envelope);
477
- }
478
- log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
598
+ const envelopes = await collectSignedEnvelopes(events);
599
+ log2.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
479
600
  return envelopes;
480
601
  }
481
602
  async function verifyAttestationEnvelope(envelope) {
@@ -487,16 +608,68 @@ async function verifyAttestationEnvelope(envelope) {
487
608
  const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
488
609
  return computedId === envelope.attestation_id;
489
610
  } catch (err) {
490
- log.error({ error: err }, "Failed to verify attestation envelope");
611
+ log2.error({ error: err }, "Failed to verify attestation envelope");
491
612
  return false;
492
613
  }
493
614
  }
615
+ function base64Url(text) {
616
+ const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
617
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
618
+ }
619
+ function signedEnvelope(envelope) {
620
+ let parsed;
621
+ try {
622
+ parsed = parseCanonicalMessage(envelope.message);
623
+ } catch {
624
+ return null;
625
+ }
626
+ if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
627
+ const { expires_at: _copy, ...rest } = envelope;
628
+ return {
629
+ ...rest,
630
+ address: parsed.core.address,
631
+ identities: parsed.core.identities,
632
+ issued_at: parsed.core.issued_at,
633
+ ...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
634
+ message_b64url: base64Url(envelope.message)
635
+ };
636
+ }
637
+ async function collectSignedEnvelopes(events, accept = () => true) {
638
+ const copies = /* @__PURE__ */ new Map();
639
+ for (const event of events) {
640
+ const raw = parseAttestationFromEvent(event);
641
+ if (!raw || !await verifyAttestationEnvelope(raw)) continue;
642
+ const envelope = signedEnvelope(raw);
643
+ if (!envelope || !accept(event, envelope)) continue;
644
+ const same = copies.get(envelope.attestation_id) ?? [];
645
+ if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
646
+ same.push(envelope);
647
+ }
648
+ copies.set(envelope.attestation_id, same);
649
+ }
650
+ const kept = [];
651
+ for (const same of copies.values()) {
652
+ if (same.length === 1) {
653
+ kept.push(same[0]);
654
+ continue;
655
+ }
656
+ let valid;
657
+ for (const e of same) {
658
+ if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
659
+ valid = e;
660
+ break;
661
+ }
662
+ }
663
+ kept.push(valid ?? same[0]);
664
+ }
665
+ return kept;
666
+ }
494
667
  function parseAttestationFromEvent(event) {
495
668
  try {
496
669
  const envelope = JSON.parse(event.content);
497
670
  return envelope;
498
671
  } catch (err) {
499
- log.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
672
+ log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
500
673
  return null;
501
674
  }
502
675
  }
@@ -504,6 +677,7 @@ function parseAttestationFromEvent(event) {
504
677
  exports.ATTESTATION_EVENT_KIND = ATTESTATION_EVENT_KIND;
505
678
  exports.DEFAULT_RELAYS = DEFAULT_RELAYS;
506
679
  exports.FANOUT_DEADLINE_MS = FANOUT_DEADLINE_MS;
680
+ exports.collectSignedEnvelopes = collectSignedEnvelopes;
507
681
  exports.createAttestationEvent = createAttestationEvent;
508
682
  exports.getAddressFromEvent = getAddressFromEvent;
509
683
  exports.getAttestationIdFromEvent = getAttestationIdFromEvent;
@@ -514,6 +688,7 @@ exports.queryByAddress = queryByAddress;
514
688
  exports.queryByAttestationId = queryByAttestationId;
515
689
  exports.queryByIdentity = queryByIdentity;
516
690
  exports.queryRecent = queryRecent;
691
+ exports.signedEnvelope = signedEnvelope;
517
692
  exports.verifyAttestationEnvelope = verifyAttestationEnvelope;
518
693
  //# sourceMappingURL=nostr.js.map
519
694
  //# sourceMappingURL=nostr.js.map