@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.mjs CHANGED
@@ -1,9 +1,9 @@
1
+ import { bech32 } from '@scure/base';
1
2
  import { Buffer } from 'buffer';
2
3
  import { Verifier } from 'bip322-js';
3
4
  import * as bitcoinMessage from 'bitcoinjs-message';
4
5
  import { schnorr } from '@noble/curves/secp256k1.js';
5
6
  import { sha256 } from '@noble/hashes/sha2.js';
6
- import { bech32 } from '@scure/base';
7
7
 
8
8
  // src/canonical.ts
9
9
  function toLowerHex(bytes) {
@@ -128,6 +128,31 @@ async function createAttestationEnvelope(message, signature, scheme, address, id
128
128
  relay_hints
129
129
  };
130
130
  }
131
+ var HEX_64_RE = /^[0-9a-f]{64}$/;
132
+ function toHex(bytes) {
133
+ let out = "";
134
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
135
+ return out;
136
+ }
137
+ function nostrPubkeyToHex(value) {
138
+ const trimmed = value.trim().toLowerCase();
139
+ if (HEX_64_RE.test(trimmed)) return trimmed;
140
+ if (!trimmed.startsWith("npub1")) return null;
141
+ try {
142
+ const decoded = bech32.decode(trimmed, 1023);
143
+ if (decoded.prefix !== "npub") return null;
144
+ const bytes = bech32.fromWords(decoded.words);
145
+ return bytes.length === 32 ? toHex(Uint8Array.from(bytes)) : null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ function nostrIdentifierForms(identifier) {
151
+ const hex = nostrPubkeyToHex(identifier);
152
+ if (!hex) return [identifier];
153
+ const bytes = Uint8Array.from(hex.match(/../g).map((b) => parseInt(b, 16)));
154
+ return [bech32.encode("npub", bech32.toWords(bytes), 1023), hex];
155
+ }
131
156
  function createLogger(namespace) {
132
157
  const shouldLog = (level) => {
133
158
  return false;
@@ -449,6 +474,7 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
449
474
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
450
475
  const events = [];
451
476
  const identityTag = `${protocol}:${identifier}`;
477
+ const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
452
478
  log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
453
479
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
454
480
  const queryPromises = relays.map(async (relayUrl) => {
@@ -467,7 +493,7 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
467
493
  subscriptionId,
468
494
  {
469
495
  kinds: [30078],
470
- "#i": [identityTag],
496
+ "#i": identityTags,
471
497
  limit: 10
472
498
  }
473
499
  ])
@@ -501,6 +527,70 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
501
527
  log.info({ eventCount: events.length }, "Query complete");
502
528
  return events;
503
529
  }
530
+ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
531
+ const events = [];
532
+ log.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
533
+ const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
534
+ const queryPromises = relays.map(async (relayUrl) => {
535
+ try {
536
+ const ws = new WebSocket(relayUrl);
537
+ await new Promise((resolve, reject) => {
538
+ const timeout = setTimeout(() => {
539
+ ws.close();
540
+ reject(new Error("Query timeout"));
541
+ }, budgetFor(deadlineAt));
542
+ ws.onopen = () => {
543
+ const subscriptionId = `ochk_recent_${Date.now()}`;
544
+ ws.send(
545
+ JSON.stringify([
546
+ "REQ",
547
+ subscriptionId,
548
+ { kinds: [ATTESTATION_EVENT_KIND], "#t": ["oc-attest"], limit }
549
+ ])
550
+ );
551
+ };
552
+ ws.onmessage = (msg) => {
553
+ try {
554
+ const data = JSON.parse(msg.data);
555
+ if (data[0] === "EVENT") {
556
+ const ev = data[2];
557
+ const scheme = ev.tags?.find((t) => t[0] === "scheme")?.[1];
558
+ const dTag = ev.tags?.find((t) => t[0] === "d")?.[1];
559
+ if ((scheme === "bip322" || scheme === "legacy") && dTag) {
560
+ events.push(ev);
561
+ }
562
+ } else if (data[0] === "EOSE") {
563
+ clearTimeout(timeout);
564
+ ws.close();
565
+ resolve();
566
+ }
567
+ } catch (err) {
568
+ log.error({ relay: relayUrl, error: err }, "Failed to parse event");
569
+ }
570
+ };
571
+ ws.onerror = (err) => {
572
+ clearTimeout(timeout);
573
+ log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
574
+ reject(err);
575
+ };
576
+ });
577
+ } catch (err) {
578
+ log.error({ relay: relayUrl, error: err }, "Failed to query relay");
579
+ }
580
+ });
581
+ await Promise.allSettled(queryPromises);
582
+ const seen = /* @__PURE__ */ new Set();
583
+ const envelopes = [];
584
+ for (const ev of events) {
585
+ const envelope = parseAttestationFromEvent(ev);
586
+ if (!envelope || seen.has(envelope.attestation_id)) continue;
587
+ if (!await verifyAttestationEnvelope(envelope)) continue;
588
+ seen.add(envelope.attestation_id);
589
+ envelopes.push(envelope);
590
+ }
591
+ log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
592
+ return envelopes;
593
+ }
504
594
  async function verifyAttestationEnvelope(envelope) {
505
595
  try {
506
596
  const encoder = new TextEncoder();
@@ -633,6 +723,12 @@ async function publishAttestation(options) {
633
723
  );
634
724
  return result;
635
725
  }
726
+ function eventIndexesEnvelope(event, envelope) {
727
+ const d = event.tags.find((t) => t[0] === "d")?.[1];
728
+ const id = envelope.attestation_id;
729
+ if (d !== id && d !== `orangecheck:${id}`) return false;
730
+ return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
731
+ }
636
732
  async function discoverAttestations(options) {
637
733
  const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
638
734
  let events = [];
@@ -651,7 +747,9 @@ async function discoverAttestations(options) {
651
747
  const envelopes = [];
652
748
  for (const event of events) {
653
749
  const envelope = parseAttestationFromEvent(event);
654
- if (envelope) {
750
+ if (envelope && !eventIndexesEnvelope(event, envelope)) {
751
+ log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
752
+ } else if (envelope) {
655
753
  const isValid = await verifyAttestationEnvelope(envelope);
656
754
  if (isValid) {
657
755
  envelopes.push(envelope);
@@ -1135,18 +1233,15 @@ async function check(params) {
1135
1233
  if (!addr && !id && !identity) {
1136
1234
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["bad_request"] };
1137
1235
  }
1138
- let envelopes = [];
1236
+ let found = [];
1139
1237
  if (id) {
1140
- envelopes = await discoverAttestations({ attestationId: id, relays });
1238
+ found = await discoverAttestations({ attestationId: id, relays });
1141
1239
  } else if (addr) {
1142
- envelopes = await getAttestationsForAddress(addr, relays);
1240
+ found = await getAttestationsForAddress(addr, relays);
1143
1241
  } else if (identity) {
1144
- envelopes = await getAttestationsForIdentity(
1145
- identity.protocol,
1146
- identity.identifier,
1147
- relays
1148
- );
1242
+ found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
1149
1243
  }
1244
+ const envelopes = found.filter((e) => signedMatch(e, { addr, id, identity }));
1150
1245
  if (envelopes.length === 0) {
1151
1246
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1152
1247
  }
@@ -1155,6 +1250,7 @@ async function check(params) {
1155
1250
  if (!env) {
1156
1251
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1157
1252
  }
1253
+ const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
1158
1254
  const outcome = await verify2(
1159
1255
  {
1160
1256
  addr: env.address,
@@ -1169,9 +1265,10 @@ async function check(params) {
1169
1265
  const score = outcome.metrics?.score ?? 0;
1170
1266
  const reasons = [];
1171
1267
  if (!outcome.ok) reasons.push(...outcome.codes);
1268
+ if (shared) reasons.push("stake_shared");
1172
1269
  if (sats < minSats) reasons.push("below_min_sats");
1173
1270
  if (days < minDays) reasons.push("below_min_days");
1174
- const ok = outcome.ok && sats >= minSats && days >= minDays;
1271
+ const ok = outcome.ok && !shared && sats >= minSats && days >= minDays;
1175
1272
  return {
1176
1273
  ok,
1177
1274
  sats,
@@ -1184,6 +1281,50 @@ async function check(params) {
1184
1281
  ...reasons.length ? { reasons } : {}
1185
1282
  };
1186
1283
  }
1284
+ function identityKey(b) {
1285
+ const protocol = b.protocol.trim().toLowerCase();
1286
+ if (protocol === "nostr") {
1287
+ const hex = nostrPubkeyToHex(b.identifier);
1288
+ return hex ? `nostr:${hex}` : null;
1289
+ }
1290
+ return `${protocol}:${b.identifier.trim()}`;
1291
+ }
1292
+ function signedIdentities(env) {
1293
+ try {
1294
+ return parseCanonicalMessage(env.message).core.identities;
1295
+ } catch {
1296
+ return [];
1297
+ }
1298
+ }
1299
+ function signedMatch(env, q) {
1300
+ if (q.id) return env.attestation_id.toLowerCase() === q.id.trim().toLowerCase();
1301
+ let signedAddress;
1302
+ try {
1303
+ signedAddress = parseCanonicalMessage(env.message).core.address;
1304
+ } catch {
1305
+ return false;
1306
+ }
1307
+ if (signedAddress !== env.address) return false;
1308
+ if (q.addr) return signedAddress === q.addr;
1309
+ if (q.identity) {
1310
+ const want = identityKey(q.identity);
1311
+ return want !== null && signedIdentities(env).some((b) => identityKey(b) === want);
1312
+ }
1313
+ return false;
1314
+ }
1315
+ async function backsOtherIdentity(env, identity, relays) {
1316
+ const want = identityKey(identity);
1317
+ const protocol = identity.protocol.trim().toLowerCase();
1318
+ const siblings = await getAttestationsForAddress(env.address, relays);
1319
+ for (const e of [env, ...siblings]) {
1320
+ if (!signedMatch(e, { addr: env.address })) continue;
1321
+ for (const b of signedIdentities(e)) {
1322
+ if (b.protocol.trim().toLowerCase() !== protocol) continue;
1323
+ if (identityKey(b) !== want) return true;
1324
+ }
1325
+ }
1326
+ return false;
1327
+ }
1187
1328
  var AUTH_HEADER = "orangecheck-auth";
1188
1329
  var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1189
1330
  function randomNonce() {
@@ -1638,8 +1779,8 @@ function getGistCreationUrl() {
1638
1779
  return "https://gist.github.com/";
1639
1780
  }
1640
1781
  var log7 = createLogger("identity-verification/nostr");
1641
- var HEX_64_RE = /^[0-9a-f]{64}$/;
1642
- function toHex(bytes) {
1782
+ var HEX_64_RE2 = /^[0-9a-f]{64}$/;
1783
+ function toHex2(bytes) {
1643
1784
  let out = "";
1644
1785
  for (const b of bytes) out += b.toString(16).padStart(2, "0");
1645
1786
  return out;
@@ -1653,22 +1794,9 @@ function fromHex(hex) {
1653
1794
  return out;
1654
1795
  }
1655
1796
  function npubToHex(npub) {
1656
- const trimmed = npub.trim();
1657
- if (HEX_64_RE.test(trimmed.toLowerCase())) {
1658
- return trimmed.toLowerCase();
1659
- }
1660
- if (!trimmed.toLowerCase().startsWith("npub1")) {
1661
- throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
1662
- }
1663
- const decoded = bech32.decode(trimmed.toLowerCase(), 1023);
1664
- if (decoded.prefix !== "npub") {
1665
- throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
1666
- }
1667
- const bytes = bech32.fromWords(decoded.words);
1668
- if (bytes.length !== 32) {
1669
- throw new Error(`invalid npub payload length: ${bytes.length}`);
1670
- }
1671
- return toHex(Uint8Array.from(bytes));
1797
+ const hex = nostrPubkeyToHex(npub);
1798
+ if (!hex) throw new Error(`not an npub: ${npub.trim().slice(0, 12)}\u2026`);
1799
+ return hex;
1672
1800
  }
1673
1801
  async function queryNostrEvents(options) {
1674
1802
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1740,7 +1868,7 @@ async function queryNostrEvents(options) {
1740
1868
  }
1741
1869
  async function verifyNostrEventSignature(event) {
1742
1870
  try {
1743
- 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)) {
1871
+ 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)) {
1744
1872
  return false;
1745
1873
  }
1746
1874
  const preimage = JSON.stringify([
@@ -1751,7 +1879,7 @@ async function verifyNostrEventSignature(event) {
1751
1879
  event.tags ?? [],
1752
1880
  event.content ?? ""
1753
1881
  ]);
1754
- const computedId = toHex(sha256(new TextEncoder().encode(preimage)));
1882
+ const computedId = toHex2(sha256(new TextEncoder().encode(preimage)));
1755
1883
  if (computedId !== event.id.toLowerCase()) {
1756
1884
  log7.warn(
1757
1885
  { eventId: event.id, computedId },
@@ -2069,12 +2197,12 @@ var STATUS_META = {
2069
2197
  },
2070
2198
  bond_zero: {
2071
2199
  label: "No confirmed bitcoin found",
2072
- detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a reputation score.",
2200
+ detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a stake.",
2073
2201
  severity: "warn"
2074
2202
  },
2075
2203
  bond_pending: {
2076
2204
  label: "Pending transactions detected",
2077
- detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your reputation score.",
2205
+ detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your stake.",
2078
2206
  severity: "info"
2079
2207
  },
2080
2208
  bond_insufficient: {
@@ -2119,6 +2247,6 @@ var STATUS_META = {
2119
2247
  }
2120
2248
  };
2121
2249
 
2122
- export { DEFAULT_RELAYS, FANOUT_DEADLINE_MS, STATUS_META, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
2250
+ export { DEFAULT_RELAYS, FANOUT_DEADLINE_MS, STATUS_META, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, nostrIdentifierForms, nostrPubkeyToHex, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
2123
2251
  //# sourceMappingURL=index.mjs.map
2124
2252
  //# sourceMappingURL=index.mjs.map