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