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