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