@orangecheck/sdk 1.6.0 → 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.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
  ])
@@ -633,6 +659,12 @@ async function publishAttestation(options) {
633
659
  );
634
660
  return result;
635
661
  }
662
+ function eventIndexesEnvelope(event, envelope) {
663
+ const d = event.tags.find((t) => t[0] === "d")?.[1];
664
+ const id = envelope.attestation_id;
665
+ if (d !== id && d !== `orangecheck:${id}`) return false;
666
+ return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
667
+ }
636
668
  async function discoverAttestations(options) {
637
669
  const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
638
670
  let events = [];
@@ -651,7 +683,9 @@ async function discoverAttestations(options) {
651
683
  const envelopes = [];
652
684
  for (const event of events) {
653
685
  const envelope = parseAttestationFromEvent(event);
654
- if (envelope) {
686
+ if (envelope && !eventIndexesEnvelope(event, envelope)) {
687
+ log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
688
+ } else if (envelope) {
655
689
  const isValid = await verifyAttestationEnvelope(envelope);
656
690
  if (isValid) {
657
691
  envelopes.push(envelope);
@@ -1135,18 +1169,15 @@ async function check(params) {
1135
1169
  if (!addr && !id && !identity) {
1136
1170
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["bad_request"] };
1137
1171
  }
1138
- let envelopes = [];
1172
+ let found = [];
1139
1173
  if (id) {
1140
- envelopes = await discoverAttestations({ attestationId: id, relays });
1174
+ found = await discoverAttestations({ attestationId: id, relays });
1141
1175
  } else if (addr) {
1142
- envelopes = await getAttestationsForAddress(addr, relays);
1176
+ found = await getAttestationsForAddress(addr, relays);
1143
1177
  } else if (identity) {
1144
- envelopes = await getAttestationsForIdentity(
1145
- identity.protocol,
1146
- identity.identifier,
1147
- relays
1148
- );
1178
+ found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
1149
1179
  }
1180
+ const envelopes = found.filter((e) => signedMatch(e, { addr, id, identity }));
1150
1181
  if (envelopes.length === 0) {
1151
1182
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1152
1183
  }
@@ -1155,6 +1186,7 @@ async function check(params) {
1155
1186
  if (!env) {
1156
1187
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1157
1188
  }
1189
+ const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
1158
1190
  const outcome = await verify2(
1159
1191
  {
1160
1192
  addr: env.address,
@@ -1169,9 +1201,10 @@ async function check(params) {
1169
1201
  const score = outcome.metrics?.score ?? 0;
1170
1202
  const reasons = [];
1171
1203
  if (!outcome.ok) reasons.push(...outcome.codes);
1204
+ if (shared) reasons.push("stake_shared");
1172
1205
  if (sats < minSats) reasons.push("below_min_sats");
1173
1206
  if (days < minDays) reasons.push("below_min_days");
1174
- const ok = outcome.ok && sats >= minSats && days >= minDays;
1207
+ const ok = outcome.ok && !shared && sats >= minSats && days >= minDays;
1175
1208
  return {
1176
1209
  ok,
1177
1210
  sats,
@@ -1184,6 +1217,50 @@ async function check(params) {
1184
1217
  ...reasons.length ? { reasons } : {}
1185
1218
  };
1186
1219
  }
1220
+ function identityKey(b) {
1221
+ const protocol = b.protocol.trim().toLowerCase();
1222
+ if (protocol === "nostr") {
1223
+ const hex = nostrPubkeyToHex(b.identifier);
1224
+ return hex ? `nostr:${hex}` : null;
1225
+ }
1226
+ return `${protocol}:${b.identifier.trim()}`;
1227
+ }
1228
+ function signedIdentities(env) {
1229
+ try {
1230
+ return parseCanonicalMessage(env.message).core.identities;
1231
+ } catch {
1232
+ return [];
1233
+ }
1234
+ }
1235
+ function signedMatch(env, q) {
1236
+ if (q.id) return env.attestation_id.toLowerCase() === q.id.trim().toLowerCase();
1237
+ let signedAddress;
1238
+ try {
1239
+ signedAddress = parseCanonicalMessage(env.message).core.address;
1240
+ } catch {
1241
+ return false;
1242
+ }
1243
+ if (signedAddress !== env.address) return false;
1244
+ if (q.addr) return signedAddress === q.addr;
1245
+ if (q.identity) {
1246
+ const want = identityKey(q.identity);
1247
+ return want !== null && signedIdentities(env).some((b) => identityKey(b) === want);
1248
+ }
1249
+ return false;
1250
+ }
1251
+ async function backsOtherIdentity(env, identity, relays) {
1252
+ const want = identityKey(identity);
1253
+ const protocol = identity.protocol.trim().toLowerCase();
1254
+ const siblings = await getAttestationsForAddress(env.address, relays);
1255
+ for (const e of [env, ...siblings]) {
1256
+ if (!signedMatch(e, { addr: env.address })) continue;
1257
+ for (const b of signedIdentities(e)) {
1258
+ if (b.protocol.trim().toLowerCase() !== protocol) continue;
1259
+ if (identityKey(b) !== want) return true;
1260
+ }
1261
+ }
1262
+ return false;
1263
+ }
1187
1264
  var AUTH_HEADER = "orangecheck-auth";
1188
1265
  var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1189
1266
  function randomNonce() {
@@ -1638,8 +1715,8 @@ function getGistCreationUrl() {
1638
1715
  return "https://gist.github.com/";
1639
1716
  }
1640
1717
  var log7 = createLogger("identity-verification/nostr");
1641
- var HEX_64_RE = /^[0-9a-f]{64}$/;
1642
- function toHex(bytes) {
1718
+ var HEX_64_RE2 = /^[0-9a-f]{64}$/;
1719
+ function toHex2(bytes) {
1643
1720
  let out = "";
1644
1721
  for (const b of bytes) out += b.toString(16).padStart(2, "0");
1645
1722
  return out;
@@ -1653,22 +1730,9 @@ function fromHex(hex) {
1653
1730
  return out;
1654
1731
  }
1655
1732
  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));
1733
+ const hex = nostrPubkeyToHex(npub);
1734
+ if (!hex) throw new Error(`not an npub: ${npub.trim().slice(0, 12)}\u2026`);
1735
+ return hex;
1672
1736
  }
1673
1737
  async function queryNostrEvents(options) {
1674
1738
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1740,7 +1804,7 @@ async function queryNostrEvents(options) {
1740
1804
  }
1741
1805
  async function verifyNostrEventSignature(event) {
1742
1806
  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)) {
1807
+ 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
1808
  return false;
1745
1809
  }
1746
1810
  const preimage = JSON.stringify([
@@ -1751,7 +1815,7 @@ async function verifyNostrEventSignature(event) {
1751
1815
  event.tags ?? [],
1752
1816
  event.content ?? ""
1753
1817
  ]);
1754
- const computedId = toHex(sha256(new TextEncoder().encode(preimage)));
1818
+ const computedId = toHex2(sha256(new TextEncoder().encode(preimage)));
1755
1819
  if (computedId !== event.id.toLowerCase()) {
1756
1820
  log7.warn(
1757
1821
  { eventId: event.id, computedId },
@@ -2119,6 +2183,6 @@ var STATUS_META = {
2119
2183
  }
2120
2184
  };
2121
2185
 
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 };
2186
+ 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, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
2123
2187
  //# sourceMappingURL=index.mjs.map
2124
2188
  //# sourceMappingURL=index.mjs.map