@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.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
var buffer = require('buffer');
|
|
4
4
|
var bip322Js = require('bip322-js');
|
|
5
5
|
var bitcoinMessage = require('bitcoinjs-message');
|
|
6
|
+
var secp256k1_js = require('@noble/curves/secp256k1.js');
|
|
7
|
+
var sha2_js = require('@noble/hashes/sha2.js');
|
|
8
|
+
var base = require('@scure/base');
|
|
6
9
|
|
|
7
10
|
function _interopNamespace(e) {
|
|
8
11
|
if (e && e.__esModule) return e;
|
|
@@ -37,15 +40,27 @@ function randomNonce16BHexLower() {
|
|
|
37
40
|
}
|
|
38
41
|
} catch {
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
throw new Error("crypto.getRandomValues unavailable \u2014 refusing to mint a weak nonce");
|
|
44
|
+
}
|
|
45
|
+
var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
46
|
+
var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
|
|
47
|
+
function assertSafeIdentity({ protocol, identifier }) {
|
|
48
|
+
if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
|
|
56
|
+
);
|
|
42
57
|
}
|
|
43
|
-
return toLowerHex(arr);
|
|
44
58
|
}
|
|
45
59
|
function formatIdentities(identities = []) {
|
|
46
60
|
if (identities.length === 0) {
|
|
47
61
|
return "";
|
|
48
62
|
}
|
|
63
|
+
for (const id of identities) assertSafeIdentity(id);
|
|
49
64
|
const formatted = identities.map(({ protocol, identifier }) => `${protocol}:${identifier}`).sort();
|
|
50
65
|
return formatted.join(",");
|
|
51
66
|
}
|
|
@@ -53,7 +68,10 @@ function parseIdentities(identitiesStr) {
|
|
|
53
68
|
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
54
69
|
return [];
|
|
55
70
|
}
|
|
56
|
-
|
|
71
|
+
if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
|
|
72
|
+
throw new Error("identities field contains forbidden characters (newline)");
|
|
73
|
+
}
|
|
74
|
+
const bindings = identitiesStr.split(",").map((binding) => {
|
|
57
75
|
const colonIndex = binding.indexOf(":");
|
|
58
76
|
if (colonIndex === -1) {
|
|
59
77
|
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
@@ -62,7 +80,14 @@ function parseIdentities(identitiesStr) {
|
|
|
62
80
|
protocol: binding.substring(0, colonIndex).trim(),
|
|
63
81
|
identifier: binding.substring(colonIndex + 1).trim()
|
|
64
82
|
};
|
|
65
|
-
})
|
|
83
|
+
});
|
|
84
|
+
for (const b of bindings) {
|
|
85
|
+
if (!b.protocol || !b.identifier) {
|
|
86
|
+
throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
|
|
87
|
+
}
|
|
88
|
+
assertSafeIdentity(b);
|
|
89
|
+
}
|
|
90
|
+
return bindings;
|
|
66
91
|
}
|
|
67
92
|
function buildCanonicalMessage({ address, identities }, extensions = {}) {
|
|
68
93
|
const nonce = randomNonce16BHexLower();
|
|
@@ -230,16 +255,15 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
230
255
|
ws = new WebSocket(relayUrl);
|
|
231
256
|
const wsRef = ws;
|
|
232
257
|
await new Promise((resolve, reject) => {
|
|
233
|
-
const
|
|
258
|
+
const deadline = setTimeout(() => {
|
|
234
259
|
wsRef.close();
|
|
235
|
-
reject(new Error("
|
|
236
|
-
},
|
|
260
|
+
reject(new Error("Relay publish timeout"));
|
|
261
|
+
}, 1e4);
|
|
237
262
|
const cleanup = () => {
|
|
238
|
-
clearTimeout(
|
|
263
|
+
clearTimeout(deadline);
|
|
239
264
|
wsRef.close();
|
|
240
265
|
};
|
|
241
266
|
wsRef.onopen = () => {
|
|
242
|
-
clearTimeout(timeout);
|
|
243
267
|
wsRef.send(JSON.stringify(["EVENT", event]));
|
|
244
268
|
};
|
|
245
269
|
wsRef.onmessage = (msg) => {
|
|
@@ -277,7 +301,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
277
301
|
reject(err);
|
|
278
302
|
};
|
|
279
303
|
wsRef.onclose = () => {
|
|
280
|
-
clearTimeout(
|
|
304
|
+
clearTimeout(deadline);
|
|
281
305
|
};
|
|
282
306
|
});
|
|
283
307
|
} catch (err) {
|
|
@@ -655,6 +679,24 @@ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT
|
|
|
655
679
|
log3.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
656
680
|
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
657
681
|
}
|
|
682
|
+
function formatIdentitiesForDisplay(identities) {
|
|
683
|
+
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
684
|
+
}
|
|
685
|
+
function getVerificationUrl(attestationId, baseUrl = "https://ochk.io") {
|
|
686
|
+
return `${baseUrl}/attest/${attestationId}`;
|
|
687
|
+
}
|
|
688
|
+
function extractAttestationIdFromUrl(url) {
|
|
689
|
+
try {
|
|
690
|
+
const u = new URL(url);
|
|
691
|
+
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
692
|
+
if (m) return m[1].toLowerCase();
|
|
693
|
+
const q = u.searchParams.get("id");
|
|
694
|
+
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
695
|
+
return null;
|
|
696
|
+
} catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
658
700
|
|
|
659
701
|
// src/scoring.ts
|
|
660
702
|
function scoreV0(sats, days) {
|
|
@@ -681,9 +723,6 @@ function computeScore(sats, days, config) {
|
|
|
681
723
|
|
|
682
724
|
// src/verify.ts
|
|
683
725
|
var log4 = createLogger("ocp/verify");
|
|
684
|
-
function scoreV02(sats, days) {
|
|
685
|
-
return computeScore(sats, days, { algorithm: "v0" });
|
|
686
|
-
}
|
|
687
726
|
function looksLikeHex(s) {
|
|
688
727
|
return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
|
|
689
728
|
}
|
|
@@ -799,7 +838,13 @@ async function verifySignature({
|
|
|
799
838
|
}
|
|
800
839
|
function parseCanonicalMessage(msg) {
|
|
801
840
|
const lines = msg.split("\n").map((l) => l.trim());
|
|
802
|
-
const getVal = (prefix) =>
|
|
841
|
+
const getVal = (prefix) => {
|
|
842
|
+
const needle = prefix + ":";
|
|
843
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
844
|
+
if (!hit) return void 0;
|
|
845
|
+
const idx = hit.indexOf(":");
|
|
846
|
+
return hit.slice(idx + 1).trim();
|
|
847
|
+
};
|
|
803
848
|
const address = getVal("address") || "";
|
|
804
849
|
const identitiesStr = getVal("identities") || "";
|
|
805
850
|
let identities = [];
|
|
@@ -854,8 +899,7 @@ async function getAddressUtxos(address, network, opts) {
|
|
|
854
899
|
const resp = await fetch(url, {
|
|
855
900
|
method: "GET",
|
|
856
901
|
headers: { Accept: "application/json" },
|
|
857
|
-
signal: AbortSignal.timeout(
|
|
858
|
-
// 10s timeout
|
|
902
|
+
signal: AbortSignal.timeout(5e3)
|
|
859
903
|
});
|
|
860
904
|
if (!resp.ok) {
|
|
861
905
|
throw new Error(`HTTP ${resp.status}`);
|
|
@@ -942,7 +986,7 @@ async function verify2(input, opts = {}) {
|
|
|
942
986
|
const metrics = {
|
|
943
987
|
sats_bonded,
|
|
944
988
|
days_unspent,
|
|
945
|
-
score:
|
|
989
|
+
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
946
990
|
// ≈ 55.26
|
|
947
991
|
};
|
|
948
992
|
codes.push("sig_ok_bip322");
|
|
@@ -960,13 +1004,18 @@ async function verify2(input, opts = {}) {
|
|
|
960
1004
|
codes.push("network_testmode");
|
|
961
1005
|
return { ok: false, codes, network, attestation_id };
|
|
962
1006
|
}
|
|
963
|
-
if (opts.expectedAud
|
|
964
|
-
|
|
1007
|
+
if (opts.expectedAud) {
|
|
1008
|
+
const aud = parsed.extensions["aud"];
|
|
1009
|
+
if (!aud || aud !== opts.expectedAud) {
|
|
1010
|
+
codes.push("aud_mismatch");
|
|
1011
|
+
return { ok: false, codes, network, attestation_id };
|
|
1012
|
+
}
|
|
965
1013
|
}
|
|
966
1014
|
if (parsed.extensions["expires"]) {
|
|
967
1015
|
const exp = Date.parse(parsed.extensions["expires"]);
|
|
968
1016
|
if (!Number.isNaN(exp) && exp < Date.now()) {
|
|
969
1017
|
codes.push("expired");
|
|
1018
|
+
return { ok: false, codes, network, attestation_id };
|
|
970
1019
|
}
|
|
971
1020
|
}
|
|
972
1021
|
const sigCode = await verifySignature({
|
|
@@ -1033,7 +1082,7 @@ async function verify2(input, opts = {}) {
|
|
|
1033
1082
|
const metrics = {
|
|
1034
1083
|
sats_bonded,
|
|
1035
1084
|
days_unspent,
|
|
1036
|
-
score:
|
|
1085
|
+
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
1037
1086
|
};
|
|
1038
1087
|
return {
|
|
1039
1088
|
ok: true,
|
|
@@ -1044,7 +1093,7 @@ async function verify2(input, opts = {}) {
|
|
|
1044
1093
|
metrics
|
|
1045
1094
|
};
|
|
1046
1095
|
} catch (utxoError) {
|
|
1047
|
-
|
|
1096
|
+
log4.error({ err: utxoError }, "UTXO fetch failed");
|
|
1048
1097
|
codes.push("bad_request");
|
|
1049
1098
|
return { ok: false, codes, network, attestation_id };
|
|
1050
1099
|
}
|
|
@@ -1171,16 +1220,20 @@ function parseChallenge(message) {
|
|
|
1171
1220
|
throw new Error("not an orangecheck-auth challenge");
|
|
1172
1221
|
}
|
|
1173
1222
|
const out = { extensions: [] };
|
|
1174
|
-
const coreKeys =
|
|
1175
|
-
|
|
1223
|
+
const coreKeys = ["address", "nonce", "issued_at", "expires_at", "ack"];
|
|
1224
|
+
const coreKeySet = new Set(coreKeys);
|
|
1225
|
+
const seenCore = /* @__PURE__ */ new Set();
|
|
1176
1226
|
for (let i = 1; i < lines.length; i++) {
|
|
1177
1227
|
const line = lines[i] ?? "";
|
|
1178
1228
|
const sep = line.indexOf(": ");
|
|
1179
1229
|
if (sep === -1) continue;
|
|
1180
1230
|
const key = line.slice(0, sep);
|
|
1181
1231
|
const val = line.slice(sep + 2);
|
|
1182
|
-
if (
|
|
1183
|
-
seenCore
|
|
1232
|
+
if (coreKeySet.has(key)) {
|
|
1233
|
+
if (seenCore.has(key)) {
|
|
1234
|
+
throw new Error(`duplicate core key in challenge: ${key}`);
|
|
1235
|
+
}
|
|
1236
|
+
seenCore.add(key);
|
|
1184
1237
|
if (key === "address") out.address = val;
|
|
1185
1238
|
else if (key === "nonce") out.nonce = val;
|
|
1186
1239
|
else if (key === "issued_at") out.issuedAt = new Date(val);
|
|
@@ -1192,8 +1245,8 @@ function parseChallenge(message) {
|
|
|
1192
1245
|
if (key === "purpose") out.purpose = val;
|
|
1193
1246
|
}
|
|
1194
1247
|
}
|
|
1195
|
-
if (seenCore !==
|
|
1196
|
-
throw new Error(`challenge missing core lines (${seenCore}
|
|
1248
|
+
if (seenCore.size !== coreKeys.length) {
|
|
1249
|
+
throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);
|
|
1197
1250
|
}
|
|
1198
1251
|
return out;
|
|
1199
1252
|
}
|
|
@@ -1294,9 +1347,25 @@ var identityVerificationLimiter = createRateLimiter(5, 6e4);
|
|
|
1294
1347
|
|
|
1295
1348
|
// src/identity/dns.ts
|
|
1296
1349
|
var log5 = createLogger("identity-verification/dns");
|
|
1350
|
+
var PRIVATE_HOST_RE = /^(?:localhost|.*\.local|.*\.internal|.*\.localhost|.*\.lan|.*\.home|.*\.arpa|\[?::1\]?|\[?::\]?)$/i;
|
|
1351
|
+
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]\.)/;
|
|
1352
|
+
var WELL_KNOWN_TIMEOUT_MS = 5e3;
|
|
1353
|
+
function isSafePublicHost(host) {
|
|
1354
|
+
const h = host.toLowerCase();
|
|
1355
|
+
if (PRIVATE_HOST_RE.test(h)) return false;
|
|
1356
|
+
if (PRIVATE_IPV4_RE.test(h)) return false;
|
|
1357
|
+
if (h.startsWith("[") || /^[0-9a-f:]+$/.test(h)) return false;
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1297
1360
|
async function verifyDnsIdentity(attestationId, domain) {
|
|
1298
1361
|
log5.info({ attestationId, domain }, "Verifying DNS identity");
|
|
1299
1362
|
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
1363
|
+
if (!isValidDomain(cleanDomain)) {
|
|
1364
|
+
return { verified: false, error: `invalid domain format: ${cleanDomain}` };
|
|
1365
|
+
}
|
|
1366
|
+
if (!isSafePublicHost(cleanDomain)) {
|
|
1367
|
+
return { verified: false, error: "domain resolves to a disallowed private host" };
|
|
1368
|
+
}
|
|
1300
1369
|
const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
|
|
1301
1370
|
if (wellKnownResult.verified) {
|
|
1302
1371
|
return wellKnownResult;
|
|
@@ -1308,38 +1377,31 @@ async function verifyDnsIdentity(attestationId, domain) {
|
|
|
1308
1377
|
};
|
|
1309
1378
|
}
|
|
1310
1379
|
async function checkWellKnownFile(attestationId, domain) {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
verified: true,
|
|
1333
|
-
method: "well-known",
|
|
1334
|
-
url
|
|
1335
|
-
};
|
|
1336
|
-
} else {
|
|
1337
|
-
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1338
|
-
}
|
|
1380
|
+
const url = `https://${domain}/.well-known/orangecheck.txt`;
|
|
1381
|
+
try {
|
|
1382
|
+
log5.info({ url }, "Checking well-known file");
|
|
1383
|
+
const response = await fetch(url, {
|
|
1384
|
+
method: "GET",
|
|
1385
|
+
headers: { Accept: "text/plain" },
|
|
1386
|
+
// Don't follow redirects to other domains
|
|
1387
|
+
redirect: "manual",
|
|
1388
|
+
signal: AbortSignal.timeout(WELL_KNOWN_TIMEOUT_MS)
|
|
1389
|
+
});
|
|
1390
|
+
if (response.ok) {
|
|
1391
|
+
const content = await response.text();
|
|
1392
|
+
if (content.includes(attestationId)) {
|
|
1393
|
+
log5.info({ domain, url }, "Found attestation ID in well-known file");
|
|
1394
|
+
return {
|
|
1395
|
+
verified: true,
|
|
1396
|
+
method: "well-known",
|
|
1397
|
+
url
|
|
1398
|
+
};
|
|
1399
|
+
} else {
|
|
1400
|
+
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1339
1401
|
}
|
|
1340
|
-
} catch (err) {
|
|
1341
|
-
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1342
1402
|
}
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1343
1405
|
}
|
|
1344
1406
|
return {
|
|
1345
1407
|
verified: false,
|
|
@@ -1412,14 +1474,32 @@ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
|
|
|
1412
1474
|
|
|
1413
1475
|
// src/identity/github.ts
|
|
1414
1476
|
var log6 = createLogger("identity-verification/github");
|
|
1477
|
+
var GITHUB_GIST_RAW_HOST = "gist.githubusercontent.com";
|
|
1478
|
+
var MAX_GISTS_SCANNED = 30;
|
|
1479
|
+
var MAX_FILES_PER_GIST = 10;
|
|
1480
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
1481
|
+
var GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
|
1482
|
+
function isSafeGistRawUrl(raw) {
|
|
1483
|
+
try {
|
|
1484
|
+
const u = new URL(raw);
|
|
1485
|
+
return u.protocol === "https:" && u.hostname === GITHUB_GIST_RAW_HOST;
|
|
1486
|
+
} catch {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1415
1490
|
async function verifyGitHubIdentity(attestationId, username) {
|
|
1416
1491
|
log6.info({ attestationId, username }, "Verifying GitHub identity");
|
|
1492
|
+
if (!GITHUB_USERNAME_RE.test(username)) {
|
|
1493
|
+
return { verified: false, error: "invalid GitHub username format" };
|
|
1494
|
+
}
|
|
1417
1495
|
try {
|
|
1418
|
-
const response = await fetch(
|
|
1419
|
-
|
|
1420
|
-
|
|
1496
|
+
const response = await fetch(
|
|
1497
|
+
`https://api.github.com/users/${encodeURIComponent(username)}/gists?per_page=${MAX_GISTS_SCANNED}`,
|
|
1498
|
+
{
|
|
1499
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
1500
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1421
1501
|
}
|
|
1422
|
-
|
|
1502
|
+
);
|
|
1423
1503
|
if (!response.ok) {
|
|
1424
1504
|
if (response.status === 404) {
|
|
1425
1505
|
return {
|
|
@@ -1434,7 +1514,7 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1434
1514
|
}
|
|
1435
1515
|
const gists = await response.json();
|
|
1436
1516
|
log6.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
|
|
1437
|
-
for (const gist of gists) {
|
|
1517
|
+
for (const gist of gists.slice(0, MAX_GISTS_SCANNED)) {
|
|
1438
1518
|
if (gist.description?.includes(attestationId)) {
|
|
1439
1519
|
log6.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
|
|
1440
1520
|
return {
|
|
@@ -1443,7 +1523,8 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1443
1523
|
gistId: gist.id
|
|
1444
1524
|
};
|
|
1445
1525
|
}
|
|
1446
|
-
|
|
1526
|
+
const files = Object.entries(gist.files).slice(0, MAX_FILES_PER_GIST);
|
|
1527
|
+
for (const [filename, file] of files) {
|
|
1447
1528
|
if (file.content && file.content.includes(attestationId)) {
|
|
1448
1529
|
log6.info(
|
|
1449
1530
|
{ username, gistId: gist.id, filename },
|
|
@@ -1455,8 +1536,17 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1455
1536
|
gistId: gist.id
|
|
1456
1537
|
};
|
|
1457
1538
|
}
|
|
1539
|
+
if (!isSafeGistRawUrl(file.raw_url)) {
|
|
1540
|
+
log6.warn(
|
|
1541
|
+
{ filename, rawUrl: file.raw_url },
|
|
1542
|
+
"skipping gist file: raw_url failed allow-list"
|
|
1543
|
+
);
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1458
1546
|
try {
|
|
1459
|
-
const fileResponse = await fetch(file.raw_url
|
|
1547
|
+
const fileResponse = await fetch(file.raw_url, {
|
|
1548
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1549
|
+
});
|
|
1460
1550
|
if (fileResponse.ok) {
|
|
1461
1551
|
const content = await fileResponse.text();
|
|
1462
1552
|
if (content.includes(attestationId)) {
|
|
@@ -1534,15 +1624,38 @@ function isGitHubIdentityVerified(attestationId, _username, cachedGists) {
|
|
|
1534
1624
|
function getGistCreationUrl() {
|
|
1535
1625
|
return "https://gist.github.com/";
|
|
1536
1626
|
}
|
|
1537
|
-
|
|
1538
|
-
// src/identity/nostr.ts
|
|
1539
1627
|
var log7 = createLogger("identity-verification/nostr");
|
|
1628
|
+
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
1629
|
+
function toHex(bytes) {
|
|
1630
|
+
let out = "";
|
|
1631
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1632
|
+
return out;
|
|
1633
|
+
}
|
|
1634
|
+
function fromHex(hex) {
|
|
1635
|
+
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
1636
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1637
|
+
for (let i = 0; i < out.length; i++) {
|
|
1638
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1639
|
+
}
|
|
1640
|
+
return out;
|
|
1641
|
+
}
|
|
1540
1642
|
function npubToHex(npub) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1643
|
+
const trimmed = npub.trim();
|
|
1644
|
+
if (HEX_64_RE.test(trimmed.toLowerCase())) {
|
|
1645
|
+
return trimmed.toLowerCase();
|
|
1646
|
+
}
|
|
1647
|
+
if (!trimmed.toLowerCase().startsWith("npub1")) {
|
|
1648
|
+
throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
|
|
1543
1649
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1650
|
+
const decoded = base.bech32.decode(trimmed.toLowerCase(), 1023);
|
|
1651
|
+
if (decoded.prefix !== "npub") {
|
|
1652
|
+
throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
|
|
1653
|
+
}
|
|
1654
|
+
const bytes = base.bech32.fromWords(decoded.words);
|
|
1655
|
+
if (bytes.length !== 32) {
|
|
1656
|
+
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
1657
|
+
}
|
|
1658
|
+
return toHex(Uint8Array.from(bytes));
|
|
1546
1659
|
}
|
|
1547
1660
|
async function queryNostrEvents(options) {
|
|
1548
1661
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1613,11 +1726,31 @@ async function queryNostrEvents(options) {
|
|
|
1613
1726
|
return events;
|
|
1614
1727
|
}
|
|
1615
1728
|
async function verifyNostrEventSignature(event) {
|
|
1616
|
-
|
|
1729
|
+
try {
|
|
1730
|
+
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)) {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
const preimage = JSON.stringify([
|
|
1734
|
+
0,
|
|
1735
|
+
event.pubkey,
|
|
1736
|
+
event.created_at,
|
|
1737
|
+
event.kind,
|
|
1738
|
+
event.tags ?? [],
|
|
1739
|
+
event.content ?? ""
|
|
1740
|
+
]);
|
|
1741
|
+
const computedId = toHex(sha2_js.sha256(new TextEncoder().encode(preimage)));
|
|
1742
|
+
if (computedId !== event.id.toLowerCase()) {
|
|
1743
|
+
log7.warn(
|
|
1744
|
+
{ eventId: event.id, computedId },
|
|
1745
|
+
"nostr event id does not match its content"
|
|
1746
|
+
);
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
return secp256k1_js.schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
|
|
1750
|
+
} catch (err) {
|
|
1751
|
+
log7.warn({ err }, "schnorr verify threw");
|
|
1617
1752
|
return false;
|
|
1618
1753
|
}
|
|
1619
|
-
log7.warn("Nostr signature verification not fully implemented");
|
|
1620
|
-
return true;
|
|
1621
1754
|
}
|
|
1622
1755
|
async function verifyNostrIdentity(attestationId, npub, relays) {
|
|
1623
1756
|
log7.info({ attestationId, npub }, "Verifying Nostr identity");
|
|
@@ -1982,7 +2115,9 @@ exports.createAttestation = createAttestation;
|
|
|
1982
2115
|
exports.createAttestationEnvelope = createAttestationEnvelope;
|
|
1983
2116
|
exports.createAttestationEvent = createAttestationEvent;
|
|
1984
2117
|
exports.discoverAttestations = discoverAttestations;
|
|
2118
|
+
exports.extractAttestationIdFromUrl = extractAttestationIdFromUrl;
|
|
1985
2119
|
exports.formatIdentities = formatIdentities;
|
|
2120
|
+
exports.formatIdentitiesForDisplay = formatIdentitiesForDisplay;
|
|
1986
2121
|
exports.generateAttestationId = generateAttestationId;
|
|
1987
2122
|
exports.generateGistTemplate = generateGistTemplate;
|
|
1988
2123
|
exports.generateTweetText = generateTweetText;
|
|
@@ -2001,6 +2136,7 @@ exports.getTwitterComposeUrl = getTwitterComposeUrl;
|
|
|
2001
2136
|
exports.getTwitterVerificationInstructions = getTwitterVerificationInstructions;
|
|
2002
2137
|
exports.getVerificationInstructions = getVerificationInstructions;
|
|
2003
2138
|
exports.getVerificationSummary = getVerificationSummary;
|
|
2139
|
+
exports.getVerificationUrl = getVerificationUrl;
|
|
2004
2140
|
exports.getWellKnownUrl = getWellKnownUrl;
|
|
2005
2141
|
exports.isDnsIdentityVerified = isDnsIdentityVerified;
|
|
2006
2142
|
exports.isGitHubIdentityVerified = isGitHubIdentityVerified;
|