@orangecheck/sdk 1.16.1 → 1.17.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
@@ -223,6 +223,44 @@ function rfc3339Ms(value) {
223
223
  return Number.isNaN(ms) ? null : ms;
224
224
  }
225
225
 
226
+ // src/options.ts
227
+ var CHECK_OPTIONS = /* @__PURE__ */ new Set([
228
+ "addr",
229
+ "id",
230
+ "identity",
231
+ "minSats",
232
+ "minDays",
233
+ "relays",
234
+ "verifyOptions"
235
+ ]);
236
+ var VERIFY_OPTIONS = /* @__PURE__ */ new Set([
237
+ "testMode",
238
+ "demoMode",
239
+ "expectedAud",
240
+ "esploraMainnetBase",
241
+ "esploraSignetBase",
242
+ "esploraTestnetBase",
243
+ "coverSats"
244
+ ]);
245
+ function refuseUnknown(where, opts, known) {
246
+ if (opts === null || typeof opts !== "object") {
247
+ throw new TypeError(`@orangecheck/sdk: ${where} must be an object`);
248
+ }
249
+ const unknown = Object.keys(opts).filter((k) => !known.has(k));
250
+ if (unknown.length > 0) {
251
+ throw new TypeError(
252
+ `@orangecheck/sdk: ${where}: unknown option ${unknown.join(", ")}. It reads only ${[...known].join(", ")}.`
253
+ );
254
+ }
255
+ }
256
+ function checkCheckParams(params) {
257
+ refuseUnknown("check()", params, CHECK_OPTIONS);
258
+ if (params.verifyOptions !== void 0) checkVerifyOptions(params.verifyOptions, "verifyOptions");
259
+ }
260
+ function checkVerifyOptions(opts, where = "verify()") {
261
+ refuseUnknown(where, opts, VERIFY_OPTIONS);
262
+ }
263
+
226
264
  // src/scoring.ts
227
265
  function scoreV0(sats, days) {
228
266
  if (sats <= 0 || days <= 0) return 0;
@@ -308,11 +346,15 @@ function parseCanonicalMessage(msg) {
308
346
  "ack"
309
347
  ]);
310
348
  const extensions = {};
349
+ const keys = /* @__PURE__ */ new Set();
350
+ const repeated = [];
311
351
  for (const line of lines) {
312
352
  const idx = line.indexOf(":");
313
353
  if (idx === -1) continue;
314
354
  const key = line.slice(0, idx).trim().toLowerCase();
315
355
  const val = line.slice(idx + 1).trim();
356
+ if (keys.has(key)) repeated.push(key);
357
+ keys.add(key);
316
358
  if (!coreKeys.has(key) && val) extensions[key] = val;
317
359
  }
318
360
  const network = extensions["network"] || "mainnet";
@@ -321,6 +363,7 @@ function parseCanonicalMessage(msg) {
321
363
  return {
322
364
  core: { address, identities, issued_at },
323
365
  expires,
366
+ repeated,
324
367
  network,
325
368
  extensions,
326
369
  bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
@@ -429,6 +472,7 @@ function computeMetrics(utxos, bondAmount, coverSats) {
429
472
  return { sats_bonded, days_unspent, confirmed_balance };
430
473
  }
431
474
  async function verify(input, opts = {}) {
475
+ checkVerifyOptions(opts);
432
476
  const codes = [];
433
477
  let network = "mainnet";
434
478
  try {
@@ -439,6 +483,10 @@ async function verify(input, opts = {}) {
439
483
  const parsed = parseCanonicalMessage(input.msg);
440
484
  network = parsed.network || "mainnet";
441
485
  const attestation_id = await generateAttestationId(input.msg);
486
+ if (parsed.repeated.length > 0) {
487
+ codes.push("decode_error");
488
+ return { ok: false, codes, network, attestation_id };
489
+ }
442
490
  if (parsed.core.address !== input.addr) {
443
491
  codes.push("bad_request");
444
492
  return { ok: false, codes, network, attestation_id };
@@ -696,188 +744,98 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
696
744
  log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
697
745
  return { success, failed };
698
746
  }
699
- async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
747
+ var PAGE_LIMIT = 500;
748
+ var MAX_PAGES = 4;
749
+ async function querySubject(filter, relays, label) {
700
750
  const events = [];
701
- log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
702
751
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
703
- const queryPromises = relays.map(async (relayUrl) => {
704
- let ws = null;
705
- try {
706
- ws = new WebSocket(relayUrl);
707
- const wsRef = ws;
708
- await new Promise((resolve, reject) => {
709
- const timeout = setTimeout(() => {
710
- wsRef.close();
711
- reject(new Error("Query timeout"));
712
- }, budgetFor(deadlineAt));
713
- const cleanup = () => {
714
- clearTimeout(timeout);
715
- wsRef.close();
716
- };
717
- wsRef.onopen = () => {
718
- const subscriptionId = `ochk_${Date.now()}`;
719
- wsRef.send(
720
- JSON.stringify([
721
- "REQ",
722
- subscriptionId,
723
- {
724
- kinds: [30078],
725
- "#d": [attestationId],
726
- limit: 1
752
+ await Promise.allSettled(
753
+ relays.map(async (relayUrl) => {
754
+ let ws = null;
755
+ try {
756
+ ws = new WebSocket(relayUrl);
757
+ const wsRef = ws;
758
+ await new Promise((resolve, reject) => {
759
+ const timeout = setTimeout(() => {
760
+ wsRef.close();
761
+ reject(new Error("Query timeout"));
762
+ }, budgetFor(deadlineAt));
763
+ const seen = /* @__PURE__ */ new Set();
764
+ let page = 0;
765
+ let inPage = 0;
766
+ let newInPage = 0;
767
+ let oldest = Infinity;
768
+ const ask = () => {
769
+ page++;
770
+ inPage = 0;
771
+ newInPage = 0;
772
+ const until = Number.isFinite(oldest) ? { until: oldest } : {};
773
+ wsRef.send(
774
+ JSON.stringify([
775
+ "REQ",
776
+ `ochk_${label}_${Date.now()}_${page}`,
777
+ { ...filter, ...until, limit: PAGE_LIMIT }
778
+ ])
779
+ );
780
+ };
781
+ wsRef.onopen = ask;
782
+ wsRef.onmessage = (msg) => {
783
+ try {
784
+ const data = JSON.parse(msg.data);
785
+ if (data[0] === "EVENT") {
786
+ const event = data[2];
787
+ const key = event.id ?? JSON.stringify(event);
788
+ inPage++;
789
+ if (!seen.has(key)) {
790
+ seen.add(key);
791
+ newInPage++;
792
+ events.push(event);
793
+ }
794
+ if (typeof event.created_at === "number") {
795
+ oldest = Math.min(oldest, event.created_at);
796
+ }
797
+ } else if (data[0] === "EOSE") {
798
+ wsRef.send(JSON.stringify(["CLOSE", data[1]]));
799
+ if (inPage >= PAGE_LIMIT && newInPage > 0 && page < MAX_PAGES) {
800
+ ask();
801
+ } else {
802
+ clearTimeout(timeout);
803
+ wsRef.close();
804
+ resolve();
805
+ }
727
806
  }
728
- ])
729
- );
730
- };
731
- wsRef.onmessage = (msg) => {
732
- try {
733
- const data = JSON.parse(msg.data);
734
- if (data[0] === "EVENT") {
735
- events.push(data[2]);
736
- } else if (data[0] === "EOSE") {
737
- cleanup();
738
- resolve();
807
+ } catch (err) {
808
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
739
809
  }
740
- } catch (err) {
741
- log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
742
- }
743
- };
744
- wsRef.onerror = (err) => {
745
- log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
746
- cleanup();
747
- reject(err);
748
- };
749
- wsRef.onclose = () => {
750
- clearTimeout(timeout);
751
- };
752
- });
753
- } catch (err) {
754
- log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
755
- } finally {
756
- if (ws && ws.readyState !== WebSocket.CLOSED) {
757
- ws.close();
810
+ };
811
+ wsRef.onerror = (err) => {
812
+ clearTimeout(timeout);
813
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
814
+ reject(err);
815
+ };
816
+ });
817
+ } catch (err) {
818
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
819
+ } finally {
820
+ if (ws && ws.readyState !== WebSocket.CLOSED) ws.close();
758
821
  }
759
- }
760
- });
761
- await Promise.allSettled(queryPromises);
762
- log2.info({ eventCount: events.length }, "Query complete");
822
+ })
823
+ );
824
+ log2.info({ label, eventCount: events.length }, "Query complete");
763
825
  return events;
764
826
  }
827
+ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
828
+ log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
829
+ return querySubject({ kinds: [30078], "#d": [attestationId] }, relays, "id");
830
+ }
765
831
  async function queryByAddress(address, relays = DEFAULT_RELAYS) {
766
- const events = [];
767
832
  log2.info({ address, relayCount: relays.length }, "Querying relays for address");
768
- const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
769
- const queryPromises = relays.map(async (relayUrl) => {
770
- try {
771
- const ws = new WebSocket(relayUrl);
772
- await new Promise((resolve, reject) => {
773
- const timeout = setTimeout(() => {
774
- ws.close();
775
- reject(new Error("Query timeout"));
776
- }, budgetFor(deadlineAt));
777
- ws.onopen = () => {
778
- const subscriptionId = `ochk_addr_${Date.now()}`;
779
- ws.send(
780
- JSON.stringify([
781
- "REQ",
782
- subscriptionId,
783
- {
784
- kinds: [30078],
785
- // `#t`, NOT `#address`: NIP-12 relays index
786
- // single-letter tags only, so `#address`
787
- // matched nothing on any relay and this
788
- // function returned empty for every real
789
- // attestation. attest.ochk.io's verifier has
790
- // queried `#t` since the same fix landed
791
- // there; this is the back-port.
792
- "#t": [address],
793
- limit: 50
794
- }
795
- ])
796
- );
797
- };
798
- ws.onmessage = (msg) => {
799
- try {
800
- const data = JSON.parse(msg.data);
801
- if (data[0] === "EVENT") {
802
- events.push(data[2]);
803
- } else if (data[0] === "EOSE") {
804
- clearTimeout(timeout);
805
- ws.close();
806
- resolve();
807
- }
808
- } catch (err) {
809
- log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
810
- }
811
- };
812
- ws.onerror = (err) => {
813
- clearTimeout(timeout);
814
- log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
815
- reject(err);
816
- };
817
- });
818
- } catch (err) {
819
- log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
820
- }
821
- });
822
- await Promise.allSettled(queryPromises);
823
- log2.info({ eventCount: events.length }, "Query complete");
824
- return events;
833
+ return querySubject({ kinds: [30078], "#t": [address] }, relays, "addr");
825
834
  }
826
835
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
827
- const events = [];
828
- const identityTag = `${protocol}:${identifier}`;
829
- const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
836
+ const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [`${protocol}:${identifier}`];
830
837
  log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
831
- const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
832
- const queryPromises = relays.map(async (relayUrl) => {
833
- try {
834
- const ws = new WebSocket(relayUrl);
835
- await new Promise((resolve, reject) => {
836
- const timeout = setTimeout(() => {
837
- ws.close();
838
- reject(new Error("Query timeout"));
839
- }, budgetFor(deadlineAt));
840
- ws.onopen = () => {
841
- const subscriptionId = `ochk_identity_${Date.now()}`;
842
- ws.send(
843
- JSON.stringify([
844
- "REQ",
845
- subscriptionId,
846
- {
847
- kinds: [30078],
848
- "#i": identityTags,
849
- limit: 10
850
- }
851
- ])
852
- );
853
- };
854
- ws.onmessage = (msg) => {
855
- try {
856
- const data = JSON.parse(msg.data);
857
- if (data[0] === "EVENT") {
858
- events.push(data[2]);
859
- } else if (data[0] === "EOSE") {
860
- clearTimeout(timeout);
861
- ws.close();
862
- resolve();
863
- }
864
- } catch (err) {
865
- log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
866
- }
867
- };
868
- ws.onerror = (err) => {
869
- clearTimeout(timeout);
870
- log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
871
- reject(err);
872
- };
873
- });
874
- } catch (err) {
875
- log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
876
- }
877
- });
878
- await Promise.allSettled(queryPromises);
879
- log2.info({ eventCount: events.length }, "Query complete");
880
- return events;
838
+ return querySubject({ kinds: [30078], "#i": identityTags }, relays, "identity");
881
839
  }
882
840
  async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
883
841
  const events = [];
@@ -960,6 +918,7 @@ function signedEnvelope(envelope) {
960
918
  return null;
961
919
  }
962
920
  if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
921
+ if (parsed.repeated.length > 0) return null;
963
922
  const { expires_at: _copy, ...rest } = envelope;
964
923
  return {
965
924
  ...rest,
@@ -1191,8 +1150,8 @@ function extractAttestationIdFromUrl(url) {
1191
1150
  }
1192
1151
 
1193
1152
  // src/check.ts
1194
- var MAX_CANDIDATES = 20;
1195
1153
  async function check(params) {
1154
+ checkCheckParams(params);
1196
1155
  const {
1197
1156
  addr,
1198
1157
  id,
@@ -1219,7 +1178,7 @@ async function check(params) {
1219
1178
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1220
1179
  }
1221
1180
  let env = envelopes[0];
1222
- for (const candidate of envelopes.slice(0, MAX_CANDIDATES)) {
1181
+ for (const candidate of envelopes) {
1223
1182
  if (await signedBy(candidate)) {
1224
1183
  env = candidate;
1225
1184
  break;