@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 +28 -3
- package/dist/{chunk-OIE72R6J.cjs → chunk-M2OJDB7I.cjs} +186 -28
- package/dist/chunk-M2OJDB7I.cjs.map +1 -0
- package/dist/{chunk-ROQWHCHP.js → chunk-ZAQ2EPF5.js} +166 -8
- package/dist/chunk-ZAQ2EPF5.js.map +1 -0
- package/dist/core.cjs +16 -2
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +64 -1
- package/dist/core.d.ts +64 -1
- package/dist/core.js +15 -1
- package/dist/embed.global.js +68 -6
- package/dist/embed.global.js.map +1 -1
- package/dist/index.cjs +48 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-OIE72R6J.cjs.map +0 -1
- package/dist/chunk-ROQWHCHP.js.map +0 -1
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
|
|
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 (
|
|
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)
|
|
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 (
|
|
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',
|
|
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 (
|
|
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',
|
|
1452
|
-
} catch (
|
|
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 (
|
|
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',
|
|
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',
|
|
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',
|
|
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 (
|
|
1831
|
+
} catch (e10) {
|
|
1770
1832
|
}
|
|
1771
1833
|
}
|
|
1772
1834
|
if (!meta) return null;
|
|
1773
1835
|
}
|
|
1774
|
-
return avatarUrl(_nullishCoalesce(_optionalChain([meta, 'optionalAccess',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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 (
|
|
2053
|
+
} catch (e11) {
|
|
1992
2054
|
return invalid("Not JSON");
|
|
1993
2055
|
}
|
|
1994
2056
|
}
|
|
1995
2057
|
case "thurin.canary": {
|
|
1996
|
-
const date = _optionalChain([t, 'access',
|
|
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 (
|
|
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 (
|
|
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-
|
|
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
|