@orangecheck/sdk 1.16.0 → 1.16.2

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
@@ -213,6 +213,7 @@ function logVerificationError(protocol, identifier, error, context) {
213
213
  }
214
214
 
215
215
  // src/utils/time.ts
216
+ var ISSUED_AHEAD_MS = 12e4;
216
217
  var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
217
218
  function rfc3339Ms(value) {
218
219
  if (!value || !RFC3339.test(value)) return null;
@@ -305,11 +306,15 @@ function parseCanonicalMessage(msg) {
305
306
  "ack"
306
307
  ]);
307
308
  const extensions = {};
309
+ const keys = /* @__PURE__ */ new Set();
310
+ const repeated = [];
308
311
  for (const line of lines) {
309
312
  const idx = line.indexOf(":");
310
313
  if (idx === -1) continue;
311
314
  const key = line.slice(0, idx).trim().toLowerCase();
312
315
  const val = line.slice(idx + 1).trim();
316
+ if (keys.has(key)) repeated.push(key);
317
+ keys.add(key);
313
318
  if (!coreKeys.has(key) && val) extensions[key] = val;
314
319
  }
315
320
  const network = extensions["network"] || "mainnet";
@@ -318,6 +323,7 @@ function parseCanonicalMessage(msg) {
318
323
  return {
319
324
  core: { address, identities, issued_at },
320
325
  expires,
326
+ repeated,
321
327
  network,
322
328
  extensions,
323
329
  bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
@@ -436,6 +442,10 @@ async function verify(input, opts = {}) {
436
442
  const parsed = parseCanonicalMessage(input.msg);
437
443
  network = parsed.network || "mainnet";
438
444
  const attestation_id = await generateAttestationId(input.msg);
445
+ if (parsed.repeated.length > 0) {
446
+ codes.push("decode_error");
447
+ return { ok: false, codes, network, attestation_id };
448
+ }
439
449
  if (parsed.core.address !== input.addr) {
440
450
  codes.push("bad_request");
441
451
  return { ok: false, codes, network, attestation_id };
@@ -451,6 +461,15 @@ async function verify(input, opts = {}) {
451
461
  return { ok: false, codes, network, attestation_id };
452
462
  }
453
463
  }
464
+ const issued = rfc3339Ms(parsed.core.issued_at);
465
+ if (issued === null) {
466
+ codes.push("decode_error");
467
+ return { ok: false, codes, network, attestation_id };
468
+ }
469
+ if (issued > Date.now() + ISSUED_AHEAD_MS) {
470
+ codes.push("not_yet_valid");
471
+ return { ok: false, codes, network, attestation_id };
472
+ }
454
473
  if (parsed.expires !== void 0) {
455
474
  const exp = rfc3339Ms(parsed.expires);
456
475
  if (exp === null || exp < Date.now()) {
@@ -684,188 +703,98 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
684
703
  log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
685
704
  return { success, failed };
686
705
  }
687
- async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
706
+ var PAGE_LIMIT = 500;
707
+ var MAX_PAGES = 4;
708
+ async function querySubject(filter, relays, label) {
688
709
  const events = [];
689
- log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
690
710
  const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
691
- const queryPromises = relays.map(async (relayUrl) => {
692
- let ws = null;
693
- try {
694
- ws = new WebSocket(relayUrl);
695
- const wsRef = ws;
696
- await new Promise((resolve, reject) => {
697
- const timeout = setTimeout(() => {
698
- wsRef.close();
699
- reject(new Error("Query timeout"));
700
- }, budgetFor(deadlineAt));
701
- const cleanup = () => {
702
- clearTimeout(timeout);
703
- wsRef.close();
704
- };
705
- wsRef.onopen = () => {
706
- const subscriptionId = `ochk_${Date.now()}`;
707
- wsRef.send(
708
- JSON.stringify([
709
- "REQ",
710
- subscriptionId,
711
- {
712
- kinds: [30078],
713
- "#d": [attestationId],
714
- limit: 1
711
+ await Promise.allSettled(
712
+ relays.map(async (relayUrl) => {
713
+ let ws = null;
714
+ try {
715
+ ws = new WebSocket(relayUrl);
716
+ const wsRef = ws;
717
+ await new Promise((resolve, reject) => {
718
+ const timeout = setTimeout(() => {
719
+ wsRef.close();
720
+ reject(new Error("Query timeout"));
721
+ }, budgetFor(deadlineAt));
722
+ const seen = /* @__PURE__ */ new Set();
723
+ let page = 0;
724
+ let inPage = 0;
725
+ let newInPage = 0;
726
+ let oldest = Infinity;
727
+ const ask = () => {
728
+ page++;
729
+ inPage = 0;
730
+ newInPage = 0;
731
+ const until = Number.isFinite(oldest) ? { until: oldest } : {};
732
+ wsRef.send(
733
+ JSON.stringify([
734
+ "REQ",
735
+ `ochk_${label}_${Date.now()}_${page}`,
736
+ { ...filter, ...until, limit: PAGE_LIMIT }
737
+ ])
738
+ );
739
+ };
740
+ wsRef.onopen = ask;
741
+ wsRef.onmessage = (msg) => {
742
+ try {
743
+ const data = JSON.parse(msg.data);
744
+ if (data[0] === "EVENT") {
745
+ const event = data[2];
746
+ const key = event.id ?? JSON.stringify(event);
747
+ inPage++;
748
+ if (!seen.has(key)) {
749
+ seen.add(key);
750
+ newInPage++;
751
+ events.push(event);
752
+ }
753
+ if (typeof event.created_at === "number") {
754
+ oldest = Math.min(oldest, event.created_at);
755
+ }
756
+ } else if (data[0] === "EOSE") {
757
+ wsRef.send(JSON.stringify(["CLOSE", data[1]]));
758
+ if (inPage >= PAGE_LIMIT && newInPage > 0 && page < MAX_PAGES) {
759
+ ask();
760
+ } else {
761
+ clearTimeout(timeout);
762
+ wsRef.close();
763
+ resolve();
764
+ }
715
765
  }
716
- ])
717
- );
718
- };
719
- wsRef.onmessage = (msg) => {
720
- try {
721
- const data = JSON.parse(msg.data);
722
- if (data[0] === "EVENT") {
723
- events.push(data[2]);
724
- } else if (data[0] === "EOSE") {
725
- cleanup();
726
- resolve();
766
+ } catch (err) {
767
+ log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
727
768
  }
728
- } catch (err) {
729
- log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
730
- }
731
- };
732
- wsRef.onerror = (err) => {
733
- log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
734
- cleanup();
735
- reject(err);
736
- };
737
- wsRef.onclose = () => {
738
- clearTimeout(timeout);
739
- };
740
- });
741
- } catch (err) {
742
- log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
743
- } finally {
744
- if (ws && ws.readyState !== WebSocket.CLOSED) {
745
- ws.close();
769
+ };
770
+ wsRef.onerror = (err) => {
771
+ clearTimeout(timeout);
772
+ log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
773
+ reject(err);
774
+ };
775
+ });
776
+ } catch (err) {
777
+ log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
778
+ } finally {
779
+ if (ws && ws.readyState !== WebSocket.CLOSED) ws.close();
746
780
  }
747
- }
748
- });
749
- await Promise.allSettled(queryPromises);
750
- log2.info({ eventCount: events.length }, "Query complete");
781
+ })
782
+ );
783
+ log2.info({ label, eventCount: events.length }, "Query complete");
751
784
  return events;
752
785
  }
786
+ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
787
+ log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
788
+ return querySubject({ kinds: [30078], "#d": [attestationId] }, relays, "id");
789
+ }
753
790
  async function queryByAddress(address, relays = DEFAULT_RELAYS) {
754
- const events = [];
755
791
  log2.info({ address, relayCount: relays.length }, "Querying relays for address");
756
- const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
757
- const queryPromises = relays.map(async (relayUrl) => {
758
- try {
759
- const ws = new WebSocket(relayUrl);
760
- await new Promise((resolve, reject) => {
761
- const timeout = setTimeout(() => {
762
- ws.close();
763
- reject(new Error("Query timeout"));
764
- }, budgetFor(deadlineAt));
765
- ws.onopen = () => {
766
- const subscriptionId = `ochk_addr_${Date.now()}`;
767
- ws.send(
768
- JSON.stringify([
769
- "REQ",
770
- subscriptionId,
771
- {
772
- kinds: [30078],
773
- // `#t`, NOT `#address`: NIP-12 relays index
774
- // single-letter tags only, so `#address`
775
- // matched nothing on any relay and this
776
- // function returned empty for every real
777
- // attestation. attest.ochk.io's verifier has
778
- // queried `#t` since the same fix landed
779
- // there; this is the back-port.
780
- "#t": [address],
781
- limit: 50
782
- }
783
- ])
784
- );
785
- };
786
- ws.onmessage = (msg) => {
787
- try {
788
- const data = JSON.parse(msg.data);
789
- if (data[0] === "EVENT") {
790
- events.push(data[2]);
791
- } else if (data[0] === "EOSE") {
792
- clearTimeout(timeout);
793
- ws.close();
794
- resolve();
795
- }
796
- } catch (err) {
797
- log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
798
- }
799
- };
800
- ws.onerror = (err) => {
801
- clearTimeout(timeout);
802
- log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
803
- reject(err);
804
- };
805
- });
806
- } catch (err) {
807
- log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
808
- }
809
- });
810
- await Promise.allSettled(queryPromises);
811
- log2.info({ eventCount: events.length }, "Query complete");
812
- return events;
792
+ return querySubject({ kinds: [30078], "#t": [address] }, relays, "addr");
813
793
  }
814
794
  async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
815
- const events = [];
816
- const identityTag = `${protocol}:${identifier}`;
817
- const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
795
+ const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [`${protocol}:${identifier}`];
818
796
  log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
819
- const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
820
- const queryPromises = relays.map(async (relayUrl) => {
821
- try {
822
- const ws = new WebSocket(relayUrl);
823
- await new Promise((resolve, reject) => {
824
- const timeout = setTimeout(() => {
825
- ws.close();
826
- reject(new Error("Query timeout"));
827
- }, budgetFor(deadlineAt));
828
- ws.onopen = () => {
829
- const subscriptionId = `ochk_identity_${Date.now()}`;
830
- ws.send(
831
- JSON.stringify([
832
- "REQ",
833
- subscriptionId,
834
- {
835
- kinds: [30078],
836
- "#i": identityTags,
837
- limit: 10
838
- }
839
- ])
840
- );
841
- };
842
- ws.onmessage = (msg) => {
843
- try {
844
- const data = JSON.parse(msg.data);
845
- if (data[0] === "EVENT") {
846
- events.push(data[2]);
847
- } else if (data[0] === "EOSE") {
848
- clearTimeout(timeout);
849
- ws.close();
850
- resolve();
851
- }
852
- } catch (err) {
853
- log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
854
- }
855
- };
856
- ws.onerror = (err) => {
857
- clearTimeout(timeout);
858
- log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
859
- reject(err);
860
- };
861
- });
862
- } catch (err) {
863
- log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
864
- }
865
- });
866
- await Promise.allSettled(queryPromises);
867
- log2.info({ eventCount: events.length }, "Query complete");
868
- return events;
797
+ return querySubject({ kinds: [30078], "#i": identityTags }, relays, "identity");
869
798
  }
870
799
  async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
871
800
  const events = [];
@@ -948,6 +877,7 @@ function signedEnvelope(envelope) {
948
877
  return null;
949
878
  }
950
879
  if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
880
+ if (parsed.repeated.length > 0) return null;
951
881
  const { expires_at: _copy, ...rest } = envelope;
952
882
  return {
953
883
  ...rest,
@@ -1179,8 +1109,6 @@ function extractAttestationIdFromUrl(url) {
1179
1109
  }
1180
1110
 
1181
1111
  // src/check.ts
1182
- var ISSUED_AHEAD_MS = 12e4;
1183
- var MAX_CANDIDATES = 20;
1184
1112
  async function check(params) {
1185
1113
  const {
1186
1114
  addr,
@@ -1208,7 +1136,7 @@ async function check(params) {
1208
1136
  return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1209
1137
  }
1210
1138
  let env = envelopes[0];
1211
- for (const candidate of envelopes.slice(0, MAX_CANDIDATES)) {
1139
+ for (const candidate of envelopes) {
1212
1140
  if (await signedBy(candidate)) {
1213
1141
  env = candidate;
1214
1142
  break;
@@ -2359,6 +2287,11 @@ var STATUS_META = {
2359
2287
  detail: "This signature has passed its expiration date. Generate a new signature with a fresh message.",
2360
2288
  severity: "error"
2361
2289
  },
2290
+ not_yet_valid: {
2291
+ label: "Not valid yet",
2292
+ detail: "The message is dated ahead of this clock. It counts once that time arrives, or once a clock that is wrong is set right.",
2293
+ severity: "error"
2294
+ },
2362
2295
  network_testmode: {
2363
2296
  label: "Test network detected",
2364
2297
  detail: "This signature uses testnet or signet. Test networks are for development only and cannot be verified in production mode.",