@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/nostr.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
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/nostr-pubkey.ts
|
|
4
6
|
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
@@ -71,8 +73,216 @@ function createLogger(namespace) {
|
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// src/utils/time.ts
|
|
77
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
78
|
+
function rfc3339Ms(value) {
|
|
79
|
+
if (!value || !RFC3339.test(value)) return null;
|
|
80
|
+
const ms = Date.parse(value);
|
|
81
|
+
return Number.isNaN(ms) ? null : ms;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/canonical.ts
|
|
85
|
+
var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
86
|
+
var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
|
|
87
|
+
function assertSafeIdentity({ protocol, identifier }) {
|
|
88
|
+
if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function parseIdentities(identitiesStr) {
|
|
100
|
+
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
|
|
104
|
+
throw new Error("identities field contains forbidden characters (newline)");
|
|
105
|
+
}
|
|
106
|
+
const bindings = identitiesStr.split(",").map((binding) => {
|
|
107
|
+
const colonIndex = binding.indexOf(":");
|
|
108
|
+
if (colonIndex === -1) {
|
|
109
|
+
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
protocol: binding.substring(0, colonIndex).trim(),
|
|
113
|
+
identifier: binding.substring(colonIndex + 1).trim()
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
for (const b of bindings) {
|
|
117
|
+
if (!b.protocol || !b.identifier) {
|
|
118
|
+
throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
|
|
119
|
+
}
|
|
120
|
+
assertSafeIdentity(b);
|
|
121
|
+
}
|
|
122
|
+
return bindings;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/verify.ts
|
|
126
|
+
var log = createLogger("ocp/verify");
|
|
127
|
+
function looksLikeHex(s) {
|
|
128
|
+
return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
|
|
129
|
+
}
|
|
130
|
+
function toBase64Maybe(sig) {
|
|
131
|
+
if (looksLikeHex(sig)) {
|
|
132
|
+
return Buffer.from(sig, "hex").toString("base64");
|
|
133
|
+
}
|
|
134
|
+
return sig;
|
|
135
|
+
}
|
|
136
|
+
function detectSignatureScheme(addr, sig) {
|
|
137
|
+
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
138
|
+
if (isSegWit) {
|
|
139
|
+
return "bip322";
|
|
140
|
+
}
|
|
141
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
142
|
+
const looksLikeLegacy = sigBase64.length === 88 && /^[IHG]/.test(sigBase64);
|
|
143
|
+
if (looksLikeLegacy) {
|
|
144
|
+
return "legacy";
|
|
145
|
+
}
|
|
146
|
+
return "bip322";
|
|
147
|
+
}
|
|
148
|
+
async function verifySignature({
|
|
149
|
+
addr,
|
|
150
|
+
msg,
|
|
151
|
+
sig,
|
|
152
|
+
scheme
|
|
153
|
+
}) {
|
|
154
|
+
const detectedScheme = scheme || detectSignatureScheme(addr, sig);
|
|
155
|
+
log.debug(
|
|
156
|
+
{ addr, msgLength: msg.length, sigLength: sig.length, scheme: detectedScheme },
|
|
157
|
+
"Verifying signature"
|
|
158
|
+
);
|
|
159
|
+
if (detectedScheme === "bip322") {
|
|
160
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
161
|
+
log.debug(
|
|
162
|
+
{ sigBase64: sigBase64.substring(0, 50) + "..." },
|
|
163
|
+
"Signature converted to base64"
|
|
164
|
+
);
|
|
165
|
+
log.debug(
|
|
166
|
+
{
|
|
167
|
+
msgFull: msg,
|
|
168
|
+
msgLines: msg.split("\n").length,
|
|
169
|
+
msgFirstLine: msg.split("\n")[0],
|
|
170
|
+
msgLastLine: msg.split("\n")[msg.split("\n").length - 1]
|
|
171
|
+
},
|
|
172
|
+
"Message being verified"
|
|
173
|
+
);
|
|
174
|
+
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
175
|
+
log.info({ addr, ok }, "BIP-322 verification result");
|
|
176
|
+
if (!ok) {
|
|
177
|
+
const isSegWit = addr.startsWith("bc1") || addr.startsWith("tb1");
|
|
178
|
+
if (isSegWit && sigBase64.length === 88) {
|
|
179
|
+
log.warn(
|
|
180
|
+
{ addr, wallet: "possibly Electrum", sigLength: sigBase64.length },
|
|
181
|
+
"BIP-322 verification failed - likely Electrum wallet (does not support BIP-322 for SegWit)"
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
log.error(
|
|
185
|
+
{
|
|
186
|
+
addr,
|
|
187
|
+
sigLength: sigBase64.length,
|
|
188
|
+
msgLength: msg.length,
|
|
189
|
+
msgPreview: msg.substring(0, 200)
|
|
190
|
+
},
|
|
191
|
+
"BIP-322 verification failed - signature does not match message"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return ok ? "sig_ok_bip322" : "sig_invalid";
|
|
196
|
+
}
|
|
197
|
+
if (detectedScheme === "legacy") {
|
|
198
|
+
const sigBase64 = toBase64Maybe(sig);
|
|
199
|
+
if (!addr.startsWith("1") && !addr.startsWith("m") && !addr.startsWith("n")) {
|
|
200
|
+
log.warn({ addr }, "Legacy signature attempted on non-P2PKH address");
|
|
201
|
+
return "sig_unsupported_script";
|
|
202
|
+
}
|
|
203
|
+
log.debug(
|
|
204
|
+
{
|
|
205
|
+
addr,
|
|
206
|
+
sigBase64: sigBase64.substring(0, 50) + "...",
|
|
207
|
+
msgPreview: msg.substring(0, 100) + "...",
|
|
208
|
+
msgLength: msg.length
|
|
209
|
+
},
|
|
210
|
+
"Verifying legacy signature"
|
|
211
|
+
);
|
|
212
|
+
const ok = verifySignature$1(addr, msg, sigBase64);
|
|
213
|
+
log.info({ addr, ok, sigBase64Length: sigBase64.length }, "Legacy verification result");
|
|
214
|
+
if (!ok) {
|
|
215
|
+
log.error(
|
|
216
|
+
{
|
|
217
|
+
addr,
|
|
218
|
+
msgFirstLine: msg.split("\n")[0],
|
|
219
|
+
sigFormat: sigBase64.substring(0, 10)
|
|
220
|
+
},
|
|
221
|
+
"Legacy signature verification failed - check message format and signature"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return ok ? "sig_ok_legacy" : "sig_invalid";
|
|
225
|
+
}
|
|
226
|
+
return "invalid_scheme";
|
|
227
|
+
}
|
|
228
|
+
async function signatureIsValid(input) {
|
|
229
|
+
try {
|
|
230
|
+
const code = await verifySignature(input);
|
|
231
|
+
return code === "sig_ok_bip322" || code === "sig_ok_legacy";
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function parseCanonicalMessage(msg) {
|
|
237
|
+
const lines = msg.split("\n").map((l) => l.trim());
|
|
238
|
+
const getVal = (prefix) => {
|
|
239
|
+
const needle = prefix + ":";
|
|
240
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
241
|
+
if (!hit) return void 0;
|
|
242
|
+
const idx = hit.indexOf(":");
|
|
243
|
+
return hit.slice(idx + 1).trim();
|
|
244
|
+
};
|
|
245
|
+
const address = getVal("address") || "";
|
|
246
|
+
const identitiesStr = getVal("identities") || "";
|
|
247
|
+
const issued_at = getVal("issued_at") || "";
|
|
248
|
+
const expires = getVal("expires");
|
|
249
|
+
let identities = [];
|
|
250
|
+
try {
|
|
251
|
+
identities = parseIdentities(identitiesStr);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
log.warn({ identitiesStr, error: err }, "Failed to parse identities field");
|
|
254
|
+
}
|
|
255
|
+
const coreKeys = /* @__PURE__ */ new Set([
|
|
256
|
+
"orangecheck",
|
|
257
|
+
"identities",
|
|
258
|
+
"address",
|
|
259
|
+
"purpose",
|
|
260
|
+
"nonce",
|
|
261
|
+
"issued_at",
|
|
262
|
+
"ack"
|
|
263
|
+
]);
|
|
264
|
+
const extensions = {};
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
const idx = line.indexOf(":");
|
|
267
|
+
if (idx === -1) continue;
|
|
268
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
269
|
+
const val = line.slice(idx + 1).trim();
|
|
270
|
+
if (!coreKeys.has(key) && val) extensions[key] = val;
|
|
271
|
+
}
|
|
272
|
+
const network = extensions["network"] || "mainnet";
|
|
273
|
+
const bondStr = extensions["bond"];
|
|
274
|
+
const bond = bondStr ? parseInt(bondStr, 10) : void 0;
|
|
275
|
+
return {
|
|
276
|
+
core: { address, identities, issued_at },
|
|
277
|
+
expires,
|
|
278
|
+
network,
|
|
279
|
+
extensions,
|
|
280
|
+
bond: bond !== void 0 && !isNaN(bond) && bond > 0 ? bond : void 0
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
74
284
|
// src/nostr.ts
|
|
75
|
-
var
|
|
285
|
+
var log2 = createLogger("ocp/nostr");
|
|
76
286
|
var ATTESTATION_EVENT_KIND = 30078;
|
|
77
287
|
var DEFAULT_RELAYS = [
|
|
78
288
|
"wss://relay.damus.io",
|
|
@@ -148,7 +358,7 @@ function getAddressFromEvent(event) {
|
|
|
148
358
|
async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
149
359
|
const success = [];
|
|
150
360
|
const failed = [];
|
|
151
|
-
|
|
361
|
+
log2.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
|
|
152
362
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
153
363
|
const publishPromises = relays.map(async (relayUrl) => {
|
|
154
364
|
let ws = null;
|
|
@@ -187,7 +397,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
187
397
|
cleanup();
|
|
188
398
|
resolve();
|
|
189
399
|
} else {
|
|
190
|
-
|
|
400
|
+
log2.warn(
|
|
191
401
|
{ relay: relayUrl, reason: data[3] },
|
|
192
402
|
"Relay rejected event"
|
|
193
403
|
);
|
|
@@ -197,7 +407,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
197
407
|
}
|
|
198
408
|
}
|
|
199
409
|
} catch (err) {
|
|
200
|
-
|
|
410
|
+
log2.error(
|
|
201
411
|
{ relay: relayUrl, error: err },
|
|
202
412
|
"Failed to parse relay response"
|
|
203
413
|
);
|
|
@@ -207,7 +417,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
207
417
|
}
|
|
208
418
|
};
|
|
209
419
|
wsRef.onerror = (err) => {
|
|
210
|
-
|
|
420
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error");
|
|
211
421
|
recordFailure();
|
|
212
422
|
cleanup();
|
|
213
423
|
reject(err);
|
|
@@ -217,7 +427,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
217
427
|
};
|
|
218
428
|
});
|
|
219
429
|
} catch (err) {
|
|
220
|
-
|
|
430
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
|
|
221
431
|
recordFailure();
|
|
222
432
|
} finally {
|
|
223
433
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
@@ -226,12 +436,12 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
226
436
|
}
|
|
227
437
|
});
|
|
228
438
|
await Promise.allSettled(publishPromises);
|
|
229
|
-
|
|
439
|
+
log2.info({ successCount: success.length, failedCount: failed.length }, "Publishing complete");
|
|
230
440
|
return { success, failed };
|
|
231
441
|
}
|
|
232
442
|
async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
233
443
|
const events = [];
|
|
234
|
-
|
|
444
|
+
log2.info({ attestationId, relayCount: relays.length }, "Querying relays for attestation");
|
|
235
445
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
236
446
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
237
447
|
let ws = null;
|
|
@@ -271,11 +481,11 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
271
481
|
resolve();
|
|
272
482
|
}
|
|
273
483
|
} catch (err) {
|
|
274
|
-
|
|
484
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
275
485
|
}
|
|
276
486
|
};
|
|
277
487
|
wsRef.onerror = (err) => {
|
|
278
|
-
|
|
488
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
279
489
|
cleanup();
|
|
280
490
|
reject(err);
|
|
281
491
|
};
|
|
@@ -284,7 +494,7 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
284
494
|
};
|
|
285
495
|
});
|
|
286
496
|
} catch (err) {
|
|
287
|
-
|
|
497
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
288
498
|
} finally {
|
|
289
499
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
290
500
|
ws.close();
|
|
@@ -292,12 +502,12 @@ async function queryByAttestationId(attestationId, relays = DEFAULT_RELAYS) {
|
|
|
292
502
|
}
|
|
293
503
|
});
|
|
294
504
|
await Promise.allSettled(queryPromises);
|
|
295
|
-
|
|
505
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
296
506
|
return events;
|
|
297
507
|
}
|
|
298
508
|
async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
299
509
|
const events = [];
|
|
300
|
-
|
|
510
|
+
log2.info({ address, relayCount: relays.length }, "Querying relays for address");
|
|
301
511
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
302
512
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
303
513
|
try {
|
|
@@ -339,28 +549,28 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
|
|
|
339
549
|
resolve();
|
|
340
550
|
}
|
|
341
551
|
} catch (err) {
|
|
342
|
-
|
|
552
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
343
553
|
}
|
|
344
554
|
};
|
|
345
555
|
ws.onerror = (err) => {
|
|
346
556
|
clearTimeout(timeout);
|
|
347
|
-
|
|
557
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
348
558
|
reject(err);
|
|
349
559
|
};
|
|
350
560
|
});
|
|
351
561
|
} catch (err) {
|
|
352
|
-
|
|
562
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
353
563
|
}
|
|
354
564
|
});
|
|
355
565
|
await Promise.allSettled(queryPromises);
|
|
356
|
-
|
|
566
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
357
567
|
return events;
|
|
358
568
|
}
|
|
359
569
|
async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
360
570
|
const events = [];
|
|
361
571
|
const identityTag = `${protocol}:${identifier}`;
|
|
362
572
|
const identityTags = protocol === "nostr" ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`) : [identityTag];
|
|
363
|
-
|
|
573
|
+
log2.info({ protocol, identifier, relayCount: relays.length }, "Querying relays for identity");
|
|
364
574
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
365
575
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
366
576
|
try {
|
|
@@ -395,26 +605,26 @@ async function queryByIdentity(protocol, identifier, relays = DEFAULT_RELAYS) {
|
|
|
395
605
|
resolve();
|
|
396
606
|
}
|
|
397
607
|
} catch (err) {
|
|
398
|
-
|
|
608
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
399
609
|
}
|
|
400
610
|
};
|
|
401
611
|
ws.onerror = (err) => {
|
|
402
612
|
clearTimeout(timeout);
|
|
403
|
-
|
|
613
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
404
614
|
reject(err);
|
|
405
615
|
};
|
|
406
616
|
});
|
|
407
617
|
} catch (err) {
|
|
408
|
-
|
|
618
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
409
619
|
}
|
|
410
620
|
});
|
|
411
621
|
await Promise.allSettled(queryPromises);
|
|
412
|
-
|
|
622
|
+
log2.info({ eventCount: events.length }, "Query complete");
|
|
413
623
|
return events;
|
|
414
624
|
}
|
|
415
625
|
async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
416
626
|
const events = [];
|
|
417
|
-
|
|
627
|
+
log2.info({ relayCount: relays.length, limit }, "Querying relays for recent attestations");
|
|
418
628
|
const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;
|
|
419
629
|
const queryPromises = relays.map(async (relayUrl) => {
|
|
420
630
|
try {
|
|
@@ -450,30 +660,22 @@ async function queryRecent(relays = DEFAULT_RELAYS, limit = 200) {
|
|
|
450
660
|
resolve();
|
|
451
661
|
}
|
|
452
662
|
} catch (err) {
|
|
453
|
-
|
|
663
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to parse event");
|
|
454
664
|
}
|
|
455
665
|
};
|
|
456
666
|
ws.onerror = (err) => {
|
|
457
667
|
clearTimeout(timeout);
|
|
458
|
-
|
|
668
|
+
log2.error({ relay: relayUrl, error: err }, "WebSocket error during query");
|
|
459
669
|
reject(err);
|
|
460
670
|
};
|
|
461
671
|
});
|
|
462
672
|
} catch (err) {
|
|
463
|
-
|
|
673
|
+
log2.error({ relay: relayUrl, error: err }, "Failed to query relay");
|
|
464
674
|
}
|
|
465
675
|
});
|
|
466
676
|
await Promise.allSettled(queryPromises);
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
for (const ev of events) {
|
|
470
|
-
const envelope = parseAttestationFromEvent(ev);
|
|
471
|
-
if (!envelope || seen.has(envelope.attestation_id)) continue;
|
|
472
|
-
if (!await verifyAttestationEnvelope(envelope)) continue;
|
|
473
|
-
seen.add(envelope.attestation_id);
|
|
474
|
-
envelopes.push(envelope);
|
|
475
|
-
}
|
|
476
|
-
log.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
|
|
677
|
+
const envelopes = await collectSignedEnvelopes(events);
|
|
678
|
+
log2.info({ eventCount: events.length, unique: envelopes.length }, "Recent query complete");
|
|
477
679
|
return envelopes;
|
|
478
680
|
}
|
|
479
681
|
async function verifyAttestationEnvelope(envelope) {
|
|
@@ -485,20 +687,72 @@ async function verifyAttestationEnvelope(envelope) {
|
|
|
485
687
|
const computedId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
486
688
|
return computedId === envelope.attestation_id;
|
|
487
689
|
} catch (err) {
|
|
488
|
-
|
|
690
|
+
log2.error({ error: err }, "Failed to verify attestation envelope");
|
|
489
691
|
return false;
|
|
490
692
|
}
|
|
491
693
|
}
|
|
694
|
+
function base64Url(text) {
|
|
695
|
+
const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));
|
|
696
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
697
|
+
}
|
|
698
|
+
function signedEnvelope(envelope) {
|
|
699
|
+
let parsed;
|
|
700
|
+
try {
|
|
701
|
+
parsed = parseCanonicalMessage(envelope.message);
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;
|
|
706
|
+
const { expires_at: _copy, ...rest } = envelope;
|
|
707
|
+
return {
|
|
708
|
+
...rest,
|
|
709
|
+
address: parsed.core.address,
|
|
710
|
+
identities: parsed.core.identities,
|
|
711
|
+
issued_at: parsed.core.issued_at,
|
|
712
|
+
...parsed.expires !== void 0 ? { expires_at: parsed.expires } : {},
|
|
713
|
+
message_b64url: base64Url(envelope.message)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
async function collectSignedEnvelopes(events, accept = () => true) {
|
|
717
|
+
const copies = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const event of events) {
|
|
719
|
+
const raw = parseAttestationFromEvent(event);
|
|
720
|
+
if (!raw || !await verifyAttestationEnvelope(raw)) continue;
|
|
721
|
+
const envelope = signedEnvelope(raw);
|
|
722
|
+
if (!envelope || !accept(event, envelope)) continue;
|
|
723
|
+
const same = copies.get(envelope.attestation_id) ?? [];
|
|
724
|
+
if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {
|
|
725
|
+
same.push(envelope);
|
|
726
|
+
}
|
|
727
|
+
copies.set(envelope.attestation_id, same);
|
|
728
|
+
}
|
|
729
|
+
const kept = [];
|
|
730
|
+
for (const same of copies.values()) {
|
|
731
|
+
if (same.length === 1) {
|
|
732
|
+
kept.push(same[0]);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
let valid;
|
|
736
|
+
for (const e of same) {
|
|
737
|
+
if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {
|
|
738
|
+
valid = e;
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
kept.push(valid ?? same[0]);
|
|
743
|
+
}
|
|
744
|
+
return kept;
|
|
745
|
+
}
|
|
492
746
|
function parseAttestationFromEvent(event) {
|
|
493
747
|
try {
|
|
494
748
|
const envelope = JSON.parse(event.content);
|
|
495
749
|
return envelope;
|
|
496
750
|
} catch (err) {
|
|
497
|
-
|
|
751
|
+
log2.error({ eventId: event.id, error: err }, "Failed to parse attestation from event");
|
|
498
752
|
return null;
|
|
499
753
|
}
|
|
500
754
|
}
|
|
501
755
|
|
|
502
|
-
export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, verifyAttestationEnvelope };
|
|
756
|
+
export { ATTESTATION_EVENT_KIND, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, collectSignedEnvelopes, createAttestationEvent, getAddressFromEvent, getAttestationIdFromEvent, parseAttestationFromEvent, parseIdentitiesFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope, verifyAttestationEnvelope };
|
|
503
757
|
//# sourceMappingURL=nostr.mjs.map
|
|
504
758
|
//# sourceMappingURL=nostr.mjs.map
|