@orangecheck/sdk 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -0
  3. package/dist/attestation.d.mts +44 -0
  4. package/dist/attestation.d.ts +44 -0
  5. package/dist/attestation.js +579 -0
  6. package/dist/attestation.js.map +1 -0
  7. package/dist/attestation.mjs +567 -0
  8. package/dist/attestation.mjs.map +1 -0
  9. package/dist/canonical.d.mts +15 -0
  10. package/dist/canonical.d.ts +15 -0
  11. package/dist/canonical.js +109 -0
  12. package/dist/canonical.js.map +1 -0
  13. package/dist/canonical.mjs +102 -0
  14. package/dist/canonical.mjs.map +1 -0
  15. package/dist/challenge.d.mts +40 -0
  16. package/dist/challenge.d.ts +40 -0
  17. package/dist/challenge.js +169 -0
  18. package/dist/challenge.js.map +1 -0
  19. package/dist/challenge.mjs +146 -0
  20. package/dist/challenge.mjs.map +1 -0
  21. package/dist/identity.d.mts +88 -0
  22. package/dist/identity.d.ts +88 -0
  23. package/dist/identity.js +730 -0
  24. package/dist/identity.js.map +1 -0
  25. package/dist/identity.mjs +702 -0
  26. package/dist/identity.mjs.map +1 -0
  27. package/dist/index.d.mts +47 -0
  28. package/dist/index.d.ts +47 -0
  29. package/dist/index.js +2004 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/index.mjs +1930 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/nostr.d.mts +19 -0
  34. package/dist/nostr.d.ts +19 -0
  35. package/dist/nostr.js +395 -0
  36. package/dist/nostr.js.map +1 -0
  37. package/dist/nostr.mjs +382 -0
  38. package/dist/nostr.mjs.map +1 -0
  39. package/dist/scoring.d.mts +16 -0
  40. package/dist/scoring.d.ts +16 -0
  41. package/dist/scoring.js +75 -0
  42. package/dist/scoring.js.map +1 -0
  43. package/dist/scoring.mjs +69 -0
  44. package/dist/scoring.mjs.map +1 -0
  45. package/dist/types-BJKDroFv.d.mts +62 -0
  46. package/dist/types-BJKDroFv.d.ts +62 -0
  47. package/dist/verify.d.mts +6 -0
  48. package/dist/verify.d.ts +6 -0
  49. package/dist/verify.js +509 -0
  50. package/dist/verify.js.map +1 -0
  51. package/dist/verify.mjs +485 -0
  52. package/dist/verify.mjs.map +1 -0
  53. package/package.json +106 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1930 @@
1
+ import { Buffer } from 'buffer';
2
+ import { Verifier } from 'bip322-js';
3
+ import * as bitcoinMessage from 'bitcoinjs-message';
4
+
5
+ // src/canonical.ts
6
+ function toLowerHex(bytes) {
7
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
8
+ }
9
+ function randomNonce16BHexLower() {
10
+ const arr = new Uint8Array(16);
11
+ try {
12
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
13
+ crypto.getRandomValues(arr);
14
+ return toLowerHex(arr);
15
+ }
16
+ } catch {
17
+ }
18
+ for (let i = 0; i < arr.length; i++) {
19
+ arr[i] = Math.floor(Math.random() * 256);
20
+ }
21
+ return toLowerHex(arr);
22
+ }
23
+ function formatIdentities(identities = []) {
24
+ if (identities.length === 0) {
25
+ return "";
26
+ }
27
+ const formatted = identities.map(({ protocol, identifier }) => `${protocol}:${identifier}`).sort();
28
+ return formatted.join(",");
29
+ }
30
+ function parseIdentities(identitiesStr) {
31
+ if (!identitiesStr || identitiesStr.trim() === "") {
32
+ return [];
33
+ }
34
+ return identitiesStr.split(",").map((binding) => {
35
+ const colonIndex = binding.indexOf(":");
36
+ if (colonIndex === -1) {
37
+ throw new Error(`Invalid identity binding format: ${binding}`);
38
+ }
39
+ return {
40
+ protocol: binding.substring(0, colonIndex).trim(),
41
+ identifier: binding.substring(colonIndex + 1).trim()
42
+ };
43
+ }).filter((binding) => binding.protocol && binding.identifier);
44
+ }
45
+ function buildCanonicalMessage({ address, identities }, extensions = {}) {
46
+ const nonce = randomNonce16BHexLower();
47
+ const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
48
+ const identitiesStr = formatIdentities(identities);
49
+ const core = [
50
+ "orangecheck",
51
+ `identities: ${identitiesStr}`,
52
+ `address: ${address}`,
53
+ "purpose: portable reputation attestation (non-custodial)",
54
+ `nonce: ${nonce}`,
55
+ `issued_at: ${issuedAt}`,
56
+ "ack: I attest control of this address and bind it to my identities."
57
+ ];
58
+ const normExt = {};
59
+ for (const [k, v] of Object.entries(extensions)) {
60
+ if (v == null || v === "") {
61
+ continue;
62
+ }
63
+ const key = k.toLowerCase();
64
+ normExt[key] = String(v);
65
+ }
66
+ const extLines = Object.keys(normExt).sort().map((k) => `${k}: ${normExt[k]}`);
67
+ const all = [...core, ...extLines];
68
+ return all.join("\n") + "\n";
69
+ }
70
+ async function generateAttestationId(message) {
71
+ const encoder = new TextEncoder();
72
+ const data = encoder.encode(message);
73
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
74
+ return toLowerHex(new Uint8Array(hashBuffer));
75
+ }
76
+ async function createAttestationEnvelope(message, signature, scheme, address, identities, extensions = {}) {
77
+ const attestation_id = await generateAttestationId(message);
78
+ const issuedAtMatch = message.match(/issued_at: ([^\n]+)/);
79
+ const issued_at = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
80
+ const expires_at = extensions.expires;
81
+ const relay_hints = extensions.relay_hints?.split(",").map((r) => r.trim());
82
+ const publish_targets = extensions.publish?.split(",").map((t) => t.trim());
83
+ const encoder = new TextEncoder();
84
+ const messageBytes = encoder.encode(message);
85
+ const base64 = btoa(String.fromCharCode(...messageBytes));
86
+ const message_b64url = base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
87
+ const verification_url = `https://ochk.io/attest/${attestation_id}`;
88
+ return {
89
+ attestation_id,
90
+ scheme,
91
+ address,
92
+ identities,
93
+ message,
94
+ message_b64url,
95
+ signature,
96
+ issued_at,
97
+ expires_at,
98
+ verification_url,
99
+ publish_targets,
100
+ relay_hints
101
+ };
102
+ }
103
+ function createLogger(namespace) {
104
+ const shouldLog = (level) => {
105
+ return false;
106
+ };
107
+ const formatMessage = (msg) => `[${namespace}] ${msg}`;
108
+ return {
109
+ debug: (contextOrMessage, message) => {
110
+ if (shouldLog()) {
111
+ if (typeof contextOrMessage === "string") {
112
+ console.debug(formatMessage(contextOrMessage));
113
+ } else {
114
+ console.debug(formatMessage(message ?? "Debug"), contextOrMessage);
115
+ }
116
+ }
117
+ },
118
+ info: (contextOrMessage, message) => {
119
+ if (shouldLog()) {
120
+ if (typeof contextOrMessage === "string") {
121
+ console.info(formatMessage(contextOrMessage));
122
+ } else {
123
+ console.info(formatMessage(message ?? "Info"), contextOrMessage);
124
+ }
125
+ }
126
+ },
127
+ warn: (contextOrMessage, message) => {
128
+ if (shouldLog()) {
129
+ if (typeof contextOrMessage === "string") {
130
+ console.warn(formatMessage(contextOrMessage));
131
+ } else {
132
+ console.warn(formatMessage(message ?? "Warning"), contextOrMessage);
133
+ }
134
+ }
135
+ },
136
+ error: (contextOrMessage, message) => {
137
+ if (shouldLog()) {
138
+ if (typeof contextOrMessage === "string") {
139
+ console.error(formatMessage(contextOrMessage));
140
+ } else {
141
+ console.error(formatMessage(message ?? "Error"), contextOrMessage);
142
+ }
143
+ }
144
+ }
145
+ };
146
+ }
147
+ function logVerificationError(protocol, identifier, error, context) {
148
+ const logger = createLogger("identity-verification");
149
+ logger.error(
150
+ {
151
+ protocol,
152
+ identifier,
153
+ error: error.message,
154
+ stack: error.stack,
155
+ ...context
156
+ },
157
+ "Verification failed"
158
+ );
159
+ }
160
+
161
+ // src/nostr.ts
162
+ var log = createLogger("ocp/nostr");
163
+ var ATTESTATION_EVENT_KIND = 30078;
164
+ var DEFAULT_RELAYS = [
165
+ "wss://relay.damus.io",
166
+ "wss://relay.nostr.band",
167
+ "wss://nos.lol",
168
+ "wss://relay.snort.social"
169
+ ];
170
+ function createAttestationEvent(envelope, pubkey) {
171
+ const tags = [
172
+ ["d", envelope.attestation_id],
173
+ // Parameterized replaceable event identifier
174
+ ["address", envelope.address],
175
+ // Bitcoin address
176
+ ["scheme", envelope.scheme],
177
+ // Signature scheme (bip322 or legacy)
178
+ ["issued_at", envelope.issued_at]
179
+ // ISO timestamp
180
+ ];
181
+ for (const identity of envelope.identities) {
182
+ tags.push(["i", `${identity.protocol}:${identity.identifier}`]);
183
+ }
184
+ if (envelope.expires_at) {
185
+ tags.push(["expires", envelope.expires_at]);
186
+ }
187
+ if (envelope.relay_hints?.length) {
188
+ for (const relay of envelope.relay_hints) {
189
+ tags.push(["relay", relay]);
190
+ }
191
+ }
192
+ const content = JSON.stringify(envelope, null, 2);
193
+ return {
194
+ kind: ATTESTATION_EVENT_KIND,
195
+ tags,
196
+ content,
197
+ created_at: Math.floor(Date.parse(envelope.issued_at) / 1e3),
198
+ pubkey
199
+ };
200
+ }
201
+ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
202
+ const success = [];
203
+ const failed = [];
204
+ log.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
205
+ const publishPromises = relays.map(async (relayUrl) => {
206
+ let ws = null;
207
+ try {
208
+ ws = new WebSocket(relayUrl);
209
+ const wsRef = ws;
210
+ await new Promise((resolve, reject) => {
211
+ const timeout = setTimeout(() => {
212
+ wsRef.close();
213
+ reject(new Error("Connection timeout"));
214
+ }, 5e3);
215
+ const cleanup = () => {
216
+ clearTimeout(timeout);
217
+ wsRef.close();
218
+ };
219
+ wsRef.onopen = () => {
220
+ clearTimeout(timeout);
221
+ wsRef.send(JSON.stringify(["EVENT", event]));
222
+ };
223
+ wsRef.onmessage = (msg) => {
224
+ try {
225
+ const data = JSON.parse(msg.data);
226
+ if (data[0] === "OK" && data[1] === event.id) {
227
+ if (data[2] === true) {
228
+ success.push(relayUrl);
229
+ cleanup();
230
+ resolve();
231
+ } else {
232
+ log.warn(
233
+ { relay: relayUrl, reason: data[3] },
234
+ "Relay rejected event"
235
+ );
236
+ failed.push(relayUrl);
237
+ cleanup();
238
+ reject(new Error(data[3] || "Relay rejected event"));
239
+ }
240
+ }
241
+ } catch (err) {
242
+ log.error(
243
+ { relay: relayUrl, error: err },
244
+ "Failed to parse relay response"
245
+ );
246
+ failed.push(relayUrl);
247
+ cleanup();
248
+ reject(err);
249
+ }
250
+ };
251
+ wsRef.onerror = (err) => {
252
+ log.error({ relay: relayUrl, error: err }, "WebSocket error");
253
+ failed.push(relayUrl);
254
+ cleanup();
255
+ reject(err);
256
+ };
257
+ wsRef.onclose = () => {
258
+ clearTimeout(timeout);
259
+ };
260
+ });
261
+ } catch (err) {
262
+ log.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
263
+ if (!failed.includes(relayUrl)) {
264
+ failed.push(relayUrl);
265
+ }
266
+ } finally {
267
+ if (ws && ws.readyState !== WebSocket.CLOSED) {
268
+ ws.close();
269
+ }
270
+ }
271
+ });
272
+ await Promise.allSettled(publishPromises);
273
+ log.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
274
+ return { success, failed };
275
+ }
276
+ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
277
+ const events = [];
278
+ log.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
279
+ const queryPromises = relays.map(async (relayUrl) => {
280
+ let ws = null;
281
+ try {
282
+ ws = new WebSocket(relayUrl);
283
+ const wsRef = ws;
284
+ await new Promise((resolve, reject) => {
285
+ const timeout = setTimeout(() => {
286
+ wsRef.close();
287
+ reject(new Error("Query timeout"));
288
+ }, 1e4);
289
+ const cleanup = () => {
290
+ clearTimeout(timeout);
291
+ wsRef.close();
292
+ };
293
+ wsRef.onopen = () => {
294
+ const subscriptionId = `ochk_${Date.now()}`;
295
+ wsRef.send(
296
+ JSON.stringify([
297
+ "REQ",
298
+ subscriptionId,
299
+ {
300
+ kinds: [30078],
301
+ "#d": [attestationId],
302
+ limit: 1
303
+ }
304
+ ])
305
+ );
306
+ };
307
+ wsRef.onmessage = (msg) => {
308
+ try {
309
+ const data = JSON.parse(msg.data);
310
+ if (data[0] === "EVENT") {
311
+ events.push(data[2]);
312
+ } else if (data[0] === "EOSE") {
313
+ cleanup();
314
+ resolve();
315
+ }
316
+ } catch (err) {
317
+ log.error({ relay: relayUrl, error: err }, "Failed to parse event");
318
+ }
319
+ };
320
+ wsRef.onerror = (err) => {
321
+ log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
322
+ cleanup();
323
+ reject(err);
324
+ };
325
+ wsRef.onclose = () => {
326
+ clearTimeout(timeout);
327
+ };
328
+ });
329
+ } catch (err) {
330
+ log.error({ relay: relayUrl, error: err }, "Failed to query relay");
331
+ } finally {
332
+ if (ws && ws.readyState !== WebSocket.CLOSED) {
333
+ ws.close();
334
+ }
335
+ }
336
+ });
337
+ await Promise.allSettled(queryPromises);
338
+ log.info({ eventCount: events.length }, "Query complete");
339
+ return events;
340
+ }
341
+ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
342
+ const events = [];
343
+ log.info({ address, relayCount: relays.length }, "Querying relays for address");
344
+ const queryPromises = relays.map(async (relayUrl) => {
345
+ try {
346
+ const ws = new WebSocket(relayUrl);
347
+ await new Promise((resolve, reject) => {
348
+ const timeout = setTimeout(() => {
349
+ ws.close();
350
+ reject(new Error("Query timeout"));
351
+ }, 1e4);
352
+ ws.onopen = () => {
353
+ const subscriptionId = `ochk_addr_${Date.now()}`;
354
+ ws.send(
355
+ JSON.stringify([
356
+ "REQ",
357
+ subscriptionId,
358
+ {
359
+ kinds: [30078],
360
+ "#address": [address],
361
+ limit: 10
362
+ }
363
+ ])
364
+ );
365
+ };
366
+ ws.onmessage = (msg) => {
367
+ try {
368
+ const data = JSON.parse(msg.data);
369
+ if (data[0] === "EVENT") {
370
+ events.push(data[2]);
371
+ } else if (data[0] === "EOSE") {
372
+ clearTimeout(timeout);
373
+ ws.close();
374
+ resolve();
375
+ }
376
+ } catch (err) {
377
+ log.error({ relay: relayUrl, error: err }, "Failed to parse event");
378
+ }
379
+ };
380
+ ws.onerror = (err) => {
381
+ clearTimeout(timeout);
382
+ log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
383
+ reject(err);
384
+ };
385
+ });
386
+ } catch (err) {
387
+ log.error({ relay: relayUrl, error: err }, "Failed to query relay");
388
+ }
389
+ });
390
+ await Promise.allSettled(queryPromises);
391
+ log.info({ eventCount: events.length }, "Query complete");
392
+ return events;
393
+ }
394
+ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
395
+ const events = [];
396
+ const identityTag = `${protocol}:${identifier}`;
397
+ log.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
398
+ const queryPromises = relays.map(async (relayUrl) => {
399
+ try {
400
+ const ws = new WebSocket(relayUrl);
401
+ await new Promise((resolve, reject) => {
402
+ const timeout = setTimeout(() => {
403
+ ws.close();
404
+ reject(new Error("Query timeout"));
405
+ }, 1e4);
406
+ ws.onopen = () => {
407
+ const subscriptionId = `ochk_identity_${Date.now()}`;
408
+ ws.send(
409
+ JSON.stringify([
410
+ "REQ",
411
+ subscriptionId,
412
+ {
413
+ kinds: [30078],
414
+ "#i": [identityTag],
415
+ limit: 10
416
+ }
417
+ ])
418
+ );
419
+ };
420
+ ws.onmessage = (msg) => {
421
+ try {
422
+ const data = JSON.parse(msg.data);
423
+ if (data[0] === "EVENT") {
424
+ events.push(data[2]);
425
+ } else if (data[0] === "EOSE") {
426
+ clearTimeout(timeout);
427
+ ws.close();
428
+ resolve();
429
+ }
430
+ } catch (err) {
431
+ log.error({ relay: relayUrl, error: err }, "Failed to parse event");
432
+ }
433
+ };
434
+ ws.onerror = (err) => {
435
+ clearTimeout(timeout);
436
+ log.error({ relay: relayUrl, error: err }, "WebSocket error during query");
437
+ reject(err);
438
+ };
439
+ });
440
+ } catch (err) {
441
+ log.error({ relay: relayUrl, error: err }, "Failed to query relay");
442
+ }
443
+ });
444
+ await Promise.allSettled(queryPromises);
445
+ log.info({ eventCount: events.length }, "Query complete");
446
+ return events;
447
+ }
448
+ async function verifyAttestationEnvelope(envelope) {
449
+ try {
450
+ const encoder = new TextEncoder();
451
+ const data = encoder.encode(envelope.message);
452
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
453
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
454
+ const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
455
+ return computedId === envelope.attestation_id;
456
+ } catch (err) {
457
+ log.error({ error: err }, "Failed to verify attestation envelope");
458
+ return false;
459
+ }
460
+ }
461
+ function parseAttestationFromEvent(event) {
462
+ try {
463
+ const envelope = JSON.parse(event.content);
464
+ return envelope;
465
+ } catch (err) {
466
+ log.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
467
+ return null;
468
+ }
469
+ }
470
+
471
+ // src/nostr-crypto.ts
472
+ var log2 = createLogger("ocp/nostr-crypto");
473
+ function isNip07Available() {
474
+ if (typeof window === "undefined") {
475
+ return false;
476
+ }
477
+ return !!window.nostr;
478
+ }
479
+ async function signNostrEvent(event) {
480
+ if (!isNip07Available()) {
481
+ log2.warn("NIP-07 extension not available");
482
+ return null;
483
+ }
484
+ try {
485
+ const nostr = window.nostr;
486
+ const signedEvent = await nostr.signEvent(event);
487
+ log2.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
488
+ return signedEvent;
489
+ } catch (err) {
490
+ log2.error({ error: err }, "Failed to sign Nostr event");
491
+ return null;
492
+ }
493
+ }
494
+
495
+ // src/attestation.ts
496
+ var log3 = createLogger("ocp/attestation");
497
+ var AttestationError = class extends Error {
498
+ constructor(message, code) {
499
+ super(message);
500
+ this.code = code;
501
+ this.name = "AttestationError";
502
+ }
503
+ };
504
+ async function createAttestation(options) {
505
+ const { message, signature, scheme, address, identities = [], extensions = {} } = options;
506
+ log3.info({ address, identityCount: identities.length }, "Creating attestation");
507
+ const envelope = await createAttestationEnvelope(
508
+ message,
509
+ signature,
510
+ scheme,
511
+ address,
512
+ identities,
513
+ extensions
514
+ );
515
+ const isValid = await verifyAttestationEnvelope(envelope);
516
+ if (!isValid) {
517
+ throw new AttestationError(
518
+ "Attestation envelope integrity check failed",
519
+ "INTEGRITY_CHECK_FAILED"
520
+ );
521
+ }
522
+ log3.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
523
+ return envelope;
524
+ }
525
+ async function publishAttestation(options) {
526
+ const { envelope, npub, relays = DEFAULT_RELAYS } = options;
527
+ log3.info(
528
+ { attestationId: envelope.attestation_id, relayCount: relays.length },
529
+ "Publishing attestation"
530
+ );
531
+ const unsignedEvent = createAttestationEvent(envelope, npub);
532
+ if (!isNip07Available()) {
533
+ log3.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
534
+ throw new Error(
535
+ "nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish"
536
+ );
537
+ }
538
+ log3.info("Using NIP-07 to sign event");
539
+ const signedEvent = await signNostrEvent(unsignedEvent);
540
+ if (!signedEvent) {
541
+ throw new Error("nostr_signing_failed: NIP-07 extension rejected or errored");
542
+ }
543
+ const result = await publishToRelays(signedEvent, relays);
544
+ log3.info(
545
+ {
546
+ attestationId: envelope.attestation_id,
547
+ successCount: result.success.length,
548
+ failedCount: result.failed.length
549
+ },
550
+ "Publishing complete"
551
+ );
552
+ return result;
553
+ }
554
+ async function discoverAttestations(options) {
555
+ const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
556
+ let events = [];
557
+ if (attestationId) {
558
+ log3.info({ attestationId }, "Discovering by attestation ID");
559
+ events = await queryByAttestationId(attestationId, relays);
560
+ } else if (address) {
561
+ log3.info({ address }, "Discovering by Bitcoin address");
562
+ events = await queryByAddress(address, relays);
563
+ } else if (identity) {
564
+ log3.info({ identity }, "Discovering by identity");
565
+ events = await queryByIdentity(identity.protocol, identity.identifier, relays);
566
+ } else {
567
+ throw new Error("Must provide attestationId, address, or identity");
568
+ }
569
+ const envelopes = [];
570
+ for (const event of events) {
571
+ const envelope = parseAttestationFromEvent(event);
572
+ if (envelope) {
573
+ const isValid = await verifyAttestationEnvelope(envelope);
574
+ if (isValid) {
575
+ envelopes.push(envelope);
576
+ } else {
577
+ log3.warn(
578
+ { attestationId: envelope.attestation_id },
579
+ "Skipping invalid attestation envelope"
580
+ );
581
+ }
582
+ }
583
+ }
584
+ log3.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
585
+ return envelopes;
586
+ }
587
+ async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
588
+ log3.info({ attestationId }, "Verifying attestation by ID");
589
+ const envelopes = await discoverAttestations({ attestationId, relays });
590
+ if (envelopes.length === 0) {
591
+ log3.warn({ attestationId }, "Attestation not found on any relay");
592
+ return null;
593
+ }
594
+ if (envelopes.length > 1) {
595
+ log3.warn(
596
+ { attestationId, count: envelopes.length },
597
+ "Multiple attestations found with same ID - using first"
598
+ );
599
+ }
600
+ return envelopes[0] || null;
601
+ }
602
+ async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
603
+ log3.info({ address }, "Getting all attestations for address");
604
+ return discoverAttestations({ address, relays });
605
+ }
606
+ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
607
+ log3.info({ protocol, identifier }, "Getting all attestations for identity");
608
+ return discoverAttestations({ identity: { protocol, identifier }, relays });
609
+ }
610
+
611
+ // src/scoring.ts
612
+ function scoreV0(sats, days) {
613
+ if (sats <= 0 || days <= 0) return 0;
614
+ return Math.round(Math.log(1 + sats) * (1 + days / 30) * 100) / 100;
615
+ }
616
+ function scoreTier(sats, days) {
617
+ if (sats >= 1e7 && days >= 365) return "platinum";
618
+ if (sats >= 1e6 && days >= 180) return "gold";
619
+ if (sats >= 1e5 && days >= 90) return "silver";
620
+ if (sats >= 1e4 && days >= 30) return "bronze";
621
+ return "none";
622
+ }
623
+ function computeScore(sats, days, config) {
624
+ switch (config.algorithm) {
625
+ case "v0":
626
+ return scoreV0(sats, days);
627
+ case "tier":
628
+ return scoreTier(sats, days);
629
+ case "none":
630
+ return null;
631
+ }
632
+ }
633
+
634
+ // src/verify.ts
635
+ var log4 = createLogger("ocp/verify");
636
+ function scoreV02(sats, days) {
637
+ return computeScore(sats, days, { algorithm: "v0" });
638
+ }
639
+ function looksLikeHex(s) {
640
+ return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
641
+ }
642
+ function toBase64Maybe(sig) {
643
+ if (looksLikeHex(sig)) {
644
+ return Buffer.from(sig, "hex").toString("base64");
645
+ }
646
+ return sig;
647
+ }
648
+ function detectSignatureScheme(addr, sig) {
649
+ const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
650
+ if (isSegWit) {
651
+ return "bip322";
652
+ }
653
+ const sigBase64 = toBase64Maybe(sig);
654
+ const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
655
+ if (looksLikeLegacy) {
656
+ return "legacy";
657
+ }
658
+ return "bip322";
659
+ }
660
+ async function verifySignature({
661
+ addr,
662
+ msg,
663
+ sig,
664
+ scheme
665
+ }) {
666
+ try {
667
+ if (sig === "demo_signature") {
668
+ log4.warn("Demo signature detected in verifySignature - should be handled by demo mode");
669
+ return "sig_invalid";
670
+ }
671
+ const detectedScheme = scheme || detectSignatureScheme(addr, sig);
672
+ log4.debug(
673
+ { addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
674
+ "Verifying signature"
675
+ );
676
+ if (detectedScheme === "bip322") {
677
+ const sigBase64 = toBase64Maybe(sig);
678
+ log4.debug(
679
+ { sigBase64: sigBase64.substring(0, 50) + "..." },
680
+ "Signature converted to base64"
681
+ );
682
+ log4.debug(
683
+ {
684
+ msgFull: msg,
685
+ msgLines: msg.split("\n").length,
686
+ msgFirstLine: msg.split("\n")[0],
687
+ msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
688
+ },
689
+ "Message being verified"
690
+ );
691
+ const ok = Verifier.verifySignature(addr, msg, sigBase64);
692
+ log4.info({ addr, ok }, "BIP-322 verification result");
693
+ if (!ok) {
694
+ const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
695
+ if (isSegWit && sigBase64.length === 88) {
696
+ log4.warn(
697
+ { addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
698
+ "BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
699
+ );
700
+ } else {
701
+ log4.error(
702
+ {
703
+ addr,
704
+ sigLength: sigBase64.length,
705
+ msgLength: msg.length,
706
+ msgPreview: msg.substring(0, 200)
707
+ },
708
+ "BIP-322 verification failed - signature does not match message"
709
+ );
710
+ }
711
+ }
712
+ return ok ? "sig_ok_bip322" : "sig_invalid";
713
+ }
714
+ if (detectedScheme === "legacy") {
715
+ const sigBase64 = toBase64Maybe(sig);
716
+ if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
717
+ log4.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
718
+ return "sig_unsupported_script";
719
+ }
720
+ log4.debug(
721
+ {
722
+ addr,
723
+ sigBase64: sigBase64.substring(0, 50) + "...",
724
+ msgPreview: msg.substring(0, 100) + "...",
725
+ msgLength: msg.length
726
+ },
727
+ "Verifying legacy signature"
728
+ );
729
+ const ok = bitcoinMessage.verify(msg, addr, sigBase64);
730
+ log4.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
731
+ if (!ok) {
732
+ log4.error(
733
+ {
734
+ addr,
735
+ msgFirstLine: msg.split("\n")[0],
736
+ sigFormat: sigBase64.substring(0, 10)
737
+ },
738
+ "Legacy signature verification failed - check message format and signature"
739
+ );
740
+ }
741
+ return ok ? "sig_ok_legacy" : "sig_invalid";
742
+ }
743
+ return "invalid_scheme";
744
+ } catch (err) {
745
+ log4.error(
746
+ { addr, error: err instanceof Error ? err.message : String(err) },
747
+ "Signature verification error"
748
+ );
749
+ return "sig_invalid";
750
+ }
751
+ }
752
+ function parseCanonicalMessage(msg) {
753
+ const lines = msg.split("\n").map((l) => l.trim());
754
+ const getVal = (prefix) => lines.find((l) => l.toLowerCase().startsWith(prefix + ":"))?.split(":", 2)[1]?.trim();
755
+ const address = getVal("address") || "";
756
+ const identitiesStr = getVal("identities") || "";
757
+ let identities = [];
758
+ try {
759
+ identities = parseIdentities(identitiesStr);
760
+ } catch (err) {
761
+ log4.warn({ identitiesStr, error: err }, "Failed to parse identities field");
762
+ }
763
+ const coreKeys = /* @__PURE__ */ new Set([
764
+ "orangecheck",
765
+ "identities",
766
+ "address",
767
+ "purpose",
768
+ "nonce",
769
+ "issued_at",
770
+ "ack"
771
+ ]);
772
+ const extensions = {};
773
+ for (const line of lines) {
774
+ const idx = line.indexOf(":");
775
+ if (idx === -1) continue;
776
+ const key = line.slice(0, idx).trim().toLowerCase();
777
+ const val = line.slice(idx + 1).trim();
778
+ if (!coreKeys.has(key) && val) extensions[key] = val;
779
+ }
780
+ const network = extensions["network"] || "mainnet";
781
+ const bondStr = extensions["bond"];
782
+ const bond = bondStr ? parseInt(bondStr, 10) : void 0;
783
+ return {
784
+ core: { address, identities },
785
+ network,
786
+ extensions,
787
+ bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
788
+ };
789
+ }
790
+ async function getAddressUtxos(address, network, opts) {
791
+ const endpoints = network === "signet" ? [
792
+ opts?.esploraSignetBase || "https://mempool.space/signet/api",
793
+ "https://blockstream.info/signet/api"
794
+ ] : network === "testnet" ? [
795
+ opts?.esploraTestnetBase || "https://mempool.space/testnet/api",
796
+ "https://blockstream.info/testnet/api"
797
+ ] : [
798
+ opts?.esploraMainnetBase || "https://mempool.space/api",
799
+ "https://blockstream.info/api"
800
+ ];
801
+ let lastError = null;
802
+ for (const base of endpoints) {
803
+ try {
804
+ const url = `${base}/address/${address}/utxo`;
805
+ log4.debug({ url, network }, "Fetching UTXOs");
806
+ const resp = await fetch(url, {
807
+ method: "GET",
808
+ headers: { Accept: "application/json" },
809
+ signal: AbortSignal.timeout(1e4)
810
+ // 10s timeout
811
+ });
812
+ if (!resp.ok) {
813
+ throw new Error(`HTTP ${resp.status}`);
814
+ }
815
+ const data = await resp.json();
816
+ if (!Array.isArray(data)) {
817
+ throw new Error("Invalid response: expected array");
818
+ }
819
+ for (const utxo of data) {
820
+ if (typeof utxo.value !== "number" || typeof utxo.status !== "object" || typeof utxo.status.confirmed !== "boolean" || typeof utxo.txid !== "string" || typeof utxo.vout !== "number") {
821
+ throw new Error("Invalid UTXO structure");
822
+ }
823
+ }
824
+ log4.info({ base, count: data.length, network }, "UTXOs fetched successfully");
825
+ return data;
826
+ } catch (err) {
827
+ lastError = err instanceof Error ? err : new Error(String(err));
828
+ log4.warn(
829
+ { base, error: lastError.message, network },
830
+ "Esplora endpoint failed, trying next"
831
+ );
832
+ }
833
+ }
834
+ log4.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
835
+ throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
836
+ }
837
+ function computeMetrics(utxos, bondAmount) {
838
+ const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
839
+ if (confirmedUtxos.length === 0) {
840
+ return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
841
+ }
842
+ const confirmed_balance = confirmedUtxos.reduce((sum, u) => sum + u.value, 0);
843
+ if (bondAmount !== void 0 && bondAmount > 0) {
844
+ const sorted = [...confirmedUtxos].sort((a, b) => {
845
+ const heightA = a.status.block_height ?? Infinity;
846
+ const heightB = b.status.block_height ?? Infinity;
847
+ if (heightA !== heightB) return heightA - heightB;
848
+ if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);
849
+ return a.vout - b.vout;
850
+ });
851
+ let sum = 0;
852
+ let youngestTime = 0;
853
+ for (const utxo of sorted) {
854
+ sum += utxo.value;
855
+ youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);
856
+ if (sum >= bondAmount) break;
857
+ }
858
+ const days_unspent2 = Math.max(
859
+ 0,
860
+ Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
861
+ );
862
+ return {
863
+ sats_bonded: bondAmount,
864
+ // Use exactly bond amount (ignore surplus)
865
+ days_unspent: days_unspent2,
866
+ confirmed_balance
867
+ };
868
+ }
869
+ const sats_bonded = confirmed_balance;
870
+ const oldestTime = Math.min(
871
+ ...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
872
+ );
873
+ const days_unspent = Math.max(0, Math.floor((Date.now() / 1e3 - oldestTime) / (24 * 60 * 60)));
874
+ return { sats_bonded, days_unspent, confirmed_balance };
875
+ }
876
+ async function verify2(input, opts = {}) {
877
+ const codes = [];
878
+ let network = "mainnet";
879
+ try {
880
+ if (!input.msg || !input.addr || !input.sig) {
881
+ codes.push("bad_request");
882
+ return { ok: false, codes, network };
883
+ }
884
+ const parsed = parseCanonicalMessage(input.msg);
885
+ network = parsed.network || "mainnet";
886
+ const attestation_id = await generateAttestationId(input.msg);
887
+ if (parsed.core.address !== input.addr) {
888
+ codes.push("bad_request");
889
+ return { ok: false, codes, network, attestation_id };
890
+ }
891
+ if (opts.demoMode) {
892
+ const sats_bonded = 1e6;
893
+ const days_unspent = 90;
894
+ const metrics = {
895
+ sats_bonded,
896
+ days_unspent,
897
+ score: scoreV02(sats_bonded, days_unspent)
898
+ // ≈ 55.26
899
+ };
900
+ codes.push("sig_ok_bip322");
901
+ codes.push("bond_confirmed");
902
+ return {
903
+ ok: true,
904
+ codes,
905
+ network,
906
+ attestation_id,
907
+ identities: parsed.core.identities,
908
+ metrics
909
+ };
910
+ }
911
+ if ((network === "testnet" || network === "signet") && !opts.testMode) {
912
+ codes.push("network_testmode");
913
+ return { ok: false, codes, network, attestation_id };
914
+ }
915
+ if (opts.expectedAud && parsed.extensions["aud"] && parsed.extensions["aud"] !== opts.expectedAud) {
916
+ codes.push("aud_mismatch");
917
+ }
918
+ if (parsed.extensions["expires"]) {
919
+ const exp = Date.parse(parsed.extensions["expires"]);
920
+ if (!Number.isNaN(exp) && exp < Date.now()) {
921
+ codes.push("expired");
922
+ }
923
+ }
924
+ const sigCode = await verifySignature({
925
+ addr: input.addr,
926
+ msg: input.msg,
927
+ sig: input.sig,
928
+ ...input.scheme && { scheme: input.scheme }
929
+ });
930
+ codes.push(sigCode);
931
+ const sigOk = sigCode === "sig_ok_bip322" || sigCode === "sig_ok_legacy";
932
+ if (!sigOk) {
933
+ return {
934
+ ok: false,
935
+ codes,
936
+ network,
937
+ attestation_id,
938
+ identities: parsed.core.identities
939
+ };
940
+ }
941
+ try {
942
+ const utxos = await getAddressUtxos(input.addr, network, {
943
+ esploraMainnetBase: opts.esploraMainnetBase,
944
+ esploraSignetBase: opts.esploraSignetBase
945
+ });
946
+ const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
947
+ utxos,
948
+ parsed.bond
949
+ );
950
+ if (parsed.bond !== void 0 && parsed.bond > 0) {
951
+ if (confirmed_balance < parsed.bond) {
952
+ codes.push("bond_insufficient");
953
+ log4.warn(
954
+ {
955
+ bond: parsed.bond,
956
+ confirmed_balance,
957
+ address: input.addr
958
+ },
959
+ "Bond extension: insufficient balance"
960
+ );
961
+ return {
962
+ ok: false,
963
+ codes,
964
+ network,
965
+ attestation_id,
966
+ identities: parsed.core.identities
967
+ };
968
+ }
969
+ }
970
+ if (sats_bonded <= 0) {
971
+ codes.push("bond_zero");
972
+ if (utxos.some((u) => !u.status.confirmed)) {
973
+ codes.push("bond_pending");
974
+ }
975
+ return {
976
+ ok: false,
977
+ codes,
978
+ network,
979
+ attestation_id,
980
+ identities: parsed.core.identities
981
+ };
982
+ } else {
983
+ codes.push("bond_confirmed");
984
+ }
985
+ const metrics = {
986
+ sats_bonded,
987
+ days_unspent,
988
+ score: scoreV02(sats_bonded, days_unspent)
989
+ };
990
+ return {
991
+ ok: true,
992
+ codes,
993
+ network,
994
+ attestation_id,
995
+ identities: parsed.core.identities,
996
+ metrics
997
+ };
998
+ } catch (utxoError) {
999
+ console.error("UTXO fetch failed:", utxoError);
1000
+ codes.push("bad_request");
1001
+ return { ok: false, codes, network, attestation_id };
1002
+ }
1003
+ } catch (e) {
1004
+ const msg = e instanceof Error ? e.message : String(e ?? "decode_error");
1005
+ if (msg === "decode_error") {
1006
+ codes.push("decode_error");
1007
+ } else {
1008
+ codes.push("bad_request");
1009
+ }
1010
+ return { ok: false, codes, network };
1011
+ }
1012
+ }
1013
+
1014
+ // src/check.ts
1015
+ async function check(params) {
1016
+ const {
1017
+ addr,
1018
+ id,
1019
+ identity,
1020
+ minSats = 0,
1021
+ minDays = 0,
1022
+ relays = DEFAULT_RELAYS,
1023
+ verifyOptions
1024
+ } = params;
1025
+ if (!addr && !id && !identity) {
1026
+ return { ok: false, sats: 0, days: 0, score: 0, reasons: ["bad_request"] };
1027
+ }
1028
+ let envelopes = [];
1029
+ if (id) {
1030
+ envelopes = await discoverAttestations({ attestationId: id, relays });
1031
+ } else if (addr) {
1032
+ envelopes = await getAttestationsForAddress(addr, relays);
1033
+ } else if (identity) {
1034
+ envelopes = await getAttestationsForIdentity(
1035
+ identity.protocol,
1036
+ identity.identifier,
1037
+ relays
1038
+ );
1039
+ }
1040
+ if (envelopes.length === 0) {
1041
+ return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1042
+ }
1043
+ envelopes.sort((a, b) => new Date(b.issued_at).getTime() - new Date(a.issued_at).getTime());
1044
+ const env = envelopes[0];
1045
+ if (!env) {
1046
+ return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
1047
+ }
1048
+ const outcome = await verify2(
1049
+ {
1050
+ addr: env.address,
1051
+ msg: env.message,
1052
+ sig: env.signature,
1053
+ scheme: env.scheme
1054
+ },
1055
+ verifyOptions
1056
+ );
1057
+ const sats = outcome.metrics?.sats_bonded ?? 0;
1058
+ const days = outcome.metrics?.days_unspent ?? 0;
1059
+ const score = outcome.metrics?.score ?? 0;
1060
+ const reasons = [];
1061
+ if (!outcome.ok) reasons.push(...outcome.codes);
1062
+ if (sats < minSats) reasons.push("below_min_sats");
1063
+ if (days < minDays) reasons.push("below_min_days");
1064
+ const ok = outcome.ok && sats >= minSats && days >= minDays;
1065
+ return {
1066
+ ok,
1067
+ sats,
1068
+ days,
1069
+ score,
1070
+ attestation_id: env.attestation_id,
1071
+ address: env.address,
1072
+ identities: env.identities,
1073
+ network: outcome.network,
1074
+ ...reasons.length ? { reasons } : {}
1075
+ };
1076
+ }
1077
+ var AUTH_HEADER = "orangecheck-auth";
1078
+ var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1079
+ function randomNonce() {
1080
+ const bytes = new Uint8Array(16);
1081
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
1082
+ globalThis.crypto.getRandomValues(bytes);
1083
+ } else {
1084
+ for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1085
+ }
1086
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1087
+ }
1088
+ function iso(date) {
1089
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
1090
+ }
1091
+ function issueChallenge(opts) {
1092
+ const now = opts.now ?? /* @__PURE__ */ new Date();
1093
+ const ttl = opts.ttlSeconds ?? 300;
1094
+ const expiresAt = new Date(now.getTime() + ttl * 1e3);
1095
+ const nonce = opts.nonce ?? randomNonce();
1096
+ if (!/^[0-9a-f]{32}$/.test(nonce)) {
1097
+ throw new Error("nonce must be 32 lowercase hex characters");
1098
+ }
1099
+ const coreLines = [
1100
+ AUTH_HEADER,
1101
+ `address: ${opts.address}`,
1102
+ `nonce: ${nonce}`,
1103
+ `issued_at: ${iso(now)}`,
1104
+ `expires_at: ${iso(expiresAt)}`,
1105
+ `ack: ${AUTH_ACK}`
1106
+ ];
1107
+ const extensions = [];
1108
+ if (opts.audience) extensions.push(["audience", opts.audience]);
1109
+ if (opts.purpose) extensions.push(["purpose", opts.purpose]);
1110
+ extensions.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1111
+ const extLines = extensions.map(([k, v]) => `${k}: ${v}`);
1112
+ const message = [...coreLines, ...extLines].join("\n") + "\n";
1113
+ return {
1114
+ message,
1115
+ nonce,
1116
+ expiresAt: expiresAt.getTime(),
1117
+ expiresAtIso: iso(expiresAt)
1118
+ };
1119
+ }
1120
+ function parseChallenge(message) {
1121
+ const lines = message.endsWith("\n") ? message.slice(0, -1).split("\n") : message.split("\n");
1122
+ if (lines[0] !== AUTH_HEADER) {
1123
+ throw new Error("not an orangecheck-auth challenge");
1124
+ }
1125
+ const out = { extensions: [] };
1126
+ const coreKeys = /* @__PURE__ */ new Set(["address", "nonce", "issued_at", "expires_at", "ack"]);
1127
+ let seenCore = 0;
1128
+ for (let i = 1; i < lines.length; i++) {
1129
+ const line = lines[i] ?? "";
1130
+ const sep = line.indexOf(": ");
1131
+ if (sep === -1) continue;
1132
+ const key = line.slice(0, sep);
1133
+ const val = line.slice(sep + 2);
1134
+ if (coreKeys.has(key)) {
1135
+ seenCore++;
1136
+ if (key === "address") out.address = val;
1137
+ else if (key === "nonce") out.nonce = val;
1138
+ else if (key === "issued_at") out.issuedAt = new Date(val);
1139
+ else if (key === "expires_at") out.expiresAt = new Date(val);
1140
+ else if (key === "ack") out.ack = val;
1141
+ } else {
1142
+ out.extensions.push([key, val]);
1143
+ if (key === "audience") out.audience = val;
1144
+ if (key === "purpose") out.purpose = val;
1145
+ }
1146
+ }
1147
+ if (seenCore !== 5) {
1148
+ throw new Error(`challenge missing core lines (${seenCore}/5)`);
1149
+ }
1150
+ return out;
1151
+ }
1152
+ async function verifyChallenge(opts) {
1153
+ let parsed;
1154
+ try {
1155
+ parsed = parseChallenge(opts.message);
1156
+ } catch {
1157
+ return { ok: false, reason: "malformed" };
1158
+ }
1159
+ const { address, nonce, issuedAt, expiresAt, ack, audience, purpose } = parsed;
1160
+ if (!address || !nonce || !issuedAt || !expiresAt || !ack) {
1161
+ return { ok: false, reason: "malformed" };
1162
+ }
1163
+ if (ack !== AUTH_ACK) {
1164
+ return { ok: false, reason: "malformed" };
1165
+ }
1166
+ if (!/^[0-9a-f]{32}$/.test(nonce)) {
1167
+ return { ok: false, reason: "malformed" };
1168
+ }
1169
+ const now = opts.now ?? /* @__PURE__ */ new Date();
1170
+ const skew = opts.clockSkewMs ?? 0;
1171
+ if (now.getTime() - skew > expiresAt.getTime()) {
1172
+ return { ok: false, reason: "expired" };
1173
+ }
1174
+ if (now.getTime() + skew < issuedAt.getTime()) {
1175
+ return { ok: false, reason: "not_yet_valid" };
1176
+ }
1177
+ if (opts.expectedNonce && nonce !== opts.expectedNonce) {
1178
+ return { ok: false, reason: "nonce_mismatch" };
1179
+ }
1180
+ if (opts.expectedAudience && audience !== opts.expectedAudience) {
1181
+ return { ok: false, reason: "audience_mismatch" };
1182
+ }
1183
+ if (opts.expectedPurpose && purpose !== opts.expectedPurpose) {
1184
+ return { ok: false, reason: "purpose_mismatch" };
1185
+ }
1186
+ const scheme = opts.scheme ?? "bip322";
1187
+ let sigValid = false;
1188
+ try {
1189
+ if (scheme === "bip322") {
1190
+ const sigBase64 = /^[0-9a-fA-F]+$/.test(opts.signature) && opts.signature.length % 2 === 0 ? Buffer.from(opts.signature, "hex").toString("base64") : opts.signature;
1191
+ sigValid = Verifier.verifySignature(address, opts.message, sigBase64);
1192
+ } else if (scheme === "legacy") {
1193
+ if (!/^[1m][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address)) {
1194
+ return { ok: false, reason: "sig_unsupported_scheme" };
1195
+ }
1196
+ sigValid = bitcoinMessage.verify(opts.message, address, opts.signature);
1197
+ } else {
1198
+ return { ok: false, reason: "sig_unsupported_scheme" };
1199
+ }
1200
+ } catch {
1201
+ sigValid = false;
1202
+ }
1203
+ if (!sigValid) return { ok: false, reason: "sig_invalid" };
1204
+ return {
1205
+ ok: true,
1206
+ reason: "ok",
1207
+ address,
1208
+ expiresAt: expiresAt.getTime(),
1209
+ nonce,
1210
+ ...audience !== void 0 ? { audience } : {},
1211
+ ...purpose !== void 0 ? { purpose } : {}
1212
+ };
1213
+ }
1214
+
1215
+ // src/utils/rate-limiter.ts
1216
+ var RateLimitError = class extends Error {
1217
+ constructor(message = "Rate limit exceeded. Please try again later.") {
1218
+ super(message);
1219
+ this.name = "RateLimitError";
1220
+ }
1221
+ };
1222
+ function createRateLimiter(maxRequests, windowMs) {
1223
+ return {
1224
+ maxRequests,
1225
+ windowMs,
1226
+ requests: /* @__PURE__ */ new Map()
1227
+ };
1228
+ }
1229
+ function checkRateLimit(limiter, key) {
1230
+ const now = Date.now();
1231
+ const requests = limiter.requests.get(key) ?? [];
1232
+ const validRequests = requests.filter((timestamp) => now - timestamp < limiter.windowMs);
1233
+ if (validRequests.length >= limiter.maxRequests) {
1234
+ return false;
1235
+ }
1236
+ validRequests.push(now);
1237
+ limiter.requests.set(key, validRequests);
1238
+ return true;
1239
+ }
1240
+ function enforceRateLimit(limiter, key) {
1241
+ if (!checkRateLimit(limiter, key)) {
1242
+ throw new RateLimitError();
1243
+ }
1244
+ }
1245
+ var identityVerificationLimiter = createRateLimiter(5, 6e4);
1246
+
1247
+ // src/identity/dns.ts
1248
+ var log5 = createLogger("identity-verification/dns");
1249
+ async function verifyDnsIdentity(attestationId, domain) {
1250
+ log5.info({ attestationId, domain }, "Verifying DNS identity");
1251
+ const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
1252
+ const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
1253
+ if (wellKnownResult.verified) {
1254
+ return wellKnownResult;
1255
+ }
1256
+ log5.info({ domain: cleanDomain }, "Well-known file not found, DNS TXT check not implemented");
1257
+ return {
1258
+ verified: false,
1259
+ error: "Verification file not found. Please create .well-known/orangecheck.txt"
1260
+ };
1261
+ }
1262
+ async function checkWellKnownFile(attestationId, domain) {
1263
+ const urls = [
1264
+ `https://${domain}/.well-known/orangecheck.txt`,
1265
+ `http://${domain}/.well-known/orangecheck.txt`
1266
+ // Fallback to HTTP
1267
+ ];
1268
+ for (const url of urls) {
1269
+ try {
1270
+ log5.info({ url }, "Checking well-known file");
1271
+ const response = await fetch(url, {
1272
+ method: "GET",
1273
+ headers: {
1274
+ Accept: "text/plain"
1275
+ },
1276
+ // Don't follow redirects to other domains
1277
+ redirect: "manual"
1278
+ });
1279
+ if (response.ok) {
1280
+ const content = await response.text();
1281
+ if (content.includes(attestationId)) {
1282
+ log5.info({ domain, url }, "Found attestation ID in well-known file");
1283
+ return {
1284
+ verified: true,
1285
+ method: "well-known",
1286
+ url
1287
+ };
1288
+ } else {
1289
+ log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
1290
+ }
1291
+ }
1292
+ } catch (err) {
1293
+ log5.warn({ error: err, url }, "Failed to fetch well-known file");
1294
+ }
1295
+ }
1296
+ return {
1297
+ verified: false,
1298
+ error: "Well-known file not found or does not contain attestation ID"
1299
+ };
1300
+ }
1301
+ function getDnsVerificationInstructions(attestationId, domain) {
1302
+ const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
1303
+ return `To verify your DNS domain (${cleanDomain}):
1304
+
1305
+ **Method 1: .well-known file (Recommended)**
1306
+
1307
+ 1. Create a file at: https://${cleanDomain}/.well-known/orangecheck.txt
1308
+ 2. Add this content to the file:
1309
+
1310
+ OrangeCheck Attestation Verification
1311
+ Domain: ${cleanDomain}
1312
+ Attestation ID: ${attestationId}
1313
+ Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
1314
+
1315
+ 3. Make sure the file is publicly accessible
1316
+ 4. Return here and click "Verify"
1317
+
1318
+ **Method 2: DNS TXT record (Advanced)**
1319
+
1320
+ 1. Add a TXT record to your DNS:
1321
+ Name: _orangecheck.${cleanDomain}
1322
+ Value: ${attestationId}
1323
+
1324
+ 2. Wait for DNS propagation (up to 24 hours)
1325
+ 3. Return here and click "Verify"
1326
+
1327
+ Note: Method 1 is faster and easier to set up.`;
1328
+ }
1329
+ function generateWellKnownContent(attestationId, domain) {
1330
+ const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
1331
+ return `OrangeCheck Attestation Verification
1332
+ =====================================
1333
+
1334
+ Domain: ${cleanDomain}
1335
+ Attestation ID: ${attestationId}
1336
+ Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
1337
+
1338
+ This file verifies that the owner of ${cleanDomain} controls the Bitcoin
1339
+ address associated with attestation ${attestationId}.
1340
+
1341
+ Learn more: https://ochk.io
1342
+
1343
+ ---
1344
+
1345
+ Verification method: .well-known/orangecheck.txt
1346
+ Protocol: OrangeCheck Protocol (OCP)
1347
+ `;
1348
+ }
1349
+ function getWellKnownUrl(domain) {
1350
+ const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
1351
+ return `https://${cleanDomain}/.well-known/orangecheck.txt`;
1352
+ }
1353
+ function isValidDomain(domain) {
1354
+ const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
1355
+ const domainPattern = /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$/i;
1356
+ return domainPattern.test(cleanDomain);
1357
+ }
1358
+ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
1359
+ if (!cachedContent) {
1360
+ return false;
1361
+ }
1362
+ return cachedContent.includes(attestationId);
1363
+ }
1364
+
1365
+ // src/identity/github.ts
1366
+ var log6 = createLogger("identity-verification/github");
1367
+ async function verifyGitHubIdentity(attestationId, username) {
1368
+ log6.info({ attestationId, username }, "Verifying GitHub identity");
1369
+ try {
1370
+ const response = await fetch(`https://api.github.com/users/${username}/gists`, {
1371
+ headers: {
1372
+ Accept: "application/vnd.github.v3+json"
1373
+ }
1374
+ });
1375
+ if (!response.ok) {
1376
+ if (response.status === 404) {
1377
+ return {
1378
+ verified: false,
1379
+ error: `GitHub user '${username}' not found`
1380
+ };
1381
+ }
1382
+ return {
1383
+ verified: false,
1384
+ error: `GitHub API error: ${response.status} ${response.statusText}`
1385
+ };
1386
+ }
1387
+ const gists = await response.json();
1388
+ log6.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
1389
+ for (const gist of gists) {
1390
+ if (gist.description?.includes(attestationId)) {
1391
+ log6.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
1392
+ return {
1393
+ verified: true,
1394
+ gistUrl: gist.html_url,
1395
+ gistId: gist.id
1396
+ };
1397
+ }
1398
+ for (const [filename, file] of Object.entries(gist.files)) {
1399
+ if (file.content && file.content.includes(attestationId)) {
1400
+ log6.info(
1401
+ { username, gistId: gist.id, filename },
1402
+ "Found attestation ID in gist file"
1403
+ );
1404
+ return {
1405
+ verified: true,
1406
+ gistUrl: gist.html_url,
1407
+ gistId: gist.id
1408
+ };
1409
+ }
1410
+ try {
1411
+ const fileResponse = await fetch(file.raw_url);
1412
+ if (fileResponse.ok) {
1413
+ const content = await fileResponse.text();
1414
+ if (content.includes(attestationId)) {
1415
+ log6.info(
1416
+ { username, gistId: gist.id, filename },
1417
+ "Found attestation ID in gist file (fetched)"
1418
+ );
1419
+ return {
1420
+ verified: true,
1421
+ gistUrl: gist.html_url,
1422
+ gistId: gist.id
1423
+ };
1424
+ }
1425
+ }
1426
+ } catch (err) {
1427
+ log6.warn({ error: err, filename }, "Failed to fetch gist file content");
1428
+ }
1429
+ }
1430
+ }
1431
+ log6.info({ username }, "No verification gist found");
1432
+ return {
1433
+ verified: false,
1434
+ error: "No gist found containing the attestation ID"
1435
+ };
1436
+ } catch (err) {
1437
+ log6.error({ error: err, username }, "Failed to verify GitHub identity");
1438
+ return {
1439
+ verified: false,
1440
+ error: err instanceof Error ? err.message : "Unknown error"
1441
+ };
1442
+ }
1443
+ }
1444
+ function getGitHubVerificationInstructions(attestationId) {
1445
+ return `To verify your GitHub identity:
1446
+
1447
+ 1. Go to https://gist.github.com
1448
+ 2. Create a new public gist
1449
+ 3. Add a file with any name (e.g., "orangecheck.txt")
1450
+ 4. Paste this text into the file:
1451
+
1452
+ OrangeCheck Attestation Verification
1453
+ Attestation ID: ${attestationId}
1454
+
1455
+ 5. Click "Create public gist"
1456
+ 6. Return here and click "Verify" to check
1457
+
1458
+ The verification will search your public gists for this attestation ID.`;
1459
+ }
1460
+ function generateGistTemplate(attestationId, username) {
1461
+ return `# OrangeCheck Attestation Verification
1462
+
1463
+ I am verifying my GitHub identity (@${username}) for OrangeCheck.
1464
+
1465
+ **Attestation ID:** \`${attestationId}\`
1466
+
1467
+ **Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}
1468
+
1469
+ This gist proves that I control both:
1470
+ - GitHub account: @${username}
1471
+ - Bitcoin address associated with attestation ${attestationId}
1472
+
1473
+ ---
1474
+
1475
+ Learn more about OrangeCheck: https://ochk.io
1476
+ `;
1477
+ }
1478
+ function isGitHubIdentityVerified(attestationId, _username, cachedGists) {
1479
+ return cachedGists.some((gist) => {
1480
+ if (gist.description?.includes(attestationId)) {
1481
+ return true;
1482
+ }
1483
+ return Object.values(gist.files).some((file) => file.content?.includes(attestationId));
1484
+ });
1485
+ }
1486
+ function getGistCreationUrl() {
1487
+ return "https://gist.github.com/";
1488
+ }
1489
+
1490
+ // src/identity/nostr.ts
1491
+ var log7 = createLogger("identity-verification/nostr");
1492
+ function npubToHex(npub) {
1493
+ if (npub.match(/^[0-9a-f]{64}$/i)) {
1494
+ return npub.toLowerCase();
1495
+ }
1496
+ log7.warn({ npub }, "npub to hex conversion not fully implemented");
1497
+ return npub;
1498
+ }
1499
+ async function queryNostrEvents(options) {
1500
+ const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
1501
+ const events = [];
1502
+ log7.info({ authors, kinds, relayCount: relays.length }, "Querying Nostr for events");
1503
+ const queryPromises = relays.map(async (relayUrl) => {
1504
+ let ws = null;
1505
+ try {
1506
+ ws = new WebSocket(relayUrl);
1507
+ const wsRef = ws;
1508
+ await new Promise((resolve, reject) => {
1509
+ const timeout = setTimeout(() => {
1510
+ wsRef.close();
1511
+ reject(new Error("Query timeout"));
1512
+ }, 1e4);
1513
+ const cleanup = () => {
1514
+ clearTimeout(timeout);
1515
+ wsRef.close();
1516
+ };
1517
+ wsRef.onopen = () => {
1518
+ const subscriptionId = `verify_${Date.now()}`;
1519
+ const filter = {
1520
+ authors,
1521
+ kinds,
1522
+ limit
1523
+ };
1524
+ wsRef.send(JSON.stringify(["REQ", subscriptionId, filter]));
1525
+ };
1526
+ wsRef.onmessage = (msg) => {
1527
+ try {
1528
+ const data = JSON.parse(msg.data);
1529
+ if (data[0] === "EVENT") {
1530
+ const event = data[2];
1531
+ if (search) {
1532
+ if (event.content.includes(search)) {
1533
+ events.push(event);
1534
+ }
1535
+ } else {
1536
+ events.push(event);
1537
+ }
1538
+ } else if (data[0] === "EOSE") {
1539
+ cleanup();
1540
+ resolve();
1541
+ }
1542
+ } catch (err) {
1543
+ log7.error({ relay: relayUrl, error: err }, "Failed to parse event");
1544
+ }
1545
+ };
1546
+ wsRef.onerror = (err) => {
1547
+ log7.error({ relay: relayUrl, error: err }, "WebSocket error during query");
1548
+ cleanup();
1549
+ reject(err);
1550
+ };
1551
+ wsRef.onclose = () => {
1552
+ clearTimeout(timeout);
1553
+ };
1554
+ });
1555
+ } catch (err) {
1556
+ log7.error({ relay: relayUrl, error: err }, "Failed to query relay");
1557
+ } finally {
1558
+ if (ws && ws.readyState !== WebSocket.CLOSED) {
1559
+ ws.close();
1560
+ }
1561
+ }
1562
+ });
1563
+ await Promise.allSettled(queryPromises);
1564
+ log7.info({ eventCount: events.length }, "Query complete");
1565
+ return events;
1566
+ }
1567
+ async function verifyNostrEventSignature(event) {
1568
+ if (!event.sig || event.sig.length !== 128) {
1569
+ return false;
1570
+ }
1571
+ log7.warn("Nostr signature verification not fully implemented");
1572
+ return true;
1573
+ }
1574
+ async function verifyNostrIdentity(attestationId, npub, relays) {
1575
+ log7.info({ attestationId, npub }, "Verifying Nostr identity");
1576
+ try {
1577
+ const pubkeyHex = npubToHex(npub);
1578
+ const events = await queryNostrEvents({
1579
+ authors: [pubkeyHex],
1580
+ kinds: [1],
1581
+ // Text notes
1582
+ search: attestationId,
1583
+ relays
1584
+ });
1585
+ if (events.length === 0) {
1586
+ log7.info({ npub }, "No verification events found");
1587
+ return {
1588
+ verified: false,
1589
+ error: "No verification event found. User must post a note containing the attestation ID."
1590
+ };
1591
+ }
1592
+ for (const event of events) {
1593
+ if (event.content.includes(attestationId)) {
1594
+ const isValid = await verifyNostrEventSignature(event);
1595
+ if (isValid) {
1596
+ log7.info({ npub, eventId: event.id }, "Nostr identity verified");
1597
+ return {
1598
+ verified: true,
1599
+ event
1600
+ };
1601
+ } else {
1602
+ log7.warn({ npub, eventId: event.id }, "Invalid event signature");
1603
+ }
1604
+ }
1605
+ }
1606
+ return {
1607
+ verified: false,
1608
+ error: "Found events but none had valid signatures"
1609
+ };
1610
+ } catch (err) {
1611
+ log7.error({ error: err, npub }, "Failed to verify Nostr identity");
1612
+ return {
1613
+ verified: false,
1614
+ error: err instanceof Error ? err.message : "Unknown error"
1615
+ };
1616
+ }
1617
+ }
1618
+ function getNostrVerificationInstructions(attestationId) {
1619
+ return `To verify your Nostr identity:
1620
+
1621
+ 1. Open your Nostr client (Damus, Amethyst, Primal, etc.)
1622
+ 2. Post a note containing this text:
1623
+
1624
+ Verifying my OrangeCheck attestation: ${attestationId}
1625
+
1626
+ 3. Wait a few seconds for the note to propagate to relays
1627
+ 4. Return here and click "Verify" to check
1628
+
1629
+ The verification will search Nostr relays for your note and confirm you control this identity.`;
1630
+ }
1631
+ function isNostrIdentityVerified(attestationId, npub, cachedEvents) {
1632
+ const pubkeyHex = npubToHex(npub);
1633
+ return cachedEvents.some(
1634
+ (event) => event.pubkey === pubkeyHex && event.kind === 1 && event.content.includes(attestationId)
1635
+ );
1636
+ }
1637
+
1638
+ // src/identity/twitter.ts
1639
+ var log8 = createLogger("identity-verification/twitter");
1640
+ async function verifyTwitterIdentity(attestationId, handle, tweetUrl) {
1641
+ log8.info({ attestationId, handle, tweetUrl }, "Verifying Twitter identity");
1642
+ if (!tweetUrl) {
1643
+ return {
1644
+ verified: false,
1645
+ error: "Tweet URL required for verification"
1646
+ };
1647
+ }
1648
+ try {
1649
+ const tweetId = extractTweetId(tweetUrl);
1650
+ if (!tweetId) {
1651
+ return {
1652
+ verified: false,
1653
+ error: "Invalid tweet URL format"
1654
+ };
1655
+ }
1656
+ log8.info({ handle, tweetId }, "Manual verification required");
1657
+ return {
1658
+ verified: false,
1659
+ tweetUrl,
1660
+ tweetId,
1661
+ error: "Automatic verification not available. Please verify manually by checking the tweet."
1662
+ };
1663
+ } catch (err) {
1664
+ log8.error({ error: err, handle }, "Failed to verify Twitter identity");
1665
+ return {
1666
+ verified: false,
1667
+ error: err instanceof Error ? err.message : "Unknown error"
1668
+ };
1669
+ }
1670
+ }
1671
+ function extractTweetId(url) {
1672
+ const patterns = [
1673
+ /(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/,
1674
+ /(?:mobile\.twitter\.com)\/\w+\/status\/(\d+)/
1675
+ ];
1676
+ for (const pattern of patterns) {
1677
+ const match = url.match(pattern);
1678
+ if (match && match[1]) {
1679
+ return match[1];
1680
+ }
1681
+ }
1682
+ return null;
1683
+ }
1684
+ function getTwitterVerificationInstructions(attestationId, handle) {
1685
+ const cleanHandle = handle.startsWith("@") ? handle.slice(1) : handle;
1686
+ return `To verify your Twitter identity (@${cleanHandle}):
1687
+
1688
+ 1. Go to https://twitter.com/compose/tweet
1689
+ 2. Post a tweet with this text:
1690
+
1691
+ Verifying my OrangeCheck attestation: ${attestationId}
1692
+
1693
+ 3. After posting, copy the tweet URL
1694
+ 4. Paste the URL below and click "Verify"
1695
+
1696
+ Example tweet URL:
1697
+ https://twitter.com/${cleanHandle}/status/1234567890
1698
+
1699
+ The verification will check that the tweet exists and contains the attestation ID.`;
1700
+ }
1701
+ function generateTweetText(attestationId) {
1702
+ return `Verifying my OrangeCheck attestation: ${attestationId}
1703
+
1704
+ Proving Bitcoin reputation with cryptographic signatures.
1705
+
1706
+ #OrangeCheck #Bitcoin`;
1707
+ }
1708
+ function getTwitterComposeUrl(attestationId) {
1709
+ const text = generateTweetText(attestationId);
1710
+ const encoded = encodeURIComponent(text);
1711
+ return `https://twitter.com/intent/tweet?text=${encoded}`;
1712
+ }
1713
+ function isValidTweetUrl(url) {
1714
+ return extractTweetId(url) !== null;
1715
+ }
1716
+ function getTweetUrl(handle, tweetId) {
1717
+ const cleanHandle = handle.startsWith("@") ? handle.slice(1) : handle;
1718
+ return `https://twitter.com/${cleanHandle}/status/${tweetId}`;
1719
+ }
1720
+ function getManualVerificationInstructions(attestationId, handle, tweetUrl) {
1721
+ return `Manual Verification Steps:
1722
+
1723
+ 1. Open this tweet: ${tweetUrl}
1724
+ 2. Verify it's posted by @${handle}
1725
+ 3. Check that the tweet contains: ${attestationId}
1726
+ 4. Confirm the tweet is public and not deleted
1727
+
1728
+ If all checks pass, the identity is verified.`;
1729
+ }
1730
+
1731
+ // src/identity/index.ts
1732
+ async function verifyIdentity(attestationId, identity, options) {
1733
+ const { protocol, identifier } = identity;
1734
+ const rateLimitKey = `${protocol}:${identifier}`;
1735
+ try {
1736
+ enforceRateLimit(identityVerificationLimiter, rateLimitKey);
1737
+ } catch (err) {
1738
+ return {
1739
+ verified: false,
1740
+ error: err instanceof Error ? err.message : "Rate limit exceeded"
1741
+ };
1742
+ }
1743
+ try {
1744
+ switch (protocol) {
1745
+ case "nostr":
1746
+ return await verifyNostrIdentity(attestationId, identifier, options?.relays);
1747
+ case "github":
1748
+ return await verifyGitHubIdentity(attestationId, identifier);
1749
+ case "twitter":
1750
+ return await verifyTwitterIdentity(attestationId, identifier, options?.tweetUrl);
1751
+ case "dns":
1752
+ return await verifyDnsIdentity(attestationId, identifier);
1753
+ case "email":
1754
+ return {
1755
+ verified: false,
1756
+ error: "Email verification not implemented"
1757
+ };
1758
+ case "web":
1759
+ return await verifyDnsIdentity(attestationId, identifier);
1760
+ case "did":
1761
+ return {
1762
+ verified: false,
1763
+ error: "DID verification not implemented"
1764
+ };
1765
+ default:
1766
+ return {
1767
+ verified: false,
1768
+ error: `Unknown protocol: ${protocol}`
1769
+ };
1770
+ }
1771
+ } catch (error) {
1772
+ logVerificationError(
1773
+ protocol,
1774
+ identifier,
1775
+ error instanceof Error ? error : new Error(String(error)),
1776
+ {
1777
+ attestationId
1778
+ }
1779
+ );
1780
+ return {
1781
+ verified: false,
1782
+ error: error instanceof Error ? error.message : "Verification failed"
1783
+ };
1784
+ }
1785
+ }
1786
+ function getVerificationInstructions(attestationId, identity) {
1787
+ const { protocol, identifier } = identity;
1788
+ switch (protocol) {
1789
+ case "nostr":
1790
+ return getNostrVerificationInstructions(attestationId);
1791
+ case "github":
1792
+ return getGitHubVerificationInstructions(attestationId);
1793
+ case "twitter":
1794
+ return getTwitterVerificationInstructions(attestationId, identifier);
1795
+ case "dns":
1796
+ return getDnsVerificationInstructions(attestationId, identifier);
1797
+ case "email":
1798
+ return `Email verification not implemented yet.`;
1799
+ case "web":
1800
+ return getDnsVerificationInstructions(attestationId, identifier);
1801
+ case "did":
1802
+ return `DID verification not implemented yet.`;
1803
+ default:
1804
+ return `Unknown protocol: ${protocol}`;
1805
+ }
1806
+ }
1807
+ function supportsAutomaticVerification(protocol) {
1808
+ return ["nostr", "github", "dns", "web"].includes(protocol);
1809
+ }
1810
+ function requiresManualVerification(protocol) {
1811
+ return ["twitter", "email"].includes(protocol);
1812
+ }
1813
+ async function verifyAllIdentities(attestationId, identities, options) {
1814
+ const results = /* @__PURE__ */ new Map();
1815
+ await Promise.all(
1816
+ identities.map(async (identity) => {
1817
+ const key = `${identity.protocol}:${identity.identifier}`;
1818
+ const tweetUrl = options?.tweetUrls?.[identity.identifier];
1819
+ const result = await verifyIdentity(attestationId, identity, {
1820
+ tweetUrl,
1821
+ relays: options?.relays
1822
+ });
1823
+ results.set(key, result);
1824
+ })
1825
+ );
1826
+ return results;
1827
+ }
1828
+ function getVerificationSummary(results) {
1829
+ let verified = 0;
1830
+ let failed = 0;
1831
+ let pending = 0;
1832
+ for (const result of results.values()) {
1833
+ if (result.verified) {
1834
+ verified++;
1835
+ } else if (result.error?.includes("not implemented")) {
1836
+ pending++;
1837
+ } else {
1838
+ failed++;
1839
+ }
1840
+ }
1841
+ return {
1842
+ total: results.size,
1843
+ verified,
1844
+ failed,
1845
+ pending
1846
+ };
1847
+ }
1848
+
1849
+ // src/messages.ts
1850
+ var STATUS_META = {
1851
+ sig_ok_bip322: {
1852
+ label: "Signature verified",
1853
+ detail: "Your BIP-322 signature is valid and proves ownership of this address.",
1854
+ severity: "success"
1855
+ },
1856
+ sig_ok_legacy: {
1857
+ label: "Signature verified (legacy)",
1858
+ detail: "Your legacy signature is valid and proves ownership of this address.",
1859
+ severity: "success"
1860
+ },
1861
+ sig_invalid: {
1862
+ label: "Signature verification failed",
1863
+ detail: "The signature does not match the message and address. Common issues: (1) Message not copied exactly from Step 2, (2) Wrong wallet/address used, (3) Wallet does not support BIP-322 (Electrum does NOT support BIP-322 for SegWit addresses - use Sparrow Wallet instead). For testnet, ensure your wallet is in testnet mode.",
1864
+ severity: "error"
1865
+ },
1866
+ sig_unsupported_script: {
1867
+ label: "Wallet does not support BIP-322",
1868
+ detail: 'Your wallet generated a legacy signature, but your SegWit address (bc1/tb1) requires BIP-322 message signing. Please use a wallet that supports BIP-322 (like Sparrow Wallet or Bitcoin Core), or switch to a P2PKH address (starting with "1" for mainnet or "m"/"n" for testnet) if your wallet only supports legacy signing.',
1869
+ severity: "error"
1870
+ },
1871
+ bond_confirmed: {
1872
+ label: "Bitcoin bond confirmed",
1873
+ detail: "Found confirmed UTXOs on the blockchain for reputation scoring.",
1874
+ severity: "success"
1875
+ },
1876
+ bond_zero: {
1877
+ label: "No confirmed bitcoin found",
1878
+ detail: "This address has no confirmed UTXOs. Send some bitcoin to this address and wait for confirmation to establish a reputation score.",
1879
+ severity: "warn"
1880
+ },
1881
+ bond_pending: {
1882
+ label: "Pending transactions detected",
1883
+ detail: "This address has unconfirmed transactions. Wait for confirmations before they count toward your reputation score.",
1884
+ severity: "info"
1885
+ },
1886
+ bond_insufficient: {
1887
+ label: "Insufficient bond amount",
1888
+ detail: "The confirmed balance is less than the declared bond amount. The bond extension requires the address to hold at least the specified amount in confirmed UTXOs.",
1889
+ severity: "error"
1890
+ },
1891
+ aud_mismatch: {
1892
+ label: "Audience mismatch",
1893
+ detail: "The message was signed for a different audience than expected. This may be intentional or indicate the signature is being used in the wrong context.",
1894
+ severity: "warn"
1895
+ },
1896
+ expired: {
1897
+ label: "Message expired",
1898
+ detail: "This signature has passed its expiration date. Generate a new signature with a fresh message.",
1899
+ severity: "error"
1900
+ },
1901
+ network_testmode: {
1902
+ label: "Test network detected",
1903
+ detail: "This signature uses testnet or signet. Test networks are for development only and cannot be verified in production mode.",
1904
+ severity: "warn"
1905
+ },
1906
+ bad_request: {
1907
+ label: "Verification error",
1908
+ detail: "Unable to verify the signature due to invalid or inconsistent data. Please ensure the address in the message matches the address you entered, and try again.",
1909
+ severity: "error"
1910
+ },
1911
+ decode_error: {
1912
+ label: "Message format error",
1913
+ detail: "The message could not be parsed. Ensure you copied the complete canonical message from Step 2 without modifications.",
1914
+ severity: "error"
1915
+ },
1916
+ invalid_scheme: {
1917
+ label: "Unsupported signing method",
1918
+ detail: "The signing scheme is not supported. Please use BIP-322 or legacy Bitcoin message signing.",
1919
+ severity: "error"
1920
+ },
1921
+ invalid_attestation_id: {
1922
+ label: "Invalid attestation ID",
1923
+ detail: "The attestation ID does not match the message hash. The attestation may have been tampered with.",
1924
+ severity: "error"
1925
+ }
1926
+ };
1927
+
1928
+ export { DEFAULT_RELAYS, STATUS_META, buildCanonicalMessage, check, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, formatIdentities, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
1929
+ //# sourceMappingURL=index.mjs.map
1930
+ //# sourceMappingURL=index.mjs.map