@orangecheck/sdk 0.1.1 → 0.1.3

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
@@ -3,6 +3,9 @@
3
3
  var buffer = require('buffer');
4
4
  var bip322Js = require('bip322-js');
5
5
  var bitcoinMessage = require('bitcoinjs-message');
6
+ var secp256k1_js = require('@noble/curves/secp256k1.js');
7
+ var sha2_js = require('@noble/hashes/sha2.js');
8
+ var base = require('@scure/base');
6
9
 
7
10
  function _interopNamespace(e) {
8
11
  if (e && e.__esModule) return e;
@@ -37,15 +40,27 @@ function randomNonce16BHexLower() {
37
40
  }
38
41
  } catch {
39
42
  }
40
- for (let i = 0; i < arr.length; i++) {
41
- arr[i] = Math.floor(Math.random() * 256);
43
+ throw new Error("crypto.getRandomValues unavailable \u2014 refusing to mint a weak nonce");
44
+ }
45
+ var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
46
+ var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
47
+ function assertSafeIdentity({ protocol, identifier }) {
48
+ if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
49
+ throw new Error(
50
+ `invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
51
+ );
52
+ }
53
+ if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
54
+ throw new Error(
55
+ `invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
56
+ );
42
57
  }
43
- return toLowerHex(arr);
44
58
  }
45
59
  function formatIdentities(identities = []) {
46
60
  if (identities.length === 0) {
47
61
  return "";
48
62
  }
63
+ for (const id of identities) assertSafeIdentity(id);
49
64
  const formatted = identities.map(({ protocol, identifier }) => `${protocol}:${identifier}`).sort();
50
65
  return formatted.join(",");
51
66
  }
@@ -53,7 +68,10 @@ function parseIdentities(identitiesStr) {
53
68
  if (!identitiesStr || identitiesStr.trim() === "") {
54
69
  return [];
55
70
  }
56
- return identitiesStr.split(",").map((binding) => {
71
+ if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
72
+ throw new Error("identities field contains forbidden characters (newline)");
73
+ }
74
+ const bindings = identitiesStr.split(",").map((binding) => {
57
75
  const colonIndex = binding.indexOf(":");
58
76
  if (colonIndex === -1) {
59
77
  throw new Error(`Invalid identity binding format: ${binding}`);
@@ -62,7 +80,14 @@ function parseIdentities(identitiesStr) {
62
80
  protocol: binding.substring(0, colonIndex).trim(),
63
81
  identifier: binding.substring(colonIndex + 1).trim()
64
82
  };
65
- }).filter((binding) => binding.protocol && binding.identifier);
83
+ });
84
+ for (const b of bindings) {
85
+ if (!b.protocol || !b.identifier) {
86
+ throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
87
+ }
88
+ assertSafeIdentity(b);
89
+ }
90
+ return bindings;
66
91
  }
67
92
  function buildCanonicalMessage({ address, identities }, extensions = {}) {
68
93
  const nonce = randomNonce16BHexLower();
@@ -230,16 +255,15 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
230
255
  ws = new WebSocket(relayUrl);
231
256
  const wsRef = ws;
232
257
  await new Promise((resolve, reject) => {
233
- const timeout = setTimeout(() => {
258
+ const deadline = setTimeout(() => {
234
259
  wsRef.close();
235
- reject(new Error("Connection timeout"));
236
- }, 5e3);
260
+ reject(new Error("Relay publish timeout"));
261
+ }, 1e4);
237
262
  const cleanup = () => {
238
- clearTimeout(timeout);
263
+ clearTimeout(deadline);
239
264
  wsRef.close();
240
265
  };
241
266
  wsRef.onopen = () => {
242
- clearTimeout(timeout);
243
267
  wsRef.send(JSON.stringify(["EVENT", event]));
244
268
  };
245
269
  wsRef.onmessage = (msg) => {
@@ -277,7 +301,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
277
301
  reject(err);
278
302
  };
279
303
  wsRef.onclose = () => {
280
- clearTimeout(timeout);
304
+ clearTimeout(deadline);
281
305
  };
282
306
  });
283
307
  } catch (err) {
@@ -655,6 +679,24 @@ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT
655
679
  log3.info({ protocol, identifier }, "Getting all attestations for identity");
656
680
  return discoverAttestations({ identity: { protocol, identifier }, relays });
657
681
  }
682
+ function formatIdentitiesForDisplay(identities) {
683
+ return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
684
+ }
685
+ function getVerificationUrl(attestationId, baseUrl = "https://ochk.io") {
686
+ return `${baseUrl}/attest/${attestationId}`;
687
+ }
688
+ function extractAttestationIdFromUrl(url) {
689
+ try {
690
+ const u = new URL(url);
691
+ const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
692
+ if (m) return m[1].toLowerCase();
693
+ const q = u.searchParams.get("id");
694
+ if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
695
+ return null;
696
+ } catch {
697
+ return null;
698
+ }
699
+ }
658
700
 
659
701
  // src/scoring.ts
660
702
  function scoreV0(sats, days) {
@@ -681,9 +723,6 @@ function computeScore(sats, days, config) {
681
723
 
682
724
  // src/verify.ts
683
725
  var log4 = createLogger("ocp/verify");
684
- function scoreV02(sats, days) {
685
- return computeScore(sats, days, { algorithm: "v0" });
686
- }
687
726
  function looksLikeHex(s) {
688
727
  return /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0;
689
728
  }
@@ -799,7 +838,13 @@ async function verifySignature({
799
838
  }
800
839
  function parseCanonicalMessage(msg) {
801
840
  const lines = msg.split("\n").map((l) => l.trim());
802
- const getVal = (prefix) => lines.find((l) => l.toLowerCase().startsWith(prefix + ":"))?.split(":", 2)[1]?.trim();
841
+ const getVal = (prefix) => {
842
+ const needle = prefix + ":";
843
+ const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
844
+ if (!hit) return void 0;
845
+ const idx = hit.indexOf(":");
846
+ return hit.slice(idx + 1).trim();
847
+ };
803
848
  const address = getVal("address") || "";
804
849
  const identitiesStr = getVal("identities") || "";
805
850
  let identities = [];
@@ -854,8 +899,7 @@ async function getAddressUtxos(address, network, opts) {
854
899
  const resp = await fetch(url, {
855
900
  method: "GET",
856
901
  headers: { Accept: "application/json" },
857
- signal: AbortSignal.timeout(1e4)
858
- // 10s timeout
902
+ signal: AbortSignal.timeout(5e3)
859
903
  });
860
904
  if (!resp.ok) {
861
905
  throw new Error(`HTTP ${resp.status}`);
@@ -942,7 +986,7 @@ async function verify2(input, opts = {}) {
942
986
  const metrics = {
943
987
  sats_bonded,
944
988
  days_unspent,
945
- score: scoreV02(sats_bonded, days_unspent)
989
+ score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
946
990
  // ≈ 55.26
947
991
  };
948
992
  codes.push("sig_ok_bip322");
@@ -960,13 +1004,18 @@ async function verify2(input, opts = {}) {
960
1004
  codes.push("network_testmode");
961
1005
  return { ok: false, codes, network, attestation_id };
962
1006
  }
963
- if (opts.expectedAud && parsed.extensions["aud"] && parsed.extensions["aud"] !== opts.expectedAud) {
964
- codes.push("aud_mismatch");
1007
+ if (opts.expectedAud) {
1008
+ const aud = parsed.extensions["aud"];
1009
+ if (!aud || aud !== opts.expectedAud) {
1010
+ codes.push("aud_mismatch");
1011
+ return { ok: false, codes, network, attestation_id };
1012
+ }
965
1013
  }
966
1014
  if (parsed.extensions["expires"]) {
967
1015
  const exp = Date.parse(parsed.extensions["expires"]);
968
1016
  if (!Number.isNaN(exp) && exp < Date.now()) {
969
1017
  codes.push("expired");
1018
+ return { ok: false, codes, network, attestation_id };
970
1019
  }
971
1020
  }
972
1021
  const sigCode = await verifySignature({
@@ -1033,7 +1082,7 @@ async function verify2(input, opts = {}) {
1033
1082
  const metrics = {
1034
1083
  sats_bonded,
1035
1084
  days_unspent,
1036
- score: scoreV02(sats_bonded, days_unspent)
1085
+ score: computeScore(sats_bonded, days_unspent, { algorithm: "v0" })
1037
1086
  };
1038
1087
  return {
1039
1088
  ok: true,
@@ -1044,7 +1093,7 @@ async function verify2(input, opts = {}) {
1044
1093
  metrics
1045
1094
  };
1046
1095
  } catch (utxoError) {
1047
- console.error("UTXO fetch failed:", utxoError);
1096
+ log4.error({ err: utxoError }, "UTXO fetch failed");
1048
1097
  codes.push("bad_request");
1049
1098
  return { ok: false, codes, network, attestation_id };
1050
1099
  }
@@ -1171,16 +1220,20 @@ function parseChallenge(message) {
1171
1220
  throw new Error("not an orangecheck-auth challenge");
1172
1221
  }
1173
1222
  const out = { extensions: [] };
1174
- const coreKeys = /* @__PURE__ */ new Set(["address", "nonce", "issued_at", "expires_at", "ack"]);
1175
- let seenCore = 0;
1223
+ const coreKeys = ["address", "nonce", "issued_at", "expires_at", "ack"];
1224
+ const coreKeySet = new Set(coreKeys);
1225
+ const seenCore = /* @__PURE__ */ new Set();
1176
1226
  for (let i = 1; i < lines.length; i++) {
1177
1227
  const line = lines[i] ?? "";
1178
1228
  const sep = line.indexOf(": ");
1179
1229
  if (sep === -1) continue;
1180
1230
  const key = line.slice(0, sep);
1181
1231
  const val = line.slice(sep + 2);
1182
- if (coreKeys.has(key)) {
1183
- seenCore++;
1232
+ if (coreKeySet.has(key)) {
1233
+ if (seenCore.has(key)) {
1234
+ throw new Error(`duplicate core key in challenge: ${key}`);
1235
+ }
1236
+ seenCore.add(key);
1184
1237
  if (key === "address") out.address = val;
1185
1238
  else if (key === "nonce") out.nonce = val;
1186
1239
  else if (key === "issued_at") out.issuedAt = new Date(val);
@@ -1192,8 +1245,8 @@ function parseChallenge(message) {
1192
1245
  if (key === "purpose") out.purpose = val;
1193
1246
  }
1194
1247
  }
1195
- if (seenCore !== 5) {
1196
- throw new Error(`challenge missing core lines (${seenCore}/5)`);
1248
+ if (seenCore.size !== coreKeys.length) {
1249
+ throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);
1197
1250
  }
1198
1251
  return out;
1199
1252
  }
@@ -1294,9 +1347,25 @@ var identityVerificationLimiter = createRateLimiter(5, 6e4);
1294
1347
 
1295
1348
  // src/identity/dns.ts
1296
1349
  var log5 = createLogger("identity-verification/dns");
1350
+ var PRIVATE_HOST_RE = /^(?:localhost|.*\.local|.*\.internal|.*\.localhost|.*\.lan|.*\.home|.*\.arpa|\[?::1\]?|\[?::\]?)$/i;
1351
+ var PRIVATE_IPV4_RE = /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|0\.|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|22[4-9]\.|23\d\.|24\d\.|25[0-5]\.)/;
1352
+ var WELL_KNOWN_TIMEOUT_MS = 5e3;
1353
+ function isSafePublicHost(host) {
1354
+ const h = host.toLowerCase();
1355
+ if (PRIVATE_HOST_RE.test(h)) return false;
1356
+ if (PRIVATE_IPV4_RE.test(h)) return false;
1357
+ if (h.startsWith("[") || /^[0-9a-f:]+$/.test(h)) return false;
1358
+ return true;
1359
+ }
1297
1360
  async function verifyDnsIdentity(attestationId, domain) {
1298
1361
  log5.info({ attestationId, domain }, "Verifying DNS identity");
1299
1362
  const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
1363
+ if (!isValidDomain(cleanDomain)) {
1364
+ return { verified: false, error: `invalid domain format: ${cleanDomain}` };
1365
+ }
1366
+ if (!isSafePublicHost(cleanDomain)) {
1367
+ return { verified: false, error: "domain resolves to a disallowed private host" };
1368
+ }
1300
1369
  const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
1301
1370
  if (wellKnownResult.verified) {
1302
1371
  return wellKnownResult;
@@ -1308,38 +1377,31 @@ async function verifyDnsIdentity(attestationId, domain) {
1308
1377
  };
1309
1378
  }
1310
1379
  async function checkWellKnownFile(attestationId, domain) {
1311
- const urls = [
1312
- `https://${domain}/.well-known/orangecheck.txt`,
1313
- `http://${domain}/.well-known/orangecheck.txt`
1314
- // Fallback to HTTP
1315
- ];
1316
- for (const url of urls) {
1317
- try {
1318
- log5.info({ url }, "Checking well-known file");
1319
- const response = await fetch(url, {
1320
- method: "GET",
1321
- headers: {
1322
- Accept: "text/plain"
1323
- },
1324
- // Don't follow redirects to other domains
1325
- redirect: "manual"
1326
- });
1327
- if (response.ok) {
1328
- const content = await response.text();
1329
- if (content.includes(attestationId)) {
1330
- log5.info({ domain, url }, "Found attestation ID in well-known file");
1331
- return {
1332
- verified: true,
1333
- method: "well-known",
1334
- url
1335
- };
1336
- } else {
1337
- log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
1338
- }
1380
+ const url = `https://${domain}/.well-known/orangecheck.txt`;
1381
+ try {
1382
+ log5.info({ url }, "Checking well-known file");
1383
+ const response = await fetch(url, {
1384
+ method: "GET",
1385
+ headers: { Accept: "text/plain" },
1386
+ // Don't follow redirects to other domains
1387
+ redirect: "manual",
1388
+ signal: AbortSignal.timeout(WELL_KNOWN_TIMEOUT_MS)
1389
+ });
1390
+ if (response.ok) {
1391
+ const content = await response.text();
1392
+ if (content.includes(attestationId)) {
1393
+ log5.info({ domain, url }, "Found attestation ID in well-known file");
1394
+ return {
1395
+ verified: true,
1396
+ method: "well-known",
1397
+ url
1398
+ };
1399
+ } else {
1400
+ log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
1339
1401
  }
1340
- } catch (err) {
1341
- log5.warn({ error: err, url }, "Failed to fetch well-known file");
1342
1402
  }
1403
+ } catch (err) {
1404
+ log5.warn({ error: err, url }, "Failed to fetch well-known file");
1343
1405
  }
1344
1406
  return {
1345
1407
  verified: false,
@@ -1412,14 +1474,32 @@ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
1412
1474
 
1413
1475
  // src/identity/github.ts
1414
1476
  var log6 = createLogger("identity-verification/github");
1477
+ var GITHUB_GIST_RAW_HOST = "gist.githubusercontent.com";
1478
+ var MAX_GISTS_SCANNED = 30;
1479
+ var MAX_FILES_PER_GIST = 10;
1480
+ var FETCH_TIMEOUT_MS = 5e3;
1481
+ var GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
1482
+ function isSafeGistRawUrl(raw) {
1483
+ try {
1484
+ const u = new URL(raw);
1485
+ return u.protocol === "https:" && u.hostname === GITHUB_GIST_RAW_HOST;
1486
+ } catch {
1487
+ return false;
1488
+ }
1489
+ }
1415
1490
  async function verifyGitHubIdentity(attestationId, username) {
1416
1491
  log6.info({ attestationId, username }, "Verifying GitHub identity");
1492
+ if (!GITHUB_USERNAME_RE.test(username)) {
1493
+ return { verified: false, error: "invalid GitHub username format" };
1494
+ }
1417
1495
  try {
1418
- const response = await fetch(`https://api.github.com/users/${username}/gists`, {
1419
- headers: {
1420
- Accept: "application/vnd.github.v3+json"
1496
+ const response = await fetch(
1497
+ `https://api.github.com/users/${encodeURIComponent(username)}/gists?per_page=${MAX_GISTS_SCANNED}`,
1498
+ {
1499
+ headers: { Accept: "application/vnd.github.v3+json" },
1500
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1421
1501
  }
1422
- });
1502
+ );
1423
1503
  if (!response.ok) {
1424
1504
  if (response.status === 404) {
1425
1505
  return {
@@ -1434,7 +1514,7 @@ async function verifyGitHubIdentity(attestationId, username) {
1434
1514
  }
1435
1515
  const gists = await response.json();
1436
1516
  log6.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
1437
- for (const gist of gists) {
1517
+ for (const gist of gists.slice(0, MAX_GISTS_SCANNED)) {
1438
1518
  if (gist.description?.includes(attestationId)) {
1439
1519
  log6.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
1440
1520
  return {
@@ -1443,7 +1523,8 @@ async function verifyGitHubIdentity(attestationId, username) {
1443
1523
  gistId: gist.id
1444
1524
  };
1445
1525
  }
1446
- for (const [filename, file] of Object.entries(gist.files)) {
1526
+ const files = Object.entries(gist.files).slice(0, MAX_FILES_PER_GIST);
1527
+ for (const [filename, file] of files) {
1447
1528
  if (file.content && file.content.includes(attestationId)) {
1448
1529
  log6.info(
1449
1530
  { username, gistId: gist.id, filename },
@@ -1455,8 +1536,17 @@ async function verifyGitHubIdentity(attestationId, username) {
1455
1536
  gistId: gist.id
1456
1537
  };
1457
1538
  }
1539
+ if (!isSafeGistRawUrl(file.raw_url)) {
1540
+ log6.warn(
1541
+ { filename, rawUrl: file.raw_url },
1542
+ "skipping gist file: raw_url failed allow-list"
1543
+ );
1544
+ continue;
1545
+ }
1458
1546
  try {
1459
- const fileResponse = await fetch(file.raw_url);
1547
+ const fileResponse = await fetch(file.raw_url, {
1548
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1549
+ });
1460
1550
  if (fileResponse.ok) {
1461
1551
  const content = await fileResponse.text();
1462
1552
  if (content.includes(attestationId)) {
@@ -1534,15 +1624,38 @@ function isGitHubIdentityVerified(attestationId, _username, cachedGists) {
1534
1624
  function getGistCreationUrl() {
1535
1625
  return "https://gist.github.com/";
1536
1626
  }
1537
-
1538
- // src/identity/nostr.ts
1539
1627
  var log7 = createLogger("identity-verification/nostr");
1628
+ var HEX_64_RE = /^[0-9a-f]{64}$/;
1629
+ function toHex(bytes) {
1630
+ let out = "";
1631
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
1632
+ return out;
1633
+ }
1634
+ function fromHex(hex) {
1635
+ if (hex.length % 2 !== 0) throw new Error("odd-length hex");
1636
+ const out = new Uint8Array(hex.length / 2);
1637
+ for (let i = 0; i < out.length; i++) {
1638
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1639
+ }
1640
+ return out;
1641
+ }
1540
1642
  function npubToHex(npub) {
1541
- if (npub.match(/^[0-9a-f]{64}$/i)) {
1542
- return npub.toLowerCase();
1643
+ const trimmed = npub.trim();
1644
+ if (HEX_64_RE.test(trimmed.toLowerCase())) {
1645
+ return trimmed.toLowerCase();
1646
+ }
1647
+ if (!trimmed.toLowerCase().startsWith("npub1")) {
1648
+ throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
1543
1649
  }
1544
- log7.warn({ npub }, "npub to hex conversion not fully implemented");
1545
- return npub;
1650
+ const decoded = base.bech32.decode(trimmed.toLowerCase(), 1023);
1651
+ if (decoded.prefix !== "npub") {
1652
+ throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
1653
+ }
1654
+ const bytes = base.bech32.fromWords(decoded.words);
1655
+ if (bytes.length !== 32) {
1656
+ throw new Error(`invalid npub payload length: ${bytes.length}`);
1657
+ }
1658
+ return toHex(Uint8Array.from(bytes));
1546
1659
  }
1547
1660
  async function queryNostrEvents(options) {
1548
1661
  const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
@@ -1613,11 +1726,31 @@ async function queryNostrEvents(options) {
1613
1726
  return events;
1614
1727
  }
1615
1728
  async function verifyNostrEventSignature(event) {
1616
- if (!event.sig || event.sig.length !== 128) {
1729
+ try {
1730
+ if (!event.id || !event.sig || !event.pubkey || typeof event.id !== "string" || typeof event.sig !== "string" || typeof event.pubkey !== "string" || event.sig.length !== 128 || !HEX_64_RE.test(event.id) || !HEX_64_RE.test(event.pubkey)) {
1731
+ return false;
1732
+ }
1733
+ const preimage = JSON.stringify([
1734
+ 0,
1735
+ event.pubkey,
1736
+ event.created_at,
1737
+ event.kind,
1738
+ event.tags ?? [],
1739
+ event.content ?? ""
1740
+ ]);
1741
+ const computedId = toHex(sha2_js.sha256(new TextEncoder().encode(preimage)));
1742
+ if (computedId !== event.id.toLowerCase()) {
1743
+ log7.warn(
1744
+ { eventId: event.id, computedId },
1745
+ "nostr event id does not match its content"
1746
+ );
1747
+ return false;
1748
+ }
1749
+ return secp256k1_js.schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
1750
+ } catch (err) {
1751
+ log7.warn({ err }, "schnorr verify threw");
1617
1752
  return false;
1618
1753
  }
1619
- log7.warn("Nostr signature verification not fully implemented");
1620
- return true;
1621
1754
  }
1622
1755
  async function verifyNostrIdentity(attestationId, npub, relays) {
1623
1756
  log7.info({ attestationId, npub }, "Verifying Nostr identity");
@@ -1982,7 +2115,9 @@ exports.createAttestation = createAttestation;
1982
2115
  exports.createAttestationEnvelope = createAttestationEnvelope;
1983
2116
  exports.createAttestationEvent = createAttestationEvent;
1984
2117
  exports.discoverAttestations = discoverAttestations;
2118
+ exports.extractAttestationIdFromUrl = extractAttestationIdFromUrl;
1985
2119
  exports.formatIdentities = formatIdentities;
2120
+ exports.formatIdentitiesForDisplay = formatIdentitiesForDisplay;
1986
2121
  exports.generateAttestationId = generateAttestationId;
1987
2122
  exports.generateGistTemplate = generateGistTemplate;
1988
2123
  exports.generateTweetText = generateTweetText;
@@ -2001,6 +2136,7 @@ exports.getTwitterComposeUrl = getTwitterComposeUrl;
2001
2136
  exports.getTwitterVerificationInstructions = getTwitterVerificationInstructions;
2002
2137
  exports.getVerificationInstructions = getVerificationInstructions;
2003
2138
  exports.getVerificationSummary = getVerificationSummary;
2139
+ exports.getVerificationUrl = getVerificationUrl;
2004
2140
  exports.getWellKnownUrl = getWellKnownUrl;
2005
2141
  exports.isDnsIdentityVerified = isDnsIdentityVerified;
2006
2142
  exports.isGitHubIdentityVerified = isGitHubIdentityVerified;