@thurinlabs/identity-kit 1.3.7 → 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/README.md CHANGED
@@ -185,7 +185,9 @@ const keyInfo = await parsePgpKey(armoredKey)
185
185
  // → { fingerprint, userIDs, algorithm, created, expires, notations, subkeys } | null
186
186
 
187
187
  const verification = await verifyAttestation({ pgpPublicKey, pgpSignature, fingerprint, ethAddress })
188
- // → { verified: boolean, reason?: string }
188
+ // → { verified, kind, at?, revocationReason?, signingKey?, expiresAt?, algorithm?, reason? }
189
+ // kind: 'verified' | 'expired' | 'signing-key-expired' | 'revoked' | 'compromised'
190
+ // | 'signing-key-revoked' | 'unsupported' | 'bad-signature' (1.4.0)
189
191
 
190
192
  // Prepare a key for publishing: drop every user ID that contains an email address.
191
193
  const stripped = await stripEmailUserIDs(armoredKey)
@@ -194,7 +196,30 @@ const stripped = await stripEmailUserIDs(armoredKey)
194
196
  await hasEmailUserID(armoredKey) // → true if any user ID contains an @
195
197
  ```
196
198
 
197
- **Published identity.** An attestation stores the armored key on-chain, permanently and publicly. Since 0.9.0 the intended shape is a key whose only user ID is a non-email one (any name — `thurin` is the suggestion), carrying the `proof@thurin.id` notations. `stripEmailUserIDs` produces that from a normal export; the stripped key still verifies (`verifyAttestation` needs at least one self-certified user ID, so a key with none is rejected) and keeps the notations on the user ID it retains. Proofs are then read from the on-chain key, never from a keyserver.
199
+ **Published identity.** An attestation stores the armored key on-chain, permanently and publicly. Since 0.9.0 the intended shape is a key whose only user ID is a non-email one (usually the name already on the key, without the email), carrying the `proof@thurin.id` notations. `stripEmailUserIDs` produces that from a normal export; the stripped key still verifies (`verifyAttestation` needs at least one self-certified user ID, so a key with none is rejected) and keeps the notations on the user ID it retains. Proofs are then read from the on-chain key, never from a keyserver.
200
+
201
+ ### Why a claim does or doesn't count (1.4.0)
202
+
203
+ `verifyAttestation` returns a `kind` along with `verified`, so a page can say why in plain words instead of showing a library error. Checked in this order: the key revoked (`compromised` when the owner's reason was compromise), the key expired, the signing subkey revoked, the signing subkey expired; then an algorithm openpgp.js refuses (`unsupported`, e.g. DSA); anything else is `bad-signature`. `at` is the date that goes with it. A verified result carries `expiresAt`: the earlier of the key's and the signing subkey's expiry.
204
+
205
+ ```ts
206
+ import { claimCheckText, expiresSoon, expiresSoonText, claimFates, claimFateText, CLAIM_CHECK_LABEL } from '@thurinlabs/identity-kit'
207
+
208
+ claimCheckText(verification)
209
+ // → { kind: 'expired', label: 'key expired',
210
+ // sentence: 'The key on this claim expired on Mar 5, 2029, so the claim no longer counts.',
211
+ // fix: 'Extend the key, then Update key. No new signature needed.' } // `fix` is for the owner
212
+
213
+ const soon = expiresSoon(verification) // within 30 days → { days, at } | null
214
+ if (soon) expiresSoonText(soon) // 'Key expires in 12 days (Mar 6, 2027).'
215
+
216
+ // Revoked or replaced: reattest revokes and attests in one transaction, so a claim revoked the
217
+ // same second a newer one was created was replaced by it.
218
+ const fates = claimFates(attestations) // Map<index, { state: 'active' | 'revoked' | 'replaced', at?, by? }>
219
+ claimFateText(fates.get(1)!) // 'Replaced by claim #2 on Oct 3, 2026.'
220
+ ```
221
+
222
+ Dates are formatted in UTC ("Mar 5, 2029") so every viewer sees the same day; pass your own formatter as the second argument.
198
223
 
199
224
  Nothing in the kit talks to a keyserver: keys come from the registry, and `thurin keyserver` / keys.thurin.id serve them over HKP for gpg.
200
225
 
@@ -336,7 +361,7 @@ The card talks directly to Ethereum and each proof platform — no intermediary,
336
361
 
337
362
  ## Key algorithms
338
363
 
339
- Any curve openpgp.js can compute is accepted: Ed25519, Cv25519, NIST P-256/384/521, brainpool, RSA, and **secp256k1**. openpgp.js rejects secp256k1 by default because RFC 9580 does not list it; identity-kit removes secp256k1 from `rejectCurves` and leaves the rest of the list alone, since that one entry is a compatibility rule, not a security one (1.3.2; earlier versions cleared the whole set). A secp256k1 PGP key doubles as an Ethereum key (the address is derived from the same public point), so anything that can sign with the PGP key can sign Ethereum transactions: hold such a key if you like, but do not fund its derived address. In Node, openpgp.js needs the `eckey-utils` package for this curve; identity-kit depends on it, so `npm install` brings it in. The browser build needs nothing extra.
364
+ Any curve openpgp.js can compute is accepted: Ed25519, Cv25519, NIST P-256/384/521, brainpool, RSA (2048 bits and up), and **secp256k1**. DSA and short RSA keys are refused by openpgp.js as too weak; since 1.4.0 such a claim reports `kind: 'unsupported'`. openpgp.js rejects secp256k1 by default because RFC 9580 does not list it; identity-kit removes secp256k1 from `rejectCurves` and leaves the rest of the list alone, since that one entry is a compatibility rule, not a security one (1.3.2; earlier versions cleared the whole set). A secp256k1 PGP key doubles as an Ethereum key (the address is derived from the same public point), so anything that can sign with the PGP key can sign Ethereum transactions: hold such a key if you like, but do not fund its derived address. In Node, openpgp.js needs the `eckey-utils` package for this curve; identity-kit depends on it, so `npm install` brings it in. The browser build needs nothing extra.
340
365
 
341
366
  ## Migrating from 0.9.x
342
367
 
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/core/abi.ts
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } async function _asyncOptionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = await fn(value); } else if (op === 'call' || op === 'optionalCall') { value = await fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/core/abi.ts
2
2
  var REGISTRY_ABI = [
3
3
  {
4
4
  "type": "function",
@@ -1392,6 +1392,61 @@ async function verifyClearsigned({ armoredKey, clearsigned }) {
1392
1392
  return { verified: false, reason: err instanceof Error ? err.message : "Verification failed", text: "" };
1393
1393
  }
1394
1394
  }
1395
+ var REVOCATION_REASON = {
1396
+ // 0 = "no reason specified", gpg's default: say nothing rather than "(reason: no reason given)"
1397
+ 1: "replaced by a newer key",
1398
+ 2: "compromised",
1399
+ 3: "no longer used",
1400
+ 32: "a user ID is no longer valid"
1401
+ };
1402
+ var iso = (d) => d instanceof Date ? d.toISOString() : typeof d === "number" && Number.isFinite(d) ? new Date(d).toISOString() : null;
1403
+ function revocationOf(sigs) {
1404
+ const sig = [..._nullishCoalesce(sigs, () => ( []))].sort((a, b) => (_nullishCoalesce(_optionalChain([b, 'access', _13 => _13.created, 'optionalAccess', _14 => _14.getTime, 'optionalCall', _15 => _15()]), () => ( 0))) - (_nullishCoalesce(_optionalChain([a, 'access', _16 => _16.created, 'optionalAccess', _17 => _17.getTime, 'optionalCall', _18 => _18()]), () => ( 0))))[0];
1405
+ const flag = _nullishCoalesce(_optionalChain([sig, 'optionalAccess', _19 => _19.reasonForRevocationFlag]), () => ( null));
1406
+ return {
1407
+ at: iso(_optionalChain([sig, 'optionalAccess', _20 => _20.created])),
1408
+ compromised: flag === 2,
1409
+ revocationReason: _optionalChain([sig, 'optionalAccess', _21 => _21.reasonForRevocationString]) || (flag !== null ? _nullishCoalesce(REVOCATION_REASON[flag], () => ( null)) : null)
1410
+ };
1411
+ }
1412
+ async function keyLife(openpgp, key, clearsigned, config) {
1413
+ const now = /* @__PURE__ */ new Date();
1414
+ let issuer = null;
1415
+ try {
1416
+ issuer = await _asyncNullishCoalesce(await _asyncOptionalChain([(await openpgp.readCleartextMessage({ cleartextMessage: clearsigned })), 'access', async _22 => _22.signature, 'access', async _23 => _23.packets, 'access', async _24 => _24[0], 'optionalAccess', async _25 => _25.issuerKeyID]), async () => ( null));
1417
+ } catch (e4) {
1418
+ }
1419
+ const signer = issuer && !key.getKeyID().equals(issuer) ? _nullishCoalesce(key.subkeys.find((s) => s.getKeyID().equals(issuer)), () => ( null)) : key;
1420
+ const signingKey = signer ? signer.getFingerprint().toUpperCase() : null;
1421
+ const safe = async (f, fallback) => {
1422
+ try {
1423
+ return await f();
1424
+ } catch (e5) {
1425
+ return fallback;
1426
+ }
1427
+ };
1428
+ const keyRevoked = await safe(() => key.isRevoked(void 0, void 0, now, config), false);
1429
+ const keyExp = await safe(() => key.getExpirationTime(void 0, config), null);
1430
+ const isSub = signer && signer !== key;
1431
+ const subRevoked = isSub ? await safe(() => signer.isRevoked(null, key.keyPacket, now, config), false) : false;
1432
+ const subExp = isSub ? await safe(() => signer.getExpirationTime(null, config), null) : null;
1433
+ const finite = (d) => d instanceof Date && Number.isFinite(d.getTime()) ? d : null;
1434
+ const kExp = finite(keyExp), sExp = finite(subExp);
1435
+ let problem = null;
1436
+ if (keyRevoked) {
1437
+ const r = revocationOf(key.revocationSignatures);
1438
+ problem = { kind: r.compromised ? "compromised" : "revoked", at: r.at, revocationReason: r.revocationReason };
1439
+ } else if (kExp && kExp <= now) {
1440
+ problem = { kind: "expired", at: kExp.toISOString() };
1441
+ } else if (subRevoked) {
1442
+ const r = revocationOf(signer.revocationSignatures);
1443
+ problem = { kind: r.compromised ? "compromised" : "signing-key-revoked", at: r.at, revocationReason: r.revocationReason };
1444
+ } else if (sExp && sExp <= now) {
1445
+ problem = { kind: "signing-key-expired", at: sExp.toISOString() };
1446
+ }
1447
+ const earliest = [kExp, sExp].filter(Boolean).sort((a, b) => a - b)[0];
1448
+ return { problem, signingKey, expiresAt: earliest ? earliest.toISOString() : null, algorithm: algorithmName(_nullishCoalesce(signer, () => ( key))) };
1449
+ }
1395
1450
  async function verifyAttestation({
1396
1451
  pgpPublicKey,
1397
1452
  pgpSignature,
@@ -1400,24 +1455,31 @@ async function verifyAttestation({
1400
1455
  }) {
1401
1456
  try {
1402
1457
  if (!pgpPublicKey || !pgpSignature) {
1403
- return { verified: false, reason: "Missing PGP data" };
1458
+ return { verified: false, reason: "Missing PGP data", kind: "bad-signature" };
1404
1459
  }
1405
1460
  const openpgp = await Promise.resolve().then(() => _interopRequireWildcard(require("openpgp")));
1406
1461
  const { readKey } = openpgp;
1407
1462
  const publicKey = await readKey({ armoredKey: pgpPublicKey });
1408
1463
  const keyFingerprint = publicKey.getFingerprint().toUpperCase();
1409
1464
  if (keyFingerprint !== fingerprint.toUpperCase()) {
1410
- return { verified: false, reason: "Key fingerprint mismatch" };
1465
+ return { verified: false, reason: "Key fingerprint mismatch", kind: "bad-signature" };
1411
1466
  }
1467
+ const life = await keyLife(openpgp, publicKey, pgpSignature, pgpConfig(openpgp));
1412
1468
  const v = await verifyClearsigned({ armoredKey: pgpPublicKey, clearsigned: pgpSignature });
1413
- if (!v.verified) return { verified: false, reason: v.reason };
1469
+ if (!v.verified) {
1470
+ if (life.problem) return { verified: false, reason: v.reason, signingKey: life.signingKey, ...life.problem };
1471
+ if (/too weak|reject|unsupported|not supported|unknown (curve|algorithm)/i.test(v.reason || "")) {
1472
+ return { verified: false, reason: v.reason, kind: "unsupported", algorithm: life.algorithm, signingKey: life.signingKey };
1473
+ }
1474
+ return { verified: false, reason: v.reason, kind: "bad-signature", signingKey: life.signingKey };
1475
+ }
1414
1476
  const signedText = v.text;
1415
1477
  if (!signedText.toLowerCase().includes(ethAddress.toLowerCase())) {
1416
- return { verified: false, reason: "Signed message does not contain ETH address" };
1478
+ return { verified: false, reason: "Signed message does not contain ETH address", kind: "bad-signature", signingKey: life.signingKey };
1417
1479
  }
1418
- return { verified: true };
1419
- } catch (e4) {
1420
- return { verified: false, reason: "Signature verification failed" };
1480
+ return { verified: true, kind: "verified", signingKey: life.signingKey, expiresAt: life.expiresAt };
1481
+ } catch (e6) {
1482
+ return { verified: false, reason: "Signature verification failed", kind: "bad-signature" };
1421
1483
  }
1422
1484
  }
1423
1485
  var EMAIL_UID = /@/;
@@ -1428,7 +1490,7 @@ async function stripEmailUserIDs(armoredKey) {
1428
1490
  const kept = [];
1429
1491
  const removed = [];
1430
1492
  const users = key.users.filter((u) => {
1431
- const uid = _optionalChain([u, 'access', _13 => _13.userID, 'optionalAccess', _14 => _14.userID]);
1493
+ const uid = _optionalChain([u, 'access', _26 => _26.userID, 'optionalAccess', _27 => _27.userID]);
1432
1494
  if (!uid) return false;
1433
1495
  if (EMAIL_UID.test(uid)) {
1434
1496
  removed.push(uid);
@@ -1440,7 +1502,7 @@ async function stripEmailUserIDs(armoredKey) {
1440
1502
  if (users.length === 0) return null;
1441
1503
  key.users = users;
1442
1504
  return { armored: key.armor(), kept, removed };
1443
- } catch (e5) {
1505
+ } catch (e7) {
1444
1506
  return null;
1445
1507
  }
1446
1508
  }
@@ -1448,8 +1510,8 @@ async function hasEmailUserID(armoredKey) {
1448
1510
  try {
1449
1511
  const { readKey } = await Promise.resolve().then(() => _interopRequireWildcard(require("openpgp")));
1450
1512
  const key = await readKey({ armoredKey });
1451
- return key.users.some((u) => EMAIL_UID.test(_nullishCoalesce(_optionalChain([u, 'access', _15 => _15.userID, 'optionalAccess', _16 => _16.userID]), () => ( ""))));
1452
- } catch (e6) {
1513
+ return key.users.some((u) => EMAIL_UID.test(_nullishCoalesce(_optionalChain([u, 'access', _28 => _28.userID, 'optionalAccess', _29 => _29.userID]), () => ( ""))));
1514
+ } catch (e8) {
1453
1515
  return false;
1454
1516
  }
1455
1517
  }
@@ -1480,7 +1542,7 @@ async function fetchEFPGraph(address) {
1480
1542
  top8 = (top8Data.following || []).map((f) => f.data);
1481
1543
  }
1482
1544
  return { followers, following, top8, hasEfp: !!listId || followers > 0 };
1483
- } catch (e7) {
1545
+ } catch (e9) {
1484
1546
  return null;
1485
1547
  }
1486
1548
  }
@@ -1577,7 +1639,7 @@ async function verifyGitHub(proof, fingerprint) {
1577
1639
  const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`);
1578
1640
  if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` };
1579
1641
  const data = await resp.json();
1580
- if (_optionalChain([data, 'access', _17 => _17.owner, 'optionalAccess', _18 => _18.login, 'optionalAccess', _19 => _19.toLowerCase, 'call', _20 => _20()]) !== proof.user.toLowerCase()) {
1642
+ if (_optionalChain([data, 'access', _30 => _30.owner, 'optionalAccess', _31 => _31.login, 'optionalAccess', _32 => _32.toLowerCase, 'call', _33 => _33()]) !== proof.user.toLowerCase()) {
1581
1643
  return { verified: false, reason: "Gist owner does not match claimed user" };
1582
1644
  }
1583
1645
  for (const file of Object.values(data.files || {})) {
@@ -1597,7 +1659,7 @@ async function verifyGitHubRepo(proof, fingerprint) {
1597
1659
  );
1598
1660
  if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` };
1599
1661
  const data = await resp.json();
1600
- if (_optionalChain([data, 'access', _21 => _21.owner, 'optionalAccess', _22 => _22.login, 'optionalAccess', _23 => _23.toLowerCase, 'call', _24 => _24()]) !== proof.user.toLowerCase()) {
1662
+ if (_optionalChain([data, 'access', _34 => _34.owner, 'optionalAccess', _35 => _35.login, 'optionalAccess', _36 => _36.toLowerCase, 'call', _37 => _37()]) !== proof.user.toLowerCase()) {
1601
1663
  return { verified: false, reason: "Repository owner does not match claimed account" };
1602
1664
  }
1603
1665
  if (data.description && containsFingerprint(data.description, fingerprint)) {
@@ -1651,7 +1713,7 @@ async function verifyFarcaster(proof, fingerprint, opts = {}) {
1651
1713
  const data = await resp.json();
1652
1714
  for (const msg of data.messages || []) {
1653
1715
  if (msg.hash && msg.hash.startsWith(proof.castHash)) {
1654
- const text = _optionalChain([msg, 'access', _25 => _25.data, 'optionalAccess', _26 => _26.castAddBody, 'optionalAccess', _27 => _27.text]) || "";
1716
+ const text = _optionalChain([msg, 'access', _38 => _38.data, 'optionalAccess', _39 => _39.castAddBody, 'optionalAccess', _40 => _40.text]) || "";
1655
1717
  if (containsFingerprint(text, fingerprint)) {
1656
1718
  return { verified: true };
1657
1719
  }
@@ -1766,19 +1828,19 @@ async function nftAvatarImage(tokenUri, tokenId, fetchFn = fetch) {
1766
1828
  meta = await res.json();
1767
1829
  break;
1768
1830
  }
1769
- } catch (e8) {
1831
+ } catch (e10) {
1770
1832
  }
1771
1833
  }
1772
1834
  if (!meta) return null;
1773
1835
  }
1774
- return avatarUrl(_nullishCoalesce(_optionalChain([meta, 'optionalAccess', _28 => _28.image]), () => ( _optionalChain([meta, 'optionalAccess', _29 => _29.image_url]))));
1836
+ return avatarUrl(_nullishCoalesce(_optionalChain([meta, 'optionalAccess', _41 => _41.image]), () => ( _optionalChain([meta, 'optionalAccess', _42 => _42.image_url]))));
1775
1837
  }
1776
1838
 
1777
1839
  // src/core/ensHint.ts
1778
1840
  var _ens = require('viem/ens');
1779
1841
  var ENS_HINT_KEY = "id.thurin";
1780
1842
  function ensHintFor(record, verifiedFingerprint) {
1781
- const raw = _nullishCoalesce(_optionalChain([record, 'optionalAccess', _30 => _30.trim, 'call', _31 => _31()]), () => ( ""));
1843
+ const raw = _nullishCoalesce(_optionalChain([record, 'optionalAccess', _43 => _43.trim, 'call', _44 => _44()]), () => ( ""));
1782
1844
  const expected = verifiedFingerprint ? normalizeFingerprint(verifiedFingerprint) : null;
1783
1845
  if (!raw) return { key: ENS_HINT_KEY, state: "unset", record: null, fingerprint: null, expected };
1784
1846
  const fingerprint = normalizeFingerprint(raw);
@@ -1947,12 +2009,12 @@ function decodeRecord(hex) {
1947
2009
  }
1948
2010
  function parsePointer(text) {
1949
2011
  const p = JSON.parse(text);
1950
- if (_optionalChain([p, 'optionalAccess', _32 => _32.v]) !== 1 || !Array.isArray(p.releases)) throw new Error("Not a v1 thurin.pointer record");
2012
+ if (_optionalChain([p, 'optionalAccess', _45 => _45.v]) !== 1 || !Array.isArray(p.releases)) throw new Error("Not a v1 thurin.pointer record");
1951
2013
  return p;
1952
2014
  }
1953
2015
  function addPointer(existing, entry) {
1954
2016
  if (!/^[0-9a-f]{64}$/i.test(entry.sha256)) throw new Error("sha256 must be 64 hex characters");
1955
- const releases = [entry, ...(_nullishCoalesce(_optionalChain([existing, 'optionalAccess', _33 => _33.releases]), () => ( []))).filter((r) => r.name !== entry.name)];
2017
+ const releases = [entry, ...(_nullishCoalesce(_optionalChain([existing, 'optionalAccess', _46 => _46.releases]), () => ( []))).filter((r) => r.name !== entry.name)];
1956
2018
  const dropped = [];
1957
2019
  const record = { v: 1, releases };
1958
2020
  while (new TextEncoder().encode(JSON.stringify(record)).length > MAX_RECORD_BYTES && releases.length > 1) dropped.push(releases.pop());
@@ -1986,14 +2048,14 @@ async function parseRecord(kind, text, opts = {}) {
1986
2048
  case "thurin.affiliation": {
1987
2049
  try {
1988
2050
  const j = JSON.parse(t);
1989
- if (_optionalChain([j, 'optionalAccess', _34 => _34.v]) !== 1 || typeof j.with !== "string" || !j.with) return invalid('Not a v1 affiliation: expected {"v":1,"with":"<address or name>"}');
2051
+ if (_optionalChain([j, 'optionalAccess', _47 => _47.v]) !== 1 || typeof j.with !== "string" || !j.with) return invalid('Not a v1 affiliation: expected {"v":1,"with":"<address or name>"}');
1990
2052
  return { ...base, valid: true, data: { type: "affiliation", with: j.with, role: typeof j.role === "string" ? j.role : null } };
1991
- } catch (e9) {
2053
+ } catch (e11) {
1992
2054
  return invalid("Not JSON");
1993
2055
  }
1994
2056
  }
1995
2057
  case "thurin.canary": {
1996
- const date = _optionalChain([t, 'access', _35 => _35.match, 'call', _36 => _36(ISO_DATE), 'optionalAccess', _37 => _37[1]]);
2058
+ const date = _optionalChain([t, 'access', _48 => _48.match, 'call', _49 => _49(ISO_DATE), 'optionalAccess', _50 => _50[1]]);
1997
2059
  if (!date) return invalid("No date in the statement");
1998
2060
  const clearsigned = t.startsWith("-----BEGIN PGP SIGNED MESSAGE-----");
1999
2061
  let statement = clearsigned ? (_nullishCoalesce(t.split(/\r?\n\r?\n/)[1], () => ( ""))).split("-----BEGIN PGP SIGNATURE-----")[0].trim() : t;
@@ -2013,7 +2075,7 @@ async function parseRecord(kind, text, opts = {}) {
2013
2075
  try {
2014
2076
  const { readMessage } = await Promise.resolve().then(() => _interopRequireWildcard(require("openpgp")));
2015
2077
  recipients = (await readMessage({ armoredMessage: t })).getEncryptionKeyIDs().length;
2016
- } catch (e10) {
2078
+ } catch (e12) {
2017
2079
  return invalid("PGP message does not parse");
2018
2080
  }
2019
2081
  return { ...base, valid: true, data: { type: "encrypted", recipients } };
@@ -2021,7 +2083,7 @@ async function parseRecord(kind, text, opts = {}) {
2021
2083
  case "thurin.pointer": {
2022
2084
  try {
2023
2085
  return { ...base, valid: true, data: { type: "pointer", releases: parsePointer(t).releases } };
2024
- } catch (e11) {
2086
+ } catch (e13) {
2025
2087
  return invalid("Not a v1 pointer record");
2026
2088
  }
2027
2089
  }
@@ -2039,6 +2101,102 @@ async function fetchRecords(client, registry, abi, owner, index, kinds = IDENTIT
2039
2101
  return out;
2040
2102
  }
2041
2103
 
2104
+ // src/core/claimStatus.ts
2105
+ var CLAIM_CHECK_LABEL = {
2106
+ verified: "pgp verified",
2107
+ expired: "key expired",
2108
+ "signing-key-expired": "signing key expired",
2109
+ revoked: "key revoked",
2110
+ compromised: "key compromised",
2111
+ "signing-key-revoked": "signing key revoked",
2112
+ unsupported: "not supported",
2113
+ "bad-signature": "doesn't verify"
2114
+ };
2115
+ function formatClaimDate(isoOrSeconds) {
2116
+ if (isoOrSeconds === null || isoOrSeconds === void 0) return "an unknown date";
2117
+ const d = typeof isoOrSeconds === "number" ? new Date(isoOrSeconds * 1e3) : new Date(isoOrSeconds);
2118
+ if (Number.isNaN(d.getTime())) return "an unknown date";
2119
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
2120
+ }
2121
+ function shortFingerprint(fpr) {
2122
+ if (!fpr) return "unknown";
2123
+ const f = fpr.toUpperCase();
2124
+ return `${f.slice(0, 4)} ${f.slice(4, 8)} \u2026`;
2125
+ }
2126
+ function claimCheckText(v, formatDate = formatClaimDate) {
2127
+ const kind = _nullishCoalesce(v.kind, () => ( (v.verified ? "verified" : "bad-signature")));
2128
+ const at = formatDate(v.at);
2129
+ const t = (sentence, fix = null) => ({ kind, label: CLAIM_CHECK_LABEL[kind], sentence, fix });
2130
+ switch (kind) {
2131
+ case "verified":
2132
+ return t("The key signed a line naming this address, and the signature checks out.");
2133
+ case "expired":
2134
+ return t(
2135
+ `The key on this claim expired on ${at}, so the claim no longer counts.`,
2136
+ "Extend the key, then Update key. No new signature needed."
2137
+ );
2138
+ case "signing-key-expired":
2139
+ return t(
2140
+ `The subkey that signed this claim (${shortFingerprint(v.signingKey)}) expired on ${at}.`,
2141
+ "Extend that subkey, then Update key."
2142
+ );
2143
+ case "revoked":
2144
+ return t(
2145
+ `Its owner revoked this PGP key on ${at}${v.revocationReason ? ` (reason: ${v.revocationReason})` : ""}. The claim no longer counts.`,
2146
+ "Revoke this claim and attest your new key."
2147
+ );
2148
+ case "compromised":
2149
+ return t(
2150
+ `Its owner revoked this PGP key on ${at} as compromised. Don't trust anything it signed after that date.`,
2151
+ "Revoke this claim and attest a new key."
2152
+ );
2153
+ case "signing-key-revoked":
2154
+ return t(
2155
+ `The subkey that signed this claim was revoked on ${at}.`,
2156
+ "Replace the claim with a signature from a current key."
2157
+ );
2158
+ case "unsupported":
2159
+ return t(`This key uses an algorithm Thurin doesn't check${v.algorithm ? ` (${v.algorithm})` : ""}. The claim is on-chain, but no lookup will vouch for it.`);
2160
+ case "bad-signature":
2161
+ default:
2162
+ return t("The signature stored with this claim doesn't match its key, so nothing from this claim is shown.");
2163
+ }
2164
+ }
2165
+ function expiresSoon(v, now = Date.now(), withinDays = 30) {
2166
+ if (!_optionalChain([v, 'optionalAccess', _51 => _51.verified]) || !v.expiresAt) return null;
2167
+ const ms = new Date(v.expiresAt).getTime() - now;
2168
+ if (!Number.isFinite(ms) || ms < 0 || ms > withinDays * 864e5) return null;
2169
+ return { days: Math.floor(ms / 864e5), at: v.expiresAt };
2170
+ }
2171
+ function expiresSoonText(soon, formatDate = formatClaimDate) {
2172
+ const when = soon.days === 0 ? "today" : soon.days === 1 ? "in 1 day" : `in ${soon.days} days`;
2173
+ return `Key expires ${when} (${formatDate(soon.at)}).`;
2174
+ }
2175
+ function claimFates(claims) {
2176
+ const out = /* @__PURE__ */ new Map();
2177
+ for (const c of claims) {
2178
+ if (!c.revokedAt) {
2179
+ out.set(c.index, { state: "active" });
2180
+ continue;
2181
+ }
2182
+ const successor = claims.filter((n) => n.index > c.index && n.createdAt === c.revokedAt).sort((a, b) => a.index - b.index)[0];
2183
+ out.set(c.index, successor ? { state: "replaced", at: c.revokedAt, by: successor.index } : { state: "revoked", at: c.revokedAt });
2184
+ }
2185
+ return out;
2186
+ }
2187
+ function claimFateText(fate, formatDate = (s) => formatClaimDate(s)) {
2188
+ if (fate.state === "revoked") return `Revoked by its owner on ${formatDate(fate.at)}.`;
2189
+ if (fate.state === "replaced") return `Replaced by claim #${fate.by} on ${formatDate(fate.at)}.`;
2190
+ return null;
2191
+ }
2192
+
2193
+
2194
+
2195
+
2196
+
2197
+
2198
+
2199
+
2042
2200
 
2043
2201
 
2044
2202
 
@@ -2103,5 +2261,5 @@ async function fetchRecords(client, registry, abi, owner, index, kinds = IDENTIT
2103
2261
 
2104
2262
 
2105
2263
 
2106
- exports.REGISTRY_ABI = REGISTRY_ABI; exports.REGISTRY_ADDRESS = REGISTRY_ADDRESS; exports.NETWORKS = NETWORKS; exports.isNetworkName = isNetworkName; exports.getRegistry = getRegistry; exports.chainFor = chainFor; exports.needsRpcProbe = needsRpcProbe; exports.identityErrorKind = identityErrorKind; exports.IDENTITY_ERROR_TEXT = IDENTITY_ERROR_TEXT; exports.normalizeFingerprint = normalizeFingerprint; exports.fingerprintToBytes = fingerprintToBytes; exports.bytesToFingerprint = bytesToFingerprint; exports.fingerprintHash = fingerprintHash; exports.keyIdOf = keyIdOf; exports.keyIdToBytes = keyIdToBytes; exports.parsePgpKey = parsePgpKey; exports.verifyClearsigned = verifyClearsigned; exports.verifyAttestation = verifyAttestation; exports.stripEmailUserIDs = stripEmailUserIDs; exports.hasEmailUserID = hasEmailUserID; exports.fetchEFPGraph = fetchEFPGraph; exports.identifyProof = identifyProof; exports.displayUrl = displayUrl; exports.proofHref = proofHref; exports.proofSecondaryHref = proofSecondaryHref; exports.FARCASTER_HUB = FARCASTER_HUB; exports.verifyProof = verifyProof; exports.IPFS_GATEWAYS = IPFS_GATEWAYS; exports.IPFS_GATEWAY = IPFS_GATEWAY; exports.ARWEAVE_GATEWAY = ARWEAVE_GATEWAY; exports.avatarUrl = avatarUrl; exports.avatarFallbacks = avatarFallbacks; exports.parseNftAvatar = parseNftAvatar; exports.NFT_AVATAR_ABI = NFT_AVATAR_ABI; exports.nftAvatarImage = nftAvatarImage; exports.ENS_HINT_KEY = ENS_HINT_KEY; exports.ensHintFor = ensHintFor; exports.ensHintValue = ensHintValue; exports.ENS_TEXT_RESOLVER_ABI = ENS_TEXT_RESOLVER_ABI; exports.ensHintWrite = ensHintWrite; exports.fetchEnsHint = fetchEnsHint; exports.EIP712_NAME = EIP712_NAME; exports.EIP712_VERSION = EIP712_VERSION; exports.registryDomain = registryDomain; exports.AUTHORIZATION_TYPES = AUTHORIZATION_TYPES; exports.attestTypedData = attestTypedData; exports.reattestTypedData = reattestTypedData; exports.updateKeyTypedData = updateKeyTypedData; exports.revokeTypedData = revokeTypedData; exports.setRecordTypedData = setRecordTypedData; exports.authorizationDigest = authorizationDigest; exports.recordKind = recordKind; exports.MAX_RECORD_BYTES = MAX_RECORD_BYTES; exports.kindName = kindName; exports.KNOWN_KINDS = KNOWN_KINDS; exports.IDENTITY_KINDS = IDENTITY_KINDS; exports.encodeRecord = encodeRecord; exports.decodeRecord = decodeRecord; exports.parsePointer = parsePointer; exports.addPointer = addPointer; exports.renderPointer = renderPointer; exports.parseRecord = parseRecord; exports.fetchRecords = fetchRecords;
2107
- //# sourceMappingURL=chunk-OIE72R6J.cjs.map
2264
+ exports.REGISTRY_ABI = REGISTRY_ABI; exports.REGISTRY_ADDRESS = REGISTRY_ADDRESS; exports.NETWORKS = NETWORKS; exports.isNetworkName = isNetworkName; exports.getRegistry = getRegistry; exports.chainFor = chainFor; exports.needsRpcProbe = needsRpcProbe; exports.identityErrorKind = identityErrorKind; exports.IDENTITY_ERROR_TEXT = IDENTITY_ERROR_TEXT; exports.normalizeFingerprint = normalizeFingerprint; exports.fingerprintToBytes = fingerprintToBytes; exports.bytesToFingerprint = bytesToFingerprint; exports.fingerprintHash = fingerprintHash; exports.keyIdOf = keyIdOf; exports.keyIdToBytes = keyIdToBytes; exports.parsePgpKey = parsePgpKey; exports.verifyClearsigned = verifyClearsigned; exports.verifyAttestation = verifyAttestation; exports.stripEmailUserIDs = stripEmailUserIDs; exports.hasEmailUserID = hasEmailUserID; exports.fetchEFPGraph = fetchEFPGraph; exports.identifyProof = identifyProof; exports.displayUrl = displayUrl; exports.proofHref = proofHref; exports.proofSecondaryHref = proofSecondaryHref; exports.FARCASTER_HUB = FARCASTER_HUB; exports.verifyProof = verifyProof; exports.IPFS_GATEWAYS = IPFS_GATEWAYS; exports.IPFS_GATEWAY = IPFS_GATEWAY; exports.ARWEAVE_GATEWAY = ARWEAVE_GATEWAY; exports.avatarUrl = avatarUrl; exports.avatarFallbacks = avatarFallbacks; exports.parseNftAvatar = parseNftAvatar; exports.NFT_AVATAR_ABI = NFT_AVATAR_ABI; exports.nftAvatarImage = nftAvatarImage; exports.ENS_HINT_KEY = ENS_HINT_KEY; exports.ensHintFor = ensHintFor; exports.ensHintValue = ensHintValue; exports.ENS_TEXT_RESOLVER_ABI = ENS_TEXT_RESOLVER_ABI; exports.ensHintWrite = ensHintWrite; exports.fetchEnsHint = fetchEnsHint; exports.EIP712_NAME = EIP712_NAME; exports.EIP712_VERSION = EIP712_VERSION; exports.registryDomain = registryDomain; exports.AUTHORIZATION_TYPES = AUTHORIZATION_TYPES; exports.attestTypedData = attestTypedData; exports.reattestTypedData = reattestTypedData; exports.updateKeyTypedData = updateKeyTypedData; exports.revokeTypedData = revokeTypedData; exports.setRecordTypedData = setRecordTypedData; exports.authorizationDigest = authorizationDigest; exports.recordKind = recordKind; exports.MAX_RECORD_BYTES = MAX_RECORD_BYTES; exports.kindName = kindName; exports.KNOWN_KINDS = KNOWN_KINDS; exports.IDENTITY_KINDS = IDENTITY_KINDS; exports.encodeRecord = encodeRecord; exports.decodeRecord = decodeRecord; exports.parsePointer = parsePointer; exports.addPointer = addPointer; exports.renderPointer = renderPointer; exports.parseRecord = parseRecord; exports.fetchRecords = fetchRecords; exports.CLAIM_CHECK_LABEL = CLAIM_CHECK_LABEL; exports.formatClaimDate = formatClaimDate; exports.claimCheckText = claimCheckText; exports.expiresSoon = expiresSoon; exports.expiresSoonText = expiresSoonText; exports.claimFates = claimFates; exports.claimFateText = claimFateText;
2265
+ //# sourceMappingURL=chunk-M2OJDB7I.cjs.map