@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/attestation.js
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var base = require('@scure/base');
|
|
4
|
+
var buffer = require('buffer');
|
|
5
|
+
var bip322 = require('@orangecheck/bip322');
|
|
4
6
|
|
|
5
7
|
// src/canonical.ts
|
|
6
8
|
function toLowerHex(bytes) {
|
|
7
9
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
8
10
|
}
|
|
11
|
+
var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
12
|
+
var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
|
|
13
|
+
function assertSafeIdentity({ protocol, identifier }) {
|
|
14
|
+
if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function parseIdentities(identitiesStr) {
|
|
26
|
+
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
|
|
30
|
+
throw new Error("identities field contains forbidden characters (newline)");
|
|
31
|
+
}
|
|
32
|
+
const bindings = identitiesStr.split(",").map((binding) => {
|
|
33
|
+
const colonIndex = binding.indexOf(":");
|
|
34
|
+
if (colonIndex === -1) {
|
|
35
|
+
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
protocol: binding.substring(0, colonIndex).trim(),
|
|
39
|
+
identifier: binding.substring(colonIndex + 1).trim()
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
for (const b of bindings) {
|
|
43
|
+
if (!b.protocol || !b.identifier) {
|
|
44
|
+
throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
|
|
45
|
+
}
|
|
46
|
+
assertSafeIdentity(b);
|
|
47
|
+
}
|
|
48
|
+
return bindings;
|
|
49
|
+
}
|
|
9
50
|
async function generateAttestationId(message) {
|
|
10
51
|
const encoder = new TextEncoder();
|
|
11
52
|
const data = encoder.encode(message);
|
|
@@ -109,8 +150,173 @@ function createLogger(namespace) {
|
|
|
109
150
|
};
|
|
110
151
|
}
|
|
111
152
|
|
|
153
|
+
// src/utils/time.ts
|
|
154
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
155
|
+
function rfc3339Ms(value) {
|
|
156
|
+
if (!value || !RFC3339.test(value)) return null;
|
|
157
|
+
const ms = Date.parse(value);
|
|
158
|
+
return Number.isNaN(ms) ? null : ms;
|
|
159
|
+
}
|
|
160
|
+
var log = createLogger("ocp/verify");
|
|
161
|
+
function looksLikeHex(s) {
|
|
162
|
+
return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
|
|
163
|
+
}
|
|
164
|
+
function toBase64Maybe(sig) {
|
|
165
|
+
if (looksLikeHex(sig)) {
|
|
166
|
+
return buffer.Buffer.from(sig, "hex").toString("base64");
|
|
167
|
+
}
|
|
168
|
+
return sig;
|
|
169
|
+
}
|
|
170
|
+
function detectSignatureScheme(addr, sig) {
|
|
171
|
+
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
172
|
+
if (isSegWit) {
|
|
173
|
+
return "bip322";
|
|
174
|
+
}
|
|
175
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
176
|
+
const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
|
|
177
|
+
if (looksLikeLegacy) {
|
|
178
|
+
return "legacy";
|
|
179
|
+
}
|
|
180
|
+
return "bip322";
|
|
181
|
+
}
|
|
182
|
+
async function verifySignature({
|
|
183
|
+
addr,
|
|
184
|
+
msg,
|
|
185
|
+
sig,
|
|
186
|
+
scheme
|
|
187
|
+
}) {
|
|
188
|
+
const detectedScheme = scheme || detectSignatureScheme(addr, sig);
|
|
189
|
+
log.debug(
|
|
190
|
+
{ addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
|
|
191
|
+
"Verifying signature"
|
|
192
|
+
);
|
|
193
|
+
if (detectedScheme === "bip322") {
|
|
194
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
195
|
+
log.debug(
|
|
196
|
+
{ sigBase64: sigBase64.substring(0, 50) + "..." },
|
|
197
|
+
"Signature converted to base64"
|
|
198
|
+
);
|
|
199
|
+
log.debug(
|
|
200
|
+
{
|
|
201
|
+
msgFull: msg,
|
|
202
|
+
msgLines: msg.split("\n").length,
|
|
203
|
+
msgFirstLine: msg.split("\n")[0],
|
|
204
|
+
msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
|
|
205
|
+
},
|
|
206
|
+
"Message being verified"
|
|
207
|
+
);
|
|
208
|
+
const ok = bip322.verifySignature(addr, msg, sigBase64);
|
|
209
|
+
log.info({ addr, ok }, "BIP-322 verification result");
|
|
210
|
+
if (!ok) {
|
|
211
|
+
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
212
|
+
if (isSegWit && sigBase64.length === 88) {
|
|
213
|
+
log.warn(
|
|
214
|
+
{ addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
|
|
215
|
+
"BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
log.error(
|
|
219
|
+
{
|
|
220
|
+
addr,
|
|
221
|
+
sigLength: sigBase64.length,
|
|
222
|
+
msgLength: msg.length,
|
|
223
|
+
msgPreview: msg.substring(0, 200)
|
|
224
|
+
},
|
|
225
|
+
"BIP-322 verification failed - signature does not match message"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return ok ? "sig_ok_bip322" : "sig_invalid";
|
|
230
|
+
}
|
|
231
|
+
if (detectedScheme === "legacy") {
|
|
232
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
233
|
+
if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
|
|
234
|
+
log.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
|
|
235
|
+
return "sig_unsupported_script";
|
|
236
|
+
}
|
|
237
|
+
log.debug(
|
|
238
|
+
{
|
|
239
|
+
addr,
|
|
240
|
+
sigBase64: sigBase64.substring(0, 50) + "...",
|
|
241
|
+
msgPreview: msg.substring(0, 100) + "...",
|
|
242
|
+
msgLength: msg.length
|
|
243
|
+
},
|
|
244
|
+
"Verifying legacy signature"
|
|
245
|
+
);
|
|
246
|
+
const ok = bip322.verifySignature(addr, msg, sigBase64);
|
|
247
|
+
log.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
|
|
248
|
+
if (!ok) {
|
|
249
|
+
log.error(
|
|
250
|
+
{
|
|
251
|
+
addr,
|
|
252
|
+
msgFirstLine: msg.split("\n")[0],
|
|
253
|
+
sigFormat: sigBase64.substring(0, 10)
|
|
254
|
+
},
|
|
255
|
+
"Legacy signature verification failed - check message format and signature"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return ok ? "sig_ok_legacy" : "sig_invalid";
|
|
259
|
+
}
|
|
260
|
+
return "invalid_scheme";
|
|
261
|
+
}
|
|
262
|
+
async function signatureIsValid(input) {
|
|
263
|
+
try {
|
|
264
|
+
const code = await verifySignature(input);
|
|
265
|
+
return code === "sig_ok_bip322" || code === "sig_ok_legacy";
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function parseCanonicalMessage(msg) {
|
|
271
|
+
const lines = msg.split("\n").map((l) => l.trim());
|
|
272
|
+
const getVal = (prefix) => {
|
|
273
|
+
const needle = prefix + ":";
|
|
274
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
275
|
+
if (!hit) return void 0;
|
|
276
|
+
const idx = hit.indexOf(":");
|
|
277
|
+
return hit.slice(idx + 1).trim();
|
|
278
|
+
};
|
|
279
|
+
const address = getVal("address") || "";
|
|
280
|
+
const identitiesStr = getVal("identities") || "";
|
|
281
|
+
const issued_at = getVal("issued_at") || "";
|
|
282
|
+
const expires = getVal("expires");
|
|
283
|
+
let identities = [];
|
|
284
|
+
try {
|
|
285
|
+
identities = parseIdentities(identitiesStr);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
|
|
288
|
+
}
|
|
289
|
+
const coreKeys = /* @__PURE__ */ new Set([
|
|
290
|
+
"orangecheck",
|
|
291
|
+
"identities",
|
|
292
|
+
"address",
|
|
293
|
+
"purpose",
|
|
294
|
+
"nonce",
|
|
295
|
+
"issued_at",
|
|
296
|
+
"ack"
|
|
297
|
+
]);
|
|
298
|
+
const extensions = {};
|
|
299
|
+
for (const line of lines) {
|
|
300
|
+
const idx = line.indexOf(":");
|
|
301
|
+
if (idx === -1) continue;
|
|
302
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
303
|
+
const val = line.slice(idx + 1).trim();
|
|
304
|
+
if (!coreKeys.has(key) && val) extensions[key] = val;
|
|
305
|
+
}
|
|
306
|
+
const network = extensions["network"] || "mainnet";
|
|
307
|
+
const bondStr = extensions["bond"];
|
|
308
|
+
const bond = bondStr ? parseInt(bondStr, 10) : void 0;
|
|
309
|
+
return {
|
|
310
|
+
core: { address, identities, issued_at },
|
|
311
|
+
expires,
|
|
312
|
+
network,
|
|
313
|
+
extensions,
|
|
314
|
+
bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
112
318
|
// src/nostr.ts
|
|
113
|
-
var
|
|
319
|
+
var log2 = createLogger("ocp/nostr");
|
|
114
320
|
var ATTESTATION_EVENT_KIND = 30078;
|
|
115
321
|
var DEFAULT_RELAYS = [
|
|
116
322
|
"wss://relay.damus.io",
|
|
@@ -163,7 +369,7 @@ function createAttestationEvent(envelope, pubkey) {
|
|
|
163
369
|
async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
164
370
|
const success = [];
|
|
165
371
|
const failed = [];
|
|
166
|
-
|
|
372
|
+
log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
|
|
167
373
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
168
374
|
const publishPromises = relays.map(async (relayUrl) => {
|
|
169
375
|
let ws = null;
|
|
@@ -202,7 +408,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
202
408
|
cleanup();
|
|
203
409
|
resolve();
|
|
204
410
|
} else {
|
|
205
|
-
|
|
411
|
+
log2.warn(
|
|
206
412
|
{ relay: relayUrl, reason: data[3] },
|
|
207
413
|
"Relay rejected event"
|
|
208
414
|
);
|
|
@@ -212,7 +418,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
212
418
|
}
|
|
213
419
|
}
|
|
214
420
|
} catch (err) {
|
|
215
|
-
|
|
421
|
+
log2.error(
|
|
216
422
|
{ relay: relayUrl, error: err },
|
|
217
423
|
"Failed to parse relay response"
|
|
218
424
|
);
|
|
@@ -222,7 +428,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
222
428
|
}
|
|
223
429
|
};
|
|
224
430
|
wsRef.onerror = (err) => {
|
|
225
|
-
|
|
431
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error");
|
|
226
432
|
recordFailure();
|
|
227
433
|
cleanup();
|
|
228
434
|
reject(err);
|
|
@@ -232,7 +438,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
232
438
|
};
|
|
233
439
|
});
|
|
234
440
|
} catch (err) {
|
|
235
|
-
|
|
441
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
|
|
236
442
|
recordFailure();
|
|
237
443
|
} finally {
|
|
238
444
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
@@ -241,12 +447,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
241
447
|
}
|
|
242
448
|
});
|
|
243
449
|
await Promise.allSettled(publishPromises);
|
|
244
|
-
|
|
450
|
+
log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
|
|
245
451
|
return { success, failed };
|
|
246
452
|
}
|
|
247
453
|
async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
248
454
|
const events = [];
|
|
249
|
-
|
|
455
|
+
log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
|
|
250
456
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
251
457
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
252
458
|
let ws = null;
|
|
@@ -286,11 +492,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
286
492
|
resolve();
|
|
287
493
|
}
|
|
288
494
|
} catch (err) {
|
|
289
|
-
|
|
495
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
290
496
|
}
|
|
291
497
|
};
|
|
292
498
|
wsRef.onerror = (err) => {
|
|
293
|
-
|
|
499
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
294
500
|
cleanup();
|
|
295
501
|
reject(err);
|
|
296
502
|
};
|
|
@@ -299,7 +505,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
299
505
|
};
|
|
300
506
|
});
|
|
301
507
|
} catch (err) {
|
|
302
|
-
|
|
508
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
303
509
|
} finally {
|
|
304
510
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
305
511
|
ws.close();
|
|
@@ -307,12 +513,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
307
513
|
}
|
|
308
514
|
});
|
|
309
515
|
await Promise.allSettled(queryPromises);
|
|
310
|
-
|
|
516
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
311
517
|
return events;
|
|
312
518
|
}
|
|
313
519
|
async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
314
520
|
const events = [];
|
|
315
|
-
|
|
521
|
+
log2.info({ address, relayCount: relays.length }, "Querying relays for address");
|
|
316
522
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
317
523
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
318
524
|
try {
|
|
@@ -354,28 +560,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
|
354
560
|
resolve();
|
|
355
561
|
}
|
|
356
562
|
} catch (err) {
|
|
357
|
-
|
|
563
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
358
564
|
}
|
|
359
565
|
};
|
|
360
566
|
ws.onerror = (err) => {
|
|
361
567
|
clearTimeout(timeout);
|
|
362
|
-
|
|
568
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
363
569
|
reject(err);
|
|
364
570
|
};
|
|
365
571
|
});
|
|
366
572
|
} catch (err) {
|
|
367
|
-
|
|
573
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
368
574
|
}
|
|
369
575
|
});
|
|
370
576
|
await Promise.allSettled(queryPromises);
|
|
371
|
-
|
|
577
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
372
578
|
return events;
|
|
373
579
|
}
|
|
374
580
|
async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
375
581
|
const events = [];
|
|
376
582
|
const identityTag = `${protocol}:${identifier}`;
|
|
377
583
|
const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
|
|
378
|
-
|
|
584
|
+
log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
|
|
379
585
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
380
586
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
381
587
|
try {
|
|
@@ -410,21 +616,21 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
|
410
616
|
resolve();
|
|
411
617
|
}
|
|
412
618
|
} catch (err) {
|
|
413
|
-
|
|
619
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
414
620
|
}
|
|
415
621
|
};
|
|
416
622
|
ws.onerror = (err) => {
|
|
417
623
|
clearTimeout(timeout);
|
|
418
|
-
|
|
624
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
419
625
|
reject(err);
|
|
420
626
|
};
|
|
421
627
|
});
|
|
422
628
|
} catch (err) {
|
|
423
|
-
|
|
629
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
424
630
|
}
|
|
425
631
|
});
|
|
426
632
|
await Promise.allSettled(queryPromises);
|
|
427
|
-
|
|
633
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
428
634
|
return events;
|
|
429
635
|
}
|
|
430
636
|
async function verifyAttestationEnvelope(envelope) {
|
|
@@ -436,22 +642,74 @@ async function verifyAttestationEnvelope(envelope) {
|
|
|
436
642
|
const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
437
643
|
return computedId === envelope.attestation_id;
|
|
438
644
|
} catch (err) {
|
|
439
|
-
|
|
645
|
+
log2.error({ error: err }, "Failed to verify attestation envelope");
|
|
440
646
|
return false;
|
|
441
647
|
}
|
|
442
648
|
}
|
|
649
|
+
function base64Url(text) {
|
|
650
|
+
const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
|
|
651
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
652
|
+
}
|
|
653
|
+
function signedEnvelope(envelope) {
|
|
654
|
+
let parsed;
|
|
655
|
+
try {
|
|
656
|
+
parsed = parseCanonicalMessage(envelope.message);
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
|
|
661
|
+
const { expires_at: _copy, ...rest } = envelope;
|
|
662
|
+
return {
|
|
663
|
+
...rest,
|
|
664
|
+
address: parsed.core.address,
|
|
665
|
+
identities: parsed.core.identities,
|
|
666
|
+
issued_at: parsed.core.issued_at,
|
|
667
|
+
...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
|
|
668
|
+
message_b64url: base64Url(envelope.message)
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async function collectSignedEnvelopes(events, accept = () => true) {
|
|
672
|
+
const copies = /* @__PURE__ */ new Map();
|
|
673
|
+
for (const event of events) {
|
|
674
|
+
const raw = parseAttestationFromEvent(event);
|
|
675
|
+
if (!raw || !await verifyAttestationEnvelope(raw)) continue;
|
|
676
|
+
const envelope = signedEnvelope(raw);
|
|
677
|
+
if (!envelope || !accept(event, envelope)) continue;
|
|
678
|
+
const same = copies.get(envelope.attestation_id) ?? [];
|
|
679
|
+
if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
|
|
680
|
+
same.push(envelope);
|
|
681
|
+
}
|
|
682
|
+
copies.set(envelope.attestation_id, same);
|
|
683
|
+
}
|
|
684
|
+
const kept = [];
|
|
685
|
+
for (const same of copies.values()) {
|
|
686
|
+
if (same.length === 1) {
|
|
687
|
+
kept.push(same[0]);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
let valid;
|
|
691
|
+
for (const e of same) {
|
|
692
|
+
if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
|
|
693
|
+
valid = e;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
kept.push(valid ?? same[0]);
|
|
698
|
+
}
|
|
699
|
+
return kept;
|
|
700
|
+
}
|
|
443
701
|
function parseAttestationFromEvent(event) {
|
|
444
702
|
try {
|
|
445
703
|
const envelope = JSON.parse(event.content);
|
|
446
704
|
return envelope;
|
|
447
705
|
} catch (err) {
|
|
448
|
-
|
|
706
|
+
log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
|
|
449
707
|
return null;
|
|
450
708
|
}
|
|
451
709
|
}
|
|
452
710
|
|
|
453
711
|
// src/nostr-crypto.ts
|
|
454
|
-
var
|
|
712
|
+
var log3 = createLogger("ocp/nostr-crypto");
|
|
455
713
|
function isNip07Available() {
|
|
456
714
|
if (typeof window === "undefined") {
|
|
457
715
|
return false;
|
|
@@ -460,22 +718,22 @@ function isNip07Available() {
|
|
|
460
718
|
}
|
|
461
719
|
async function signNostrEvent(event) {
|
|
462
720
|
if (!isNip07Available()) {
|
|
463
|
-
|
|
721
|
+
log3.warn("NIP-07 extension not available");
|
|
464
722
|
return null;
|
|
465
723
|
}
|
|
466
724
|
try {
|
|
467
725
|
const nostr = window.nostr;
|
|
468
726
|
const signedEvent = await nostr.signEvent(event);
|
|
469
|
-
|
|
727
|
+
log3.info({ eventId: signedEvent.id }, "Signed Nostr event with NIP-07");
|
|
470
728
|
return signedEvent;
|
|
471
729
|
} catch (err) {
|
|
472
|
-
|
|
730
|
+
log3.error({ error: err }, "Failed to sign Nostr event");
|
|
473
731
|
return null;
|
|
474
732
|
}
|
|
475
733
|
}
|
|
476
734
|
|
|
477
735
|
// src/attestation.ts
|
|
478
|
-
var
|
|
736
|
+
var log4 = createLogger("ocp/attestation");
|
|
479
737
|
var AttestationError = class extends Error {
|
|
480
738
|
constructor(message, code) {
|
|
481
739
|
super(message);
|
|
@@ -485,7 +743,7 @@ var AttestationError = class extends Error {
|
|
|
485
743
|
};
|
|
486
744
|
async function createAttestation(options) {
|
|
487
745
|
const { message, signature, scheme, address, identities = [], extensions = {} } = options;
|
|
488
|
-
|
|
746
|
+
log4.info({ address, identityCount: identities.length }, "Creating attestation");
|
|
489
747
|
const envelope = await createAttestationEnvelope(
|
|
490
748
|
message,
|
|
491
749
|
signature,
|
|
@@ -501,29 +759,29 @@ async function createAttestation(options) {
|
|
|
501
759
|
"INTEGRITY_CHECK_FAILED"
|
|
502
760
|
);
|
|
503
761
|
}
|
|
504
|
-
|
|
762
|
+
log4.info({ attestationId: envelope.attestation_id }, "Attestation created successfully");
|
|
505
763
|
return envelope;
|
|
506
764
|
}
|
|
507
765
|
async function publishAttestation(options) {
|
|
508
766
|
const { envelope, npub, relays = DEFAULT_RELAYS } = options;
|
|
509
|
-
|
|
767
|
+
log4.info(
|
|
510
768
|
{ attestationId: envelope.attestation_id, relayCount: relays.length },
|
|
511
769
|
"Publishing attestation"
|
|
512
770
|
);
|
|
513
771
|
const unsignedEvent = createAttestationEvent(envelope, npub);
|
|
514
772
|
if (!isNip07Available()) {
|
|
515
|
-
|
|
773
|
+
log4.warn("NIP-07 unavailable \u2014 skipping Nostr publish");
|
|
516
774
|
throw new Error(
|
|
517
775
|
"nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish"
|
|
518
776
|
);
|
|
519
777
|
}
|
|
520
|
-
|
|
778
|
+
log4.info("Using NIP-07 to sign event");
|
|
521
779
|
const signedEvent = await signNostrEvent(unsignedEvent);
|
|
522
780
|
if (!signedEvent) {
|
|
523
781
|
throw new Error("nostr_signing_failed: NIP-07 extension rejected or errored");
|
|
524
782
|
}
|
|
525
783
|
const result = await publishToRelays(signedEvent, relays);
|
|
526
|
-
|
|
784
|
+
log4.info(
|
|
527
785
|
{
|
|
528
786
|
attestationId: envelope.attestation_id,
|
|
529
787
|
successCount: result.success.length,
|
|
@@ -543,46 +801,34 @@ async function discoverAttestations(options) {
|
|
|
543
801
|
const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;
|
|
544
802
|
let events = [];
|
|
545
803
|
if (attestationId) {
|
|
546
|
-
|
|
804
|
+
log4.info({ attestationId }, "Discovering by attestation ID");
|
|
547
805
|
events = await queryByAttestationId(attestationId, relays);
|
|
548
806
|
} else if (address) {
|
|
549
|
-
|
|
807
|
+
log4.info({ address }, "Discovering by Bitcoin address");
|
|
550
808
|
events = await queryByAddress(address, relays);
|
|
551
809
|
} else if (identity) {
|
|
552
|
-
|
|
810
|
+
log4.info({ identity }, "Discovering by identity");
|
|
553
811
|
events = await queryByIdentity(identity.protocol, identity.identifier, relays);
|
|
554
812
|
} else {
|
|
555
813
|
throw new Error("Must provide attestationId, address, or identity");
|
|
556
814
|
}
|
|
557
|
-
const envelopes =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const isValid = await verifyAttestationEnvelope(envelope);
|
|
564
|
-
if (isValid) {
|
|
565
|
-
envelopes.push(envelope);
|
|
566
|
-
} else {
|
|
567
|
-
log3.warn(
|
|
568
|
-
{ attestationId: envelope.attestation_id },
|
|
569
|
-
"Skipping invalid attestation envelope"
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
log3.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
|
|
815
|
+
const envelopes = await collectSignedEnvelopes(events, (event, envelope) => {
|
|
816
|
+
if (eventIndexesEnvelope(event, envelope)) return true;
|
|
817
|
+
log4.warn({ eventId: event.id }, "Skipping event whose index tags disagree with its envelope");
|
|
818
|
+
return false;
|
|
819
|
+
});
|
|
820
|
+
log4.info({ eventCount: events.length, validCount: envelopes.length }, "Discovery complete");
|
|
575
821
|
return envelopes;
|
|
576
822
|
}
|
|
577
823
|
async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
|
|
578
|
-
|
|
824
|
+
log4.info({ attestationId }, "Verifying attestation by ID");
|
|
579
825
|
const envelopes = await discoverAttestations({ attestationId, relays });
|
|
580
826
|
if (envelopes.length === 0) {
|
|
581
|
-
|
|
827
|
+
log4.warn({ attestationId }, "Attestation not found on any relay");
|
|
582
828
|
return null;
|
|
583
829
|
}
|
|
584
830
|
if (envelopes.length > 1) {
|
|
585
|
-
|
|
831
|
+
log4.warn(
|
|
586
832
|
{ attestationId, count: envelopes.length },
|
|
587
833
|
"Multiple attestations found with same ID - using first"
|
|
588
834
|
);
|
|
@@ -590,11 +836,11 @@ async function verifyAttestationById(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
590
836
|
return envelopes[0] || null;
|
|
591
837
|
}
|
|
592
838
|
async function getAttestationsForAddress(address, relays = DEFAULT_RELAYS) {
|
|
593
|
-
|
|
839
|
+
log4.info({ address }, "Getting all attestations for address");
|
|
594
840
|
return discoverAttestations({ address, relays });
|
|
595
841
|
}
|
|
596
842
|
async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
597
|
-
|
|
843
|
+
log4.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
598
844
|
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
599
845
|
}
|
|
600
846
|
function formatIdentitiesForDisplay(identities) {
|