@orangecheck/sdk 1.6.0 → 1.8.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/attestation.d.mts +1 -1
- package/dist/attestation.d.ts +1 -1
- package/dist/attestation.js +38 -2
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +38 -2
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.d.mts +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/challenge.d.mts +1 -1
- package/dist/challenge.d.ts +1 -1
- package/dist/identity.d.mts +1 -1
- package/dist/identity.d.ts +1 -1
- package/dist/identity.js +26 -20
- package/dist/identity.js.map +1 -1
- package/dist/identity.mjs +26 -20
- package/dist/identity.mjs.map +1 -1
- package/dist/index.d.mts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +165 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +163 -35
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.d.mts +3 -2
- package/dist/nostr.d.ts +3 -2
- package/dist/nostr.js +95 -2
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +95 -3
- package/dist/nostr.mjs.map +1 -1
- package/dist/{types-BJKDroFv.d.mts → types-D9Tb1roj.d.mts} +1 -1
- package/dist/{types-BJKDroFv.d.ts → types-D9Tb1roj.d.ts} +1 -1
- package/dist/verify.d.mts +11 -2
- package/dist/verify.d.ts +11 -2
- package/dist/verify.js +1 -0
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +1 -1
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
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":
|
|
518
|
+
"#i": identityTags,
|
|
493
519
|
limit: 10
|
|
494
520
|
}
|
|
495
521
|
])
|
|
@@ -523,6 +549,70 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
|
523
549
|
log.info({ eventCount: events.length }, "Query complete");
|
|
524
550
|
return events;
|
|
525
551
|
}
|
|
552
|
+
async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
553
|
+
const events = [];
|
|
554
|
+
log.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
|
|
555
|
+
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
556
|
+
const queryPromises = relays.map(async (relayUrl) => {
|
|
557
|
+
try {
|
|
558
|
+
const ws = new WebSocket(relayUrl);
|
|
559
|
+
await new Promise((resolve, reject) => {
|
|
560
|
+
const timeout = setTimeout(() => {
|
|
561
|
+
ws.close();
|
|
562
|
+
reject(new Error("Query timeout"));
|
|
563
|
+
}, budgetFor(deadlineAt));
|
|
564
|
+
ws.onopen = () => {
|
|
565
|
+
const subscriptionId = `ochk_recent_${Date.now()}`;
|
|
566
|
+
ws.send(
|
|
567
|
+
JSON.stringify([
|
|
568
|
+
"REQ",
|
|
569
|
+
subscriptionId,
|
|
570
|
+
{ kinds: [ATTESTATION_EVENT_KIND], "#t": ["oc-attest"], limit }
|
|
571
|
+
])
|
|
572
|
+
);
|
|
573
|
+
};
|
|
574
|
+
ws.onmessage = (msg) => {
|
|
575
|
+
try {
|
|
576
|
+
const data = JSON.parse(msg.data);
|
|
577
|
+
if (data[0] === "EVENT") {
|
|
578
|
+
const ev = data[2];
|
|
579
|
+
const scheme = ev.tags?.find((t) => t[0] === "scheme")?.[1];
|
|
580
|
+
const dTag = ev.tags?.find((t) => t[0] === "d")?.[1];
|
|
581
|
+
if ((scheme === "bip322" || scheme === "legacy") && dTag) {
|
|
582
|
+
events.push(ev);
|
|
583
|
+
}
|
|
584
|
+
} else if (data[0] === "EOSE") {
|
|
585
|
+
clearTimeout(timeout);
|
|
586
|
+
ws.close();
|
|
587
|
+
resolve();
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
log.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
ws.onerror = (err) => {
|
|
594
|
+
clearTimeout(timeout);
|
|
595
|
+
log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
596
|
+
reject(err);
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
log.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
await Promise.allSettled(queryPromises);
|
|
604
|
+
const seen = /* @__PURE__ */ new Set();
|
|
605
|
+
const envelopes = [];
|
|
606
|
+
for (const ev of events) {
|
|
607
|
+
const envelope = parseAttestationFromEvent(ev);
|
|
608
|
+
if (!envelope || seen.has(envelope.attestation_id)) continue;
|
|
609
|
+
if (!await verifyAttestationEnvelope(envelope)) continue;
|
|
610
|
+
seen.add(envelope.attestation_id);
|
|
611
|
+
envelopes.push(envelope);
|
|
612
|
+
}
|
|
613
|
+
log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
|
|
614
|
+
return envelopes;
|
|
615
|
+
}
|
|
526
616
|
async function verifyAttestationEnvelope(envelope) {
|
|
527
617
|
try {
|
|
528
618
|
const encoder = new TextEncoder();
|
|
@@ -655,6 +745,12 @@ async function publishAttestation(options) {
|
|
|
655
745
|
);
|
|
656
746
|
return result;
|
|
657
747
|
}
|
|
748
|
+
function eventIndexesEnvelope(event, envelope) {
|
|
749
|
+
const d = event.tags.find((t) => t[0] === "d")?.[1];
|
|
750
|
+
const id = envelope.attestation_id;
|
|
751
|
+
if (d !== id && d !== `orangecheck:${id}`) return false;
|
|
752
|
+
return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
|
|
753
|
+
}
|
|
658
754
|
async function discoverAttestations(options) {
|
|
659
755
|
const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
|
|
660
756
|
let events = [];
|
|
@@ -673,7 +769,9 @@ async function discoverAttestations(options) {
|
|
|
673
769
|
const envelopes = [];
|
|
674
770
|
for (const event of events) {
|
|
675
771
|
const envelope = parseAttestationFromEvent(event);
|
|
676
|
-
if (envelope) {
|
|
772
|
+
if (envelope && !eventIndexesEnvelope(event, envelope)) {
|
|
773
|
+
log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
|
|
774
|
+
} else if (envelope) {
|
|
677
775
|
const isValid = await verifyAttestationEnvelope(envelope);
|
|
678
776
|
if (isValid) {
|
|
679
777
|
envelopes.push(envelope);
|
|
@@ -1157,18 +1255,15 @@ async function check(params) {
|
|
|
1157
1255
|
if (!addr && !id && !identity) {
|
|
1158
1256
|
return { ok: false, sats: 0, days: 0, score: 0, reasons: ["bad_request"] };
|
|
1159
1257
|
}
|
|
1160
|
-
let
|
|
1258
|
+
let found = [];
|
|
1161
1259
|
if (id) {
|
|
1162
|
-
|
|
1260
|
+
found = await discoverAttestations({ attestationId: id, relays });
|
|
1163
1261
|
} else if (addr) {
|
|
1164
|
-
|
|
1262
|
+
found = await getAttestationsForAddress(addr, relays);
|
|
1165
1263
|
} else if (identity) {
|
|
1166
|
-
|
|
1167
|
-
identity.protocol,
|
|
1168
|
-
identity.identifier,
|
|
1169
|
-
relays
|
|
1170
|
-
);
|
|
1264
|
+
found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
|
|
1171
1265
|
}
|
|
1266
|
+
const envelopes = found.filter((e) => signedMatch(e, { addr, id, identity }));
|
|
1172
1267
|
if (envelopes.length === 0) {
|
|
1173
1268
|
return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
|
|
1174
1269
|
}
|
|
@@ -1177,6 +1272,7 @@ async function check(params) {
|
|
|
1177
1272
|
if (!env) {
|
|
1178
1273
|
return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
|
|
1179
1274
|
}
|
|
1275
|
+
const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
|
|
1180
1276
|
const outcome = await verify2(
|
|
1181
1277
|
{
|
|
1182
1278
|
addr: env.address,
|
|
@@ -1191,9 +1287,10 @@ async function check(params) {
|
|
|
1191
1287
|
const score = outcome.metrics?.score ?? 0;
|
|
1192
1288
|
const reasons = [];
|
|
1193
1289
|
if (!outcome.ok) reasons.push(...outcome.codes);
|
|
1290
|
+
if (shared) reasons.push("stake_shared");
|
|
1194
1291
|
if (sats < minSats) reasons.push("below_min_sats");
|
|
1195
1292
|
if (days < minDays) reasons.push("below_min_days");
|
|
1196
|
-
const ok = outcome.ok && sats >= minSats && days >= minDays;
|
|
1293
|
+
const ok = outcome.ok && !shared && sats >= minSats && days >= minDays;
|
|
1197
1294
|
return {
|
|
1198
1295
|
ok,
|
|
1199
1296
|
sats,
|
|
@@ -1206,6 +1303,50 @@ async function check(params) {
|
|
|
1206
1303
|
...reasons.length ? { reasons } : {}
|
|
1207
1304
|
};
|
|
1208
1305
|
}
|
|
1306
|
+
function identityKey(b) {
|
|
1307
|
+
const protocol = b.protocol.trim().toLowerCase();
|
|
1308
|
+
if (protocol === "nostr") {
|
|
1309
|
+
const hex = nostrPubkeyToHex(b.identifier);
|
|
1310
|
+
return hex ? `nostr:${hex}` : null;
|
|
1311
|
+
}
|
|
1312
|
+
return `${protocol}:${b.identifier.trim()}`;
|
|
1313
|
+
}
|
|
1314
|
+
function signedIdentities(env) {
|
|
1315
|
+
try {
|
|
1316
|
+
return parseCanonicalMessage(env.message).core.identities;
|
|
1317
|
+
} catch {
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function signedMatch(env, q) {
|
|
1322
|
+
if (q.id) return env.attestation_id.toLowerCase() === q.id.trim().toLowerCase();
|
|
1323
|
+
let signedAddress;
|
|
1324
|
+
try {
|
|
1325
|
+
signedAddress = parseCanonicalMessage(env.message).core.address;
|
|
1326
|
+
} catch {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
if (signedAddress !== env.address) return false;
|
|
1330
|
+
if (q.addr) return signedAddress === q.addr;
|
|
1331
|
+
if (q.identity) {
|
|
1332
|
+
const want = identityKey(q.identity);
|
|
1333
|
+
return want !== null && signedIdentities(env).some((b) => identityKey(b) === want);
|
|
1334
|
+
}
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
async function backsOtherIdentity(env, identity, relays) {
|
|
1338
|
+
const want = identityKey(identity);
|
|
1339
|
+
const protocol = identity.protocol.trim().toLowerCase();
|
|
1340
|
+
const siblings = await getAttestationsForAddress(env.address, relays);
|
|
1341
|
+
for (const e of [env, ...siblings]) {
|
|
1342
|
+
if (!signedMatch(e, { addr: env.address })) continue;
|
|
1343
|
+
for (const b of signedIdentities(e)) {
|
|
1344
|
+
if (b.protocol.trim().toLowerCase() !== protocol) continue;
|
|
1345
|
+
if (identityKey(b) !== want) return true;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1209
1350
|
var AUTH_HEADER = "orangecheck-auth";
|
|
1210
1351
|
var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
|
|
1211
1352
|
function randomNonce() {
|
|
@@ -1660,8 +1801,8 @@ function getGistCreationUrl() {
|
|
|
1660
1801
|
return "https://gist.github.com/";
|
|
1661
1802
|
}
|
|
1662
1803
|
var log7 = createLogger("identity-verification/nostr");
|
|
1663
|
-
var
|
|
1664
|
-
function
|
|
1804
|
+
var HEX_64_RE2 = /^[0-9a-f]{64}$/;
|
|
1805
|
+
function toHex2(bytes) {
|
|
1665
1806
|
let out = "";
|
|
1666
1807
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1667
1808
|
return out;
|
|
@@ -1675,22 +1816,9 @@ function fromHex(hex) {
|
|
|
1675
1816
|
return out;
|
|
1676
1817
|
}
|
|
1677
1818
|
function npubToHex(npub) {
|
|
1678
|
-
const
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
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));
|
|
1819
|
+
const hex = nostrPubkeyToHex(npub);
|
|
1820
|
+
if (!hex) throw new Error(`not an npub: ${npub.trim().slice(0, 12)}\u2026`);
|
|
1821
|
+
return hex;
|
|
1694
1822
|
}
|
|
1695
1823
|
async function queryNostrEvents(options) {
|
|
1696
1824
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1762,7 +1890,7 @@ async function queryNostrEvents(options) {
|
|
|
1762
1890
|
}
|
|
1763
1891
|
async function verifyNostrEventSignature(event) {
|
|
1764
1892
|
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 || !
|
|
1893
|
+
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
1894
|
return false;
|
|
1767
1895
|
}
|
|
1768
1896
|
const preimage = JSON.stringify([
|
|
@@ -1773,7 +1901,7 @@ async function verifyNostrEventSignature(event) {
|
|
|
1773
1901
|
event.tags ?? [],
|
|
1774
1902
|
event.content ?? ""
|
|
1775
1903
|
]);
|
|
1776
|
-
const computedId =
|
|
1904
|
+
const computedId = toHex2(sha2_js.sha256(new TextEncoder().encode(preimage)));
|
|
1777
1905
|
if (computedId !== event.id.toLowerCase()) {
|
|
1778
1906
|
log7.warn(
|
|
1779
1907
|
{ eventId: event.id, computedId },
|
|
@@ -2091,12 +2219,12 @@ var STATUS_META = {
|
|
|
2091
2219
|
},
|
|
2092
2220
|
bond_zero: {
|
|
2093
2221
|
label: "No confirmed bitcoin found",
|
|
2094
|
-
detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a
|
|
2222
|
+
detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a stake.",
|
|
2095
2223
|
severity: "warn"
|
|
2096
2224
|
},
|
|
2097
2225
|
bond_pending: {
|
|
2098
2226
|
label: "Pending transactions detected",
|
|
2099
|
-
detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your
|
|
2227
|
+
detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your stake.",
|
|
2100
2228
|
severity: "info"
|
|
2101
2229
|
},
|
|
2102
2230
|
bond_insufficient: {
|
|
@@ -2182,6 +2310,8 @@ exports.isNostrIdentityVerified = isNostrIdentityVerified;
|
|
|
2182
2310
|
exports.isValidDomain = isValidDomain;
|
|
2183
2311
|
exports.isValidTweetUrl = isValidTweetUrl;
|
|
2184
2312
|
exports.issueChallenge = issueChallenge;
|
|
2313
|
+
exports.nostrIdentifierForms = nostrIdentifierForms;
|
|
2314
|
+
exports.nostrPubkeyToHex = nostrPubkeyToHex;
|
|
2185
2315
|
exports.parseAttestationFromEvent = parseAttestationFromEvent;
|
|
2186
2316
|
exports.parseIdentities = parseIdentities;
|
|
2187
2317
|
exports.publishAttestation = publishAttestation;
|
|
@@ -2189,6 +2319,7 @@ exports.publishToRelays = publishToRelays;
|
|
|
2189
2319
|
exports.queryByAddress = queryByAddress;
|
|
2190
2320
|
exports.queryByAttestationId = queryByAttestationId;
|
|
2191
2321
|
exports.queryByIdentity = queryByIdentity;
|
|
2322
|
+
exports.queryRecent = queryRecent;
|
|
2192
2323
|
exports.requiresManualVerification = requiresManualVerification;
|
|
2193
2324
|
exports.signNostrEvent = signNostrEvent;
|
|
2194
2325
|
exports.supportsAutomaticVerification = supportsAutomaticVerification;
|