@orangecheck/sdk 1.14.0 → 1.15.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/dist/attestation.js +307 -61
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +307 -61
- package/dist/attestation.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 +676 -615
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +675 -616
- 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 +294 -38
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +293 -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 +24 -4
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +24 -5
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -212,8 +212,426 @@ 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
|
+
|
|
249
|
+
// src/verify.ts
|
|
250
|
+
var log = createLogger("ocp/verify");
|
|
251
|
+
function looksLikeHex(s) {
|
|
252
|
+
return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
|
|
253
|
+
}
|
|
254
|
+
function toBase64Maybe(sig) {
|
|
255
|
+
if (looksLikeHex(sig)) {
|
|
256
|
+
return Buffer.from(sig, "hex").toString("base64");
|
|
257
|
+
}
|
|
258
|
+
return sig;
|
|
259
|
+
}
|
|
260
|
+
function detectSignatureScheme(addr, sig) {
|
|
261
|
+
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
262
|
+
if (isSegWit) {
|
|
263
|
+
return "bip322";
|
|
264
|
+
}
|
|
265
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
266
|
+
const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
|
|
267
|
+
if (looksLikeLegacy) {
|
|
268
|
+
return "legacy";
|
|
269
|
+
}
|
|
270
|
+
return "bip322";
|
|
271
|
+
}
|
|
272
|
+
async function verifySignature({
|
|
273
|
+
addr,
|
|
274
|
+
msg,
|
|
275
|
+
sig,
|
|
276
|
+
scheme
|
|
277
|
+
}) {
|
|
278
|
+
const detectedScheme = scheme || detectSignatureScheme(addr, sig);
|
|
279
|
+
log.debug(
|
|
280
|
+
{ addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
|
|
281
|
+
"Verifying signature"
|
|
282
|
+
);
|
|
283
|
+
if (detectedScheme === "bip322") {
|
|
284
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
285
|
+
log.debug(
|
|
286
|
+
{ sigBase64: sigBase64.substring(0, 50) + "..." },
|
|
287
|
+
"Signature converted to base64"
|
|
288
|
+
);
|
|
289
|
+
log.debug(
|
|
290
|
+
{
|
|
291
|
+
msgFull: msg,
|
|
292
|
+
msgLines: msg.split("\n").length,
|
|
293
|
+
msgFirstLine: msg.split("\n")[0],
|
|
294
|
+
msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
|
|
295
|
+
},
|
|
296
|
+
"Message being verified"
|
|
297
|
+
);
|
|
298
|
+
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
299
|
+
log.info({ addr, ok }, "BIP-322 verification result");
|
|
300
|
+
if (!ok) {
|
|
301
|
+
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
302
|
+
if (isSegWit && sigBase64.length === 88) {
|
|
303
|
+
log.warn(
|
|
304
|
+
{ addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
|
|
305
|
+
"BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
|
|
306
|
+
);
|
|
307
|
+
} else {
|
|
308
|
+
log.error(
|
|
309
|
+
{
|
|
310
|
+
addr,
|
|
311
|
+
sigLength: sigBase64.length,
|
|
312
|
+
msgLength: msg.length,
|
|
313
|
+
msgPreview: msg.substring(0, 200)
|
|
314
|
+
},
|
|
315
|
+
"BIP-322 verification failed - signature does not match message"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return ok ? "sig_ok_bip322" : "sig_invalid";
|
|
320
|
+
}
|
|
321
|
+
if (detectedScheme === "legacy") {
|
|
322
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
323
|
+
if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
|
|
324
|
+
log.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
|
|
325
|
+
return "sig_unsupported_script";
|
|
326
|
+
}
|
|
327
|
+
log.debug(
|
|
328
|
+
{
|
|
329
|
+
addr,
|
|
330
|
+
sigBase64: sigBase64.substring(0, 50) + "...",
|
|
331
|
+
msgPreview: msg.substring(0, 100) + "...",
|
|
332
|
+
msgLength: msg.length
|
|
333
|
+
},
|
|
334
|
+
"Verifying legacy signature"
|
|
335
|
+
);
|
|
336
|
+
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
337
|
+
log.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
|
|
338
|
+
if (!ok) {
|
|
339
|
+
log.error(
|
|
340
|
+
{
|
|
341
|
+
addr,
|
|
342
|
+
msgFirstLine: msg.split("\n")[0],
|
|
343
|
+
sigFormat: sigBase64.substring(0, 10)
|
|
344
|
+
},
|
|
345
|
+
"Legacy signature verification failed - check message format and signature"
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return ok ? "sig_ok_legacy" : "sig_invalid";
|
|
349
|
+
}
|
|
350
|
+
return "invalid_scheme";
|
|
351
|
+
}
|
|
352
|
+
async function signatureIsValid(input) {
|
|
353
|
+
try {
|
|
354
|
+
const code = await verifySignature(input);
|
|
355
|
+
return code === "sig_ok_bip322" || code === "sig_ok_legacy";
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function parseCanonicalMessage(msg) {
|
|
361
|
+
const lines = msg.split("\n").map((l) => l.trim());
|
|
362
|
+
const getVal = (prefix) => {
|
|
363
|
+
const needle = prefix + ":";
|
|
364
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
365
|
+
if (!hit) return void 0;
|
|
366
|
+
const idx = hit.indexOf(":");
|
|
367
|
+
return hit.slice(idx + 1).trim();
|
|
368
|
+
};
|
|
369
|
+
const address = getVal("address") || "";
|
|
370
|
+
const identitiesStr = getVal("identities") || "";
|
|
371
|
+
const issued_at = getVal("issued_at") || "";
|
|
372
|
+
const expires = getVal("expires");
|
|
373
|
+
let identities = [];
|
|
374
|
+
try {
|
|
375
|
+
identities = parseIdentities(identitiesStr);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
|
|
378
|
+
}
|
|
379
|
+
const coreKeys = /* @__PURE__ */ new Set([
|
|
380
|
+
"orangecheck",
|
|
381
|
+
"identities",
|
|
382
|
+
"address",
|
|
383
|
+
"purpose",
|
|
384
|
+
"nonce",
|
|
385
|
+
"issued_at",
|
|
386
|
+
"ack"
|
|
387
|
+
]);
|
|
388
|
+
const extensions = {};
|
|
389
|
+
for (const line of lines) {
|
|
390
|
+
const idx = line.indexOf(":");
|
|
391
|
+
if (idx === -1) continue;
|
|
392
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
393
|
+
const val = line.slice(idx + 1).trim();
|
|
394
|
+
if (!coreKeys.has(key) && val) extensions[key] = val;
|
|
395
|
+
}
|
|
396
|
+
const network = extensions["network"] || "mainnet";
|
|
397
|
+
const bondStr = extensions["bond"];
|
|
398
|
+
const bond = bondStr ? parseInt(bondStr, 10) : void 0;
|
|
399
|
+
return {
|
|
400
|
+
core: { address, identities, issued_at },
|
|
401
|
+
expires,
|
|
402
|
+
network,
|
|
403
|
+
extensions,
|
|
404
|
+
bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async function getAddressUtxos(address, network, opts) {
|
|
408
|
+
const endpoints = network === "signet" ? [
|
|
409
|
+
opts?.esploraSignetBase || "https://mempool.space/signet/api",
|
|
410
|
+
"https://blockstream.info/signet/api"
|
|
411
|
+
] : network === "testnet" ? [
|
|
412
|
+
opts?.esploraTestnetBase || "https://mempool.space/testnet/api",
|
|
413
|
+
"https://blockstream.info/testnet/api"
|
|
414
|
+
] : [
|
|
415
|
+
opts?.esploraMainnetBase || "https://mempool.space/api",
|
|
416
|
+
"https://blockstream.info/api"
|
|
417
|
+
];
|
|
418
|
+
let lastError = null;
|
|
419
|
+
for (const base of endpoints) {
|
|
420
|
+
try {
|
|
421
|
+
const url = `${base}/address/${address}/utxo`;
|
|
422
|
+
log.debug({ url, network }, "Fetching UTXOs");
|
|
423
|
+
const resp = await fetch(url, {
|
|
424
|
+
method: "GET",
|
|
425
|
+
headers: { Accept: "application/json" },
|
|
426
|
+
signal: AbortSignal.timeout(5e3)
|
|
427
|
+
});
|
|
428
|
+
if (!resp.ok) {
|
|
429
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
430
|
+
}
|
|
431
|
+
const data = await resp.json();
|
|
432
|
+
if (!Array.isArray(data)) {
|
|
433
|
+
throw new Error("Invalid response: expected array");
|
|
434
|
+
}
|
|
435
|
+
for (const utxo of data) {
|
|
436
|
+
if (typeof utxo.value !== "number" || typeof utxo.status !== "object" || typeof utxo.status.confirmed !== "boolean" || typeof utxo.txid !== "string" || typeof utxo.vout !== "number") {
|
|
437
|
+
throw new Error("Invalid UTXO structure");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
log.info({ base, count: data.length, network }, "UTXOs fetched successfully");
|
|
441
|
+
return data;
|
|
442
|
+
} catch (err) {
|
|
443
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
444
|
+
log.warn(
|
|
445
|
+
{ base, error: lastError.message, network },
|
|
446
|
+
"Esplora endpoint failed, trying next"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
log.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
|
|
451
|
+
throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
|
|
452
|
+
}
|
|
453
|
+
function computeMetrics(utxos, bondAmount, coverSats) {
|
|
454
|
+
const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
|
|
455
|
+
if (confirmedUtxos.length === 0) {
|
|
456
|
+
return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
|
|
457
|
+
}
|
|
458
|
+
const confirmed_balance = confirmedUtxos.reduce((sum, u) => sum + u.value, 0);
|
|
459
|
+
if (bondAmount !== void 0 && bondAmount > 0) {
|
|
460
|
+
const sorted = [...confirmedUtxos].sort((a, b) => {
|
|
461
|
+
const heightA = a.status.block_height ?? Infinity;
|
|
462
|
+
const heightB = b.status.block_height ?? Infinity;
|
|
463
|
+
if (heightA !== heightB) return heightA - heightB;
|
|
464
|
+
if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);
|
|
465
|
+
return a.vout - b.vout;
|
|
466
|
+
});
|
|
467
|
+
let sum = 0;
|
|
468
|
+
let youngestTime = 0;
|
|
469
|
+
for (const utxo of sorted) {
|
|
470
|
+
sum += utxo.value;
|
|
471
|
+
youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);
|
|
472
|
+
if (sum >= bondAmount) break;
|
|
473
|
+
}
|
|
474
|
+
const days_unspent2 = Math.max(
|
|
475
|
+
0,
|
|
476
|
+
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
477
|
+
);
|
|
478
|
+
return {
|
|
479
|
+
sats_bonded: bondAmount,
|
|
480
|
+
// Use exactly bond amount (ignore surplus)
|
|
481
|
+
days_unspent: days_unspent2,
|
|
482
|
+
confirmed_balance
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const sats_bonded = confirmed_balance;
|
|
486
|
+
if (coverSats !== void 0 && coverSats > 0) {
|
|
487
|
+
const sorted = [...confirmedUtxos].sort(
|
|
488
|
+
(a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)
|
|
489
|
+
);
|
|
490
|
+
let sum = 0;
|
|
491
|
+
let youngestTime = 0;
|
|
492
|
+
for (const utxo of sorted) {
|
|
493
|
+
sum += utxo.value;
|
|
494
|
+
youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
|
|
495
|
+
if (sum >= coverSats) break;
|
|
496
|
+
}
|
|
497
|
+
const days_unspent2 = Math.max(
|
|
498
|
+
0,
|
|
499
|
+
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
500
|
+
);
|
|
501
|
+
return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
|
|
502
|
+
}
|
|
503
|
+
const oldestTime = Math.min(
|
|
504
|
+
...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
|
|
505
|
+
);
|
|
506
|
+
const days_unspent = Math.max(0, Math.floor((Date.now() / 1e3 - oldestTime) / (24 * 60 * 60)));
|
|
507
|
+
return { sats_bonded, days_unspent, confirmed_balance };
|
|
508
|
+
}
|
|
509
|
+
async function verify(input, opts = {}) {
|
|
510
|
+
const codes = [];
|
|
511
|
+
let network = "mainnet";
|
|
512
|
+
try {
|
|
513
|
+
if (!input.msg || !input.addr || !input.sig) {
|
|
514
|
+
codes.push("bad_request");
|
|
515
|
+
return { ok: false, codes, network };
|
|
516
|
+
}
|
|
517
|
+
const parsed = parseCanonicalMessage(input.msg);
|
|
518
|
+
network = parsed.network || "mainnet";
|
|
519
|
+
const attestation_id = await generateAttestationId(input.msg);
|
|
520
|
+
if (parsed.core.address !== input.addr) {
|
|
521
|
+
codes.push("bad_request");
|
|
522
|
+
return { ok: false, codes, network, attestation_id };
|
|
523
|
+
}
|
|
524
|
+
if ((network === "testnet" || network === "signet") && !opts.testMode) {
|
|
525
|
+
codes.push("network_testmode");
|
|
526
|
+
return { ok: false, codes, network, attestation_id };
|
|
527
|
+
}
|
|
528
|
+
if (opts.expectedAud) {
|
|
529
|
+
const aud = parsed.extensions["aud"];
|
|
530
|
+
if (!aud || aud !== opts.expectedAud) {
|
|
531
|
+
codes.push("aud_mismatch");
|
|
532
|
+
return { ok: false, codes, network, attestation_id };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (parsed.expires !== void 0) {
|
|
536
|
+
const exp = rfc3339Ms(parsed.expires);
|
|
537
|
+
if (exp === null || exp < Date.now()) {
|
|
538
|
+
codes.push("expired");
|
|
539
|
+
return { ok: false, codes, network, attestation_id };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const sigCode = await verifySignature({
|
|
543
|
+
addr: input.addr,
|
|
544
|
+
msg: input.msg,
|
|
545
|
+
sig: input.sig,
|
|
546
|
+
...input.scheme && { scheme: input.scheme }
|
|
547
|
+
});
|
|
548
|
+
codes.push(sigCode);
|
|
549
|
+
const sigOk = sigCode === "sig_ok_bip322" || sigCode === "sig_ok_legacy";
|
|
550
|
+
if (!sigOk) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
codes,
|
|
554
|
+
network,
|
|
555
|
+
attestation_id,
|
|
556
|
+
identities: parsed.core.identities
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const utxos = await getAddressUtxos(input.addr, network, {
|
|
561
|
+
esploraMainnetBase: opts.esploraMainnetBase,
|
|
562
|
+
esploraSignetBase: opts.esploraSignetBase
|
|
563
|
+
});
|
|
564
|
+
const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
|
|
565
|
+
utxos,
|
|
566
|
+
parsed.bond,
|
|
567
|
+
opts.coverSats
|
|
568
|
+
);
|
|
569
|
+
if (parsed.bond !== void 0 && parsed.bond > 0) {
|
|
570
|
+
if (confirmed_balance < parsed.bond) {
|
|
571
|
+
codes.push("bond_insufficient");
|
|
572
|
+
log.warn(
|
|
573
|
+
{
|
|
574
|
+
bond: parsed.bond,
|
|
575
|
+
confirmed_balance,
|
|
576
|
+
address: input.addr
|
|
577
|
+
},
|
|
578
|
+
"Bond extension: insufficient balance"
|
|
579
|
+
);
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
codes,
|
|
583
|
+
network,
|
|
584
|
+
attestation_id,
|
|
585
|
+
identities: parsed.core.identities
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (sats_bonded <= 0) {
|
|
590
|
+
codes.push("bond_zero");
|
|
591
|
+
if (utxos.some((u) => !u.status.confirmed)) {
|
|
592
|
+
codes.push("bond_pending");
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
codes,
|
|
597
|
+
network,
|
|
598
|
+
attestation_id,
|
|
599
|
+
identities: parsed.core.identities
|
|
600
|
+
};
|
|
601
|
+
} else {
|
|
602
|
+
codes.push("bond_confirmed");
|
|
603
|
+
}
|
|
604
|
+
const metrics = {
|
|
605
|
+
sats_bonded,
|
|
606
|
+
days_unspent,
|
|
607
|
+
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
608
|
+
};
|
|
609
|
+
return {
|
|
610
|
+
ok: true,
|
|
611
|
+
codes,
|
|
612
|
+
network,
|
|
613
|
+
attestation_id,
|
|
614
|
+
identities: parsed.core.identities,
|
|
615
|
+
metrics
|
|
616
|
+
};
|
|
617
|
+
} catch (utxoError) {
|
|
618
|
+
log.error({ err: utxoError }, "UTXO fetch failed");
|
|
619
|
+
codes.push("bad_request");
|
|
620
|
+
return { ok: false, codes, network, attestation_id };
|
|
621
|
+
}
|
|
622
|
+
} catch (e) {
|
|
623
|
+
const msg = e instanceof Error ? e.message : String(e ?? "decode_error");
|
|
624
|
+
if (msg === "decode_error") {
|
|
625
|
+
codes.push("decode_error");
|
|
626
|
+
} else {
|
|
627
|
+
codes.push("bad_request");
|
|
628
|
+
}
|
|
629
|
+
return { ok: false, codes, network };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
215
633
|
// src/nostr.ts
|
|
216
|
-
var
|
|
634
|
+
var log2 = createLogger("ocp/nostr");
|
|
217
635
|
var ATTESTATION_EVENT_KIND = 30078;
|
|
218
636
|
var DEFAULT_RELAYS = [
|
|
219
637
|
"wss://relay.damus.io",
|
|
@@ -266,7 +684,7 @@ function createAttestationEvent(envelope, pubkey) {
|
|
|
266
684
|
async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
267
685
|
const success = [];
|
|
268
686
|
const failed = [];
|
|
269
|
-
|
|
687
|
+
log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
|
|
270
688
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
271
689
|
const publishPromises = relays.map(async (relayUrl) => {
|
|
272
690
|
let ws = null;
|
|
@@ -305,7 +723,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
305
723
|
cleanup();
|
|
306
724
|
resolve();
|
|
307
725
|
} else {
|
|
308
|
-
|
|
726
|
+
log2.warn(
|
|
309
727
|
{ relay: relayUrl, reason: data[3] },
|
|
310
728
|
"Relay rejected event"
|
|
311
729
|
);
|
|
@@ -315,7 +733,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
315
733
|
}
|
|
316
734
|
}
|
|
317
735
|
} catch (err) {
|
|
318
|
-
|
|
736
|
+
log2.error(
|
|
319
737
|
{ relay: relayUrl, error: err },
|
|
320
738
|
"Failed to parse relay response"
|
|
321
739
|
);
|
|
@@ -325,7 +743,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
325
743
|
}
|
|
326
744
|
};
|
|
327
745
|
wsRef.onerror = (err) => {
|
|
328
|
-
|
|
746
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error");
|
|
329
747
|
recordFailure();
|
|
330
748
|
cleanup();
|
|
331
749
|
reject(err);
|
|
@@ -335,7 +753,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
335
753
|
};
|
|
336
754
|
});
|
|
337
755
|
} catch (err) {
|
|
338
|
-
|
|
756
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
|
|
339
757
|
recordFailure();
|
|
340
758
|
} finally {
|
|
341
759
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
@@ -344,12 +762,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
344
762
|
}
|
|
345
763
|
});
|
|
346
764
|
await Promise.allSettled(publishPromises);
|
|
347
|
-
|
|
765
|
+
log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
|
|
348
766
|
return { success, failed };
|
|
349
767
|
}
|
|
350
768
|
async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
351
769
|
const events = [];
|
|
352
|
-
|
|
770
|
+
log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
|
|
353
771
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
354
772
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
355
773
|
let ws = null;
|
|
@@ -389,11 +807,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
389
807
|
resolve();
|
|
390
808
|
}
|
|
391
809
|
} catch (err) {
|
|
392
|
-
|
|
810
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
393
811
|
}
|
|
394
812
|
};
|
|
395
813
|
wsRef.onerror = (err) => {
|
|
396
|
-
|
|
814
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
397
815
|
cleanup();
|
|
398
816
|
reject(err);
|
|
399
817
|
};
|
|
@@ -402,7 +820,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
402
820
|
};
|
|
403
821
|
});
|
|
404
822
|
} catch (err) {
|
|
405
|
-
|
|
823
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
406
824
|
} finally {
|
|
407
825
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
408
826
|
ws.close();
|
|
@@ -410,12 +828,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
410
828
|
}
|
|
411
829
|
});
|
|
412
830
|
await Promise.allSettled(queryPromises);
|
|
413
|
-
|
|
831
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
414
832
|
return events;
|
|
415
833
|
}
|
|
416
834
|
async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
417
835
|
const events = [];
|
|
418
|
-
|
|
836
|
+
log2.info({ address, relayCount: relays.length }, "Querying relays for address");
|
|
419
837
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
420
838
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
421
839
|
try {
|
|
@@ -457,28 +875,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
|
457
875
|
resolve();
|
|
458
876
|
}
|
|
459
877
|
} catch (err) {
|
|
460
|
-
|
|
878
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
461
879
|
}
|
|
462
880
|
};
|
|
463
881
|
ws.onerror = (err) => {
|
|
464
882
|
clearTimeout(timeout);
|
|
465
|
-
|
|
883
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
466
884
|
reject(err);
|
|
467
885
|
};
|
|
468
886
|
});
|
|
469
887
|
} catch (err) {
|
|
470
|
-
|
|
888
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
471
889
|
}
|
|
472
890
|
});
|
|
473
891
|
await Promise.allSettled(queryPromises);
|
|
474
|
-
|
|
892
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
475
893
|
return events;
|
|
476
894
|
}
|
|
477
895
|
async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
478
896
|
const events = [];
|
|
479
897
|
const identityTag = `${protocol}:${identifier}`;
|
|
480
898
|
const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
|
|
481
|
-
|
|
899
|
+
log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
|
|
482
900
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
483
901
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
484
902
|
try {
|
|
@@ -513,26 +931,26 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
|
513
931
|
resolve();
|
|
514
932
|
}
|
|
515
933
|
} catch (err) {
|
|
516
|
-
|
|
934
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
517
935
|
}
|
|
518
936
|
};
|
|
519
937
|
ws.onerror = (err) => {
|
|
520
938
|
clearTimeout(timeout);
|
|
521
|
-
|
|
939
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
522
940
|
reject(err);
|
|
523
941
|
};
|
|
524
942
|
});
|
|
525
943
|
} catch (err) {
|
|
526
|
-
|
|
944
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
527
945
|
}
|
|
528
946
|
});
|
|
529
947
|
await Promise.allSettled(queryPromises);
|
|
530
|
-
|
|
948
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
531
949
|
return events;
|
|
532
950
|
}
|
|
533
951
|
async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
534
952
|
const events = [];
|
|
535
|
-
|
|
953
|
+
log2.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
|
|
536
954
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
537
955
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
538
956
|
try {
|
|
@@ -568,30 +986,22 @@ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
|
568
986
|
resolve();
|
|
569
987
|
}
|
|
570
988
|
} catch (err) {
|
|
571
|
-
|
|
989
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
572
990
|
}
|
|
573
991
|
};
|
|
574
992
|
ws.onerror = (err) => {
|
|
575
993
|
clearTimeout(timeout);
|
|
576
|
-
|
|
994
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
577
995
|
reject(err);
|
|
578
996
|
};
|
|
579
997
|
});
|
|
580
998
|
} catch (err) {
|
|
581
|
-
|
|
999
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
582
1000
|
}
|
|
583
1001
|
});
|
|
584
1002
|
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");
|
|
1003
|
+
const envelopes = await collectSignedEnvelopes(events);
|
|
1004
|
+
log2.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
|
|
595
1005
|
return envelopes;
|
|
596
1006
|
}
|
|
597
1007
|
async function verifyAttestationEnvelope(envelope) {
|
|
@@ -603,612 +1013,255 @@ async function verifyAttestationEnvelope(envelope) {
|
|
|
603
1013
|
const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
604
1014
|
return computedId === envelope.attestation_id;
|
|
605
1015
|
} catch (err) {
|
|
606
|
-
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
function parseAttestationFromEvent(event) {
|
|
611
|
-
try {
|
|
612
|
-
const envelope = JSON.parse(event.content);
|
|
613
|
-
return envelope;
|
|
614
|
-
} catch (err) {
|
|
615
|
-
log.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// src/nostr-crypto.ts
|
|
621
|
-
var log2 = createLogger("ocp/nostr-crypto");
|
|
622
|
-
function isNip07Available() {
|
|
623
|
-
if (typeof window === "undefined") {
|
|
1016
|
+
log2.error({ error: err }, "Failed to verify attestation envelope");
|
|
624
1017
|
return false;
|
|
625
|
-
}
|
|
626
|
-
return !!window.nostr;
|
|
627
|
-
}
|
|
628
|
-
async function getNostrPublicKey() {
|
|
629
|
-
if (!isNip07Available()) {
|
|
630
|
-
log2.warn("NIP-07 extension not available");
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
try {
|
|
634
|
-
const nostr = window.nostr;
|
|
635
|
-
const pubkey = await nostr.getPublicKey();
|
|
636
|
-
log2.info({ pubkey }, "Got Nostr public key from NIP-07");
|
|
637
|
-
return pubkey;
|
|
638
|
-
} catch (err) {
|
|
639
|
-
log2.error({ error: err }, "Failed to get Nostr public key");
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
async function signNostrEvent(event) {
|
|
644
|
-
if (!isNip07Available()) {
|
|
645
|
-
log2.warn("NIP-07 extension not available");
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
try {
|
|
649
|
-
const nostr = window.nostr;
|
|
650
|
-
const signedEvent = await nostr.signEvent(event);
|
|
651
|
-
log2.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
|
|
652
|
-
return signedEvent;
|
|
653
|
-
} catch (err) {
|
|
654
|
-
log2.error({ error: err }, "Failed to sign Nostr event");
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
function getNip07Info() {
|
|
659
|
-
if (!isNip07Available()) {
|
|
660
|
-
return { available: false };
|
|
661
|
-
}
|
|
662
|
-
const nostr = window.nostr;
|
|
663
|
-
return {
|
|
664
|
-
available: true,
|
|
665
|
-
name: nostr._metadata?.name,
|
|
666
|
-
version: nostr._metadata?.version
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/attestation.ts
|
|
671
|
-
var log3 = createLogger("ocp/attestation");
|
|
672
|
-
var AttestationError = class extends Error {
|
|
673
|
-
constructor(message, code) {
|
|
674
|
-
super(message);
|
|
675
|
-
this.code = code;
|
|
676
|
-
this.name = "AttestationError";
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
|
-
async function createAttestation(options) {
|
|
680
|
-
const { message, signature, scheme, address, identities = [], extensions = {} } = options;
|
|
681
|
-
log3.info({ address, identityCount: identities.length }, "Creating attestation");
|
|
682
|
-
const envelope = await createAttestationEnvelope(
|
|
683
|
-
message,
|
|
684
|
-
signature,
|
|
685
|
-
scheme,
|
|
686
|
-
address,
|
|
687
|
-
identities,
|
|
688
|
-
extensions
|
|
689
|
-
);
|
|
690
|
-
const isValid = await verifyAttestationEnvelope(envelope);
|
|
691
|
-
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
|
-
for (const event of events) {
|
|
752
|
-
const envelope = parseAttestationFromEvent(event);
|
|
753
|
-
if (envelope && !eventIndexesEnvelope(event, envelope)) {
|
|
754
|
-
log3.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
|
|
755
|
-
} else if (envelope) {
|
|
756
|
-
const isValid = await verifyAttestationEnvelope(envelope);
|
|
757
|
-
if (isValid) {
|
|
758
|
-
envelopes.push(envelope);
|
|
759
|
-
} else {
|
|
760
|
-
log3.warn(
|
|
761
|
-
{ attestationId: envelope.attestation_id },
|
|
762
|
-
"Skipping invalid attestation envelope"
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
log3.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
|
|
768
|
-
return envelopes;
|
|
769
|
-
}
|
|
770
|
-
async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
|
|
771
|
-
log3.info({ attestationId }, "Verifying attestation by ID");
|
|
772
|
-
const envelopes = await discoverAttestations({ attestationId, relays });
|
|
773
|
-
if (envelopes.length === 0) {
|
|
774
|
-
log3.warn({ attestationId }, "Attestation not found on any relay");
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
if (envelopes.length > 1) {
|
|
778
|
-
log3.warn(
|
|
779
|
-
{ attestationId, count: envelopes.length },
|
|
780
|
-
"Multiple attestations found with same ID - using first"
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
return envelopes[0] || null;
|
|
784
|
-
}
|
|
785
|
-
async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
|
|
786
|
-
log3.info({ address }, "Getting all attestations for address");
|
|
787
|
-
return discoverAttestations({ address, relays });
|
|
788
|
-
}
|
|
789
|
-
async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
790
|
-
log3.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
791
|
-
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
792
|
-
}
|
|
793
|
-
function formatIdentitiesForDisplay(identities) {
|
|
794
|
-
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
1018
|
+
}
|
|
795
1019
|
}
|
|
796
|
-
function
|
|
797
|
-
|
|
1020
|
+
function base64Url(text) {
|
|
1021
|
+
const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
|
|
1022
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
798
1023
|
}
|
|
799
|
-
function
|
|
1024
|
+
function signedEnvelope(envelope) {
|
|
1025
|
+
let parsed;
|
|
800
1026
|
try {
|
|
801
|
-
|
|
802
|
-
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
803
|
-
if (m) return m[1].toLowerCase();
|
|
804
|
-
const q = u.searchParams.get("id");
|
|
805
|
-
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
806
|
-
return null;
|
|
1027
|
+
parsed = parseCanonicalMessage(envelope.message);
|
|
807
1028
|
} catch {
|
|
808
1029
|
return null;
|
|
809
1030
|
}
|
|
1031
|
+
if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
|
|
1032
|
+
const { expires_at: _copy, ...rest } = envelope;
|
|
1033
|
+
return {
|
|
1034
|
+
...rest,
|
|
1035
|
+
address: parsed.core.address,
|
|
1036
|
+
identities: parsed.core.identities,
|
|
1037
|
+
issued_at: parsed.core.issued_at,
|
|
1038
|
+
...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
|
|
1039
|
+
message_b64url: base64Url(envelope.message)
|
|
1040
|
+
};
|
|
810
1041
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1042
|
+
async function collectSignedEnvelopes(events, accept = () => true) {
|
|
1043
|
+
const copies = /* @__PURE__ */ new Map();
|
|
1044
|
+
for (const event of events) {
|
|
1045
|
+
const raw = parseAttestationFromEvent(event);
|
|
1046
|
+
if (!raw || !await verifyAttestationEnvelope(raw)) continue;
|
|
1047
|
+
const envelope = signedEnvelope(raw);
|
|
1048
|
+
if (!envelope || !accept(event, envelope)) continue;
|
|
1049
|
+
const same = copies.get(envelope.attestation_id) ?? [];
|
|
1050
|
+
if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
|
|
1051
|
+
same.push(envelope);
|
|
1052
|
+
}
|
|
1053
|
+
copies.set(envelope.attestation_id, same);
|
|
1054
|
+
}
|
|
1055
|
+
const kept = [];
|
|
1056
|
+
for (const same of copies.values()) {
|
|
1057
|
+
if (same.length === 1) {
|
|
1058
|
+
kept.push(same[0]);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
let valid;
|
|
1062
|
+
for (const e of same) {
|
|
1063
|
+
if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
|
|
1064
|
+
valid = e;
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
kept.push(valid ?? same[0]);
|
|
832
1069
|
}
|
|
1070
|
+
return kept;
|
|
833
1071
|
}
|
|
834
|
-
function
|
|
835
|
-
|
|
1072
|
+
function parseAttestationFromEvent(event) {
|
|
1073
|
+
try {
|
|
1074
|
+
const envelope = JSON.parse(event.content);
|
|
1075
|
+
return envelope;
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
836
1080
|
}
|
|
837
1081
|
|
|
838
|
-
// src/
|
|
839
|
-
var
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
function toBase64Maybe(sig) {
|
|
844
|
-
if (looksLikeHex(sig)) {
|
|
845
|
-
return Buffer.from(sig, "hex").toString("base64");
|
|
1082
|
+
// src/nostr-crypto.ts
|
|
1083
|
+
var log3 = createLogger("ocp/nostr-crypto");
|
|
1084
|
+
function isNip07Available() {
|
|
1085
|
+
if (typeof window === "undefined") {
|
|
1086
|
+
return false;
|
|
846
1087
|
}
|
|
847
|
-
return
|
|
1088
|
+
return !!window.nostr;
|
|
848
1089
|
}
|
|
849
|
-
function
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return
|
|
1090
|
+
async function getNostrPublicKey() {
|
|
1091
|
+
if (!isNip07Available()) {
|
|
1092
|
+
log3.warn("NIP-07 extension not available");
|
|
1093
|
+
return null;
|
|
853
1094
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1095
|
+
try {
|
|
1096
|
+
const nostr = window.nostr;
|
|
1097
|
+
const pubkey = await nostr.getPublicKey();
|
|
1098
|
+
log3.info({ pubkey }, "Got Nostr public key from NIP-07");
|
|
1099
|
+
return pubkey;
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
log3.error({ error: err }, "Failed to get Nostr public key");
|
|
1102
|
+
return null;
|
|
858
1103
|
}
|
|
859
|
-
return "bip322";
|
|
860
1104
|
}
|
|
861
|
-
async function
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
scheme
|
|
866
|
-
}) {
|
|
867
|
-
const detectedScheme = scheme || detectSignatureScheme(addr, sig);
|
|
868
|
-
log4.debug(
|
|
869
|
-
{ addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
|
|
870
|
-
"Verifying signature"
|
|
871
|
-
);
|
|
872
|
-
if (detectedScheme === "bip322") {
|
|
873
|
-
const sigBase64 = toBase64Maybe(sig);
|
|
874
|
-
log4.debug(
|
|
875
|
-
{ sigBase64: sigBase64.substring(0, 50) + "..." },
|
|
876
|
-
"Signature converted to base64"
|
|
877
|
-
);
|
|
878
|
-
log4.debug(
|
|
879
|
-
{
|
|
880
|
-
msgFull: msg,
|
|
881
|
-
msgLines: msg.split("\n").length,
|
|
882
|
-
msgFirstLine: msg.split("\n")[0],
|
|
883
|
-
msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
|
|
884
|
-
},
|
|
885
|
-
"Message being verified"
|
|
886
|
-
);
|
|
887
|
-
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
888
|
-
log4.info({ addr, ok }, "BIP-322 verification result");
|
|
889
|
-
if (!ok) {
|
|
890
|
-
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
891
|
-
if (isSegWit && sigBase64.length === 88) {
|
|
892
|
-
log4.warn(
|
|
893
|
-
{ addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
|
|
894
|
-
"BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
|
|
895
|
-
);
|
|
896
|
-
} else {
|
|
897
|
-
log4.error(
|
|
898
|
-
{
|
|
899
|
-
addr,
|
|
900
|
-
sigLength: sigBase64.length,
|
|
901
|
-
msgLength: msg.length,
|
|
902
|
-
msgPreview: msg.substring(0, 200)
|
|
903
|
-
},
|
|
904
|
-
"BIP-322 verification failed - signature does not match message"
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return ok ? "sig_ok_bip322" : "sig_invalid";
|
|
909
|
-
}
|
|
910
|
-
if (detectedScheme === "legacy") {
|
|
911
|
-
const sigBase64 = toBase64Maybe(sig);
|
|
912
|
-
if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
|
|
913
|
-
log4.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
|
|
914
|
-
return "sig_unsupported_script";
|
|
915
|
-
}
|
|
916
|
-
log4.debug(
|
|
917
|
-
{
|
|
918
|
-
addr,
|
|
919
|
-
sigBase64: sigBase64.substring(0, 50) + "...",
|
|
920
|
-
msgPreview: msg.substring(0, 100) + "...",
|
|
921
|
-
msgLength: msg.length
|
|
922
|
-
},
|
|
923
|
-
"Verifying legacy signature"
|
|
924
|
-
);
|
|
925
|
-
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
926
|
-
log4.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
|
|
927
|
-
if (!ok) {
|
|
928
|
-
log4.error(
|
|
929
|
-
{
|
|
930
|
-
addr,
|
|
931
|
-
msgFirstLine: msg.split("\n")[0],
|
|
932
|
-
sigFormat: sigBase64.substring(0, 10)
|
|
933
|
-
},
|
|
934
|
-
"Legacy signature verification failed - check message format and signature"
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
return ok ? "sig_ok_legacy" : "sig_invalid";
|
|
1105
|
+
async function signNostrEvent(event) {
|
|
1106
|
+
if (!isNip07Available()) {
|
|
1107
|
+
log3.warn("NIP-07 extension not available");
|
|
1108
|
+
return null;
|
|
938
1109
|
}
|
|
939
|
-
return "invalid_scheme";
|
|
940
|
-
}
|
|
941
|
-
function parseCanonicalMessage(msg) {
|
|
942
|
-
const lines = msg.split("\n").map((l) => l.trim());
|
|
943
|
-
const getVal = (prefix) => {
|
|
944
|
-
const needle = prefix + ":";
|
|
945
|
-
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
946
|
-
if (!hit) return void 0;
|
|
947
|
-
const idx = hit.indexOf(":");
|
|
948
|
-
return hit.slice(idx + 1).trim();
|
|
949
|
-
};
|
|
950
|
-
const address = getVal("address") || "";
|
|
951
|
-
const identitiesStr = getVal("identities") || "";
|
|
952
|
-
let identities = [];
|
|
953
1110
|
try {
|
|
954
|
-
|
|
1111
|
+
const nostr = window.nostr;
|
|
1112
|
+
const signedEvent = await nostr.signEvent(event);
|
|
1113
|
+
log3.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
|
|
1114
|
+
return signedEvent;
|
|
955
1115
|
} catch (err) {
|
|
956
|
-
|
|
1116
|
+
log3.error({ error: err }, "Failed to sign Nostr event");
|
|
1117
|
+
return null;
|
|
957
1118
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
"purpose",
|
|
963
|
-
"nonce",
|
|
964
|
-
"issued_at",
|
|
965
|
-
"ack"
|
|
966
|
-
]);
|
|
967
|
-
const extensions = {};
|
|
968
|
-
for (const line of lines) {
|
|
969
|
-
const idx = line.indexOf(":");
|
|
970
|
-
if (idx === -1) continue;
|
|
971
|
-
const key = line.slice(0, idx).trim().toLowerCase();
|
|
972
|
-
const val = line.slice(idx + 1).trim();
|
|
973
|
-
if (!coreKeys.has(key) && val) extensions[key] = val;
|
|
1119
|
+
}
|
|
1120
|
+
function getNip07Info() {
|
|
1121
|
+
if (!isNip07Available()) {
|
|
1122
|
+
return { available: false };
|
|
974
1123
|
}
|
|
975
|
-
const
|
|
976
|
-
const bondStr = extensions["bond"];
|
|
977
|
-
const bond = bondStr ? parseInt(bondStr, 10) : void 0;
|
|
1124
|
+
const nostr = window.nostr;
|
|
978
1125
|
return {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
|
|
1126
|
+
available: true,
|
|
1127
|
+
name: nostr._metadata?.name,
|
|
1128
|
+
version: nostr._metadata?.version
|
|
983
1129
|
};
|
|
984
1130
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1013
|
-
for (const utxo of data) {
|
|
1014
|
-
if (typeof utxo.value !== "number" || typeof utxo.status !== "object" || typeof utxo.status.confirmed !== "boolean" || typeof utxo.txid !== "string" || typeof utxo.vout !== "number") {
|
|
1015
|
-
throw new Error("Invalid UTXO structure");
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
log4.info({ base, count: data.length, network }, "UTXOs fetched successfully");
|
|
1019
|
-
return data;
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1022
|
-
log4.warn(
|
|
1023
|
-
{ base, error: lastError.message, network },
|
|
1024
|
-
"Esplora endpoint failed, trying next"
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1131
|
+
|
|
1132
|
+
// src/attestation.ts
|
|
1133
|
+
var log4 = createLogger("ocp/attestation");
|
|
1134
|
+
var AttestationError = class extends Error {
|
|
1135
|
+
constructor(message, code) {
|
|
1136
|
+
super(message);
|
|
1137
|
+
this.code = code;
|
|
1138
|
+
this.name = "AttestationError";
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
async function createAttestation(options) {
|
|
1142
|
+
const { message, signature, scheme, address, identities = [], extensions = {} } = options;
|
|
1143
|
+
log4.info({ address, identityCount: identities.length }, "Creating attestation");
|
|
1144
|
+
const envelope = await createAttestationEnvelope(
|
|
1145
|
+
message,
|
|
1146
|
+
signature,
|
|
1147
|
+
scheme,
|
|
1148
|
+
address,
|
|
1149
|
+
identities,
|
|
1150
|
+
extensions
|
|
1151
|
+
);
|
|
1152
|
+
const isValid = await verifyAttestationEnvelope(envelope);
|
|
1153
|
+
if (!isValid) {
|
|
1154
|
+
throw new AttestationError(
|
|
1155
|
+
"Attestation envelope integrity check failed",
|
|
1156
|
+
"INTEGRITY_CHECK_FAILED"
|
|
1157
|
+
);
|
|
1027
1158
|
}
|
|
1028
|
-
log4.
|
|
1029
|
-
|
|
1159
|
+
log4.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
|
|
1160
|
+
return envelope;
|
|
1030
1161
|
}
|
|
1031
|
-
function
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);
|
|
1043
|
-
return a.vout - b.vout;
|
|
1044
|
-
});
|
|
1045
|
-
let sum = 0;
|
|
1046
|
-
let youngestTime = 0;
|
|
1047
|
-
for (const utxo of sorted) {
|
|
1048
|
-
sum += utxo.value;
|
|
1049
|
-
youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);
|
|
1050
|
-
if (sum >= bondAmount) break;
|
|
1051
|
-
}
|
|
1052
|
-
const days_unspent2 = Math.max(
|
|
1053
|
-
0,
|
|
1054
|
-
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
1162
|
+
async function publishAttestation(options) {
|
|
1163
|
+
const { envelope, npub, relays = DEFAULT_RELAYS } = options;
|
|
1164
|
+
log4.info(
|
|
1165
|
+
{ attestationId: envelope.attestation_id, relayCount: relays.length },
|
|
1166
|
+
"Publishing attestation"
|
|
1167
|
+
);
|
|
1168
|
+
const unsignedEvent = createAttestationEvent(envelope, npub);
|
|
1169
|
+
if (!isNip07Available()) {
|
|
1170
|
+
log4.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
"nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish"
|
|
1055
1173
|
);
|
|
1056
|
-
return {
|
|
1057
|
-
sats_bonded: bondAmount,
|
|
1058
|
-
// Use exactly bond amount (ignore surplus)
|
|
1059
|
-
days_unspent: days_unspent2,
|
|
1060
|
-
confirmed_balance
|
|
1061
|
-
};
|
|
1062
1174
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
);
|
|
1068
|
-
let sum = 0;
|
|
1069
|
-
let youngestTime = 0;
|
|
1070
|
-
for (const utxo of sorted) {
|
|
1071
|
-
sum += utxo.value;
|
|
1072
|
-
youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
|
|
1073
|
-
if (sum >= coverSats) break;
|
|
1074
|
-
}
|
|
1075
|
-
const days_unspent2 = Math.max(
|
|
1076
|
-
0,
|
|
1077
|
-
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
1078
|
-
);
|
|
1079
|
-
return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
|
|
1175
|
+
log4.info("Using NIP-07 to sign event");
|
|
1176
|
+
const signedEvent = await signNostrEvent(unsignedEvent);
|
|
1177
|
+
if (!signedEvent) {
|
|
1178
|
+
throw new Error("nostr_signing_failed: NIP-07 extension rejected or errored");
|
|
1080
1179
|
}
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1180
|
+
const result = await publishToRelays(signedEvent, relays);
|
|
1181
|
+
log4.info(
|
|
1182
|
+
{
|
|
1183
|
+
attestationId: envelope.attestation_id,
|
|
1184
|
+
successCount: result.success.length,
|
|
1185
|
+
failedCount: result.failed.length
|
|
1186
|
+
},
|
|
1187
|
+
"Publishing complete"
|
|
1083
1188
|
);
|
|
1084
|
-
|
|
1085
|
-
return { sats_bonded, days_unspent, confirmed_balance };
|
|
1189
|
+
return result;
|
|
1086
1190
|
}
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1191
|
+
function eventIndexesEnvelope(event, envelope) {
|
|
1192
|
+
const d = event.tags.find((t) => t[0] === "d")?.[1];
|
|
1193
|
+
const id = envelope.attestation_id;
|
|
1194
|
+
if (d !== id && d !== `orangecheck:${id}`) return false;
|
|
1195
|
+
return event.tags.some((t) => t[0] === "t" && t[1] === envelope.address);
|
|
1196
|
+
}
|
|
1197
|
+
async function discoverAttestations(options) {
|
|
1198
|
+
const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
|
|
1199
|
+
let events = [];
|
|
1200
|
+
if (attestationId) {
|
|
1201
|
+
log4.info({ attestationId }, "Discovering by attestation ID");
|
|
1202
|
+
events = await queryByAttestationId(attestationId, relays);
|
|
1203
|
+
} else if (address) {
|
|
1204
|
+
log4.info({ address }, "Discovering by Bitcoin address");
|
|
1205
|
+
events = await queryByAddress(address, relays);
|
|
1206
|
+
} else if (identity) {
|
|
1207
|
+
log4.info({ identity }, "Discovering by identity");
|
|
1208
|
+
events = await queryByIdentity(identity.protocol, identity.identifier, relays);
|
|
1209
|
+
} else {
|
|
1210
|
+
throw new Error("Must provide attestationId, address, or identity");
|
|
1211
|
+
}
|
|
1212
|
+
const envelopes = await collectSignedEnvelopes(events, (event, envelope) => {
|
|
1213
|
+
if (eventIndexesEnvelope(event, envelope)) return true;
|
|
1214
|
+
log4.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
|
|
1215
|
+
return false;
|
|
1216
|
+
});
|
|
1217
|
+
log4.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
|
|
1218
|
+
return envelopes;
|
|
1219
|
+
}
|
|
1220
|
+
async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
|
|
1221
|
+
log4.info({ attestationId }, "Verifying attestation by ID");
|
|
1222
|
+
const envelopes = await discoverAttestations({ attestationId, relays });
|
|
1223
|
+
if (envelopes.length === 0) {
|
|
1224
|
+
log4.warn({ attestationId }, "Attestation not found on any relay");
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
if (envelopes.length > 1) {
|
|
1228
|
+
log4.warn(
|
|
1229
|
+
{ attestationId, count: envelopes.length },
|
|
1230
|
+
"Multiple attestations found with same ID - using first"
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
return envelopes[0] || null;
|
|
1234
|
+
}
|
|
1235
|
+
async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
|
|
1236
|
+
log4.info({ address }, "Getting all attestations for address");
|
|
1237
|
+
return discoverAttestations({ address, relays });
|
|
1238
|
+
}
|
|
1239
|
+
async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
1240
|
+
log4.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
1241
|
+
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
1242
|
+
}
|
|
1243
|
+
function formatIdentitiesForDisplay(identities) {
|
|
1244
|
+
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
1245
|
+
}
|
|
1246
|
+
function getVerificationUrl(attestationId, baseUrl = "https://attest.ochk.io") {
|
|
1247
|
+
return `${baseUrl}/attest/${attestationId}`;
|
|
1248
|
+
}
|
|
1249
|
+
function extractAttestationIdFromUrl(url) {
|
|
1090
1250
|
try {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
codes.push("bad_request");
|
|
1100
|
-
return { ok: false, codes, network, attestation_id };
|
|
1101
|
-
}
|
|
1102
|
-
if ((network === "testnet" || network === "signet") && !opts.testMode) {
|
|
1103
|
-
codes.push("network_testmode");
|
|
1104
|
-
return { ok: false, codes, network, attestation_id };
|
|
1105
|
-
}
|
|
1106
|
-
if (opts.expectedAud) {
|
|
1107
|
-
const aud = parsed.extensions["aud"];
|
|
1108
|
-
if (!aud || aud !== opts.expectedAud) {
|
|
1109
|
-
codes.push("aud_mismatch");
|
|
1110
|
-
return { ok: false, codes, network, attestation_id };
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
if (parsed.extensions["expires"]) {
|
|
1114
|
-
const exp = Date.parse(parsed.extensions["expires"]);
|
|
1115
|
-
if (!Number.isNaN(exp) && exp < Date.now()) {
|
|
1116
|
-
codes.push("expired");
|
|
1117
|
-
return { ok: false, codes, network, attestation_id };
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
const sigCode = await verifySignature({
|
|
1121
|
-
addr: input.addr,
|
|
1122
|
-
msg: input.msg,
|
|
1123
|
-
sig: input.sig,
|
|
1124
|
-
...input.scheme && { scheme: input.scheme }
|
|
1125
|
-
});
|
|
1126
|
-
codes.push(sigCode);
|
|
1127
|
-
const sigOk = sigCode === "sig_ok_bip322" || sigCode === "sig_ok_legacy";
|
|
1128
|
-
if (!sigOk) {
|
|
1129
|
-
return {
|
|
1130
|
-
ok: false,
|
|
1131
|
-
codes,
|
|
1132
|
-
network,
|
|
1133
|
-
attestation_id,
|
|
1134
|
-
identities: parsed.core.identities
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
try {
|
|
1138
|
-
const utxos = await getAddressUtxos(input.addr, network, {
|
|
1139
|
-
esploraMainnetBase: opts.esploraMainnetBase,
|
|
1140
|
-
esploraSignetBase: opts.esploraSignetBase
|
|
1141
|
-
});
|
|
1142
|
-
const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
|
|
1143
|
-
utxos,
|
|
1144
|
-
parsed.bond,
|
|
1145
|
-
opts.coverSats
|
|
1146
|
-
);
|
|
1147
|
-
if (parsed.bond !== void 0 && parsed.bond > 0) {
|
|
1148
|
-
if (confirmed_balance < parsed.bond) {
|
|
1149
|
-
codes.push("bond_insufficient");
|
|
1150
|
-
log4.warn(
|
|
1151
|
-
{
|
|
1152
|
-
bond: parsed.bond,
|
|
1153
|
-
confirmed_balance,
|
|
1154
|
-
address: input.addr
|
|
1155
|
-
},
|
|
1156
|
-
"Bond extension: insufficient balance"
|
|
1157
|
-
);
|
|
1158
|
-
return {
|
|
1159
|
-
ok: false,
|
|
1160
|
-
codes,
|
|
1161
|
-
network,
|
|
1162
|
-
attestation_id,
|
|
1163
|
-
identities: parsed.core.identities
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
if (sats_bonded <= 0) {
|
|
1168
|
-
codes.push("bond_zero");
|
|
1169
|
-
if (utxos.some((u) => !u.status.confirmed)) {
|
|
1170
|
-
codes.push("bond_pending");
|
|
1171
|
-
}
|
|
1172
|
-
return {
|
|
1173
|
-
ok: false,
|
|
1174
|
-
codes,
|
|
1175
|
-
network,
|
|
1176
|
-
attestation_id,
|
|
1177
|
-
identities: parsed.core.identities
|
|
1178
|
-
};
|
|
1179
|
-
} else {
|
|
1180
|
-
codes.push("bond_confirmed");
|
|
1181
|
-
}
|
|
1182
|
-
const metrics = {
|
|
1183
|
-
sats_bonded,
|
|
1184
|
-
days_unspent,
|
|
1185
|
-
score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
|
|
1186
|
-
};
|
|
1187
|
-
return {
|
|
1188
|
-
ok: true,
|
|
1189
|
-
codes,
|
|
1190
|
-
network,
|
|
1191
|
-
attestation_id,
|
|
1192
|
-
identities: parsed.core.identities,
|
|
1193
|
-
metrics
|
|
1194
|
-
};
|
|
1195
|
-
} catch (utxoError) {
|
|
1196
|
-
log4.error({ err: utxoError }, "UTXO fetch failed");
|
|
1197
|
-
codes.push("bad_request");
|
|
1198
|
-
return { ok: false, codes, network, attestation_id };
|
|
1199
|
-
}
|
|
1200
|
-
} catch (e) {
|
|
1201
|
-
const msg = e instanceof Error ? e.message : String(e ?? "decode_error");
|
|
1202
|
-
if (msg === "decode_error") {
|
|
1203
|
-
codes.push("decode_error");
|
|
1204
|
-
} else {
|
|
1205
|
-
codes.push("bad_request");
|
|
1206
|
-
}
|
|
1207
|
-
return { ok: false, codes, network };
|
|
1251
|
+
const u = new URL(url);
|
|
1252
|
+
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
1253
|
+
if (m) return m[1].toLowerCase();
|
|
1254
|
+
const q = u.searchParams.get("id");
|
|
1255
|
+
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
1256
|
+
return null;
|
|
1257
|
+
} catch {
|
|
1258
|
+
return null;
|
|
1208
1259
|
}
|
|
1209
1260
|
}
|
|
1210
1261
|
|
|
1211
1262
|
// src/check.ts
|
|
1263
|
+
var ISSUED_AHEAD_MS = 12e4;
|
|
1264
|
+
var MAX_CANDIDATES = 20;
|
|
1212
1265
|
async function check(params) {
|
|
1213
1266
|
const {
|
|
1214
1267
|
addr,
|
|
@@ -1230,14 +1283,17 @@ async function check(params) {
|
|
|
1230
1283
|
} else if (identity) {
|
|
1231
1284
|
found = await getAttestationsForIdentity(identity.protocol, identity.identifier, relays);
|
|
1232
1285
|
}
|
|
1233
|
-
const
|
|
1286
|
+
const latest = Date.now() + ISSUED_AHEAD_MS;
|
|
1287
|
+
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);
|
|
1234
1288
|
if (envelopes.length === 0) {
|
|
1235
1289
|
return { ok: false, sats: 0, days: 0, score: 0, reasons: ["not_found"] };
|
|
1236
1290
|
}
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1291
|
+
let env = envelopes[0];
|
|
1292
|
+
for (const candidate of envelopes.slice(0, MAX_CANDIDATES)) {
|
|
1293
|
+
if (await signedBy(candidate)) {
|
|
1294
|
+
env = candidate;
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1241
1297
|
}
|
|
1242
1298
|
const shared = identity ? await backsOtherIdentity(env, identity, relays) : false;
|
|
1243
1299
|
const outcome = await verify(
|
|
@@ -1265,7 +1321,7 @@ async function check(params) {
|
|
|
1265
1321
|
score,
|
|
1266
1322
|
attestation_id: env.attestation_id,
|
|
1267
1323
|
address: env.address,
|
|
1268
|
-
identities: env
|
|
1324
|
+
identities: signedIdentities(env),
|
|
1269
1325
|
network: outcome.network,
|
|
1270
1326
|
...reasons.length ? { reasons } : {}
|
|
1271
1327
|
};
|
|
@@ -1301,16 +1357,19 @@ function signedMatch(env, q) {
|
|
|
1301
1357
|
}
|
|
1302
1358
|
return false;
|
|
1303
1359
|
}
|
|
1360
|
+
function signedBy(e) {
|
|
1361
|
+
return signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme });
|
|
1362
|
+
}
|
|
1304
1363
|
async function backsOtherIdentity(env, identity, relays) {
|
|
1305
1364
|
const want = identityKey(identity);
|
|
1306
1365
|
const protocol = identity.protocol.trim().toLowerCase();
|
|
1307
1366
|
const siblings = await getAttestationsForAddress(env.address, relays);
|
|
1308
1367
|
for (const e of [env, ...siblings]) {
|
|
1309
1368
|
if (!signedMatch(e, { addr: env.address })) continue;
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1369
|
+
const other = signedIdentities(e).some(
|
|
1370
|
+
(b) => b.protocol.trim().toLowerCase() === protocol && identityKey(b) !== want
|
|
1371
|
+
);
|
|
1372
|
+
if (other && (e === env || await signedBy(e))) return true;
|
|
1314
1373
|
}
|
|
1315
1374
|
return false;
|
|
1316
1375
|
}
|
|
@@ -2415,6 +2474,6 @@ var STATUS_META = {
|
|
|
2415
2474
|
}
|
|
2416
2475
|
};
|
|
2417
2476
|
|
|
2418
|
-
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 };
|
|
2477
|
+
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 };
|
|
2419
2478
|
//# sourceMappingURL=index.mjs.map
|
|
2420
2479
|
//# sourceMappingURL=index.mjs.map
|