@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/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 urls = [
109
- `https://${domain}/.well-known/orangecheck.txt`,
110
- `http://${domain}/.well-known/orangecheck.txt`
111
- // Fallback to HTTP
112
- ];
113
- for (const url of urls) {
114
- try {
115
- log.info({ url }, "Checking well-known file");
116
- const response = await fetch(url, {
117
- method: "GET",
118
- headers: {
119
- Accept: "text/plain"
120
- },
121
- // Don't follow redirects to other domains
122
- redirect: "manual"
123
- });
124
- if (response.ok) {
125
- const content = await response.text();
126
- if (content.includes(attestationId)) {
127
- log.info({ domain, url }, "Found attestation ID in well-known file");
128
- return {
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(`https://api.github.com/users/${username}/gists`, {
216
- headers: {
217
- Accept: "application/vnd.github.v3+json"
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
- for (const [filename, file] of Object.entries(gist.files)) {
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
- if (npub.match(/^[0-9a-f]{64}$/i)) {
345
- return npub.toLowerCase();
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
- log4.warn({ npub }, "npub to hex conversion not fully implemented");
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
- if (!event.sig || event.sig.length !== 128) {
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");