@orangecheck/sdk 1.2.0 → 1.4.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/index.js CHANGED
@@ -215,14 +215,19 @@ var DEFAULT_RELAYS = [
215
215
  "wss://relay.damus.io",
216
216
  "wss://relay.nostr.band",
217
217
  "wss://nos.lol",
218
- "wss://relay.snort.social"
218
+ "wss://relay.snort.social",
219
+ "wss://relay.ochk.io"
219
220
  ];
220
221
  function createAttestationEvent(envelope, pubkey) {
221
222
  const tags = [
222
223
  ["d", envelope.attestation_id],
223
- // Parameterized replaceable event identifier
224
+ // Parameterized replaceable event identifier (indexed)
225
+ ["t", envelope.address],
226
+ // Indexed btc-address tag — query with `#t`
227
+ ["t", "oc-attest"],
228
+ // Family marker — `#t=["oc-attest"]` for cross-cut discovery
224
229
  ["address", envelope.address],
225
- // Bitcoin address
230
+ // Diagnostic / legacy — relay won't index this
226
231
  ["scheme", envelope.scheme],
227
232
  // Signature scheme (bip322 or legacy)
228
233
  ["issued_at", envelope.issued_at]
@@ -254,6 +259,17 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
254
259
  log.info({ eventId: event.id, relayCount: relays.length }, "Publishing event to relays");
255
260
  const publishPromises = relays.map(async (relayUrl) => {
256
261
  let ws = null;
262
+ let settled = false;
263
+ const recordSuccess = () => {
264
+ if (settled) return;
265
+ settled = true;
266
+ success.push(relayUrl);
267
+ };
268
+ const recordFailure = () => {
269
+ if (settled) return;
270
+ settled = true;
271
+ failed.push(relayUrl);
272
+ };
257
273
  try {
258
274
  ws = new WebSocket(relayUrl);
259
275
  const wsRef = ws;
@@ -274,7 +290,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
274
290
  const data = JSON.parse(msg.data);
275
291
  if (data[0] === "OK" && data[1] === event.id) {
276
292
  if (data[2] === true) {
277
- success.push(relayUrl);
293
+ recordSuccess();
278
294
  cleanup();
279
295
  resolve();
280
296
  } else {
@@ -282,7 +298,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
282
298
  { relay: relayUrl, reason: data[3] },
283
299
  "Relay rejected event"
284
300
  );
285
- failed.push(relayUrl);
301
+ recordFailure();
286
302
  cleanup();
287
303
  reject(new Error(data[3] || "Relay rejected event"));
288
304
  }
@@ -292,14 +308,14 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
292
308
  { relay: relayUrl, error: err },
293
309
  "Failed to parse relay response"
294
310
  );
295
- failed.push(relayUrl);
311
+ recordFailure();
296
312
  cleanup();
297
313
  reject(err);
298
314
  }
299
315
  };
300
316
  wsRef.onerror = (err) => {
301
317
  log.error({ relay: relayUrl, error: err }, "WebSocket error");
302
- failed.push(relayUrl);
318
+ recordFailure();
303
319
  cleanup();
304
320
  reject(err);
305
321
  };
@@ -309,9 +325,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
309
325
  });
310
326
  } catch (err) {
311
327
  log.error({ relay: relayUrl, error: err }, "Failed to publish to relay");
312
- if (!failed.includes(relayUrl)) {
313
- failed.push(relayUrl);
314
- }
328
+ recordFailure();
315
329
  } finally {
316
330
  if (ws && ws.readyState !== WebSocket.CLOSED) {
317
331
  ws.close();
@@ -406,8 +420,15 @@ async function queryByAddress(address, relays = DEFAULT_RELAYS) {
406
420
  subscriptionId,
407
421
  {
408
422
  kinds: [30078],
409
- "#address": [address],
410
- limit: 10
423
+ // `#t`, NOT `#address`: NIP-12 relays index
424
+ // single-letter tags only, so `#address`
425
+ // matched nothing on any relay and this
426
+ // function returned empty for every real
427
+ // attestation. attest.ochk.io's verifier has
428
+ // queried `#t` since the same fix landed
429
+ // there; this is the back-port.
430
+ "#t": [address],
431
+ limit: 50
411
432
  }
412
433
  ])
413
434
  );
@@ -1177,246 +1198,6 @@ async function check(params) {
1177
1198
  ...reasons.length ? { reasons } : {}
1178
1199
  };
1179
1200
  }
1180
- var HEADER = "orangecheck-binding";
1181
- var ACK = "I attest the keys named in this message are one principal.";
1182
- var BINDING_KIND = 30079;
1183
- function toHex(bytes) {
1184
- let out = "";
1185
- for (const b of bytes) out += b.toString(16).padStart(2, "0");
1186
- return out;
1187
- }
1188
- function fromHex(hex) {
1189
- const clean = hex.toLowerCase();
1190
- if (clean.length % 2 !== 0 || /[^0-9a-f]/.test(clean)) {
1191
- throw new Error("invalid hex");
1192
- }
1193
- const out = new Uint8Array(clean.length / 2);
1194
- for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1195
- return out;
1196
- }
1197
- function assertPrintableAscii(field, value) {
1198
- for (let i = 0; i < value.length; i++) {
1199
- const c = value.charCodeAt(i);
1200
- if (c < 32 || c > 126) {
1201
- throw new Error(
1202
- `binding ${field} contains an unsafe character (0x${c.toString(16)}) \u2014 line-smuggling defense (SPEC-BINDING \xA73.5)`
1203
- );
1204
- }
1205
- }
1206
- }
1207
- function buildBindingMessage(input) {
1208
- assertPrintableAscii("principal", input.principal);
1209
- assertPrintableAscii("btc", input.btc);
1210
- assertPrintableAscii("nostr", input.nostr);
1211
- assertPrintableAscii("nonce", input.nonce);
1212
- assertPrintableAscii("issued_at", input.issued_at);
1213
- const core = [
1214
- HEADER,
1215
- "v: 1",
1216
- `principal: ${input.principal}`,
1217
- `btc: ${input.btc}`,
1218
- `nostr: ${input.nostr}`,
1219
- `nonce: ${input.nonce}`,
1220
- `issued_at: ${input.issued_at}`,
1221
- `ack: ${ACK}`
1222
- ];
1223
- const ext = [];
1224
- if (input.extensions) {
1225
- for (const key of Object.keys(input.extensions).sort()) {
1226
- const value = input.extensions[key];
1227
- if (value == null) continue;
1228
- if (!/^[a-z]+$/.test(key)) {
1229
- throw new Error(`binding extension key "${key}" must be lowercase ASCII`);
1230
- }
1231
- assertPrintableAscii(`extension ${key}`, value);
1232
- ext.push(`${key}: ${value}`);
1233
- }
1234
- }
1235
- return [...core, ...ext].join("\n") + "\n";
1236
- }
1237
- function bindingId(message) {
1238
- return toHex(sha2_js.sha256(new TextEncoder().encode(message)));
1239
- }
1240
- function linePrefix(line, prefix) {
1241
- if (typeof line !== "string" || !line.startsWith(prefix)) return null;
1242
- return line.slice(prefix.length);
1243
- }
1244
- function nostrEventId(ev) {
1245
- const serialized = JSON.stringify([
1246
- 0,
1247
- ev.pubkey,
1248
- ev.created_at,
1249
- ev.kind,
1250
- ev.tags,
1251
- ev.content
1252
- ]);
1253
- return toHex(sha2_js.sha256(new TextEncoder().encode(serialized)));
1254
- }
1255
- function npubToXOnlyHex(npub) {
1256
- const decoded = base.bech32.decode(npub, 90);
1257
- if (decoded.prefix !== "npub") throw new Error("not an npub");
1258
- const bytes = Uint8Array.from(base.bech32.fromWords(decoded.words));
1259
- if (bytes.length !== 32) throw new Error("npub payload is not 32 bytes");
1260
- return toHex(bytes);
1261
- }
1262
- function verifyBinding(envelope) {
1263
- const fail = (status) => ({
1264
- valid: false,
1265
- status
1266
- });
1267
- try {
1268
- const msg = envelope.message;
1269
- if (typeof msg !== "string" || msg.length === 0) return fail("malformed");
1270
- const lines = msg.split("\n");
1271
- if ((lines[0] ?? "") !== HEADER) return fail("bad_header");
1272
- if (lines.length < 9 || lines[lines.length - 1] !== "") return fail("malformed");
1273
- const body = lines.slice(0, -1);
1274
- if (body.length < 8) return fail("malformed");
1275
- if (body[1] !== "v: 1") return fail("malformed");
1276
- if (body[7] !== `ack: ${ACK}`) return fail("bad_ack");
1277
- const principal = linePrefix(body[2], "principal: ");
1278
- const btc = linePrefix(body[3], "btc: ");
1279
- const nostr = linePrefix(body[4], "nostr: ");
1280
- const nonce = linePrefix(body[5], "nonce: ");
1281
- const issuedAt = linePrefix(body[6], "issued_at: ");
1282
- if (principal == null || btc == null || nostr == null || nonce == null || issuedAt == null) {
1283
- return fail("malformed");
1284
- }
1285
- for (const value of [principal, btc, nostr, nonce, issuedAt]) {
1286
- for (let i = 0; i < value.length; i++) {
1287
- const c = value.charCodeAt(i);
1288
- if (c < 32 || c > 126) return fail("malformed");
1289
- }
1290
- }
1291
- if (!/^did:oc:[0-9a-f]{32}$/.test(principal)) return fail("bad_principal");
1292
- if (!/^[0-9a-f]{32}$/.test(nonce)) return fail("malformed");
1293
- const ext = {};
1294
- let prevKey = "";
1295
- for (const line of body.slice(8)) {
1296
- const idx = line.indexOf(": ");
1297
- if (idx <= 0) return fail("malformed");
1298
- const key = line.slice(0, idx);
1299
- const value = line.slice(idx + 2);
1300
- if (!/^[a-z]+$/.test(key)) return fail("malformed");
1301
- if (key <= prevKey) return fail("ext_unsorted");
1302
- prevKey = key;
1303
- ext[key] = value;
1304
- }
1305
- const bid = bindingId(msg);
1306
- if (envelope.binding_id && envelope.binding_id.toLowerCase() !== bid) {
1307
- return fail("id_mismatch");
1308
- }
1309
- if (envelope.btc && envelope.btc !== btc) return fail("field_mismatch");
1310
- if (envelope.nostr && envelope.nostr !== nostr) return fail("field_mismatch");
1311
- if (envelope.principal && envelope.principal !== principal) return fail("field_mismatch");
1312
- if (ext.expires) {
1313
- const t = Date.parse(ext.expires);
1314
- if (!Number.isNaN(t) && Date.now() > t) return fail("expired");
1315
- }
1316
- if (typeof envelope.btc_signature !== "string" || !envelope.btc_signature) {
1317
- return fail("btc_sig_invalid");
1318
- }
1319
- const scheme = envelope.btc_scheme ?? "bip322";
1320
- let btcOk = false;
1321
- try {
1322
- if (scheme === "bip322") {
1323
- btcOk = bip322Js.Verifier.verifySignature(btc, msg, envelope.btc_signature);
1324
- } else if (scheme === "legacy") {
1325
- btcOk = bitcoinMessage__namespace.verify(msg, btc, envelope.btc_signature);
1326
- } else {
1327
- return fail("bad_scheme");
1328
- }
1329
- } catch {
1330
- btcOk = false;
1331
- }
1332
- if (!btcOk) return fail("btc_sig_invalid");
1333
- const ev = envelope.nostr_event;
1334
- if (!ev || ev.kind !== BINDING_KIND || typeof ev.id !== "string") return fail("malformed");
1335
- if (nostrEventId(ev) !== ev.id.toLowerCase()) return fail("nostr_id_invalid");
1336
- let nostrSigOk = false;
1337
- try {
1338
- nostrSigOk = secp256k1_js.schnorr.verify(fromHex(ev.sig), fromHex(ev.id), fromHex(ev.pubkey));
1339
- } catch {
1340
- nostrSigOk = false;
1341
- }
1342
- if (!nostrSigOk) return fail("nostr_sig_invalid");
1343
- let nostrXOnly;
1344
- try {
1345
- nostrXOnly = npubToXOnlyHex(nostr);
1346
- } catch {
1347
- return fail("nostr_key_mismatch");
1348
- }
1349
- if (nostrXOnly !== ev.pubkey.toLowerCase()) return fail("nostr_key_mismatch");
1350
- let content;
1351
- try {
1352
- content = JSON.parse(ev.content);
1353
- } catch {
1354
- return fail("message_mismatch");
1355
- }
1356
- if (content.message !== msg) return fail("message_mismatch");
1357
- if (content.btc_signature !== envelope.btc_signature) return fail("message_mismatch");
1358
- return { valid: true, status: "binding_ok", binding_id: bid, principal, btc, nostr };
1359
- } catch {
1360
- return fail("malformed");
1361
- }
1362
- }
1363
- function xOnlyHexToNpub(hex) {
1364
- const bytes = fromHex(hex);
1365
- if (bytes.length !== 32) throw new Error("x-only Nostr key must be 32 bytes");
1366
- return base.bech32.encode("npub", base.bech32.toWords(bytes), 90);
1367
- }
1368
- function buildBindingEventTemplate(params) {
1369
- const id = bindingId(params.message);
1370
- return {
1371
- kind: BINDING_KIND,
1372
- pubkey: params.nostrPubkeyHex.toLowerCase(),
1373
- created_at: params.createdAt ?? Math.floor(Date.now() / 1e3),
1374
- tags: [
1375
- ["d", `oc-attest-binding:${id}`],
1376
- ["btc", params.btc],
1377
- ["oc", "binding-attestation"],
1378
- ["v", "1"]
1379
- ],
1380
- content: JSON.stringify({ message: params.message, btc_signature: params.btcSignature })
1381
- };
1382
- }
1383
- function parseBindingFields(message) {
1384
- const body = message.split("\n");
1385
- const at = (i, prefix) => {
1386
- const line = body[i];
1387
- if (typeof line !== "string" || !line.startsWith(prefix)) {
1388
- throw new Error(`binding message line ${i + 1} is malformed`);
1389
- }
1390
- return line.slice(prefix.length);
1391
- };
1392
- return {
1393
- principal: at(2, "principal: "),
1394
- btc: at(3, "btc: "),
1395
- nostr: at(4, "nostr: ")
1396
- };
1397
- }
1398
- function toBase64Url(s) {
1399
- const bytes = new TextEncoder().encode(s);
1400
- let bin = "";
1401
- for (const b of bytes) bin += String.fromCharCode(b);
1402
- const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
1403
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1404
- }
1405
- function assembleBindingEnvelope(params) {
1406
- const { principal, btc, nostr } = parseBindingFields(params.message);
1407
- return {
1408
- binding_id: bindingId(params.message),
1409
- v: 1,
1410
- principal,
1411
- btc,
1412
- nostr,
1413
- message: params.message,
1414
- message_b64url: toBase64Url(params.message),
1415
- btc_signature: params.btcSignature,
1416
- btc_scheme: params.btcScheme ?? "bip322",
1417
- nostr_event: params.nostrEvent
1418
- };
1419
- }
1420
1201
  var AUTH_HEADER = "orangecheck-auth";
1421
1202
  var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1422
1203
  function randomNonce() {
@@ -1872,12 +1653,12 @@ function getGistCreationUrl() {
1872
1653
  }
1873
1654
  var log7 = createLogger("identity-verification/nostr");
1874
1655
  var HEX_64_RE = /^[0-9a-f]{64}$/;
1875
- function toHex2(bytes) {
1656
+ function toHex(bytes) {
1876
1657
  let out = "";
1877
1658
  for (const b of bytes) out += b.toString(16).padStart(2, "0");
1878
1659
  return out;
1879
1660
  }
1880
- function fromHex2(hex) {
1661
+ function fromHex(hex) {
1881
1662
  if (hex.length % 2 !== 0) throw new Error("odd-length hex");
1882
1663
  const out = new Uint8Array(hex.length / 2);
1883
1664
  for (let i = 0; i < out.length; i++) {
@@ -1901,7 +1682,7 @@ function npubToHex(npub) {
1901
1682
  if (bytes.length !== 32) {
1902
1683
  throw new Error(`invalid npub payload length: ${bytes.length}`);
1903
1684
  }
1904
- return toHex2(Uint8Array.from(bytes));
1685
+ return toHex(Uint8Array.from(bytes));
1905
1686
  }
1906
1687
  async function queryNostrEvents(options) {
1907
1688
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1984,7 +1765,7 @@ async function verifyNostrEventSignature(event) {
1984
1765
  event.tags ?? [],
1985
1766
  event.content ?? ""
1986
1767
  ]);
1987
- const computedId = toHex2(sha2_js.sha256(new TextEncoder().encode(preimage)));
1768
+ const computedId = toHex(sha2_js.sha256(new TextEncoder().encode(preimage)));
1988
1769
  if (computedId !== event.id.toLowerCase()) {
1989
1770
  log7.warn(
1990
1771
  { eventId: event.id, computedId },
@@ -1992,7 +1773,7 @@ async function verifyNostrEventSignature(event) {
1992
1773
  );
1993
1774
  return false;
1994
1775
  }
1995
- return secp256k1_js.schnorr.verify(fromHex2(event.sig), fromHex2(event.id), fromHex2(event.pubkey));
1776
+ return secp256k1_js.schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
1996
1777
  } catch (err) {
1997
1778
  log7.warn({ err }, "schnorr verify threw");
1998
1779
  return false;
@@ -2354,10 +2135,6 @@ var STATUS_META = {
2354
2135
 
2355
2136
  exports.DEFAULT_RELAYS = DEFAULT_RELAYS;
2356
2137
  exports.STATUS_META = STATUS_META;
2357
- exports.assembleBindingEnvelope = assembleBindingEnvelope;
2358
- exports.bindingId = bindingId;
2359
- exports.buildBindingEventTemplate = buildBindingEventTemplate;
2360
- exports.buildBindingMessage = buildBindingMessage;
2361
2138
  exports.buildCanonicalMessage = buildCanonicalMessage;
2362
2139
  exports.check = check;
2363
2140
  exports.computeAllScores = computeAllScores;
@@ -2409,13 +2186,11 @@ exports.supportsAutomaticVerification = supportsAutomaticVerification;
2409
2186
  exports.verify = verify2;
2410
2187
  exports.verifyAllIdentities = verifyAllIdentities;
2411
2188
  exports.verifyAttestationById = verifyAttestationById;
2412
- exports.verifyBinding = verifyBinding;
2413
2189
  exports.verifyChallenge = verifyChallenge;
2414
2190
  exports.verifyDnsIdentity = verifyDnsIdentity;
2415
2191
  exports.verifyGitHubIdentity = verifyGitHubIdentity;
2416
2192
  exports.verifyIdentity = verifyIdentity;
2417
2193
  exports.verifyNostrIdentity = verifyNostrIdentity;
2418
2194
  exports.verifyTwitterIdentity = verifyTwitterIdentity;
2419
- exports.xOnlyHexToNpub = xOnlyHexToNpub;
2420
2195
  //# sourceMappingURL=index.js.map
2421
2196
  //# sourceMappingURL=index.js.map