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