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