@orangecheck/sdk 1.14.1 → 1.16.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.
- package/README.md +2 -1
- package/dist/attestation.js +228 -64
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +228 -64
- package/dist/attestation.mjs.map +1 -1
- package/dist/challenge.d.mts +1 -0
- package/dist/challenge.d.ts +1 -0
- package/dist/challenge.js +20 -14
- package/dist/challenge.js.map +1 -1
- package/dist/challenge.mjs +20 -14
- package/dist/challenge.mjs.map +1 -1
- package/dist/identity.js +19 -17
- package/dist/identity.js.map +1 -1
- package/dist/identity.mjs +19 -17
- package/dist/identity.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +555 -585
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +554 -586
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.d.mts +3 -1
- package/dist/nostr.d.ts +3 -1
- package/dist/nostr.js +213 -38
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +212 -39
- package/dist/nostr.mjs.map +1 -1
- package/dist/verify.d.mts +11 -2
- package/dist/verify.d.ts +11 -2
- package/dist/verify.js +39 -100
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +39 -101
- package/dist/verify.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bech32 } from '@scure/base';
|
|
2
|
-
import { Buffer } from 'buffer';
|
|
3
2
|
import { verifySignature as verifySignature$1, decodeAddress } from '@orangecheck/bip322';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
4
|
import { hmac } from '@noble/hashes/hmac.js';
|
|
5
5
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
6
6
|
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
@@ -212,8 +212,345 @@ function logVerificationError(protocol, identifier, error, context) {
|
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// src/utils/time.ts
|
|
216
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
217
|
+
function rfc3339Ms(value) {
|
|
218
|
+
if (!value || !RFC3339.test(value)) return null;
|
|
219
|
+
const ms = Date.parse(value);
|
|
220
|
+
return Number.isNaN(ms) ? null : ms;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/scoring.ts
|
|
224
|
+
function scoreV0(sats, days) {
|
|
225
|
+
if (sats <= 0 || days <= 0) return 0;
|
|
226
|
+
return Math.round(Math.log(1 + sats) * (1 + days / 30) * 100) / 100;
|
|
227
|
+
}
|
|
228
|
+
function scoreTier(sats, days) {
|
|
229
|
+
if (sats >= 1e7 && days >= 365) return "platinum";
|
|
230
|
+
if (sats >= 1e6 && days >= 180) return "gold";
|
|
231
|
+
if (sats >= 1e5 && days >= 90) return "silver";
|
|
232
|
+
if (sats >= 1e4 && days >= 30) return "bronze";
|
|
233
|
+
return "none";
|
|
234
|
+
}
|
|
235
|
+
function computeScore(sats, days, config) {
|
|
236
|
+
switch (config.algorithm) {
|
|
237
|
+
case "v0":
|
|
238
|
+
return scoreV0(sats, days);
|
|
239
|
+
case "tier":
|
|
240
|
+
return scoreTier(sats, days);
|
|
241
|
+
case "none":
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function computeAllScores(sats, days) {
|
|
246
|
+
return { v0: scoreV0(sats, days), tier: scoreTier(sats, days) };
|
|
247
|
+
}
|
|
248
|
+
function toBase64Maybe(sig) {
|
|
249
|
+
if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {
|
|
250
|
+
return Buffer.from(sig, "hex").toString("base64");
|
|
251
|
+
}
|
|
252
|
+
return sig;
|
|
253
|
+
}
|
|
254
|
+
function signatureScheme(sigBase64) {
|
|
255
|
+
if (/^(ful|pof)/.test(sigBase64)) return null;
|
|
256
|
+
if (sigBase64.startsWith("smp")) return "bip322";
|
|
257
|
+
return Buffer.from(sigBase64, "base64").length === 65 ? "legacy" : "bip322";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/verify.ts
|
|
261
|
+
var log = createLogger("ocp/verify");
|
|
262
|
+
async function verifySignature({ addr, msg, sig }) {
|
|
263
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
264
|
+
const scheme = signatureScheme(sigBase64);
|
|
265
|
+
if (!scheme) return "sig_unsupported_script";
|
|
266
|
+
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
267
|
+
log.info({ addr, ok, scheme, msgLength: msg.length }, "Signature verification result");
|
|
268
|
+
if (!ok) return "sig_invalid";
|
|
269
|
+
return scheme === "legacy" ? "sig_ok_legacy" : "sig_ok_bip322";
|
|
270
|
+
}
|
|
271
|
+
async function signatureIsValid(input) {
|
|
272
|
+
try {
|
|
273
|
+
const code = await verifySignature(input);
|
|
274
|
+
return code === "sig_ok_bip322" || code === "sig_ok_legacy";
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function parseCanonicalMessage(msg) {
|
|
280
|
+
const lines = msg.split("\n").map((l) => l.trim());
|
|
281
|
+
const getVal = (prefix) => {
|
|
282
|
+
const needle = prefix + ":";
|
|
283
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
284
|
+
if (!hit) return void 0;
|
|
285
|
+
const idx = hit.indexOf(":");
|
|
286
|
+
return hit.slice(idx + 1).trim();
|
|
287
|
+
};
|
|
288
|
+
const address = getVal("address") || "";
|
|
289
|
+
const identitiesStr = getVal("identities") || "";
|
|
290
|
+
const issued_at = getVal("issued_at") || "";
|
|
291
|
+
const expires = getVal("expires");
|
|
292
|
+
let identities = [];
|
|
293
|
+
try {
|
|
294
|
+
identities = parseIdentities(identitiesStr);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
|
|
297
|
+
}
|
|
298
|
+
const coreKeys = /* @__PURE__ */ new Set([
|
|
299
|
+
"orangecheck",
|
|
300
|
+
"identities",
|
|
301
|
+
"address",
|
|
302
|
+
"purpose",
|
|
303
|
+
"nonce",
|
|
304
|
+
"issued_at",
|
|
305
|
+
"ack"
|
|
306
|
+
]);
|
|
307
|
+
const extensions = {};
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
const idx = line.indexOf(":");
|
|
310
|
+
if (idx === -1) continue;
|
|
311
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
312
|
+
const val = line.slice(idx + 1).trim();
|
|
313
|
+
if (!coreKeys.has(key) && val) extensions[key] = val;
|
|
314
|
+
}
|
|
315
|
+
const network = extensions["network"] || "mainnet";
|
|
316
|
+
const bondStr = extensions["bond"];
|
|
317
|
+
const bond = bondStr ? parseInt(bondStr, 10) : void 0;
|
|
318
|
+
return {
|
|
319
|
+
core: { address, identities, issued_at },
|
|
320
|
+
expires,
|
|
321
|
+
network,
|
|
322
|
+
extensions,
|
|
323
|
+
bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async function getAddressUtxos(address, network, opts) {
|
|
327
|
+
const endpoints = network === "signet" ? [
|
|
328
|
+
opts?.esploraSignetBase || "https://mempool.space/signet/api",
|
|
329
|
+
"https://blockstream.info/signet/api"
|
|
330
|
+
] : network === "testnet" ? [
|
|
331
|
+
opts?.esploraTestnetBase || "https://mempool.space/testnet/api",
|
|
332
|
+
"https://blockstream.info/testnet/api"
|
|
333
|
+
] : [
|
|
334
|
+
opts?.esploraMainnetBase || "https://mempool.space/api",
|
|
335
|
+
"https://blockstream.info/api"
|
|
336
|
+
];
|
|
337
|
+
let lastError = null;
|
|
338
|
+
for (const base of endpoints) {
|
|
339
|
+
try {
|
|
340
|
+
const url = `${base}/address/${address}/utxo`;
|
|
341
|
+
log.debug({ url, network }, "Fetching UTXOs");
|
|
342
|
+
const resp = await fetch(url, {
|
|
343
|
+
method: "GET",
|
|
344
|
+
headers: { Accept: "application/json" },
|
|
345
|
+
signal: AbortSignal.timeout(5e3)
|
|
346
|
+
});
|
|
347
|
+
if (!resp.ok) {
|
|
348
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
349
|
+
}
|
|
350
|
+
const data = await resp.json();
|
|
351
|
+
if (!Array.isArray(data)) {
|
|
352
|
+
throw new Error("Invalid response: expected array");
|
|
353
|
+
}
|
|
354
|
+
for (const utxo of data) {
|
|
355
|
+
if (typeof utxo.value !== "number" || typeof utxo.status !== "object" || typeof utxo.status.confirmed !== "boolean" || typeof utxo.txid !== "string" || typeof utxo.vout !== "number") {
|
|
356
|
+
throw new Error("Invalid UTXO structure");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
log.info({ base, count: data.length, network }, "UTXOs fetched successfully");
|
|
360
|
+
return data;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
363
|
+
log.warn(
|
|
364
|
+
{ base, error: lastError.message, network },
|
|
365
|
+
"Esplora endpoint failed, trying next"
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
log.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
|
|
370
|
+
throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
|
|
371
|
+
}
|
|
372
|
+
function computeMetrics(utxos, bondAmount, coverSats) {
|
|
373
|
+
const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
|
|
374
|
+
if (confirmedUtxos.length === 0) {
|
|
375
|
+
return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
|
|
376
|
+
}
|
|
377
|
+
const confirmed_balance = confirmedUtxos.reduce((sum, u) => sum + u.value, 0);
|
|
378
|
+
if (bondAmount !== void 0 && bondAmount > 0) {
|
|
379
|
+
const sorted = [...confirmedUtxos].sort((a, b) => {
|
|
380
|
+
const heightA = a.status.block_height ?? Infinity;
|
|
381
|
+
const heightB = b.status.block_height ?? Infinity;
|
|
382
|
+
if (heightA !== heightB) return heightA - heightB;
|
|
383
|
+
if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);
|
|
384
|
+
return a.vout - b.vout;
|
|
385
|
+
});
|
|
386
|
+
let sum = 0;
|
|
387
|
+
let youngestTime = 0;
|
|
388
|
+
for (const utxo of sorted) {
|
|
389
|
+
sum += utxo.value;
|
|
390
|
+
youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);
|
|
391
|
+
if (sum >= bondAmount) break;
|
|
392
|
+
}
|
|
393
|
+
const days_unspent2 = Math.max(
|
|
394
|
+
0,
|
|
395
|
+
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
sats_bonded: bondAmount,
|
|
399
|
+
// Use exactly bond amount (ignore surplus)
|
|
400
|
+
days_unspent: days_unspent2,
|
|
401
|
+
confirmed_balance
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const sats_bonded = confirmed_balance;
|
|
405
|
+
if (coverSats !== void 0 && coverSats > 0) {
|
|
406
|
+
const sorted = [...confirmedUtxos].sort(
|
|
407
|
+
(a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)
|
|
408
|
+
);
|
|
409
|
+
let sum = 0;
|
|
410
|
+
let youngestTime = 0;
|
|
411
|
+
for (const utxo of sorted) {
|
|
412
|
+
sum += utxo.value;
|
|
413
|
+
youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
|
|
414
|
+
if (sum >= coverSats) break;
|
|
415
|
+
}
|
|
416
|
+
const days_unspent2 = Math.max(
|
|
417
|
+
0,
|
|
418
|
+
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
419
|
+
);
|
|
420
|
+
return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
|
|
421
|
+
}
|
|
422
|
+
const oldestTime = Math.min(
|
|
423
|
+
...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
|
|
424
|
+
);
|
|
425
|
+
const days_unspent = Math.max(0, Math.floor((Date.now() / 1e3 - oldestTime) / (24 * 60 * 60)));
|
|
426
|
+
return { sats_bonded, days_unspent, confirmed_balance };
|
|
427
|
+
}
|
|
428
|
+
async function verify(input, opts = {}) {
|
|
429
|
+
const codes = [];
|
|
430
|
+
let network = "mainnet";
|
|
431
|
+
try {
|
|
432
|
+
if (!input.msg || !input.addr || !input.sig) {
|
|
433
|
+
codes.push("bad_request");
|
|
434
|
+
return { ok: false, codes, network };
|
|
435
|
+
}
|
|
436
|
+
const parsed = parseCanonicalMessage(input.msg);
|
|
437
|
+
network = parsed.network || "mainnet";
|
|
438
|
+
const attestation_id = await generateAttestationId(input.msg);
|
|
439
|
+
if (parsed.core.address !== input.addr) {
|
|
440
|
+
codes.push("bad_request");
|
|
441
|
+
return { ok: false, codes, network, attestation_id };
|
|
442
|
+
}
|
|
443
|
+
if ((network === "testnet" || network === "signet") && !opts.testMode) {
|
|
444
|
+
codes.push("network_testmode");
|
|
445
|
+
return { ok: false, codes, network, attestation_id };
|
|
446
|
+
}
|
|
447
|
+
if (opts.expectedAud) {
|
|
448
|
+
const aud = parsed.extensions["aud"];
|
|
449
|
+
if (!aud || aud !== opts.expectedAud) {
|
|
450
|
+
codes.push("aud_mismatch");
|
|
451
|
+
return { ok: false, codes, network, attestation_id };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (parsed.expires !== void 0) {
|
|
455
|
+
const exp = rfc3339Ms(parsed.expires);
|
|
456
|
+
if (exp === null || exp < Date.now()) {
|
|
457
|
+
codes.push("expired");
|
|
458
|
+
return { ok: false, codes, network, attestation_id };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const sigCode = await verifySignature({
|
|
462
|
+
addr: input.addr,
|
|
463
|
+
msg: input.msg,
|
|
464
|
+
sig: input.sig,
|
|
465
|
+
...input.scheme && { scheme: input.scheme }
|
|
466
|
+
});
|
|
467
|
+
codes.push(sigCode);
|
|
468
|
+
const sigOk = sigCode === "sig_ok_bip322" || sigCode === "sig_ok_legacy";
|
|
469
|
+
if (!sigOk) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
codes,
|
|
473
|
+
network,
|
|
474
|
+
attestation_id,
|
|
475
|
+
identities: parsed.core.identities
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const utxos = await getAddressUtxos(input.addr, network, {
|
|
480
|
+
esploraMainnetBase: opts.esploraMainnetBase,
|
|
481
|
+
esploraSignetBase: opts.esploraSignetBase
|
|
482
|
+
});
|
|
483
|
+
const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
|
|
484
|
+
utxos,
|
|
485
|
+
parsed.bond,
|
|
486
|
+
opts.coverSats
|
|
487
|
+
);
|
|
488
|
+
if (parsed.bond !== void 0 && parsed.bond > 0) {
|
|
489
|
+
if (confirmed_balance < parsed.bond) {
|
|
490
|
+
codes.push("bond_insufficient");
|
|
491
|
+
log.warn(
|
|
492
|
+
{
|
|
493
|
+
bond: parsed.bond,
|
|
494
|
+
confirmed_balance,
|
|
495
|
+
address: input.addr
|
|
496
|
+
},
|
|
497
|
+
"Bond extension: insufficient balance"
|
|
498
|
+
);
|
|
499
|
+
return {
|
|
500
|
+
ok: false,
|
|
501
|
+
codes,
|
|
502
|
+
network,
|
|
503
|
+
attestation_id,
|
|
504
|
+
identities: parsed.core.identities
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (sats_bonded <= 0) {
|
|
509
|
+
codes.push("bond_zero");
|
|
510
|
+
if (utxos.some((u) => !u.status.confirmed)) {
|
|
511
|
+
codes.push("bond_pending");
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
ok: false,
|
|
515
|
+
codes,
|
|
516
|
+
network,
|
|
517
|
+
attestation_id,
|
|
518
|
+
identities: parsed.core.identities
|
|
519
|
+
};
|
|
520
|
+
} else {
|
|
521
|
+
codes.push("bond_confirmed");
|
|
522
|
+
}
|
|
523
|
+
const metrics = {
|
|
524
|
+
sats_bonded,
|
|
525
|
+
days_unspent,
|
|
526
|
+
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
527
|
+
};
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
codes,
|
|
531
|
+
network,
|
|
532
|
+
attestation_id,
|
|
533
|
+
identities: parsed.core.identities,
|
|
534
|
+
metrics
|
|
535
|
+
};
|
|
536
|
+
} catch (utxoError) {
|
|
537
|
+
log.error({ err: utxoError }, "UTXO fetch failed");
|
|
538
|
+
codes.push("bad_request");
|
|
539
|
+
return { ok: false, codes, network, attestation_id };
|
|
540
|
+
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
const msg = e instanceof Error ? e.message : String(e ?? "decode_error");
|
|
543
|
+
if (msg === "decode_error") {
|
|
544
|
+
codes.push("decode_error");
|
|
545
|
+
} else {
|
|
546
|
+
codes.push("bad_request");
|
|
547
|
+
}
|
|
548
|
+
return { ok: false, codes, network };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
215
552
|
// src/nostr.ts
|
|
216
|
-
var
|
|
553
|
+
var log2 = createLogger("ocp/nostr");
|
|
217
554
|
var ATTESTATION_EVENT_KIND = 30078;
|
|
218
555
|
var DEFAULT_RELAYS = [
|
|
219
556
|
"wss://relay.damus.io",
|
|
@@ -266,7 +603,7 @@ function createAttestationEvent(envelope, pubkey) {
|
|
|
266
603
|
async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
267
604
|
const success = [];
|
|
268
605
|
const failed = [];
|
|
269
|
-
|
|
606
|
+
log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
|
|
270
607
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
271
608
|
const publishPromises = relays.map(async (relayUrl) => {
|
|
272
609
|
let ws = null;
|
|
@@ -305,7 +642,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
305
642
|
cleanup();
|
|
306
643
|
resolve();
|
|
307
644
|
} else {
|
|
308
|
-
|
|
645
|
+
log2.warn(
|
|
309
646
|
{ relay: relayUrl, reason: data[3] },
|
|
310
647
|
"Relay rejected event"
|
|
311
648
|
);
|
|
@@ -315,7 +652,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
315
652
|
}
|
|
316
653
|
}
|
|
317
654
|
} catch (err) {
|
|
318
|
-
|
|
655
|
+
log2.error(
|
|
319
656
|
{ relay: relayUrl, error: err },
|
|
320
657
|
"Failed to parse relay response"
|
|
321
658
|
);
|
|
@@ -325,7 +662,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
325
662
|
}
|
|
326
663
|
};
|
|
327
664
|
wsRef.onerror = (err) => {
|
|
328
|
-
|
|
665
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error");
|
|
329
666
|
recordFailure();
|
|
330
667
|
cleanup();
|
|
331
668
|
reject(err);
|
|
@@ -335,7 +672,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
335
672
|
};
|
|
336
673
|
});
|
|
337
674
|
} catch (err) {
|
|
338
|
-
|
|
675
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
|
|
339
676
|
recordFailure();
|
|
340
677
|
} finally {
|
|
341
678
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
@@ -344,12 +681,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
344
681
|
}
|
|
345
682
|
});
|
|
346
683
|
await Promise.allSettled(publishPromises);
|
|
347
|
-
|
|
684
|
+
log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
|
|
348
685
|
return { success, failed };
|
|
349
686
|
}
|
|
350
687
|
async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
351
688
|
const events = [];
|
|
352
|
-
|
|
689
|
+
log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
|
|
353
690
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
354
691
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
355
692
|
let ws = null;
|
|
@@ -389,11 +726,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
389
726
|
resolve();
|
|
390
727
|
}
|
|
391
728
|
} catch (err) {
|
|
392
|
-
|
|
729
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
393
730
|
}
|
|
394
731
|
};
|
|
395
732
|
wsRef.onerror = (err) => {
|
|
396
|
-
|
|
733
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
397
734
|
cleanup();
|
|
398
735
|
reject(err);
|
|
399
736
|
};
|
|
@@ -402,7 +739,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
402
739
|
};
|
|
403
740
|
});
|
|
404
741
|
} catch (err) {
|
|
405
|
-
|
|
742
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
406
743
|
} finally {
|
|
407
744
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
408
745
|
ws.close();
|
|
@@ -410,12 +747,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
410
747
|
}
|
|
411
748
|
});
|
|
412
749
|
await Promise.allSettled(queryPromises);
|
|
413
|
-
|
|
750
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
414
751
|
return events;
|
|
415
752
|
}
|
|
416
753
|
async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
417
754
|
const events = [];
|
|
418
|
-
|
|
755
|
+
log2.info({ address, relayCount: relays.length }, "Querying relays for address");
|
|
419
756
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
420
757
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
421
758
|
try {
|
|
@@ -457,28 +794,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
|
457
794
|
resolve();
|
|
458
795
|
}
|
|
459
796
|
} catch (err) {
|
|
460
|
-
|
|
797
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
461
798
|
}
|
|
462
799
|
};
|
|
463
800
|
ws.onerror = (err) => {
|
|
464
801
|
clearTimeout(timeout);
|
|
465
|
-
|
|
802
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
466
803
|
reject(err);
|
|
467
804
|
};
|
|
468
805
|
});
|
|
469
806
|
} catch (err) {
|
|
470
|
-
|
|
807
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
471
808
|
}
|
|
472
809
|
});
|
|
473
810
|
await Promise.allSettled(queryPromises);
|
|
474
|
-
|
|
811
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
475
812
|
return events;
|
|
476
813
|
}
|
|
477
814
|
async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
478
815
|
const events = [];
|
|
479
816
|
const identityTag = `${protocol}:${identifier}`;
|
|
480
817
|
const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
|
|
481
|
-
|
|
818
|
+
log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
|
|
482
819
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
483
820
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
484
821
|
try {
|
|
@@ -513,26 +850,26 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
|
513
850
|
resolve();
|
|
514
851
|
}
|
|
515
852
|
} catch (err) {
|
|
516
|
-
|
|
853
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
517
854
|
}
|
|
518
855
|
};
|
|
519
856
|
ws.onerror = (err) => {
|
|
520
857
|
clearTimeout(timeout);
|
|
521
|
-
|
|
858
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
522
859
|
reject(err);
|
|
523
860
|
};
|
|
524
861
|
});
|
|
525
862
|
} catch (err) {
|
|
526
|
-
|
|
863
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
527
864
|
}
|
|
528
865
|
});
|
|
529
866
|
await Promise.allSettled(queryPromises);
|
|
530
|
-
|
|
867
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
531
868
|
return events;
|
|
532
869
|
}
|
|
533
870
|
async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
534
871
|
const events = [];
|
|
535
|
-
|
|
872
|
+
log2.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
|
|
536
873
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
537
874
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
538
875
|
try {
|
|
@@ -568,30 +905,22 @@ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
|
568
905
|
resolve();
|
|
569
906
|
}
|
|
570
907
|
} catch (err) {
|
|
571
|
-
|
|
908
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
572
909
|
}
|
|
573
910
|
};
|
|
574
911
|
ws.onerror = (err) => {
|
|
575
912
|
clearTimeout(timeout);
|
|
576
|
-
|
|
913
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
577
914
|
reject(err);
|
|
578
915
|
};
|
|
579
916
|
});
|
|
580
917
|
} catch (err) {
|
|
581
|
-
|
|
918
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
582
919
|
}
|
|
583
920
|
});
|
|
584
921
|
await Promise.allSettled(queryPromises);
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
for (const ev of events) {
|
|
588
|
-
const envelope = parseAttestationFromEvent(ev);
|
|
589
|
-
if (!envelope || seen.has(envelope.attestation_id)) continue;
|
|
590
|
-
if (!await verifyAttestationEnvelope(envelope)) continue;
|
|
591
|
-
seen.add(envelope.attestation_id);
|
|
592
|
-
envelopes.push(envelope);
|
|
593
|
-
}
|
|
594
|
-
log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
|
|
922
|
+
const envelopes = await collectSignedEnvelopes(events);
|
|
923
|
+
log2.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
|
|
595
924
|
return envelopes;
|
|
596
925
|
}
|
|
597
926
|
async function verifyAttestationEnvelope(envelope) {
|
|
@@ -603,22 +932,74 @@ async function verifyAttestationEnvelope(envelope) {
|
|
|
603
932
|
const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
604
933
|
return computedId === envelope.attestation_id;
|
|
605
934
|
} catch (err) {
|
|
606
|
-
|
|
935
|
+
log2.error({ error: err }, "Failed to verify attestation envelope");
|
|
607
936
|
return false;
|
|
608
937
|
}
|
|
609
938
|
}
|
|
939
|
+
function base64Url(text) {
|
|
940
|
+
const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
|
|
941
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
942
|
+
}
|
|
943
|
+
function signedEnvelope(envelope) {
|
|
944
|
+
let parsed;
|
|
945
|
+
try {
|
|
946
|
+
parsed = parseCanonicalMessage(envelope.message);
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
|
|
951
|
+
const { expires_at: _copy, ...rest } = envelope;
|
|
952
|
+
return {
|
|
953
|
+
...rest,
|
|
954
|
+
address: parsed.core.address,
|
|
955
|
+
identities: parsed.core.identities,
|
|
956
|
+
issued_at: parsed.core.issued_at,
|
|
957
|
+
...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
|
|
958
|
+
message_b64url: base64Url(envelope.message)
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
async function collectSignedEnvelopes(events, accept = () => true) {
|
|
962
|
+
const copies = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const event of events) {
|
|
964
|
+
const raw = parseAttestationFromEvent(event);
|
|
965
|
+
if (!raw || !await verifyAttestationEnvelope(raw)) continue;
|
|
966
|
+
const envelope = signedEnvelope(raw);
|
|
967
|
+
if (!envelope || !accept(event, envelope)) continue;
|
|
968
|
+
const same = copies.get(envelope.attestation_id) ?? [];
|
|
969
|
+
if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
|
|
970
|
+
same.push(envelope);
|
|
971
|
+
}
|
|
972
|
+
copies.set(envelope.attestation_id, same);
|
|
973
|
+
}
|
|
974
|
+
const kept = [];
|
|
975
|
+
for (const same of copies.values()) {
|
|
976
|
+
if (same.length === 1) {
|
|
977
|
+
kept.push(same[0]);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
let valid;
|
|
981
|
+
for (const e of same) {
|
|
982
|
+
if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
|
|
983
|
+
valid = e;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
kept.push(valid ?? same[0]);
|
|
988
|
+
}
|
|
989
|
+
return kept;
|
|
990
|
+
}
|
|
610
991
|
function parseAttestationFromEvent(event) {
|
|
611
992
|
try {
|
|
612
993
|
const envelope = JSON.parse(event.content);
|
|
613
994
|
return envelope;
|
|
614
995
|
} catch (err) {
|
|
615
|
-
|
|
996
|
+
log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
|
|
616
997
|
return null;
|
|
617
998
|
}
|
|
618
999
|
}
|
|
619
1000
|
|
|
620
1001
|
// src/nostr-crypto.ts
|
|
621
|
-
var
|
|
1002
|
+
var log3 = createLogger("ocp/nostr-crypto");
|
|
622
1003
|
function isNip07Available() {
|
|
623
1004
|
if (typeof window === "undefined") {
|
|
624
1005
|
return false;
|
|
@@ -627,31 +1008,31 @@ function isNip07Available() {
|
|
|
627
1008
|
}
|
|
628
1009
|
async function getNostrPublicKey() {
|
|
629
1010
|
if (!isNip07Available()) {
|
|
630
|
-
|
|
1011
|
+
log3.warn("NIP-07 extension not available");
|
|
631
1012
|
return null;
|
|
632
1013
|
}
|
|
633
1014
|
try {
|
|
634
1015
|
const nostr = window.nostr;
|
|
635
1016
|
const pubkey = await nostr.getPublicKey();
|
|
636
|
-
|
|
1017
|
+
log3.info({ pubkey }, "Got Nostr public key from NIP-07");
|
|
637
1018
|
return pubkey;
|
|
638
1019
|
} catch (err) {
|
|
639
|
-
|
|
1020
|
+
log3.error({ error: err }, "Failed to get Nostr public key");
|
|
640
1021
|
return null;
|
|
641
1022
|
}
|
|
642
1023
|
}
|
|
643
1024
|
async function signNostrEvent(event) {
|
|
644
1025
|
if (!isNip07Available()) {
|
|
645
|
-
|
|
1026
|
+
log3.warn("NIP-07 extension not available");
|
|
646
1027
|
return null;
|
|
647
1028
|
}
|
|
648
1029
|
try {
|
|
649
1030
|
const nostr = window.nostr;
|
|
650
1031
|
const signedEvent = await nostr.signEvent(event);
|
|
651
|
-
|
|
1032
|
+
log3.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
|
|
652
1033
|
return signedEvent;
|
|
653
1034
|
} catch (err) {
|
|
654
|
-
|
|
1035
|
+
log3.error({ error: err }, "Failed to sign Nostr event");
|
|
655
1036
|
return null;
|
|
656
1037
|
}
|
|
657
1038
|
}
|
|
@@ -668,7 +1049,7 @@ function getNip07Info() {
|
|
|
668
1049
|
}
|
|
669
1050
|
|
|
670
1051
|
// src/attestation.ts
|
|
671
|
-
var
|
|
1052
|
+
var log4 = createLogger("ocp/attestation");
|
|
672
1053
|
var AttestationError = class extends Error {
|
|
673
1054
|
constructor(message, code) {
|
|
674
1055
|
super(message);
|
|
@@ -678,7 +1059,7 @@ var AttestationError = class extends Error {
|
|
|
678
1059
|
};
|
|
679
1060
|
async function createAttestation(options) {
|
|
680
1061
|
const { message, signature, scheme, address, identities = [], extensions = {} } = options;
|
|
681
|
-
|
|
1062
|
+
log4.info({ address, identityCount: identities.length }, "Creating attestation");
|
|
682
1063
|
const envelope = await createAttestationEnvelope(
|
|
683
1064
|
message,
|
|
684
1065
|
signature,
|
|
@@ -689,529 +1070,117 @@ async function createAttestation(options) {
|
|
|
689
1070
|
);
|
|
690
1071
|
const isValid = await verifyAttestationEnvelope(envelope);
|
|
691
1072
|
if (!isValid) {
|
|
692
|
-
throw new AttestationError(
|
|
693
|
-
"Attestation envelope integrity check failed",
|
|
694
|
-
"INTEGRITY_CHECK_FAILED"
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
log3.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
|
|
698
|
-
return envelope;
|
|
699
|
-
}
|
|
700
|
-
async function publishAttestation(options) {
|
|
701
|
-
const { envelope, npub, relays = DEFAULT_RELAYS } = options;
|
|
702
|
-
log3.info(
|
|
703
|
-
{ attestationId: envelope.attestation_id, relayCount: relays.length },
|
|
704
|
-
"Publishing attestation"
|
|
705
|
-
);
|
|
706
|
-
const unsignedEvent = createAttestationEvent(envelope, npub);
|
|
707
|
-
if (!isNip07Available()) {
|
|
708
|
-
log3.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
|
|
709
|
-
throw new Error(
|
|
710
|
-
"nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish"
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
log3.info("Using NIP-07 to sign event");
|
|
714
|
-
const signedEvent = await signNostrEvent(unsignedEvent);
|
|
715
|
-
if (!signedEvent) {
|
|
716
|
-
throw new Error("nostr_signing_failed: NIP-07 extension rejected or errored");
|
|
717
|
-
}
|
|
718
|
-
const result = await publishToRelays(signedEvent, relays);
|
|
719
|
-
log3.info(
|
|
720
|
-
{
|
|
721
|
-
attestationId: envelope.attestation_id,
|
|
722
|
-
successCount: result.success.length,
|
|
723
|
-
failedCount: result.failed.length
|
|
724
|
-
},
|
|
725
|
-
"Publishing complete"
|
|
726
|
-
);
|
|
727
|
-
return result;
|
|
728
|
-
}
|
|
729
|
-
function eventIndexesEnvelope(event, envelope) {
|
|
730
|
-
const d = event.tags.find((t) => t[0] === "d")?.[1];
|
|
731
|
-
const id = envelope.attestation_id;
|
|
732
|
-
if (d !== id && d !== `orangecheck:${id}`) return false;
|
|
733
|
-
return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
|
|
734
|
-
}
|
|
735
|
-
async function discoverAttestations(options) {
|
|
736
|
-
const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
|
|
737
|
-
let events = [];
|
|
738
|
-
if (attestationId) {
|
|
739
|
-
log3.info({ attestationId }, "Discovering by attestation ID");
|
|
740
|
-
events = await queryByAttestationId(attestationId, relays);
|
|
741
|
-
} else if (address) {
|
|
742
|
-
log3.info({ address }, "Discovering by Bitcoin address");
|
|
743
|
-
events = await queryByAddress(address, relays);
|
|
744
|
-
} else if (identity) {
|
|
745
|
-
log3.info({ identity }, "Discovering by identity");
|
|
746
|
-
events = await queryByIdentity(identity.protocol, identity.identifier, relays);
|
|
747
|
-
} else {
|
|
748
|
-
throw new Error("Must provide attestationId, address, or identity");
|
|
749
|
-
}
|
|
750
|
-
const envelopes = [];
|
|
751
|
-
const kept = /* @__PURE__ */ new Set();
|
|
752
|
-
for (const event of events) {
|
|
753
|
-
const envelope = parseAttestationFromEvent(event);
|
|
754
|
-
if (envelope && kept.has(envelope.attestation_id)) continue;
|
|
755
|
-
if (envelope && !eventIndexesEnvelope(event, envelope)) {
|
|
756
|
-
log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
|
|
757
|
-
} else if (envelope) {
|
|
758
|
-
const isValid = await verifyAttestationEnvelope(envelope);
|
|
759
|
-
if (isValid) {
|
|
760
|
-
kept.add(envelope.attestation_id);
|
|
761
|
-
envelopes.push(envelope);
|
|
762
|
-
} else {
|
|
763
|
-
log3.warn(
|
|
764
|
-
{ attestationId: envelope.attestation_id },
|
|
765
|
-
"Skipping invalid attestation envelope"
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
log3.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
|
|
771
|
-
return envelopes;
|
|
772
|
-
}
|
|
773
|
-
async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
|
|
774
|
-
log3.info({ attestationId }, "Verifying attestation by ID");
|
|
775
|
-
const envelopes = await discoverAttestations({ attestationId, relays });
|
|
776
|
-
if (envelopes.length === 0) {
|
|
777
|
-
log3.warn({ attestationId }, "Attestation not found on any relay");
|
|
778
|
-
return null;
|
|
779
|
-
}
|
|
780
|
-
if (envelopes.length > 1) {
|
|
781
|
-
log3.warn(
|
|
782
|
-
{ attestationId, count: envelopes.length },
|
|
783
|
-
"Multiple attestations found with same ID - using first"
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
return envelopes[0] || null;
|
|
787
|
-
}
|
|
788
|
-
async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
|
|
789
|
-
log3.info({ address }, "Getting all attestations for address");
|
|
790
|
-
return discoverAttestations({ address, relays });
|
|
791
|
-
}
|
|
792
|
-
async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
793
|
-
log3.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
794
|
-
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
795
|
-
}
|
|
796
|
-
function formatIdentitiesForDisplay(identities) {
|
|
797
|
-
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
798
|
-
}
|
|
799
|
-
function getVerificationUrl(attestationId, baseUrl = "https://attest.ochk.io") {
|
|
800
|
-
return `${baseUrl}/attest/${attestationId}`;
|
|
801
|
-
}
|
|
802
|
-
function extractAttestationIdFromUrl(url) {
|
|
803
|
-
try {
|
|
804
|
-
const u = new URL(url);
|
|
805
|
-
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
806
|
-
if (m) return m[1].toLowerCase();
|
|
807
|
-
const q = u.searchParams.get("id");
|
|
808
|
-
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
809
|
-
return null;
|
|
810
|
-
} catch {
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// src/scoring.ts
|
|
816
|
-
function scoreV0(sats, days) {
|
|
817
|
-
if (sats <= 0 || days <= 0) return 0;
|
|
818
|
-
return Math.round(Math.log(1 + sats) * (1 + days / 30) * 100) / 100;
|
|
819
|
-
}
|
|
820
|
-
function scoreTier(sats, days) {
|
|
821
|
-
if (sats >= 1e7 && days >= 365) return "platinum";
|
|
822
|
-
if (sats >= 1e6 && days >= 180) return "gold";
|
|
823
|
-
if (sats >= 1e5 && days >= 90) return "silver";
|
|
824
|
-
if (sats >= 1e4 && days >= 30) return "bronze";
|
|
825
|
-
return "none";
|
|
826
|
-
}
|
|
827
|
-
function computeScore(sats, days, config) {
|
|
828
|
-
switch (config.algorithm) {
|
|
829
|
-
case "v0":
|
|
830
|
-
return scoreV0(sats, days);
|
|
831
|
-
case "tier":
|
|
832
|
-
return scoreTier(sats, days);
|
|
833
|
-
case "none":
|
|
834
|
-
return null;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
function computeAllScores(sats, days) {
|
|
838
|
-
return { v0: scoreV0(sats, days), tier: scoreTier(sats, days) };
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// src/verify.ts
|
|
842
|
-
var log4 = createLogger("ocp/verify");
|
|
843
|
-
function looksLikeHex(s) {
|
|
844
|
-
return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
|
|
845
|
-
}
|
|
846
|
-
function toBase64Maybe(sig) {
|
|
847
|
-
if (looksLikeHex(sig)) {
|
|
848
|
-
return Buffer.from(sig, "hex").toString("base64");
|
|
849
|
-
}
|
|
850
|
-
return sig;
|
|
851
|
-
}
|
|
852
|
-
function detectSignatureScheme(addr, sig) {
|
|
853
|
-
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
854
|
-
if (isSegWit) {
|
|
855
|
-
return "bip322";
|
|
856
|
-
}
|
|
857
|
-
const sigBase64 = toBase64Maybe(sig);
|
|
858
|
-
const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
|
|
859
|
-
if (looksLikeLegacy) {
|
|
860
|
-
return "legacy";
|
|
861
|
-
}
|
|
862
|
-
return "bip322";
|
|
863
|
-
}
|
|
864
|
-
async function verifySignature({
|
|
865
|
-
addr,
|
|
866
|
-
msg,
|
|
867
|
-
sig,
|
|
868
|
-
scheme
|
|
869
|
-
}) {
|
|
870
|
-
const detectedScheme = scheme || detectSignatureScheme(addr, sig);
|
|
871
|
-
log4.debug(
|
|
872
|
-
{ addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
|
|
873
|
-
"Verifying signature"
|
|
874
|
-
);
|
|
875
|
-
if (detectedScheme === "bip322") {
|
|
876
|
-
const sigBase64 = toBase64Maybe(sig);
|
|
877
|
-
log4.debug(
|
|
878
|
-
{ sigBase64: sigBase64.substring(0, 50) + "..." },
|
|
879
|
-
"Signature converted to base64"
|
|
880
|
-
);
|
|
881
|
-
log4.debug(
|
|
882
|
-
{
|
|
883
|
-
msgFull: msg,
|
|
884
|
-
msgLines: msg.split("\n").length,
|
|
885
|
-
msgFirstLine: msg.split("\n")[0],
|
|
886
|
-
msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
|
|
887
|
-
},
|
|
888
|
-
"Message being verified"
|
|
889
|
-
);
|
|
890
|
-
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
891
|
-
log4.info({ addr, ok }, "BIP-322 verification result");
|
|
892
|
-
if (!ok) {
|
|
893
|
-
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
894
|
-
if (isSegWit && sigBase64.length === 88) {
|
|
895
|
-
log4.warn(
|
|
896
|
-
{ addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
|
|
897
|
-
"BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
|
|
898
|
-
);
|
|
899
|
-
} else {
|
|
900
|
-
log4.error(
|
|
901
|
-
{
|
|
902
|
-
addr,
|
|
903
|
-
sigLength: sigBase64.length,
|
|
904
|
-
msgLength: msg.length,
|
|
905
|
-
msgPreview: msg.substring(0, 200)
|
|
906
|
-
},
|
|
907
|
-
"BIP-322 verification failed - signature does not match message"
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return ok ? "sig_ok_bip322" : "sig_invalid";
|
|
912
|
-
}
|
|
913
|
-
if (detectedScheme === "legacy") {
|
|
914
|
-
const sigBase64 = toBase64Maybe(sig);
|
|
915
|
-
if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
|
|
916
|
-
log4.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
|
|
917
|
-
return "sig_unsupported_script";
|
|
918
|
-
}
|
|
919
|
-
log4.debug(
|
|
920
|
-
{
|
|
921
|
-
addr,
|
|
922
|
-
sigBase64: sigBase64.substring(0, 50) + "...",
|
|
923
|
-
msgPreview: msg.substring(0, 100) + "...",
|
|
924
|
-
msgLength: msg.length
|
|
925
|
-
},
|
|
926
|
-
"Verifying legacy signature"
|
|
927
|
-
);
|
|
928
|
-
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
929
|
-
log4.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
|
|
930
|
-
if (!ok) {
|
|
931
|
-
log4.error(
|
|
932
|
-
{
|
|
933
|
-
addr,
|
|
934
|
-
msgFirstLine: msg.split("\n")[0],
|
|
935
|
-
sigFormat: sigBase64.substring(0, 10)
|
|
936
|
-
},
|
|
937
|
-
"Legacy signature verification failed - check message format and signature"
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
return ok ? "sig_ok_legacy" : "sig_invalid";
|
|
941
|
-
}
|
|
942
|
-
return "invalid_scheme";
|
|
943
|
-
}
|
|
944
|
-
function parseCanonicalMessage(msg) {
|
|
945
|
-
const lines = msg.split("\n").map((l) => l.trim());
|
|
946
|
-
const getVal = (prefix) => {
|
|
947
|
-
const needle = prefix + ":";
|
|
948
|
-
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
949
|
-
if (!hit) return void 0;
|
|
950
|
-
const idx = hit.indexOf(":");
|
|
951
|
-
return hit.slice(idx + 1).trim();
|
|
952
|
-
};
|
|
953
|
-
const address = getVal("address") || "";
|
|
954
|
-
const identitiesStr = getVal("identities") || "";
|
|
955
|
-
let identities = [];
|
|
956
|
-
try {
|
|
957
|
-
identities = parseIdentities(identitiesStr);
|
|
958
|
-
} catch (err) {
|
|
959
|
-
log4.warn({ identitiesStr, error: err }, "Failed to parse identities field");
|
|
960
|
-
}
|
|
961
|
-
const coreKeys = /* @__PURE__ */ new Set([
|
|
962
|
-
"orangecheck",
|
|
963
|
-
"identities",
|
|
964
|
-
"address",
|
|
965
|
-
"purpose",
|
|
966
|
-
"nonce",
|
|
967
|
-
"issued_at",
|
|
968
|
-
"ack"
|
|
969
|
-
]);
|
|
970
|
-
const extensions = {};
|
|
971
|
-
for (const line of lines) {
|
|
972
|
-
const idx = line.indexOf(":");
|
|
973
|
-
if (idx === -1) continue;
|
|
974
|
-
const key = line.slice(0, idx).trim().toLowerCase();
|
|
975
|
-
const val = line.slice(idx + 1).trim();
|
|
976
|
-
if (!coreKeys.has(key) && val) extensions[key] = val;
|
|
977
|
-
}
|
|
978
|
-
const network = extensions["network"] || "mainnet";
|
|
979
|
-
const bondStr = extensions["bond"];
|
|
980
|
-
const bond = bondStr ? parseInt(bondStr, 10) : void 0;
|
|
981
|
-
return {
|
|
982
|
-
core: { address, identities },
|
|
983
|
-
network,
|
|
984
|
-
extensions,
|
|
985
|
-
bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
async function getAddressUtxos(address, network, opts) {
|
|
989
|
-
const endpoints = network === "signet" ? [
|
|
990
|
-
opts?.esploraSignetBase || "https://mempool.space/signet/api",
|
|
991
|
-
"https://blockstream.info/signet/api"
|
|
992
|
-
] : network === "testnet" ? [
|
|
993
|
-
opts?.esploraTestnetBase || "https://mempool.space/testnet/api",
|
|
994
|
-
"https://blockstream.info/testnet/api"
|
|
995
|
-
] : [
|
|
996
|
-
opts?.esploraMainnetBase || "https://mempool.space/api",
|
|
997
|
-
"https://blockstream.info/api"
|
|
998
|
-
];
|
|
999
|
-
let lastError = null;
|
|
1000
|
-
for (const base of endpoints) {
|
|
1001
|
-
try {
|
|
1002
|
-
const url = `${base}/address/${address}/utxo`;
|
|
1003
|
-
log4.debug({ url, network }, "Fetching UTXOs");
|
|
1004
|
-
const resp = await fetch(url, {
|
|
1005
|
-
method: "GET",
|
|
1006
|
-
headers: { Accept: "application/json" },
|
|
1007
|
-
signal: AbortSignal.timeout(5e3)
|
|
1008
|
-
});
|
|
1009
|
-
if (!resp.ok) {
|
|
1010
|
-
throw new Error(`HTTP ${resp.status}`);
|
|
1011
|
-
}
|
|
1012
|
-
const data = await resp.json();
|
|
1013
|
-
if (!Array.isArray(data)) {
|
|
1014
|
-
throw new Error("Invalid response: expected array");
|
|
1015
|
-
}
|
|
1016
|
-
for (const utxo of data) {
|
|
1017
|
-
if (typeof utxo.value !== "number" || typeof utxo.status !== "object" || typeof utxo.status.confirmed !== "boolean" || typeof utxo.txid !== "string" || typeof utxo.vout !== "number") {
|
|
1018
|
-
throw new Error("Invalid UTXO structure");
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
log4.info({ base, count: data.length, network }, "UTXOs fetched successfully");
|
|
1022
|
-
return data;
|
|
1023
|
-
} catch (err) {
|
|
1024
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1025
|
-
log4.warn(
|
|
1026
|
-
{ base, error: lastError.message, network },
|
|
1027
|
-
"Esplora endpoint failed, trying next"
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1073
|
+
throw new AttestationError(
|
|
1074
|
+
"Attestation envelope integrity check failed",
|
|
1075
|
+
"INTEGRITY_CHECK_FAILED"
|
|
1076
|
+
);
|
|
1030
1077
|
}
|
|
1031
|
-
log4.
|
|
1032
|
-
|
|
1078
|
+
log4.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
|
|
1079
|
+
return envelope;
|
|
1033
1080
|
}
|
|
1034
|
-
function
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);
|
|
1046
|
-
return a.vout - b.vout;
|
|
1047
|
-
});
|
|
1048
|
-
let sum = 0;
|
|
1049
|
-
let youngestTime = 0;
|
|
1050
|
-
for (const utxo of sorted) {
|
|
1051
|
-
sum += utxo.value;
|
|
1052
|
-
youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);
|
|
1053
|
-
if (sum >= bondAmount) break;
|
|
1054
|
-
}
|
|
1055
|
-
const days_unspent2 = Math.max(
|
|
1056
|
-
0,
|
|
1057
|
-
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
1081
|
+
async function publishAttestation(options) {
|
|
1082
|
+
const { envelope, npub, relays = DEFAULT_RELAYS } = options;
|
|
1083
|
+
log4.info(
|
|
1084
|
+
{ attestationId: envelope.attestation_id, relayCount: relays.length },
|
|
1085
|
+
"Publishing attestation"
|
|
1086
|
+
);
|
|
1087
|
+
const unsignedEvent = createAttestationEvent(envelope, npub);
|
|
1088
|
+
if (!isNip07Available()) {
|
|
1089
|
+
log4.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
"nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish"
|
|
1058
1092
|
);
|
|
1059
|
-
return {
|
|
1060
|
-
sats_bonded: bondAmount,
|
|
1061
|
-
// Use exactly bond amount (ignore surplus)
|
|
1062
|
-
days_unspent: days_unspent2,
|
|
1063
|
-
confirmed_balance
|
|
1064
|
-
};
|
|
1065
1093
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
);
|
|
1071
|
-
let sum = 0;
|
|
1072
|
-
let youngestTime = 0;
|
|
1073
|
-
for (const utxo of sorted) {
|
|
1074
|
-
sum += utxo.value;
|
|
1075
|
-
youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
|
|
1076
|
-
if (sum >= coverSats) break;
|
|
1077
|
-
}
|
|
1078
|
-
const days_unspent2 = Math.max(
|
|
1079
|
-
0,
|
|
1080
|
-
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
1081
|
-
);
|
|
1082
|
-
return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
|
|
1094
|
+
log4.info("Using NIP-07 to sign event");
|
|
1095
|
+
const signedEvent = await signNostrEvent(unsignedEvent);
|
|
1096
|
+
if (!signedEvent) {
|
|
1097
|
+
throw new Error("nostr_signing_failed: NIP-07 extension rejected or errored");
|
|
1083
1098
|
}
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1099
|
+
const result = await publishToRelays(signedEvent, relays);
|
|
1100
|
+
log4.info(
|
|
1101
|
+
{
|
|
1102
|
+
attestationId: envelope.attestation_id,
|
|
1103
|
+
successCount: result.success.length,
|
|
1104
|
+
failedCount: result.failed.length
|
|
1105
|
+
},
|
|
1106
|
+
"Publishing complete"
|
|
1086
1107
|
);
|
|
1087
|
-
|
|
1088
|
-
return { sats_bonded, days_unspent, confirmed_balance };
|
|
1108
|
+
return result;
|
|
1089
1109
|
}
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1110
|
+
function eventIndexesEnvelope(event, envelope) {
|
|
1111
|
+
const d = event.tags.find((t) => t[0] === "d")?.[1];
|
|
1112
|
+
const id = envelope.attestation_id;
|
|
1113
|
+
if (d !== id && d !== `orangecheck:${id}`) return false;
|
|
1114
|
+
return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
|
|
1115
|
+
}
|
|
1116
|
+
async function discoverAttestations(options) {
|
|
1117
|
+
const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
|
|
1118
|
+
let events = [];
|
|
1119
|
+
if (attestationId) {
|
|
1120
|
+
log4.info({ attestationId }, "Discovering by attestation ID");
|
|
1121
|
+
events = await queryByAttestationId(attestationId, relays);
|
|
1122
|
+
} else if (address) {
|
|
1123
|
+
log4.info({ address }, "Discovering by Bitcoin address");
|
|
1124
|
+
events = await queryByAddress(address, relays);
|
|
1125
|
+
} else if (identity) {
|
|
1126
|
+
log4.info({ identity }, "Discovering by identity");
|
|
1127
|
+
events = await queryByIdentity(identity.protocol, identity.identifier, relays);
|
|
1128
|
+
} else {
|
|
1129
|
+
throw new Error("Must provide attestationId, address, or identity");
|
|
1130
|
+
}
|
|
1131
|
+
const envelopes = await collectSignedEnvelopes(events, (event, envelope) => {
|
|
1132
|
+
if (eventIndexesEnvelope(event, envelope)) return true;
|
|
1133
|
+
log4.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
|
|
1134
|
+
return false;
|
|
1135
|
+
});
|
|
1136
|
+
log4.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
|
|
1137
|
+
return envelopes;
|
|
1138
|
+
}
|
|
1139
|
+
async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
|
|
1140
|
+
log4.info({ attestationId }, "Verifying attestation by ID");
|
|
1141
|
+
const envelopes = await discoverAttestations({ attestationId, relays });
|
|
1142
|
+
if (envelopes.length === 0) {
|
|
1143
|
+
log4.warn({ attestationId }, "Attestation not found on any relay");
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
if (envelopes.length > 1) {
|
|
1147
|
+
log4.warn(
|
|
1148
|
+
{ attestationId, count: envelopes.length },
|
|
1149
|
+
"Multiple attestations found with same ID - using first"
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
return envelopes[0] || null;
|
|
1153
|
+
}
|
|
1154
|
+
async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
|
|
1155
|
+
log4.info({ address }, "Getting all attestations for address");
|
|
1156
|
+
return discoverAttestations({ address, relays });
|
|
1157
|
+
}
|
|
1158
|
+
async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
1159
|
+
log4.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
1160
|
+
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
1161
|
+
}
|
|
1162
|
+
function formatIdentitiesForDisplay(identities) {
|
|
1163
|
+
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
1164
|
+
}
|
|
1165
|
+
function getVerificationUrl(attestationId, baseUrl = "https://attest.ochk.io") {
|
|
1166
|
+
return `${baseUrl}/attest/${attestationId}`;
|
|
1167
|
+
}
|
|
1168
|
+
function extractAttestationIdFromUrl(url) {
|
|
1093
1169
|
try {
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
codes.push("bad_request");
|
|
1103
|
-
return { ok: false, codes, network, attestation_id };
|
|
1104
|
-
}
|
|
1105
|
-
if ((network === "testnet" || network === "signet") && !opts.testMode) {
|
|
1106
|
-
codes.push("network_testmode");
|
|
1107
|
-
return { ok: false, codes, network, attestation_id };
|
|
1108
|
-
}
|
|
1109
|
-
if (opts.expectedAud) {
|
|
1110
|
-
const aud = parsed.extensions["aud"];
|
|
1111
|
-
if (!aud || aud !== opts.expectedAud) {
|
|
1112
|
-
codes.push("aud_mismatch");
|
|
1113
|
-
return { ok: false, codes, network, attestation_id };
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if (parsed.extensions["expires"]) {
|
|
1117
|
-
const exp = Date.parse(parsed.extensions["expires"]);
|
|
1118
|
-
if (!Number.isNaN(exp) && exp < Date.now()) {
|
|
1119
|
-
codes.push("expired");
|
|
1120
|
-
return { ok: false, codes, network, attestation_id };
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
const sigCode = await verifySignature({
|
|
1124
|
-
addr: input.addr,
|
|
1125
|
-
msg: input.msg,
|
|
1126
|
-
sig: input.sig,
|
|
1127
|
-
...input.scheme && { scheme: input.scheme }
|
|
1128
|
-
});
|
|
1129
|
-
codes.push(sigCode);
|
|
1130
|
-
const sigOk = sigCode === "sig_ok_bip322" || sigCode === "sig_ok_legacy";
|
|
1131
|
-
if (!sigOk) {
|
|
1132
|
-
return {
|
|
1133
|
-
ok: false,
|
|
1134
|
-
codes,
|
|
1135
|
-
network,
|
|
1136
|
-
attestation_id,
|
|
1137
|
-
identities: parsed.core.identities
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
try {
|
|
1141
|
-
const utxos = await getAddressUtxos(input.addr, network, {
|
|
1142
|
-
esploraMainnetBase: opts.esploraMainnetBase,
|
|
1143
|
-
esploraSignetBase: opts.esploraSignetBase
|
|
1144
|
-
});
|
|
1145
|
-
const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
|
|
1146
|
-
utxos,
|
|
1147
|
-
parsed.bond,
|
|
1148
|
-
opts.coverSats
|
|
1149
|
-
);
|
|
1150
|
-
if (parsed.bond !== void 0 && parsed.bond > 0) {
|
|
1151
|
-
if (confirmed_balance < parsed.bond) {
|
|
1152
|
-
codes.push("bond_insufficient");
|
|
1153
|
-
log4.warn(
|
|
1154
|
-
{
|
|
1155
|
-
bond: parsed.bond,
|
|
1156
|
-
confirmed_balance,
|
|
1157
|
-
address: input.addr
|
|
1158
|
-
},
|
|
1159
|
-
"Bond extension: insufficient balance"
|
|
1160
|
-
);
|
|
1161
|
-
return {
|
|
1162
|
-
ok: false,
|
|
1163
|
-
codes,
|
|
1164
|
-
network,
|
|
1165
|
-
attestation_id,
|
|
1166
|
-
identities: parsed.core.identities
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
if (sats_bonded <= 0) {
|
|
1171
|
-
codes.push("bond_zero");
|
|
1172
|
-
if (utxos.some((u) => !u.status.confirmed)) {
|
|
1173
|
-
codes.push("bond_pending");
|
|
1174
|
-
}
|
|
1175
|
-
return {
|
|
1176
|
-
ok: false,
|
|
1177
|
-
codes,
|
|
1178
|
-
network,
|
|
1179
|
-
attestation_id,
|
|
1180
|
-
identities: parsed.core.identities
|
|
1181
|
-
};
|
|
1182
|
-
} else {
|
|
1183
|
-
codes.push("bond_confirmed");
|
|
1184
|
-
}
|
|
1185
|
-
const metrics = {
|
|
1186
|
-
sats_bonded,
|
|
1187
|
-
days_unspent,
|
|
1188
|
-
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
1189
|
-
};
|
|
1190
|
-
return {
|
|
1191
|
-
ok: true,
|
|
1192
|
-
codes,
|
|
1193
|
-
network,
|
|
1194
|
-
attestation_id,
|
|
1195
|
-
identities: parsed.core.identities,
|
|
1196
|
-
metrics
|
|
1197
|
-
};
|
|
1198
|
-
} catch (utxoError) {
|
|
1199
|
-
log4.error({ err: utxoError }, "UTXO fetch failed");
|
|
1200
|
-
codes.push("bad_request");
|
|
1201
|
-
return { ok: false, codes, network, attestation_id };
|
|
1202
|
-
}
|
|
1203
|
-
} catch (e) {
|
|
1204
|
-
const msg = e instanceof Error ? e.message : String(e ?? "decode_error");
|
|
1205
|
-
if (msg === "decode_error") {
|
|
1206
|
-
codes.push("decode_error");
|
|
1207
|
-
} else {
|
|
1208
|
-
codes.push("bad_request");
|
|
1209
|
-
}
|
|
1210
|
-
return { ok: false, codes, network };
|
|
1170
|
+
const u = new URL(url);
|
|
1171
|
+
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
1172
|
+
if (m) return m[1].toLowerCase();
|
|
1173
|
+
const q = u.searchParams.get("id");
|
|
1174
|
+
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
1175
|
+
return null;
|
|
1176
|
+
} catch {
|
|
1177
|
+
return null;
|
|
1211
1178
|
}
|
|
1212
1179
|
}
|
|
1213
1180
|
|
|
1214
1181
|
// src/check.ts
|
|
1182
|
+
var ISSUED_AHEAD_MS = 12e4;
|
|
1183
|
+
var MAX_CANDIDATES = 20;
|
|
1215
1184
|
async function check(params) {
|
|
1216
1185
|
const {
|
|
1217
1186
|
addr,
|
|
@@ -1233,14 +1202,17 @@ async function check(params) {
|
|
|
1233
1202
|
} else if (identity) {
|
|
1234
1203
|
found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
|
|
1235
1204
|
}
|
|
1236
|
-
const
|
|
1205
|
+
const latest = Date.now() + ISSUED_AHEAD_MS;
|
|
1206
|
+
const envelopes = found.map(signedEnvelope).filter((e) => e !== null).filter((e) => signedMatch(e, { addr, id, identity })).map((e) => ({ e, at: rfc3339Ms(e.issued_at) })).filter(({ at }) => at <= latest).sort((a, b) => b.at - a.at || (a.e.attestation_id < b.e.attestation_id ? -1 : 1)).map(({ e }) => e);
|
|
1237
1207
|
if (envelopes.length === 0) {
|
|
1238
1208
|
return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
|
|
1239
1209
|
}
|
|
1240
|
-
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1210
|
+
let env = envelopes[0];
|
|
1211
|
+
for (const candidate of envelopes.slice(0, MAX_CANDIDATES)) {
|
|
1212
|
+
if (await signedBy(candidate)) {
|
|
1213
|
+
env = candidate;
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1244
1216
|
}
|
|
1245
1217
|
const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
|
|
1246
1218
|
const outcome = await verify(
|
|
@@ -1268,7 +1240,7 @@ async function check(params) {
|
|
|
1268
1240
|
score,
|
|
1269
1241
|
attestation_id: env.attestation_id,
|
|
1270
1242
|
address: env.address,
|
|
1271
|
-
identities: env
|
|
1243
|
+
identities: signedIdentities(env),
|
|
1272
1244
|
network: outcome.network,
|
|
1273
1245
|
...reasons.length ? { reasons } : {}
|
|
1274
1246
|
};
|
|
@@ -1304,16 +1276,19 @@ function signedMatch(env, q) {
|
|
|
1304
1276
|
}
|
|
1305
1277
|
return false;
|
|
1306
1278
|
}
|
|
1279
|
+
function signedBy(e) {
|
|
1280
|
+
return signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme });
|
|
1281
|
+
}
|
|
1307
1282
|
async function backsOtherIdentity(env, identity, relays) {
|
|
1308
1283
|
const want = identityKey(identity);
|
|
1309
1284
|
const protocol = identity.protocol.trim().toLowerCase();
|
|
1310
1285
|
const siblings = await getAttestationsForAddress(env.address, relays);
|
|
1311
1286
|
for (const e of [env, ...siblings]) {
|
|
1312
1287
|
if (!signedMatch(e, { addr: env.address })) continue;
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1288
|
+
const other = signedIdentities(e).some(
|
|
1289
|
+
(b) => b.protocol.trim().toLowerCase() === protocol && identityKey(b) !== want
|
|
1290
|
+
);
|
|
1291
|
+
if (other && (e === env || await signedBy(e))) return true;
|
|
1317
1292
|
}
|
|
1318
1293
|
return false;
|
|
1319
1294
|
}
|
|
@@ -1430,24 +1405,17 @@ async function verifyChallenge(opts) {
|
|
|
1430
1405
|
if (opts.expectedPurpose && purpose !== opts.expectedPurpose) {
|
|
1431
1406
|
return { ok: false, reason: "purpose_mismatch" };
|
|
1432
1407
|
}
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
if (scheme
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
} else if (scheme === "legacy") {
|
|
1439
|
-
if (!/^[1m][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address)) {
|
|
1440
|
-
return { ok: false, reason: "sig_unsupported_scheme" };
|
|
1441
|
-
}
|
|
1442
|
-
sigValid = verifySignature$1(address, opts.message, opts.signature);
|
|
1443
|
-
} else {
|
|
1444
|
-
return { ok: false, reason: "sig_unsupported_scheme" };
|
|
1408
|
+
const sigBase64 = toBase64Maybe(opts.signature);
|
|
1409
|
+
const scheme = signatureScheme(sigBase64);
|
|
1410
|
+
if (!scheme) return { ok: false, reason: "sig_unsupported_scheme" };
|
|
1411
|
+
if (!verifySignature$1(address, opts.message, sigBase64)) {
|
|
1412
|
+
return { ok: false, reason: "sig_invalid" };
|
|
1445
1413
|
}
|
|
1446
|
-
if (!sigValid) return { ok: false, reason: "sig_invalid" };
|
|
1447
1414
|
return {
|
|
1448
1415
|
ok: true,
|
|
1449
1416
|
reason: "ok",
|
|
1450
1417
|
address,
|
|
1418
|
+
scheme,
|
|
1451
1419
|
expiresAt: expiresAt.getTime(),
|
|
1452
1420
|
nonce,
|
|
1453
1421
|
...audience !== void 0 ? { audience } : {},
|
|
@@ -2353,12 +2321,12 @@ var STATUS_META = {
|
|
|
2353
2321
|
},
|
|
2354
2322
|
sig_invalid: {
|
|
2355
2323
|
label: "Signature verification failed",
|
|
2356
|
-
detail: "The signature does not match the message and address. Common issues: (1)
|
|
2324
|
+
detail: "The signature does not match the message and address. Common issues: (1) the message was not copied exactly, (2) a different address signed it, (3) the wallet signed on another network.",
|
|
2357
2325
|
severity: "error"
|
|
2358
2326
|
},
|
|
2359
2327
|
sig_unsupported_script: {
|
|
2360
|
-
label: "
|
|
2361
|
-
detail:
|
|
2328
|
+
label: "Signature form not supported",
|
|
2329
|
+
detail: "Your wallet made a BIP-322 full or proof-of-funds signature. Sign with a BIP-322 simple signature, or with legacy message signing, which every address type here accepts.",
|
|
2362
2330
|
severity: "error"
|
|
2363
2331
|
},
|
|
2364
2332
|
bond_confirmed: {
|
|
@@ -2418,6 +2386,6 @@ var STATUS_META = {
|
|
|
2418
2386
|
}
|
|
2419
2387
|
};
|
|
2420
2388
|
|
|
2421
|
-
export { BOND_DAYS_FLOORS, BOND_MAX_WINDOW_SECONDS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, STATUS_META, bondFloors, bondId, bondNullifier, buildBondMessage, buildCanonicalMessage, check, checkBondCoin, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, esploraSource, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueBondMessage, issueChallenge, nostrIdentifierForms, nostrPubkeyToHex, parseAttestationFromEvent, parseBondMessage, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify, verifyAllIdentities, verifyAttestationById, verifyBond, verifyBondSignature, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
|
|
2389
|
+
export { BOND_DAYS_FLOORS, BOND_MAX_WINDOW_SECONDS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, STATUS_META, bondFloors, bondId, bondNullifier, buildBondMessage, buildCanonicalMessage, check, checkBondCoin, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, esploraSource, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueBondMessage, issueChallenge, nostrIdentifierForms, nostrPubkeyToHex, parseAttestationFromEvent, parseBondMessage, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, requiresManualVerification, signNostrEvent, signatureIsValid, signedEnvelope, supportsAutomaticVerification, verify, verifyAllIdentities, verifyAttestationById, verifyBond, verifyBondSignature, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
|
|
2422
2390
|
//# sourceMappingURL=index.mjs.map
|
|
2423
2391
|
//# sourceMappingURL=index.mjs.map
|