@orangecheck/sdk 1.6.0 → 1.8.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/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var base = require('@scure/base');
3
4
  var buffer = require('buffer');
4
5
  var bip322Js = require('bip322-js');
5
6
  var bitcoinMessage = require('bitcoinjs-message');
6
7
  var secp256k1_js = require('@noble/curves/secp256k1.js');
7
8
  var sha2_js = require('@noble/hashes/sha2.js');
8
- var base = require('@scure/base');
9
9
 
10
10
  function _interopNamespace(e) {
11
11
  if (e && e.__esModule) return e;
@@ -150,6 +150,31 @@ async function createAttestationEnvelope(message, signature, scheme, address, id
150
150
  relay_hints
151
151
  };
152
152
  }
153
+ var HEX_64_RE = /^[0-9a-f]{64}$/;
154
+ function toHex(bytes) {
155
+ let out = "";
156
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
157
+ return out;
158
+ }
159
+ function nostrPubkeyToHex(value) {
160
+ const trimmed = value.trim().toLowerCase();
161
+ if (HEX_64_RE.test(trimmed)) return trimmed;
162
+ if (!trimmed.startsWith("npub1")) return null;
163
+ try {
164
+ const decoded = base.bech32.decode(trimmed, 1023);
165
+ if (decoded.prefix !== "npub") return null;
166
+ const bytes = base.bech32.fromWords(decoded.words);
167
+ return bytes.length === 32 ? toHex(Uint8Array.from(bytes)) : null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+ function nostrIdentifierForms(identifier) {
173
+ const hex = nostrPubkeyToHex(identifier);
174
+ if (!hex) return [identifier];
175
+ const bytes = Uint8Array.from(hex.match(/../g).map((b) => parseInt(b, 16)));
176
+ return [base.bech32.encode("npub", base.bech32.toWords(bytes), 1023), hex];
177
+ }
153
178
  function createLogger(namespace) {
154
179
  const shouldLog = (level) => {
155
180
  return false;
@@ -471,6 +496,7 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
471
496
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
472
497
  const events = [];
473
498
  const identityTag = `${protocol}:${identifier}`;
499
+ const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
474
500
  log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
475
501
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
476
502
  const queryPromises = relays.map(async (relayUrl) => {
@@ -489,7 +515,7 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
489
515
  subscriptionId,
490
516
  {
491
517
  kinds: [30078],
492
- "#i": [identityTag],
518
+ "#i": identityTags,
493
519
  limit: 10
494
520
  }
495
521
  ])
@@ -523,6 +549,70 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
523
549
  log.info({ eventCount: events.length }, "Query complete");
524
550
  return events;
525
551
  }
552
+ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
553
+ const events = [];
554
+ log.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
555
+ const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
556
+ const queryPromises = relays.map(async (relayUrl) => {
557
+ try {
558
+ const ws = new WebSocket(relayUrl);
559
+ await new Promise((resolve, reject) => {
560
+ const timeout = setTimeout(() => {
561
+ ws.close();
562
+ reject(new Error("Query timeout"));
563
+ }, budgetFor(deadlineAt));
564
+ ws.onopen = () => {
565
+ const subscriptionId = `ochk_recent_${Date.now()}`;
566
+ ws.send(
567
+ JSON.stringify([
568
+ "REQ",
569
+ subscriptionId,
570
+ { kinds: [ATTESTATION_EVENT_KIND], "#t": ["oc-attest"], limit }
571
+ ])
572
+ );
573
+ };
574
+ ws.onmessage = (msg) => {
575
+ try {
576
+ const data = JSON.parse(msg.data);
577
+ if (data[0] === "EVENT") {
578
+ const ev = data[2];
579
+ const scheme = ev.tags?.find((t) => t[0] === "scheme")?.[1];
580
+ const dTag = ev.tags?.find((t) => t[0] === "d")?.[1];
581
+ if ((scheme === "bip322" || scheme === "legacy") && dTag) {
582
+ events.push(ev);
583
+ }
584
+ } else if (data[0] === "EOSE") {
585
+ clearTimeout(timeout);
586
+ ws.close();
587
+ resolve();
588
+ }
589
+ } catch (err) {
590
+ log.error({ relay: relayUrl, error: err }, "Failed to parse event");
591
+ }
592
+ };
593
+ ws.onerror = (err) => {
594
+ clearTimeout(timeout);
595
+ log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
596
+ reject(err);
597
+ };
598
+ });
599
+ } catch (err) {
600
+ log.error({ relay: relayUrl, error: err }, "Failed to query relay");
601
+ }
602
+ });
603
+ await Promise.allSettled(queryPromises);
604
+ const seen = /* @__PURE__ */ new Set();
605
+ const envelopes = [];
606
+ for (const ev of events) {
607
+ const envelope = parseAttestationFromEvent(ev);
608
+ if (!envelope || seen.has(envelope.attestation_id)) continue;
609
+ if (!await verifyAttestationEnvelope(envelope)) continue;
610
+ seen.add(envelope.attestation_id);
611
+ envelopes.push(envelope);
612
+ }
613
+ log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
614
+ return envelopes;
615
+ }
526
616
  async function verifyAttestationEnvelope(envelope) {
527
617
  try {
528
618
  const encoder = new TextEncoder();
@@ -655,6 +745,12 @@ async function publishAttestation(options) {
655
745
  );
656
746
  return result;
657
747
  }
748
+ function eventIndexesEnvelope(event, envelope) {
749
+ const d = event.tags.find((t) => t[0] === "d")?.[1];
750
+ const id = envelope.attestation_id;
751
+ if (d !== id && d !== `orangecheck:${id}`) return false;
752
+ return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
753
+ }
658
754
  async function discoverAttestations(options) {
659
755
  const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
660
756
  let events = [];
@@ -673,7 +769,9 @@ async function discoverAttestations(options) {
673
769
  const envelopes = [];
674
770
  for (const event of events) {
675
771
  const envelope = parseAttestationFromEvent(event);
676
- if (envelope) {
772
+ if (envelope && !eventIndexesEnvelope(event, envelope)) {
773
+ log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
774
+ } else if (envelope) {
677
775
  const isValid = await verifyAttestationEnvelope(envelope);
678
776
  if (isValid) {
679
777
  envelopes.push(envelope);
@@ -1157,18 +1255,15 @@ async function check(params) {
1157
1255
  if (!addr && !id && !identity) {
1158
1256
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["bad_request"] };
1159
1257
  }
1160
- let envelopes = [];
1258
+ let found = [];
1161
1259
  if (id) {
1162
- envelopes = await discoverAttestations({ attestationId: id, relays });
1260
+ found = await discoverAttestations({ attestationId: id, relays });
1163
1261
  } else if (addr) {
1164
- envelopes = await getAttestationsForAddress(addr, relays);
1262
+ found = await getAttestationsForAddress(addr, relays);
1165
1263
  } else if (identity) {
1166
- envelopes = await getAttestationsForIdentity(
1167
- identity.protocol,
1168
- identity.identifier,
1169
- relays
1170
- );
1264
+ found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
1171
1265
  }
1266
+ const envelopes = found.filter((e) => signedMatch(e, { addr, id, identity }));
1172
1267
  if (envelopes.length === 0) {
1173
1268
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1174
1269
  }
@@ -1177,6 +1272,7 @@ async function check(params) {
1177
1272
  if (!env) {
1178
1273
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1179
1274
  }
1275
+ const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
1180
1276
  const outcome = await verify2(
1181
1277
  {
1182
1278
  addr: env.address,
@@ -1191,9 +1287,10 @@ async function check(params) {
1191
1287
  const score = outcome.metrics?.score ?? 0;
1192
1288
  const reasons = [];
1193
1289
  if (!outcome.ok) reasons.push(...outcome.codes);
1290
+ if (shared) reasons.push("stake_shared");
1194
1291
  if (sats < minSats) reasons.push("below_min_sats");
1195
1292
  if (days < minDays) reasons.push("below_min_days");
1196
- const ok = outcome.ok && sats >= minSats && days >= minDays;
1293
+ const ok = outcome.ok && !shared && sats >= minSats && days >= minDays;
1197
1294
  return {
1198
1295
  ok,
1199
1296
  sats,
@@ -1206,6 +1303,50 @@ async function check(params) {
1206
1303
  ...reasons.length ? { reasons } : {}
1207
1304
  };
1208
1305
  }
1306
+ function identityKey(b) {
1307
+ const protocol = b.protocol.trim().toLowerCase();
1308
+ if (protocol === "nostr") {
1309
+ const hex = nostrPubkeyToHex(b.identifier);
1310
+ return hex ? `nostr:${hex}` : null;
1311
+ }
1312
+ return `${protocol}:${b.identifier.trim()}`;
1313
+ }
1314
+ function signedIdentities(env) {
1315
+ try {
1316
+ return parseCanonicalMessage(env.message).core.identities;
1317
+ } catch {
1318
+ return [];
1319
+ }
1320
+ }
1321
+ function signedMatch(env, q) {
1322
+ if (q.id) return env.attestation_id.toLowerCase() === q.id.trim().toLowerCase();
1323
+ let signedAddress;
1324
+ try {
1325
+ signedAddress = parseCanonicalMessage(env.message).core.address;
1326
+ } catch {
1327
+ return false;
1328
+ }
1329
+ if (signedAddress !== env.address) return false;
1330
+ if (q.addr) return signedAddress === q.addr;
1331
+ if (q.identity) {
1332
+ const want = identityKey(q.identity);
1333
+ return want !== null && signedIdentities(env).some((b) => identityKey(b) === want);
1334
+ }
1335
+ return false;
1336
+ }
1337
+ async function backsOtherIdentity(env, identity, relays) {
1338
+ const want = identityKey(identity);
1339
+ const protocol = identity.protocol.trim().toLowerCase();
1340
+ const siblings = await getAttestationsForAddress(env.address, relays);
1341
+ for (const e of [env, ...siblings]) {
1342
+ if (!signedMatch(e, { addr: env.address })) continue;
1343
+ for (const b of signedIdentities(e)) {
1344
+ if (b.protocol.trim().toLowerCase() !== protocol) continue;
1345
+ if (identityKey(b) !== want) return true;
1346
+ }
1347
+ }
1348
+ return false;
1349
+ }
1209
1350
  var AUTH_HEADER = "orangecheck-auth";
1210
1351
  var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1211
1352
  function randomNonce() {
@@ -1660,8 +1801,8 @@ function getGistCreationUrl() {
1660
1801
  return "https://gist.github.com/";
1661
1802
  }
1662
1803
  var log7 = createLogger("identity-verification/nostr");
1663
- var HEX_64_RE = /^[0-9a-f]{64}$/;
1664
- function toHex(bytes) {
1804
+ var HEX_64_RE2 = /^[0-9a-f]{64}$/;
1805
+ function toHex2(bytes) {
1665
1806
  let out = "";
1666
1807
  for (const b of bytes) out += b.toString(16).padStart(2, "0");
1667
1808
  return out;
@@ -1675,22 +1816,9 @@ function fromHex(hex) {
1675
1816
  return out;
1676
1817
  }
1677
1818
  function npubToHex(npub) {
1678
- const trimmed = npub.trim();
1679
- if (HEX_64_RE.test(trimmed.toLowerCase())) {
1680
- return trimmed.toLowerCase();
1681
- }
1682
- if (!trimmed.toLowerCase().startsWith("npub1")) {
1683
- throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
1684
- }
1685
- const decoded = base.bech32.decode(trimmed.toLowerCase(), 1023);
1686
- if (decoded.prefix !== "npub") {
1687
- throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
1688
- }
1689
- const bytes = base.bech32.fromWords(decoded.words);
1690
- if (bytes.length !== 32) {
1691
- throw new Error(`invalid npub payload length: ${bytes.length}`);
1692
- }
1693
- return toHex(Uint8Array.from(bytes));
1819
+ const hex = nostrPubkeyToHex(npub);
1820
+ if (!hex) throw new Error(`not an npub: ${npub.trim().slice(0, 12)}\u2026`);
1821
+ return hex;
1694
1822
  }
1695
1823
  async function queryNostrEvents(options) {
1696
1824
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1762,7 +1890,7 @@ async function queryNostrEvents(options) {
1762
1890
  }
1763
1891
  async function verifyNostrEventSignature(event) {
1764
1892
  try {
1765
- if (!event.id || !event.sig || !event.pubkey || typeof event.id !== "string" || typeof event.sig !== "string" || typeof event.pubkey !== "string" || event.sig.length !== 128 || !HEX_64_RE.test(event.id) || !HEX_64_RE.test(event.pubkey)) {
1893
+ if (!event.id || !event.sig || !event.pubkey || typeof event.id !== "string" || typeof event.sig !== "string" || typeof event.pubkey !== "string" || event.sig.length !== 128 || !HEX_64_RE2.test(event.id) || !HEX_64_RE2.test(event.pubkey)) {
1766
1894
  return false;
1767
1895
  }
1768
1896
  const preimage = JSON.stringify([
@@ -1773,7 +1901,7 @@ async function verifyNostrEventSignature(event) {
1773
1901
  event.tags ?? [],
1774
1902
  event.content ?? ""
1775
1903
  ]);
1776
- const computedId = toHex(sha2_js.sha256(new TextEncoder().encode(preimage)));
1904
+ const computedId = toHex2(sha2_js.sha256(new TextEncoder().encode(preimage)));
1777
1905
  if (computedId !== event.id.toLowerCase()) {
1778
1906
  log7.warn(
1779
1907
  { eventId: event.id, computedId },
@@ -2091,12 +2219,12 @@ var STATUS_META = {
2091
2219
  },
2092
2220
  bond_zero: {
2093
2221
  label: "No confirmed bitcoin found",
2094
- detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a reputation score.",
2222
+ detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a stake.",
2095
2223
  severity: "warn"
2096
2224
  },
2097
2225
  bond_pending: {
2098
2226
  label: "Pending transactions detected",
2099
- detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your reputation score.",
2227
+ detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your stake.",
2100
2228
  severity: "info"
2101
2229
  },
2102
2230
  bond_insufficient: {
@@ -2182,6 +2310,8 @@ exports.isNostrIdentityVerified = isNostrIdentityVerified;
2182
2310
  exports.isValidDomain = isValidDomain;
2183
2311
  exports.isValidTweetUrl = isValidTweetUrl;
2184
2312
  exports.issueChallenge = issueChallenge;
2313
+ exports.nostrIdentifierForms = nostrIdentifierForms;
2314
+ exports.nostrPubkeyToHex = nostrPubkeyToHex;
2185
2315
  exports.parseAttestationFromEvent = parseAttestationFromEvent;
2186
2316
  exports.parseIdentities = parseIdentities;
2187
2317
  exports.publishAttestation = publishAttestation;
@@ -2189,6 +2319,7 @@ exports.publishToRelays = publishToRelays;
2189
2319
  exports.queryByAddress = queryByAddress;
2190
2320
  exports.queryByAttestationId = queryByAttestationId;
2191
2321
  exports.queryByIdentity = queryByIdentity;
2322
+ exports.queryRecent = queryRecent;
2192
2323
  exports.requiresManualVerification = requiresManualVerification;
2193
2324
  exports.signNostrEvent = signNostrEvent;
2194
2325
  exports.supportsAutomaticVerification = supportsAutomaticVerification;