@orangecheck/sdk 0.1.1 → 0.1.3
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/README.md +7 -8
- package/dist/attestation.d.mts +1 -2
- package/dist/attestation.d.ts +1 -2
- package/dist/attestation.js +12 -28
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +13 -28
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.js +27 -5
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +27 -5
- package/dist/canonical.mjs.map +1 -1
- package/dist/challenge.js +10 -6
- package/dist/challenge.js.map +1 -1
- package/dist/challenge.mjs +10 -6
- package/dist/challenge.mjs.map +1 -1
- package/dist/identity.js +130 -44
- package/dist/identity.js.map +1 -1
- package/dist/identity.mjs +130 -44
- package/dist/identity.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +210 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +208 -75
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.js +5 -6
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +5 -6
- package/dist/nostr.mjs.map +1 -1
- package/dist/verify.js +44 -13
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +44 -13
- package/dist/verify.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import { Verifier } from 'bip322-js';
|
|
3
3
|
import * as bitcoinMessage from 'bitcoinjs-message';
|
|
4
|
+
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
5
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
6
|
+
import { bech32 } from '@scure/base';
|
|
4
7
|
|
|
5
8
|
// src/canonical.ts
|
|
6
9
|
function toLowerHex(bytes) {
|
|
@@ -15,15 +18,27 @@ function randomNonce16BHexLower() {
|
|
|
15
18
|
}
|
|
16
19
|
} catch {
|
|
17
20
|
}
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
throw new Error("crypto.getRandomValues unavailable \u2014 refusing to mint a weak nonce");
|
|
22
|
+
}
|
|
23
|
+
var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
24
|
+
var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
|
|
25
|
+
function assertSafeIdentity({ protocol, identifier }) {
|
|
26
|
+
if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
|
|
34
|
+
);
|
|
20
35
|
}
|
|
21
|
-
return toLowerHex(arr);
|
|
22
36
|
}
|
|
23
37
|
function formatIdentities(identities = []) {
|
|
24
38
|
if (identities.length === 0) {
|
|
25
39
|
return "";
|
|
26
40
|
}
|
|
41
|
+
for (const id of identities) assertSafeIdentity(id);
|
|
27
42
|
const formatted = identities.map(({ protocol, identifier }) => `${protocol}:${identifier}`).sort();
|
|
28
43
|
return formatted.join(",");
|
|
29
44
|
}
|
|
@@ -31,7 +46,10 @@ function parseIdentities(identitiesStr) {
|
|
|
31
46
|
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
32
47
|
return [];
|
|
33
48
|
}
|
|
34
|
-
|
|
49
|
+
if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
|
|
50
|
+
throw new Error("identities field contains forbidden characters (newline)");
|
|
51
|
+
}
|
|
52
|
+
const bindings = identitiesStr.split(",").map((binding) => {
|
|
35
53
|
const colonIndex = binding.indexOf(":");
|
|
36
54
|
if (colonIndex === -1) {
|
|
37
55
|
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
@@ -40,7 +58,14 @@ function parseIdentities(identitiesStr) {
|
|
|
40
58
|
protocol: binding.substring(0, colonIndex).trim(),
|
|
41
59
|
identifier: binding.substring(colonIndex + 1).trim()
|
|
42
60
|
};
|
|
43
|
-
})
|
|
61
|
+
});
|
|
62
|
+
for (const b of bindings) {
|
|
63
|
+
if (!b.protocol || !b.identifier) {
|
|
64
|
+
throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
|
|
65
|
+
}
|
|
66
|
+
assertSafeIdentity(b);
|
|
67
|
+
}
|
|
68
|
+
return bindings;
|
|
44
69
|
}
|
|
45
70
|
function buildCanonicalMessage({ address, identities }, extensions = {}) {
|
|
46
71
|
const nonce = randomNonce16BHexLower();
|
|
@@ -208,16 +233,15 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
208
233
|
ws = new WebSocket(relayUrl);
|
|
209
234
|
const wsRef = ws;
|
|
210
235
|
await new Promise((resolve, reject) => {
|
|
211
|
-
const
|
|
236
|
+
const deadline = setTimeout(() => {
|
|
212
237
|
wsRef.close();
|
|
213
|
-
reject(new Error("
|
|
214
|
-
},
|
|
238
|
+
reject(new Error("Relay publish timeout"));
|
|
239
|
+
}, 1e4);
|
|
215
240
|
const cleanup = () => {
|
|
216
|
-
clearTimeout(
|
|
241
|
+
clearTimeout(deadline);
|
|
217
242
|
wsRef.close();
|
|
218
243
|
};
|
|
219
244
|
wsRef.onopen = () => {
|
|
220
|
-
clearTimeout(timeout);
|
|
221
245
|
wsRef.send(JSON.stringify(["EVENT", event]));
|
|
222
246
|
};
|
|
223
247
|
wsRef.onmessage = (msg) => {
|
|
@@ -255,7 +279,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
255
279
|
reject(err);
|
|
256
280
|
};
|
|
257
281
|
wsRef.onclose = () => {
|
|
258
|
-
clearTimeout(
|
|
282
|
+
clearTimeout(deadline);
|
|
259
283
|
};
|
|
260
284
|
});
|
|
261
285
|
} catch (err) {
|
|
@@ -633,6 +657,24 @@ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT
|
|
|
633
657
|
log3.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
634
658
|
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
635
659
|
}
|
|
660
|
+
function formatIdentitiesForDisplay(identities) {
|
|
661
|
+
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
662
|
+
}
|
|
663
|
+
function getVerificationUrl(attestationId, baseUrl = "https://ochk.io") {
|
|
664
|
+
return `${baseUrl}/attest/${attestationId}`;
|
|
665
|
+
}
|
|
666
|
+
function extractAttestationIdFromUrl(url) {
|
|
667
|
+
try {
|
|
668
|
+
const u = new URL(url);
|
|
669
|
+
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
670
|
+
if (m) return m[1].toLowerCase();
|
|
671
|
+
const q = u.searchParams.get("id");
|
|
672
|
+
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
673
|
+
return null;
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
636
678
|
|
|
637
679
|
// src/scoring.ts
|
|
638
680
|
function scoreV0(sats, days) {
|
|
@@ -659,9 +701,6 @@ function computeScore(sats, days, config) {
|
|
|
659
701
|
|
|
660
702
|
// src/verify.ts
|
|
661
703
|
var log4 = createLogger("ocp/verify");
|
|
662
|
-
function scoreV02(sats, days) {
|
|
663
|
-
return computeScore(sats, days, { algorithm: "v0" });
|
|
664
|
-
}
|
|
665
704
|
function looksLikeHex(s) {
|
|
666
705
|
return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
|
|
667
706
|
}
|
|
@@ -777,7 +816,13 @@ async function verifySignature({
|
|
|
777
816
|
}
|
|
778
817
|
function parseCanonicalMessage(msg) {
|
|
779
818
|
const lines = msg.split("\n").map((l) => l.trim());
|
|
780
|
-
const getVal = (prefix) =>
|
|
819
|
+
const getVal = (prefix) => {
|
|
820
|
+
const needle = prefix + ":";
|
|
821
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
822
|
+
if (!hit) return void 0;
|
|
823
|
+
const idx = hit.indexOf(":");
|
|
824
|
+
return hit.slice(idx + 1).trim();
|
|
825
|
+
};
|
|
781
826
|
const address = getVal("address") || "";
|
|
782
827
|
const identitiesStr = getVal("identities") || "";
|
|
783
828
|
let identities = [];
|
|
@@ -832,8 +877,7 @@ async function getAddressUtxos(address, network, opts) {
|
|
|
832
877
|
const resp = await fetch(url, {
|
|
833
878
|
method: "GET",
|
|
834
879
|
headers: { Accept: "application/json" },
|
|
835
|
-
signal: AbortSignal.timeout(
|
|
836
|
-
// 10s timeout
|
|
880
|
+
signal: AbortSignal.timeout(5e3)
|
|
837
881
|
});
|
|
838
882
|
if (!resp.ok) {
|
|
839
883
|
throw new Error(`HTTP ${resp.status}`);
|
|
@@ -920,7 +964,7 @@ async function verify2(input, opts = {}) {
|
|
|
920
964
|
const metrics = {
|
|
921
965
|
sats_bonded,
|
|
922
966
|
days_unspent,
|
|
923
|
-
score:
|
|
967
|
+
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
924
968
|
// ≈ 55.26
|
|
925
969
|
};
|
|
926
970
|
codes.push("sig_ok_bip322");
|
|
@@ -938,13 +982,18 @@ async function verify2(input, opts = {}) {
|
|
|
938
982
|
codes.push("network_testmode");
|
|
939
983
|
return { ok: false, codes, network, attestation_id };
|
|
940
984
|
}
|
|
941
|
-
if (opts.expectedAud
|
|
942
|
-
|
|
985
|
+
if (opts.expectedAud) {
|
|
986
|
+
const aud = parsed.extensions["aud"];
|
|
987
|
+
if (!aud || aud !== opts.expectedAud) {
|
|
988
|
+
codes.push("aud_mismatch");
|
|
989
|
+
return { ok: false, codes, network, attestation_id };
|
|
990
|
+
}
|
|
943
991
|
}
|
|
944
992
|
if (parsed.extensions["expires"]) {
|
|
945
993
|
const exp = Date.parse(parsed.extensions["expires"]);
|
|
946
994
|
if (!Number.isNaN(exp) && exp < Date.now()) {
|
|
947
995
|
codes.push("expired");
|
|
996
|
+
return { ok: false, codes, network, attestation_id };
|
|
948
997
|
}
|
|
949
998
|
}
|
|
950
999
|
const sigCode = await verifySignature({
|
|
@@ -1011,7 +1060,7 @@ async function verify2(input, opts = {}) {
|
|
|
1011
1060
|
const metrics = {
|
|
1012
1061
|
sats_bonded,
|
|
1013
1062
|
days_unspent,
|
|
1014
|
-
score:
|
|
1063
|
+
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
1015
1064
|
};
|
|
1016
1065
|
return {
|
|
1017
1066
|
ok: true,
|
|
@@ -1022,7 +1071,7 @@ async function verify2(input, opts = {}) {
|
|
|
1022
1071
|
metrics
|
|
1023
1072
|
};
|
|
1024
1073
|
} catch (utxoError) {
|
|
1025
|
-
|
|
1074
|
+
log4.error({ err: utxoError }, "UTXO fetch failed");
|
|
1026
1075
|
codes.push("bad_request");
|
|
1027
1076
|
return { ok: false, codes, network, attestation_id };
|
|
1028
1077
|
}
|
|
@@ -1149,16 +1198,20 @@ function parseChallenge(message) {
|
|
|
1149
1198
|
throw new Error("not an orangecheck-auth challenge");
|
|
1150
1199
|
}
|
|
1151
1200
|
const out = { extensions: [] };
|
|
1152
|
-
const coreKeys =
|
|
1153
|
-
|
|
1201
|
+
const coreKeys = ["address", "nonce", "issued_at", "expires_at", "ack"];
|
|
1202
|
+
const coreKeySet = new Set(coreKeys);
|
|
1203
|
+
const seenCore = /* @__PURE__ */ new Set();
|
|
1154
1204
|
for (let i = 1; i < lines.length; i++) {
|
|
1155
1205
|
const line = lines[i] ?? "";
|
|
1156
1206
|
const sep = line.indexOf(": ");
|
|
1157
1207
|
if (sep === -1) continue;
|
|
1158
1208
|
const key = line.slice(0, sep);
|
|
1159
1209
|
const val = line.slice(sep + 2);
|
|
1160
|
-
if (
|
|
1161
|
-
seenCore
|
|
1210
|
+
if (coreKeySet.has(key)) {
|
|
1211
|
+
if (seenCore.has(key)) {
|
|
1212
|
+
throw new Error(`duplicate core key in challenge: ${key}`);
|
|
1213
|
+
}
|
|
1214
|
+
seenCore.add(key);
|
|
1162
1215
|
if (key === "address") out.address = val;
|
|
1163
1216
|
else if (key === "nonce") out.nonce = val;
|
|
1164
1217
|
else if (key === "issued_at") out.issuedAt = new Date(val);
|
|
@@ -1170,8 +1223,8 @@ function parseChallenge(message) {
|
|
|
1170
1223
|
if (key === "purpose") out.purpose = val;
|
|
1171
1224
|
}
|
|
1172
1225
|
}
|
|
1173
|
-
if (seenCore !==
|
|
1174
|
-
throw new Error(`challenge missing core lines (${seenCore}
|
|
1226
|
+
if (seenCore.size !== coreKeys.length) {
|
|
1227
|
+
throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);
|
|
1175
1228
|
}
|
|
1176
1229
|
return out;
|
|
1177
1230
|
}
|
|
@@ -1272,9 +1325,25 @@ var identityVerificationLimiter = createRateLimiter(5, 6e4);
|
|
|
1272
1325
|
|
|
1273
1326
|
// src/identity/dns.ts
|
|
1274
1327
|
var log5 = createLogger("identity-verification/dns");
|
|
1328
|
+
var PRIVATE_HOST_RE = /^(?:localhost|.*\.local|.*\.internal|.*\.localhost|.*\.lan|.*\.home|.*\.arpa|\[?::1\]?|\[?::\]?)$/i;
|
|
1329
|
+
var PRIVATE_IPV4_RE = /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|0\.|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|22[4-9]\.|23\d\.|24\d\.|25[0-5]\.)/;
|
|
1330
|
+
var WELL_KNOWN_TIMEOUT_MS = 5e3;
|
|
1331
|
+
function isSafePublicHost(host) {
|
|
1332
|
+
const h = host.toLowerCase();
|
|
1333
|
+
if (PRIVATE_HOST_RE.test(h)) return false;
|
|
1334
|
+
if (PRIVATE_IPV4_RE.test(h)) return false;
|
|
1335
|
+
if (h.startsWith("[") || /^[0-9a-f:]+$/.test(h)) return false;
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1275
1338
|
async function verifyDnsIdentity(attestationId, domain) {
|
|
1276
1339
|
log5.info({ attestationId, domain }, "Verifying DNS identity");
|
|
1277
1340
|
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
1341
|
+
if (!isValidDomain(cleanDomain)) {
|
|
1342
|
+
return { verified: false, error: `invalid domain format: ${cleanDomain}` };
|
|
1343
|
+
}
|
|
1344
|
+
if (!isSafePublicHost(cleanDomain)) {
|
|
1345
|
+
return { verified: false, error: "domain resolves to a disallowed private host" };
|
|
1346
|
+
}
|
|
1278
1347
|
const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
|
|
1279
1348
|
if (wellKnownResult.verified) {
|
|
1280
1349
|
return wellKnownResult;
|
|
@@ -1286,38 +1355,31 @@ async function verifyDnsIdentity(attestationId, domain) {
|
|
|
1286
1355
|
};
|
|
1287
1356
|
}
|
|
1288
1357
|
async function checkWellKnownFile(attestationId, domain) {
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
verified: true,
|
|
1311
|
-
method: "well-known",
|
|
1312
|
-
url
|
|
1313
|
-
};
|
|
1314
|
-
} else {
|
|
1315
|
-
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1316
|
-
}
|
|
1358
|
+
const url = `https://${domain}/.well-known/orangecheck.txt`;
|
|
1359
|
+
try {
|
|
1360
|
+
log5.info({ url }, "Checking well-known file");
|
|
1361
|
+
const response = await fetch(url, {
|
|
1362
|
+
method: "GET",
|
|
1363
|
+
headers: { Accept: "text/plain" },
|
|
1364
|
+
// Don't follow redirects to other domains
|
|
1365
|
+
redirect: "manual",
|
|
1366
|
+
signal: AbortSignal.timeout(WELL_KNOWN_TIMEOUT_MS)
|
|
1367
|
+
});
|
|
1368
|
+
if (response.ok) {
|
|
1369
|
+
const content = await response.text();
|
|
1370
|
+
if (content.includes(attestationId)) {
|
|
1371
|
+
log5.info({ domain, url }, "Found attestation ID in well-known file");
|
|
1372
|
+
return {
|
|
1373
|
+
verified: true,
|
|
1374
|
+
method: "well-known",
|
|
1375
|
+
url
|
|
1376
|
+
};
|
|
1377
|
+
} else {
|
|
1378
|
+
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1317
1379
|
}
|
|
1318
|
-
} catch (err) {
|
|
1319
|
-
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1320
1380
|
}
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1321
1383
|
}
|
|
1322
1384
|
return {
|
|
1323
1385
|
verified: false,
|
|
@@ -1390,14 +1452,32 @@ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
|
|
|
1390
1452
|
|
|
1391
1453
|
// src/identity/github.ts
|
|
1392
1454
|
var log6 = createLogger("identity-verification/github");
|
|
1455
|
+
var GITHUB_GIST_RAW_HOST = "gist.githubusercontent.com";
|
|
1456
|
+
var MAX_GISTS_SCANNED = 30;
|
|
1457
|
+
var MAX_FILES_PER_GIST = 10;
|
|
1458
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
1459
|
+
var GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
|
1460
|
+
function isSafeGistRawUrl(raw) {
|
|
1461
|
+
try {
|
|
1462
|
+
const u = new URL(raw);
|
|
1463
|
+
return u.protocol === "https:" && u.hostname === GITHUB_GIST_RAW_HOST;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1393
1468
|
async function verifyGitHubIdentity(attestationId, username) {
|
|
1394
1469
|
log6.info({ attestationId, username }, "Verifying GitHub identity");
|
|
1470
|
+
if (!GITHUB_USERNAME_RE.test(username)) {
|
|
1471
|
+
return { verified: false, error: "invalid GitHub username format" };
|
|
1472
|
+
}
|
|
1395
1473
|
try {
|
|
1396
|
-
const response = await fetch(
|
|
1397
|
-
|
|
1398
|
-
|
|
1474
|
+
const response = await fetch(
|
|
1475
|
+
`https://api.github.com/users/${encodeURIComponent(username)}/gists?per_page=${MAX_GISTS_SCANNED}`,
|
|
1476
|
+
{
|
|
1477
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
1478
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1399
1479
|
}
|
|
1400
|
-
|
|
1480
|
+
);
|
|
1401
1481
|
if (!response.ok) {
|
|
1402
1482
|
if (response.status === 404) {
|
|
1403
1483
|
return {
|
|
@@ -1412,7 +1492,7 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1412
1492
|
}
|
|
1413
1493
|
const gists = await response.json();
|
|
1414
1494
|
log6.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
|
|
1415
|
-
for (const gist of gists) {
|
|
1495
|
+
for (const gist of gists.slice(0, MAX_GISTS_SCANNED)) {
|
|
1416
1496
|
if (gist.description?.includes(attestationId)) {
|
|
1417
1497
|
log6.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
|
|
1418
1498
|
return {
|
|
@@ -1421,7 +1501,8 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1421
1501
|
gistId: gist.id
|
|
1422
1502
|
};
|
|
1423
1503
|
}
|
|
1424
|
-
|
|
1504
|
+
const files = Object.entries(gist.files).slice(0, MAX_FILES_PER_GIST);
|
|
1505
|
+
for (const [filename, file] of files) {
|
|
1425
1506
|
if (file.content && file.content.includes(attestationId)) {
|
|
1426
1507
|
log6.info(
|
|
1427
1508
|
{ username, gistId: gist.id, filename },
|
|
@@ -1433,8 +1514,17 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1433
1514
|
gistId: gist.id
|
|
1434
1515
|
};
|
|
1435
1516
|
}
|
|
1517
|
+
if (!isSafeGistRawUrl(file.raw_url)) {
|
|
1518
|
+
log6.warn(
|
|
1519
|
+
{ filename, rawUrl: file.raw_url },
|
|
1520
|
+
"skipping gist file: raw_url failed allow-list"
|
|
1521
|
+
);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1436
1524
|
try {
|
|
1437
|
-
const fileResponse = await fetch(file.raw_url
|
|
1525
|
+
const fileResponse = await fetch(file.raw_url, {
|
|
1526
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1527
|
+
});
|
|
1438
1528
|
if (fileResponse.ok) {
|
|
1439
1529
|
const content = await fileResponse.text();
|
|
1440
1530
|
if (content.includes(attestationId)) {
|
|
@@ -1512,15 +1602,38 @@ function isGitHubIdentityVerified(attestationId, _username, cachedGists) {
|
|
|
1512
1602
|
function getGistCreationUrl() {
|
|
1513
1603
|
return "https://gist.github.com/";
|
|
1514
1604
|
}
|
|
1515
|
-
|
|
1516
|
-
// src/identity/nostr.ts
|
|
1517
1605
|
var log7 = createLogger("identity-verification/nostr");
|
|
1606
|
+
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
1607
|
+
function toHex(bytes) {
|
|
1608
|
+
let out = "";
|
|
1609
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1610
|
+
return out;
|
|
1611
|
+
}
|
|
1612
|
+
function fromHex(hex) {
|
|
1613
|
+
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
1614
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1615
|
+
for (let i = 0; i < out.length; i++) {
|
|
1616
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1617
|
+
}
|
|
1618
|
+
return out;
|
|
1619
|
+
}
|
|
1518
1620
|
function npubToHex(npub) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1621
|
+
const trimmed = npub.trim();
|
|
1622
|
+
if (HEX_64_RE.test(trimmed.toLowerCase())) {
|
|
1623
|
+
return trimmed.toLowerCase();
|
|
1624
|
+
}
|
|
1625
|
+
if (!trimmed.toLowerCase().startsWith("npub1")) {
|
|
1626
|
+
throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
|
|
1521
1627
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1628
|
+
const decoded = bech32.decode(trimmed.toLowerCase(), 1023);
|
|
1629
|
+
if (decoded.prefix !== "npub") {
|
|
1630
|
+
throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
|
|
1631
|
+
}
|
|
1632
|
+
const bytes = bech32.fromWords(decoded.words);
|
|
1633
|
+
if (bytes.length !== 32) {
|
|
1634
|
+
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
1635
|
+
}
|
|
1636
|
+
return toHex(Uint8Array.from(bytes));
|
|
1524
1637
|
}
|
|
1525
1638
|
async function queryNostrEvents(options) {
|
|
1526
1639
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1591,11 +1704,31 @@ async function queryNostrEvents(options) {
|
|
|
1591
1704
|
return events;
|
|
1592
1705
|
}
|
|
1593
1706
|
async function verifyNostrEventSignature(event) {
|
|
1594
|
-
|
|
1707
|
+
try {
|
|
1708
|
+
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)) {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
const preimage = JSON.stringify([
|
|
1712
|
+
0,
|
|
1713
|
+
event.pubkey,
|
|
1714
|
+
event.created_at,
|
|
1715
|
+
event.kind,
|
|
1716
|
+
event.tags ?? [],
|
|
1717
|
+
event.content ?? ""
|
|
1718
|
+
]);
|
|
1719
|
+
const computedId = toHex(sha256(new TextEncoder().encode(preimage)));
|
|
1720
|
+
if (computedId !== event.id.toLowerCase()) {
|
|
1721
|
+
log7.warn(
|
|
1722
|
+
{ eventId: event.id, computedId },
|
|
1723
|
+
"nostr event id does not match its content"
|
|
1724
|
+
);
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
return schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
log7.warn({ err }, "schnorr verify threw");
|
|
1595
1730
|
return false;
|
|
1596
1731
|
}
|
|
1597
|
-
log7.warn("Nostr signature verification not fully implemented");
|
|
1598
|
-
return true;
|
|
1599
1732
|
}
|
|
1600
1733
|
async function verifyNostrIdentity(attestationId, npub, relays) {
|
|
1601
1734
|
log7.info({ attestationId, npub }, "Verifying Nostr identity");
|
|
@@ -1951,6 +2084,6 @@ var STATUS_META = {
|
|
|
1951
2084
|
}
|
|
1952
2085
|
};
|
|
1953
2086
|
|
|
1954
|
-
export { DEFAULT_RELAYS, STATUS_META, buildCanonicalMessage, check, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, formatIdentities, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, 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 };
|
|
2087
|
+
export { DEFAULT_RELAYS, STATUS_META, buildCanonicalMessage, check, 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 };
|
|
1955
2088
|
//# sourceMappingURL=index.mjs.map
|
|
1956
2089
|
//# sourceMappingURL=index.mjs.map
|