@orangecheck/sdk 1.5.2 → 1.7.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
  ])
@@ -655,6 +681,12 @@ async function publishAttestation(options) {
655
681
  );
656
682
  return result;
657
683
  }
684
+ function eventIndexesEnvelope(event, envelope) {
685
+ const d = event.tags.find((t) => t[0] === "d")?.[1];
686
+ const id = envelope.attestation_id;
687
+ if (d !== id && d !== `orangecheck:${id}`) return false;
688
+ return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
689
+ }
658
690
  async function discoverAttestations(options) {
659
691
  const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
660
692
  let events = [];
@@ -673,7 +705,9 @@ async function discoverAttestations(options) {
673
705
  const envelopes = [];
674
706
  for (const event of events) {
675
707
  const envelope = parseAttestationFromEvent(event);
676
- if (envelope) {
708
+ if (envelope && !eventIndexesEnvelope(event, envelope)) {
709
+ log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
710
+ } else if (envelope) {
677
711
  const isValid = await verifyAttestationEnvelope(envelope);
678
712
  if (isValid) {
679
713
  envelopes.push(envelope);
@@ -1157,18 +1191,15 @@ async function check(params) {
1157
1191
  if (!addr && !id && !identity) {
1158
1192
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["bad_request"] };
1159
1193
  }
1160
- let envelopes = [];
1194
+ let found = [];
1161
1195
  if (id) {
1162
- envelopes = await discoverAttestations({ attestationId: id, relays });
1196
+ found = await discoverAttestations({ attestationId: id, relays });
1163
1197
  } else if (addr) {
1164
- envelopes = await getAttestationsForAddress(addr, relays);
1198
+ found = await getAttestationsForAddress(addr, relays);
1165
1199
  } else if (identity) {
1166
- envelopes = await getAttestationsForIdentity(
1167
- identity.protocol,
1168
- identity.identifier,
1169
- relays
1170
- );
1200
+ found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
1171
1201
  }
1202
+ const envelopes = found.filter((e) => signedMatch(e, { addr, id, identity }));
1172
1203
  if (envelopes.length === 0) {
1173
1204
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1174
1205
  }
@@ -1177,6 +1208,7 @@ async function check(params) {
1177
1208
  if (!env) {
1178
1209
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1179
1210
  }
1211
+ const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
1180
1212
  const outcome = await verify2(
1181
1213
  {
1182
1214
  addr: env.address,
@@ -1191,9 +1223,10 @@ async function check(params) {
1191
1223
  const score = outcome.metrics?.score ?? 0;
1192
1224
  const reasons = [];
1193
1225
  if (!outcome.ok) reasons.push(...outcome.codes);
1226
+ if (shared) reasons.push("stake_shared");
1194
1227
  if (sats < minSats) reasons.push("below_min_sats");
1195
1228
  if (days < minDays) reasons.push("below_min_days");
1196
- const ok = outcome.ok && sats >= minSats && days >= minDays;
1229
+ const ok = outcome.ok && !shared && sats >= minSats && days >= minDays;
1197
1230
  return {
1198
1231
  ok,
1199
1232
  sats,
@@ -1206,6 +1239,50 @@ async function check(params) {
1206
1239
  ...reasons.length ? { reasons } : {}
1207
1240
  };
1208
1241
  }
1242
+ function identityKey(b) {
1243
+ const protocol = b.protocol.trim().toLowerCase();
1244
+ if (protocol === "nostr") {
1245
+ const hex = nostrPubkeyToHex(b.identifier);
1246
+ return hex ? `nostr:${hex}` : null;
1247
+ }
1248
+ return `${protocol}:${b.identifier.trim()}`;
1249
+ }
1250
+ function signedIdentities(env) {
1251
+ try {
1252
+ return parseCanonicalMessage(env.message).core.identities;
1253
+ } catch {
1254
+ return [];
1255
+ }
1256
+ }
1257
+ function signedMatch(env, q) {
1258
+ if (q.id) return env.attestation_id.toLowerCase() === q.id.trim().toLowerCase();
1259
+ let signedAddress;
1260
+ try {
1261
+ signedAddress = parseCanonicalMessage(env.message).core.address;
1262
+ } catch {
1263
+ return false;
1264
+ }
1265
+ if (signedAddress !== env.address) return false;
1266
+ if (q.addr) return signedAddress === q.addr;
1267
+ if (q.identity) {
1268
+ const want = identityKey(q.identity);
1269
+ return want !== null && signedIdentities(env).some((b) => identityKey(b) === want);
1270
+ }
1271
+ return false;
1272
+ }
1273
+ async function backsOtherIdentity(env, identity, relays) {
1274
+ const want = identityKey(identity);
1275
+ const protocol = identity.protocol.trim().toLowerCase();
1276
+ const siblings = await getAttestationsForAddress(env.address, relays);
1277
+ for (const e of [env, ...siblings]) {
1278
+ if (!signedMatch(e, { addr: env.address })) continue;
1279
+ for (const b of signedIdentities(e)) {
1280
+ if (b.protocol.trim().toLowerCase() !== protocol) continue;
1281
+ if (identityKey(b) !== want) return true;
1282
+ }
1283
+ }
1284
+ return false;
1285
+ }
1209
1286
  var AUTH_HEADER = "orangecheck-auth";
1210
1287
  var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1211
1288
  function randomNonce() {
@@ -1660,8 +1737,8 @@ function getGistCreationUrl() {
1660
1737
  return "https://gist.github.com/";
1661
1738
  }
1662
1739
  var log7 = createLogger("identity-verification/nostr");
1663
- var HEX_64_RE = /^[0-9a-f]{64}$/;
1664
- function toHex(bytes) {
1740
+ var HEX_64_RE2 = /^[0-9a-f]{64}$/;
1741
+ function toHex2(bytes) {
1665
1742
  let out = "";
1666
1743
  for (const b of bytes) out += b.toString(16).padStart(2, "0");
1667
1744
  return out;
@@ -1675,22 +1752,9 @@ function fromHex(hex) {
1675
1752
  return out;
1676
1753
  }
1677
1754
  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));
1755
+ const hex = nostrPubkeyToHex(npub);
1756
+ if (!hex) throw new Error(`not an npub: ${npub.trim().slice(0, 12)}\u2026`);
1757
+ return hex;
1694
1758
  }
1695
1759
  async function queryNostrEvents(options) {
1696
1760
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1762,7 +1826,7 @@ async function queryNostrEvents(options) {
1762
1826
  }
1763
1827
  async function verifyNostrEventSignature(event) {
1764
1828
  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)) {
1829
+ 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
1830
  return false;
1767
1831
  }
1768
1832
  const preimage = JSON.stringify([
@@ -1773,7 +1837,7 @@ async function verifyNostrEventSignature(event) {
1773
1837
  event.tags ?? [],
1774
1838
  event.content ?? ""
1775
1839
  ]);
1776
- const computedId = toHex(sha2_js.sha256(new TextEncoder().encode(preimage)));
1840
+ const computedId = toHex2(sha2_js.sha256(new TextEncoder().encode(preimage)));
1777
1841
  if (computedId !== event.id.toLowerCase()) {
1778
1842
  log7.warn(
1779
1843
  { eventId: event.id, computedId },
@@ -2182,6 +2246,8 @@ exports.isNostrIdentityVerified = isNostrIdentityVerified;
2182
2246
  exports.isValidDomain = isValidDomain;
2183
2247
  exports.isValidTweetUrl = isValidTweetUrl;
2184
2248
  exports.issueChallenge = issueChallenge;
2249
+ exports.nostrIdentifierForms = nostrIdentifierForms;
2250
+ exports.nostrPubkeyToHex = nostrPubkeyToHex;
2185
2251
  exports.parseAttestationFromEvent = parseAttestationFromEvent;
2186
2252
  exports.parseIdentities = parseIdentities;
2187
2253
  exports.publishAttestation = publishAttestation;