@orangecheck/sdk 1.0.0 → 1.2.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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Buffer } from 'buffer';
1
+ import { Buffer as Buffer$1 } from 'buffer';
2
2
  import { Verifier } from 'bip322-js';
3
3
  import * as bitcoinMessage from 'bitcoinjs-message';
4
4
  import { schnorr } from '@noble/curves/secp256k1.js';
@@ -712,7 +712,7 @@ function looksLikeHex(s) {
712
712
  }
713
713
  function toBase64Maybe(sig) {
714
714
  if (looksLikeHex(sig)) {
715
- return Buffer.from(sig, "hex").toString("base64");
715
+ return Buffer$1.from(sig, "hex").toString("base64");
716
716
  }
717
717
  return sig;
718
718
  }
@@ -1155,6 +1155,246 @@ async function check(params) {
1155
1155
  ...reasons.length ? { reasons } : {}
1156
1156
  };
1157
1157
  }
1158
+ var HEADER = "orangecheck-binding";
1159
+ var ACK = "I attest the keys named in this message are one principal.";
1160
+ var BINDING_KIND = 30079;
1161
+ function toHex(bytes) {
1162
+ let out = "";
1163
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
1164
+ return out;
1165
+ }
1166
+ function fromHex(hex) {
1167
+ const clean = hex.toLowerCase();
1168
+ if (clean.length % 2 !== 0 || /[^0-9a-f]/.test(clean)) {
1169
+ throw new Error("invalid hex");
1170
+ }
1171
+ const out = new Uint8Array(clean.length / 2);
1172
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1173
+ return out;
1174
+ }
1175
+ function assertPrintableAscii(field, value) {
1176
+ for (let i = 0; i < value.length; i++) {
1177
+ const c = value.charCodeAt(i);
1178
+ if (c < 32 || c > 126) {
1179
+ throw new Error(
1180
+ `binding ${field} contains an unsafe character (0x${c.toString(16)}) \u2014 line-smuggling defense (SPEC-BINDING \xA73.5)`
1181
+ );
1182
+ }
1183
+ }
1184
+ }
1185
+ function buildBindingMessage(input) {
1186
+ assertPrintableAscii("principal", input.principal);
1187
+ assertPrintableAscii("btc", input.btc);
1188
+ assertPrintableAscii("nostr", input.nostr);
1189
+ assertPrintableAscii("nonce", input.nonce);
1190
+ assertPrintableAscii("issued_at", input.issued_at);
1191
+ const core = [
1192
+ HEADER,
1193
+ "v: 1",
1194
+ `principal: ${input.principal}`,
1195
+ `btc: ${input.btc}`,
1196
+ `nostr: ${input.nostr}`,
1197
+ `nonce: ${input.nonce}`,
1198
+ `issued_at: ${input.issued_at}`,
1199
+ `ack: ${ACK}`
1200
+ ];
1201
+ const ext = [];
1202
+ if (input.extensions) {
1203
+ for (const key of Object.keys(input.extensions).sort()) {
1204
+ const value = input.extensions[key];
1205
+ if (value == null) continue;
1206
+ if (!/^[a-z]+$/.test(key)) {
1207
+ throw new Error(`binding extension key "${key}" must be lowercase ASCII`);
1208
+ }
1209
+ assertPrintableAscii(`extension ${key}`, value);
1210
+ ext.push(`${key}: ${value}`);
1211
+ }
1212
+ }
1213
+ return [...core, ...ext].join("\n") + "\n";
1214
+ }
1215
+ function bindingId(message) {
1216
+ return toHex(sha256(new TextEncoder().encode(message)));
1217
+ }
1218
+ function linePrefix(line, prefix) {
1219
+ if (typeof line !== "string" || !line.startsWith(prefix)) return null;
1220
+ return line.slice(prefix.length);
1221
+ }
1222
+ function nostrEventId(ev) {
1223
+ const serialized = JSON.stringify([
1224
+ 0,
1225
+ ev.pubkey,
1226
+ ev.created_at,
1227
+ ev.kind,
1228
+ ev.tags,
1229
+ ev.content
1230
+ ]);
1231
+ return toHex(sha256(new TextEncoder().encode(serialized)));
1232
+ }
1233
+ function npubToXOnlyHex(npub) {
1234
+ const decoded = bech32.decode(npub, 90);
1235
+ if (decoded.prefix !== "npub") throw new Error("not an npub");
1236
+ const bytes = Uint8Array.from(bech32.fromWords(decoded.words));
1237
+ if (bytes.length !== 32) throw new Error("npub payload is not 32 bytes");
1238
+ return toHex(bytes);
1239
+ }
1240
+ function verifyBinding(envelope) {
1241
+ const fail = (status) => ({
1242
+ valid: false,
1243
+ status
1244
+ });
1245
+ try {
1246
+ const msg = envelope.message;
1247
+ if (typeof msg !== "string" || msg.length === 0) return fail("malformed");
1248
+ const lines = msg.split("\n");
1249
+ if ((lines[0] ?? "") !== HEADER) return fail("bad_header");
1250
+ if (lines.length < 9 || lines[lines.length - 1] !== "") return fail("malformed");
1251
+ const body = lines.slice(0, -1);
1252
+ if (body.length < 8) return fail("malformed");
1253
+ if (body[1] !== "v: 1") return fail("malformed");
1254
+ if (body[7] !== `ack: ${ACK}`) return fail("bad_ack");
1255
+ const principal = linePrefix(body[2], "principal: ");
1256
+ const btc = linePrefix(body[3], "btc: ");
1257
+ const nostr = linePrefix(body[4], "nostr: ");
1258
+ const nonce = linePrefix(body[5], "nonce: ");
1259
+ const issuedAt = linePrefix(body[6], "issued_at: ");
1260
+ if (principal == null || btc == null || nostr == null || nonce == null || issuedAt == null) {
1261
+ return fail("malformed");
1262
+ }
1263
+ for (const value of [principal, btc, nostr, nonce, issuedAt]) {
1264
+ for (let i = 0; i < value.length; i++) {
1265
+ const c = value.charCodeAt(i);
1266
+ if (c < 32 || c > 126) return fail("malformed");
1267
+ }
1268
+ }
1269
+ if (!/^did:oc:[0-9a-f]{32}$/.test(principal)) return fail("bad_principal");
1270
+ if (!/^[0-9a-f]{32}$/.test(nonce)) return fail("malformed");
1271
+ const ext = {};
1272
+ let prevKey = "";
1273
+ for (const line of body.slice(8)) {
1274
+ const idx = line.indexOf(": ");
1275
+ if (idx <= 0) return fail("malformed");
1276
+ const key = line.slice(0, idx);
1277
+ const value = line.slice(idx + 2);
1278
+ if (!/^[a-z]+$/.test(key)) return fail("malformed");
1279
+ if (key <= prevKey) return fail("ext_unsorted");
1280
+ prevKey = key;
1281
+ ext[key] = value;
1282
+ }
1283
+ const bid = bindingId(msg);
1284
+ if (envelope.binding_id && envelope.binding_id.toLowerCase() !== bid) {
1285
+ return fail("id_mismatch");
1286
+ }
1287
+ if (envelope.btc && envelope.btc !== btc) return fail("field_mismatch");
1288
+ if (envelope.nostr && envelope.nostr !== nostr) return fail("field_mismatch");
1289
+ if (envelope.principal && envelope.principal !== principal) return fail("field_mismatch");
1290
+ if (ext.expires) {
1291
+ const t = Date.parse(ext.expires);
1292
+ if (!Number.isNaN(t) && Date.now() > t) return fail("expired");
1293
+ }
1294
+ if (typeof envelope.btc_signature !== "string" || !envelope.btc_signature) {
1295
+ return fail("btc_sig_invalid");
1296
+ }
1297
+ const scheme = envelope.btc_scheme ?? "bip322";
1298
+ let btcOk = false;
1299
+ try {
1300
+ if (scheme === "bip322") {
1301
+ btcOk = Verifier.verifySignature(btc, msg, envelope.btc_signature);
1302
+ } else if (scheme === "legacy") {
1303
+ btcOk = bitcoinMessage.verify(msg, btc, envelope.btc_signature);
1304
+ } else {
1305
+ return fail("bad_scheme");
1306
+ }
1307
+ } catch {
1308
+ btcOk = false;
1309
+ }
1310
+ if (!btcOk) return fail("btc_sig_invalid");
1311
+ const ev = envelope.nostr_event;
1312
+ if (!ev || ev.kind !== BINDING_KIND || typeof ev.id !== "string") return fail("malformed");
1313
+ if (nostrEventId(ev) !== ev.id.toLowerCase()) return fail("nostr_id_invalid");
1314
+ let nostrSigOk = false;
1315
+ try {
1316
+ nostrSigOk = schnorr.verify(fromHex(ev.sig), fromHex(ev.id), fromHex(ev.pubkey));
1317
+ } catch {
1318
+ nostrSigOk = false;
1319
+ }
1320
+ if (!nostrSigOk) return fail("nostr_sig_invalid");
1321
+ let nostrXOnly;
1322
+ try {
1323
+ nostrXOnly = npubToXOnlyHex(nostr);
1324
+ } catch {
1325
+ return fail("nostr_key_mismatch");
1326
+ }
1327
+ if (nostrXOnly !== ev.pubkey.toLowerCase()) return fail("nostr_key_mismatch");
1328
+ let content;
1329
+ try {
1330
+ content = JSON.parse(ev.content);
1331
+ } catch {
1332
+ return fail("message_mismatch");
1333
+ }
1334
+ if (content.message !== msg) return fail("message_mismatch");
1335
+ if (content.btc_signature !== envelope.btc_signature) return fail("message_mismatch");
1336
+ return { valid: true, status: "binding_ok", binding_id: bid, principal, btc, nostr };
1337
+ } catch {
1338
+ return fail("malformed");
1339
+ }
1340
+ }
1341
+ function xOnlyHexToNpub(hex) {
1342
+ const bytes = fromHex(hex);
1343
+ if (bytes.length !== 32) throw new Error("x-only Nostr key must be 32 bytes");
1344
+ return bech32.encode("npub", bech32.toWords(bytes), 90);
1345
+ }
1346
+ function buildBindingEventTemplate(params) {
1347
+ const id = bindingId(params.message);
1348
+ return {
1349
+ kind: BINDING_KIND,
1350
+ pubkey: params.nostrPubkeyHex.toLowerCase(),
1351
+ created_at: params.createdAt ?? Math.floor(Date.now() / 1e3),
1352
+ tags: [
1353
+ ["d", `oc-attest-binding:${id}`],
1354
+ ["btc", params.btc],
1355
+ ["oc", "binding-attestation"],
1356
+ ["v", "1"]
1357
+ ],
1358
+ content: JSON.stringify({ message: params.message, btc_signature: params.btcSignature })
1359
+ };
1360
+ }
1361
+ function parseBindingFields(message) {
1362
+ const body = message.split("\n");
1363
+ const at = (i, prefix) => {
1364
+ const line = body[i];
1365
+ if (typeof line !== "string" || !line.startsWith(prefix)) {
1366
+ throw new Error(`binding message line ${i + 1} is malformed`);
1367
+ }
1368
+ return line.slice(prefix.length);
1369
+ };
1370
+ return {
1371
+ principal: at(2, "principal: "),
1372
+ btc: at(3, "btc: "),
1373
+ nostr: at(4, "nostr: ")
1374
+ };
1375
+ }
1376
+ function toBase64Url(s) {
1377
+ const bytes = new TextEncoder().encode(s);
1378
+ let bin = "";
1379
+ for (const b of bytes) bin += String.fromCharCode(b);
1380
+ const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
1381
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1382
+ }
1383
+ function assembleBindingEnvelope(params) {
1384
+ const { principal, btc, nostr } = parseBindingFields(params.message);
1385
+ return {
1386
+ binding_id: bindingId(params.message),
1387
+ v: 1,
1388
+ principal,
1389
+ btc,
1390
+ nostr,
1391
+ message: params.message,
1392
+ message_b64url: toBase64Url(params.message),
1393
+ btc_signature: params.btcSignature,
1394
+ btc_scheme: params.btcScheme ?? "bip322",
1395
+ nostr_event: params.nostrEvent
1396
+ };
1397
+ }
1158
1398
  var AUTH_HEADER = "orangecheck-auth";
1159
1399
  var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
1160
1400
  function randomNonce() {
@@ -1272,7 +1512,7 @@ async function verifyChallenge(opts) {
1272
1512
  let sigValid = false;
1273
1513
  try {
1274
1514
  if (scheme === "bip322") {
1275
- const sigBase64 = /^[0-9a-fA-F]+$/.test(opts.signature) && opts.signature.length % 2 === 0 ? Buffer.from(opts.signature, "hex").toString("base64") : opts.signature;
1515
+ const sigBase64 = /^[0-9a-fA-F]+$/.test(opts.signature) && opts.signature.length % 2 === 0 ? Buffer$1.from(opts.signature, "hex").toString("base64") : opts.signature;
1276
1516
  sigValid = Verifier.verifySignature(address, opts.message, sigBase64);
1277
1517
  } else if (scheme === "legacy") {
1278
1518
  if (!/^[1m][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address)) {
@@ -1610,12 +1850,12 @@ function getGistCreationUrl() {
1610
1850
  }
1611
1851
  var log7 = createLogger("identity-verification/nostr");
1612
1852
  var HEX_64_RE = /^[0-9a-f]{64}$/;
1613
- function toHex(bytes) {
1853
+ function toHex2(bytes) {
1614
1854
  let out = "";
1615
1855
  for (const b of bytes) out += b.toString(16).padStart(2, "0");
1616
1856
  return out;
1617
1857
  }
1618
- function fromHex(hex) {
1858
+ function fromHex2(hex) {
1619
1859
  if (hex.length % 2 !== 0) throw new Error("odd-length hex");
1620
1860
  const out = new Uint8Array(hex.length / 2);
1621
1861
  for (let i = 0; i < out.length; i++) {
@@ -1639,7 +1879,7 @@ function npubToHex(npub) {
1639
1879
  if (bytes.length !== 32) {
1640
1880
  throw new Error(`invalid npub payload length: ${bytes.length}`);
1641
1881
  }
1642
- return toHex(Uint8Array.from(bytes));
1882
+ return toHex2(Uint8Array.from(bytes));
1643
1883
  }
1644
1884
  async function queryNostrEvents(options) {
1645
1885
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1722,7 +1962,7 @@ async function verifyNostrEventSignature(event) {
1722
1962
  event.tags ?? [],
1723
1963
  event.content ?? ""
1724
1964
  ]);
1725
- const computedId = toHex(sha256(new TextEncoder().encode(preimage)));
1965
+ const computedId = toHex2(sha256(new TextEncoder().encode(preimage)));
1726
1966
  if (computedId !== event.id.toLowerCase()) {
1727
1967
  log7.warn(
1728
1968
  { eventId: event.id, computedId },
@@ -1730,7 +1970,7 @@ async function verifyNostrEventSignature(event) {
1730
1970
  );
1731
1971
  return false;
1732
1972
  }
1733
- return schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
1973
+ return schnorr.verify(fromHex2(event.sig), fromHex2(event.id), fromHex2(event.pubkey));
1734
1974
  } catch (err) {
1735
1975
  log7.warn({ err }, "schnorr verify threw");
1736
1976
  return false;
@@ -2090,6 +2330,6 @@ var STATUS_META = {
2090
2330
  }
2091
2331
  };
2092
2332
 
2093
- export { DEFAULT_RELAYS, STATUS_META, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
2333
+ export { DEFAULT_RELAYS, STATUS_META, assembleBindingEnvelope, bindingId, buildBindingEventTemplate, buildBindingMessage, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyBinding, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity, xOnlyHexToNpub };
2094
2334
  //# sourceMappingURL=index.mjs.map
2095
2335
  //# sourceMappingURL=index.mjs.map