@orangecheck/sdk 0.1.0 → 0.1.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/attestation.js +26 -2
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +26 -2
- 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/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 +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +200 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +199 -58
- package/dist/index.mjs.map +1 -1
- package/dist/verify.js +42 -8
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +42 -8
- package/dist/verify.mjs.map +1 -1
- package/package.json +2 -1
package/dist/identity.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var secp256k1 = require('@noble/curves/secp256k1');
|
|
4
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
5
|
+
var base = require('@scure/base');
|
|
6
|
+
|
|
3
7
|
// src/utils/logger.ts
|
|
4
8
|
function createLogger(namespace) {
|
|
5
9
|
const shouldLog = (level) => {
|
|
@@ -93,9 +97,25 @@ var identityVerificationLimiter = createRateLimiter(5, 6e4);
|
|
|
93
97
|
|
|
94
98
|
// src/identity/dns.ts
|
|
95
99
|
var log = createLogger("identity-verification/dns");
|
|
100
|
+
var PRIVATE_HOST_RE = /^(?:localhost|.*\.local|.*\.internal|.*\.localhost|.*\.lan|.*\.home|.*\.arpa|\[?::1\]?|\[?::\]?)$/i;
|
|
101
|
+
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]\.)/;
|
|
102
|
+
var WELL_KNOWN_TIMEOUT_MS = 5e3;
|
|
103
|
+
function isSafePublicHost(host) {
|
|
104
|
+
const h = host.toLowerCase();
|
|
105
|
+
if (PRIVATE_HOST_RE.test(h)) return false;
|
|
106
|
+
if (PRIVATE_IPV4_RE.test(h)) return false;
|
|
107
|
+
if (h.startsWith("[") || /^[0-9a-f:]+$/.test(h)) return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
96
110
|
async function verifyDnsIdentity(attestationId, domain) {
|
|
97
111
|
log.info({ attestationId, domain }, "Verifying DNS identity");
|
|
98
112
|
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
113
|
+
if (!isValidDomain(cleanDomain)) {
|
|
114
|
+
return { verified: false, error: `invalid domain format: ${cleanDomain}` };
|
|
115
|
+
}
|
|
116
|
+
if (!isSafePublicHost(cleanDomain)) {
|
|
117
|
+
return { verified: false, error: "domain resolves to a disallowed private host" };
|
|
118
|
+
}
|
|
99
119
|
const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
|
|
100
120
|
if (wellKnownResult.verified) {
|
|
101
121
|
return wellKnownResult;
|
|
@@ -107,38 +127,31 @@ async function verifyDnsIdentity(attestationId, domain) {
|
|
|
107
127
|
};
|
|
108
128
|
}
|
|
109
129
|
async function checkWellKnownFile(attestationId, domain) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
verified: true,
|
|
132
|
-
method: "well-known",
|
|
133
|
-
url
|
|
134
|
-
};
|
|
135
|
-
} else {
|
|
136
|
-
log.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
137
|
-
}
|
|
130
|
+
const url = `https://${domain}/.well-known/orangecheck.txt`;
|
|
131
|
+
try {
|
|
132
|
+
log.info({ url }, "Checking well-known file");
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method: "GET",
|
|
135
|
+
headers: { Accept: "text/plain" },
|
|
136
|
+
// Don't follow redirects to other domains
|
|
137
|
+
redirect: "manual",
|
|
138
|
+
signal: AbortSignal.timeout(WELL_KNOWN_TIMEOUT_MS)
|
|
139
|
+
});
|
|
140
|
+
if (response.ok) {
|
|
141
|
+
const content = await response.text();
|
|
142
|
+
if (content.includes(attestationId)) {
|
|
143
|
+
log.info({ domain, url }, "Found attestation ID in well-known file");
|
|
144
|
+
return {
|
|
145
|
+
verified: true,
|
|
146
|
+
method: "well-known",
|
|
147
|
+
url
|
|
148
|
+
};
|
|
149
|
+
} else {
|
|
150
|
+
log.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
138
151
|
}
|
|
139
|
-
} catch (err) {
|
|
140
|
-
log.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
141
152
|
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
log.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
142
155
|
}
|
|
143
156
|
return {
|
|
144
157
|
verified: false,
|
|
@@ -211,14 +224,32 @@ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
|
|
|
211
224
|
|
|
212
225
|
// src/identity/github.ts
|
|
213
226
|
var log2 = createLogger("identity-verification/github");
|
|
227
|
+
var GITHUB_GIST_RAW_HOST = "gist.githubusercontent.com";
|
|
228
|
+
var MAX_GISTS_SCANNED = 30;
|
|
229
|
+
var MAX_FILES_PER_GIST = 10;
|
|
230
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
231
|
+
var GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
|
232
|
+
function isSafeGistRawUrl(raw) {
|
|
233
|
+
try {
|
|
234
|
+
const u = new URL(raw);
|
|
235
|
+
return u.protocol === "https:" && u.hostname === GITHUB_GIST_RAW_HOST;
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
214
240
|
async function verifyGitHubIdentity(attestationId, username) {
|
|
215
241
|
log2.info({ attestationId, username }, "Verifying GitHub identity");
|
|
242
|
+
if (!GITHUB_USERNAME_RE.test(username)) {
|
|
243
|
+
return { verified: false, error: "invalid GitHub username format" };
|
|
244
|
+
}
|
|
216
245
|
try {
|
|
217
|
-
const response = await fetch(
|
|
218
|
-
|
|
219
|
-
|
|
246
|
+
const response = await fetch(
|
|
247
|
+
`https://api.github.com/users/${encodeURIComponent(username)}/gists?per_page=${MAX_GISTS_SCANNED}`,
|
|
248
|
+
{
|
|
249
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
250
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
220
251
|
}
|
|
221
|
-
|
|
252
|
+
);
|
|
222
253
|
if (!response.ok) {
|
|
223
254
|
if (response.status === 404) {
|
|
224
255
|
return {
|
|
@@ -233,7 +264,7 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
233
264
|
}
|
|
234
265
|
const gists = await response.json();
|
|
235
266
|
log2.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
|
|
236
|
-
for (const gist of gists) {
|
|
267
|
+
for (const gist of gists.slice(0, MAX_GISTS_SCANNED)) {
|
|
237
268
|
if (gist.description?.includes(attestationId)) {
|
|
238
269
|
log2.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
|
|
239
270
|
return {
|
|
@@ -242,7 +273,8 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
242
273
|
gistId: gist.id
|
|
243
274
|
};
|
|
244
275
|
}
|
|
245
|
-
|
|
276
|
+
const files = Object.entries(gist.files).slice(0, MAX_FILES_PER_GIST);
|
|
277
|
+
for (const [filename, file] of files) {
|
|
246
278
|
if (file.content && file.content.includes(attestationId)) {
|
|
247
279
|
log2.info(
|
|
248
280
|
{ username, gistId: gist.id, filename },
|
|
@@ -254,8 +286,17 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
254
286
|
gistId: gist.id
|
|
255
287
|
};
|
|
256
288
|
}
|
|
289
|
+
if (!isSafeGistRawUrl(file.raw_url)) {
|
|
290
|
+
log2.warn(
|
|
291
|
+
{ filename, rawUrl: file.raw_url },
|
|
292
|
+
"skipping gist file: raw_url failed allow-list"
|
|
293
|
+
);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
257
296
|
try {
|
|
258
|
-
const fileResponse = await fetch(file.raw_url
|
|
297
|
+
const fileResponse = await fetch(file.raw_url, {
|
|
298
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
299
|
+
});
|
|
259
300
|
if (fileResponse.ok) {
|
|
260
301
|
const content = await fileResponse.text();
|
|
261
302
|
if (content.includes(attestationId)) {
|
|
@@ -342,12 +383,37 @@ var DEFAULT_RELAYS = [
|
|
|
342
383
|
|
|
343
384
|
// src/identity/nostr.ts
|
|
344
385
|
var log4 = createLogger("identity-verification/nostr");
|
|
386
|
+
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
387
|
+
function toHex(bytes) {
|
|
388
|
+
let out = "";
|
|
389
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
function fromHex(hex) {
|
|
393
|
+
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
394
|
+
const out = new Uint8Array(hex.length / 2);
|
|
395
|
+
for (let i = 0; i < out.length; i++) {
|
|
396
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
345
400
|
function npubToHex(npub) {
|
|
346
|
-
|
|
347
|
-
|
|
401
|
+
const trimmed = npub.trim();
|
|
402
|
+
if (HEX_64_RE.test(trimmed.toLowerCase())) {
|
|
403
|
+
return trimmed.toLowerCase();
|
|
404
|
+
}
|
|
405
|
+
if (!trimmed.toLowerCase().startsWith("npub1")) {
|
|
406
|
+
throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
|
|
407
|
+
}
|
|
408
|
+
const decoded = base.bech32.decode(trimmed.toLowerCase(), 1023);
|
|
409
|
+
if (decoded.prefix !== "npub") {
|
|
410
|
+
throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
|
|
411
|
+
}
|
|
412
|
+
const bytes = base.bech32.fromWords(decoded.words);
|
|
413
|
+
if (bytes.length !== 32) {
|
|
414
|
+
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
348
415
|
}
|
|
349
|
-
|
|
350
|
-
return npub;
|
|
416
|
+
return toHex(Uint8Array.from(bytes));
|
|
351
417
|
}
|
|
352
418
|
async function queryNostrEvents(options) {
|
|
353
419
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -418,11 +484,31 @@ async function queryNostrEvents(options) {
|
|
|
418
484
|
return events;
|
|
419
485
|
}
|
|
420
486
|
async function verifyNostrEventSignature(event) {
|
|
421
|
-
|
|
487
|
+
try {
|
|
488
|
+
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)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const preimage = JSON.stringify([
|
|
492
|
+
0,
|
|
493
|
+
event.pubkey,
|
|
494
|
+
event.created_at,
|
|
495
|
+
event.kind,
|
|
496
|
+
event.tags ?? [],
|
|
497
|
+
event.content ?? ""
|
|
498
|
+
]);
|
|
499
|
+
const computedId = toHex(sha2.sha256(new TextEncoder().encode(preimage)));
|
|
500
|
+
if (computedId !== event.id.toLowerCase()) {
|
|
501
|
+
log4.warn(
|
|
502
|
+
{ eventId: event.id, computedId },
|
|
503
|
+
"nostr event id does not match its content"
|
|
504
|
+
);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
return secp256k1.schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
|
|
508
|
+
} catch (err) {
|
|
509
|
+
log4.warn({ err }, "schnorr verify threw");
|
|
422
510
|
return false;
|
|
423
511
|
}
|
|
424
|
-
log4.warn("Nostr signature verification not fully implemented");
|
|
425
|
-
return true;
|
|
426
512
|
}
|
|
427
513
|
async function verifyNostrIdentity(attestationId, npub, relays) {
|
|
428
514
|
log4.info({ attestationId, npub }, "Verifying Nostr identity");
|