@thurinlabs/identity-kit 0.7.1 → 0.7.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/README.md +56 -1
- package/dist/embed.global.js +42 -12
- package/dist/embed.global.js.map +1 -1
- package/dist/index.cjs +55 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +42 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -134,30 +134,46 @@ async function parsePgpKey(armoredKey) {
|
|
|
134
134
|
const { readKey } = await Promise.resolve().then(() => _interopRequireWildcard(require("openpgp")));
|
|
135
135
|
const key = await readKey({ armoredKey });
|
|
136
136
|
const fingerprint = key.getFingerprint().toUpperCase();
|
|
137
|
-
const userIDs = key.users.map((u) => _optionalChain([u, 'access', _2 => _2.userID, 'optionalAccess', _3 => _3.userID])).filter(Boolean);
|
|
138
137
|
const algorithm = String(key.keyPacket.algorithm);
|
|
139
|
-
const created = _nullishCoalesce(_optionalChain([key, 'access',
|
|
138
|
+
const created = _nullishCoalesce(_optionalChain([key, 'access', _2 => _2.keyPacket, 'access', _3 => _3.created, 'optionalAccess', _4 => _4.toISOString, 'call', _5 => _5()]), () => ( null));
|
|
140
139
|
const expiration = await key.getExpirationTime();
|
|
141
140
|
const expires = expiration && expiration !== Infinity ? new Date(expiration).toISOString() : null;
|
|
141
|
+
const userIDs = [];
|
|
142
142
|
const notations = [];
|
|
143
143
|
const seen = /* @__PURE__ */ new Set();
|
|
144
144
|
for (const user of key.users) {
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
const certs = user.selfCertifications;
|
|
146
|
+
if (!certs || certs.length === 0) continue;
|
|
147
|
+
let userVerified = false;
|
|
148
|
+
for (const cert of certs) {
|
|
149
|
+
try {
|
|
150
|
+
await cert.verify(
|
|
151
|
+
key.keyPacket,
|
|
152
|
+
cert.signatureType,
|
|
153
|
+
{ userID: user.userID, key: key.keyPacket }
|
|
154
|
+
);
|
|
155
|
+
} catch (e2) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
userVerified = true;
|
|
147
159
|
if (cert.rawNotations) {
|
|
148
160
|
for (const n of cert.rawNotations) {
|
|
149
161
|
const name = typeof n.name === "string" ? n.name : new TextDecoder().decode(n.name);
|
|
150
162
|
const value = n.value instanceof Uint8Array ? new TextDecoder().decode(n.value) : typeof n.value === "string" ? n.value : null;
|
|
151
163
|
if (value) {
|
|
152
|
-
const
|
|
153
|
-
if (!seen.has(
|
|
154
|
-
seen.add(
|
|
164
|
+
const dedupeKey = `${name}:${value}`;
|
|
165
|
+
if (!seen.has(dedupeKey)) {
|
|
166
|
+
seen.add(dedupeKey);
|
|
155
167
|
notations.push({ name, value });
|
|
156
168
|
}
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
}
|
|
160
172
|
}
|
|
173
|
+
if (userVerified) {
|
|
174
|
+
const uid = _optionalChain([user, 'access', _6 => _6.userID, 'optionalAccess', _7 => _7.userID]);
|
|
175
|
+
if (uid) userIDs.push(uid);
|
|
176
|
+
}
|
|
161
177
|
}
|
|
162
178
|
const subkeys = key.subkeys.map((sk) => ({
|
|
163
179
|
algorithm: String(sk.keyPacket.algorithm),
|
|
@@ -165,7 +181,7 @@ async function parsePgpKey(armoredKey) {
|
|
|
165
181
|
fingerprint: sk.getFingerprint().toUpperCase()
|
|
166
182
|
}));
|
|
167
183
|
return { fingerprint, userIDs, algorithm, created, expires, notations, subkeys };
|
|
168
|
-
} catch (
|
|
184
|
+
} catch (e3) {
|
|
169
185
|
return null;
|
|
170
186
|
}
|
|
171
187
|
}
|
|
@@ -193,7 +209,7 @@ async function verifyAttestation({
|
|
|
193
209
|
return { verified: false, reason: "Signed message does not contain ETH address" };
|
|
194
210
|
}
|
|
195
211
|
return { verified: true };
|
|
196
|
-
} catch (
|
|
212
|
+
} catch (e4) {
|
|
197
213
|
return { verified: false, reason: "Signature verification failed" };
|
|
198
214
|
}
|
|
199
215
|
}
|
|
@@ -203,7 +219,7 @@ async function fetchKeyByFingerprint(fingerprint) {
|
|
|
203
219
|
const resp = await fetch(`${KEYSERVER_BASE}/by-fingerprint/${fingerprint}`);
|
|
204
220
|
if (!resp.ok) return null;
|
|
205
221
|
return await resp.text();
|
|
206
|
-
} catch (
|
|
222
|
+
} catch (e5) {
|
|
207
223
|
return null;
|
|
208
224
|
}
|
|
209
225
|
}
|
|
@@ -212,7 +228,7 @@ async function fetchKeyByKeyId(keyId) {
|
|
|
212
228
|
const resp = await fetch(`${KEYSERVER_BASE}/by-keyid/${keyId}`);
|
|
213
229
|
if (!resp.ok) return null;
|
|
214
230
|
return await resp.text();
|
|
215
|
-
} catch (
|
|
231
|
+
} catch (e6) {
|
|
216
232
|
return null;
|
|
217
233
|
}
|
|
218
234
|
}
|
|
@@ -267,11 +283,13 @@ function useSignetClaims(address) {
|
|
|
267
283
|
queryFn: async () => {
|
|
268
284
|
if (!attestations || !address) return [];
|
|
269
285
|
const results = [];
|
|
286
|
+
const logsByIndex = {};
|
|
287
|
+
for (const l of eventData || []) logsByIndex[Number(l.args.index)] = l;
|
|
270
288
|
for (let i = 0; i < attestations.length; i++) {
|
|
271
289
|
const att = attestations[i];
|
|
272
290
|
if (att.status !== "success") continue;
|
|
273
291
|
const [fingerprint, createdAt, revoked] = att.result;
|
|
274
|
-
const log =
|
|
292
|
+
const log = logsByIndex[i];
|
|
275
293
|
const pgpSignature = _nullishCoalesce(_optionalChain([log, 'optionalAccess', _12 => _12.args, 'optionalAccess', _13 => _13.pgpSignature]), () => ( null));
|
|
276
294
|
const pgpPublicKey = _nullishCoalesce(_optionalChain([log, 'optionalAccess', _14 => _14.args, 'optionalAccess', _15 => _15.pgpPublicKey]), () => ( null));
|
|
277
295
|
const txHash = _nullishCoalesce(_optionalChain([log, 'optionalAccess', _16 => _16.transactionHash]), () => ( null));
|
|
@@ -302,7 +320,8 @@ function useSignetClaims(address) {
|
|
|
302
320
|
});
|
|
303
321
|
const isLoading = countLoading || attestationsLoading || verifyLoading;
|
|
304
322
|
const activeClaims = (claims || []).filter((c) => !c.revoked);
|
|
305
|
-
const
|
|
323
|
+
const verifiedClaims = activeClaims.filter((c) => _optionalChain([c, 'access', _17 => _17.verification, 'optionalAccess', _18 => _18.verified]));
|
|
324
|
+
const currentFingerprint = verifiedClaims.length > 0 ? verifiedClaims[verifiedClaims.length - 1].fingerprint : null;
|
|
306
325
|
return {
|
|
307
326
|
claims: claims || [],
|
|
308
327
|
totalClaims: attestationCount,
|
|
@@ -338,7 +357,7 @@ async function fetchEFPGraph(address) {
|
|
|
338
357
|
top8: (top8Data.following || []).map((f) => f.data),
|
|
339
358
|
hasEfp: true
|
|
340
359
|
};
|
|
341
|
-
} catch (
|
|
360
|
+
} catch (e7) {
|
|
342
361
|
return null;
|
|
343
362
|
}
|
|
344
363
|
}
|
|
@@ -371,7 +390,9 @@ var PROVIDERS = [
|
|
|
371
390
|
{
|
|
372
391
|
provider: "dns",
|
|
373
392
|
label: "DNS",
|
|
374
|
-
|
|
393
|
+
// domain is restricted to a hostname (labels may include underscores for
|
|
394
|
+
// records like _thurin.example.com); rejects slashes, userinfo, and spaces.
|
|
395
|
+
pattern: /^dns:([a-z0-9._-]+)\?type=TXT$/i,
|
|
375
396
|
parse: (m) => ({ domain: m[1] })
|
|
376
397
|
},
|
|
377
398
|
{
|
|
@@ -389,12 +410,15 @@ var PROVIDERS = [
|
|
|
389
410
|
{
|
|
390
411
|
provider: "mastodon",
|
|
391
412
|
label: "Mastodon",
|
|
392
|
-
|
|
413
|
+
// instance is restricted to a bare hostname (optional port) so a value
|
|
414
|
+
// like "mastodon.social@evil.com" can't redirect the lookup elsewhere.
|
|
415
|
+
pattern: /^https:\/\/([a-z0-9.-]+(?::\d+)?)\/@([^/@?#\s]+)$/i,
|
|
393
416
|
parse: (m) => ({ instance: m[1], user: m[2] })
|
|
394
417
|
}
|
|
395
418
|
];
|
|
419
|
+
var PROOF_NAMESPACES = /* @__PURE__ */ new Set(["proof@thurin.id", "proof@ariadne.id"]);
|
|
396
420
|
function identifyProof(notation) {
|
|
397
|
-
if (notation.name
|
|
421
|
+
if (!PROOF_NAMESPACES.has(notation.name)) return null;
|
|
398
422
|
for (const { provider, label, pattern, parse } of PROVIDERS) {
|
|
399
423
|
const m = notation.value.match(pattern);
|
|
400
424
|
if (m) {
|
|
@@ -439,6 +463,9 @@ async function verifyGitHub(proof, fingerprint) {
|
|
|
439
463
|
const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`);
|
|
440
464
|
if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` };
|
|
441
465
|
const data = await resp.json();
|
|
466
|
+
if (_optionalChain([data, 'access', _19 => _19.owner, 'optionalAccess', _20 => _20.login, 'optionalAccess', _21 => _21.toLowerCase, 'call', _22 => _22()]) !== proof.user.toLowerCase()) {
|
|
467
|
+
return { verified: false, reason: "Gist owner does not match claimed user" };
|
|
468
|
+
}
|
|
442
469
|
for (const file of Object.values(data.files || {})) {
|
|
443
470
|
if (file.content && containsFingerprint(file.content, fingerprint)) {
|
|
444
471
|
return { verified: true };
|
|
@@ -458,6 +485,7 @@ async function verifyDNS(proof, fingerprint) {
|
|
|
458
485
|
if (!resp.ok) return { verified: false, reason: `DNS query returned ${resp.status}` };
|
|
459
486
|
const data = await resp.json();
|
|
460
487
|
for (const answer of data.Answer || []) {
|
|
488
|
+
if (answer.type !== 16) continue;
|
|
461
489
|
if (answer.data && containsFingerprint(answer.data, fingerprint)) {
|
|
462
490
|
return { verified: true };
|
|
463
491
|
}
|
|
@@ -493,11 +521,10 @@ async function verifyFarcaster(proof, fingerprint, neynarApiKey) {
|
|
|
493
521
|
const data = await resp.json();
|
|
494
522
|
for (const msg of data.messages || []) {
|
|
495
523
|
if (msg.hash && msg.hash.startsWith(proof.castHash)) {
|
|
496
|
-
const text = _optionalChain([msg, 'access',
|
|
524
|
+
const text = _optionalChain([msg, 'access', _23 => _23.data, 'optionalAccess', _24 => _24.castAddBody, 'optionalAccess', _25 => _25.text]) || "";
|
|
497
525
|
if (containsFingerprint(text, fingerprint)) {
|
|
498
526
|
return { verified: true };
|
|
499
527
|
}
|
|
500
|
-
return { verified: false, reason: "Cast found but fingerprint token not in text" };
|
|
501
528
|
}
|
|
502
529
|
}
|
|
503
530
|
if (!data.nextPageToken) break;
|
|
@@ -525,6 +552,9 @@ async function verifyCodeberg(proof, fingerprint) {
|
|
|
525
552
|
}
|
|
526
553
|
async function verifyMastodon(proof, fingerprint) {
|
|
527
554
|
try {
|
|
555
|
+
if (!proof.instance || !/^[a-z0-9.-]+(?::\d+)?$/i.test(proof.instance)) {
|
|
556
|
+
return { verified: false, reason: "Invalid Mastodon instance host" };
|
|
557
|
+
}
|
|
528
558
|
const resp = await fetch(
|
|
529
559
|
`https://${proof.instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(proof.user)}`
|
|
530
560
|
);
|
|
@@ -594,8 +624,8 @@ function usePGPProofs(fingerprint) {
|
|
|
594
624
|
staleTime: 3e5
|
|
595
625
|
});
|
|
596
626
|
return {
|
|
597
|
-
keyInfo: _nullishCoalesce(_optionalChain([data, 'optionalAccess',
|
|
598
|
-
proofs: _nullishCoalesce(_optionalChain([data, 'optionalAccess',
|
|
627
|
+
keyInfo: _nullishCoalesce(_optionalChain([data, 'optionalAccess', _26 => _26.keyInfo]), () => ( null)),
|
|
628
|
+
proofs: _nullishCoalesce(_optionalChain([data, 'optionalAccess', _27 => _27.proofs]), () => ( [])),
|
|
599
629
|
isLoading
|
|
600
630
|
};
|
|
601
631
|
}
|
|
@@ -607,7 +637,7 @@ function isAddress(value) {
|
|
|
607
637
|
function safeNormalize(name) {
|
|
608
638
|
try {
|
|
609
639
|
return _ens.normalize.call(void 0, name);
|
|
610
|
-
} catch (
|
|
640
|
+
} catch (e8) {
|
|
611
641
|
return void 0;
|
|
612
642
|
}
|
|
613
643
|
}
|
|
@@ -681,7 +711,7 @@ function ScryCard({ ens, address, theme = "thurin" }) {
|
|
|
681
711
|
}
|
|
682
712
|
const displayAddress = identity.address || null;
|
|
683
713
|
const verifiedProofs = identity.proofs.filter((p) => p.status === "verified").length;
|
|
684
|
-
const hasVerifiedPgp = identity.claims.some((c) => _optionalChain([c, 'access',
|
|
714
|
+
const hasVerifiedPgp = identity.claims.some((c) => _optionalChain([c, 'access', _28 => _28.verification, 'optionalAccess', _29 => _29.verified]) && !c.revoked);
|
|
685
715
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "scry-card", "data-scry-theme": theme, children: [
|
|
686
716
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "a", { className: "scry-card-header", href: scryUrl || void 0, target: "_blank", rel: "noopener noreferrer", children: [
|
|
687
717
|
identity.ensAvatar ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
@@ -707,13 +737,13 @@ function ScryCard({ ens, address, theme = "thurin" }) {
|
|
|
707
737
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "scry-card-stat-label", children: "Proofs" })
|
|
708
738
|
] }),
|
|
709
739
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "scry-card-stat", children: [
|
|
710
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "scry-card-stat-value", children: _nullishCoalesce(_optionalChain([identity, 'access',
|
|
740
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "scry-card-stat-value", children: _nullishCoalesce(_optionalChain([identity, 'access', _30 => _30.efp, 'optionalAccess', _31 => _31.followers]), () => ( 0)) }),
|
|
711
741
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "scry-card-stat-label", children: "Followers" })
|
|
712
742
|
] })
|
|
713
743
|
] }),
|
|
714
744
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "scry-card-badges", children: [
|
|
715
745
|
hasVerifiedPgp && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "scry-card-badge scry-card-badge--verified", children: "PGP Verified" }),
|
|
716
|
-
_optionalChain([identity, 'access',
|
|
746
|
+
_optionalChain([identity, 'access', _32 => _32.efp, 'optionalAccess', _33 => _33.hasEfp]) && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "scry-card-badge scry-card-badge--verified", children: "EFP" }),
|
|
717
747
|
identity.proofs.map((p, i) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
718
748
|
"span",
|
|
719
749
|
{
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/benwoodall/Development/thurinlabs/identity-kit/dist/index.cjs","../src/provider.tsx","../src/context.ts","../src/hooks/useScryIdentity.ts","../src/hooks/useSignetClaims.ts","../src/core/contract.ts","../src/core/logs.ts","../src/core/pgp.ts","../src/hooks/useEFPGraph.ts","../src/core/efp.ts","../src/hooks/usePGPProofs.ts","../src/core/proofs.ts","../src/components/ScryCard/ScryCard.tsx"],"names":["key","mainnet","useQuery","jsx"],"mappings":"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","file":"/Users/benwoodall/Development/thurinlabs/identity-kit/dist/index.cjs","sourcesContent":[null,"import { type ReactNode, useMemo } from 'react'\nimport { WagmiProvider, createConfig, http, useConfig } from 'wagmi'\nimport { mainnet } from 'wagmi/chains'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { IdentityKitContext } from './context'\nimport type { IdentityKitConfig } from './core/types'\n\nconst defaultQueryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 60_000,\n retry: 1,\n },\n },\n})\n\nfunction createDefaultWagmiConfig(rpcUrl?: string) {\n return createConfig({\n chains: [mainnet],\n transports: {\n [mainnet.id]: http(rpcUrl || 'https://ethereum-rpc.publicnode.com'),\n },\n })\n}\n\nfunction WagmiDetector({ children, rpcUrl }: { children: ReactNode; rpcUrl?: string }) {\n // Try to use existing wagmi config\n let hasWagmi = false\n try {\n useConfig()\n hasWagmi = true\n } catch {\n // No WagmiProvider above us\n }\n\n const wagmiConfig = useMemo(() => createDefaultWagmiConfig(rpcUrl), [rpcUrl])\n\n if (hasWagmi) {\n return <>{children}</>\n }\n\n return (\n <WagmiProvider config={wagmiConfig}>\n <QueryClientProvider client={defaultQueryClient}>\n {children}\n </QueryClientProvider>\n </WagmiProvider>\n )\n}\n\nexport interface IdentityKitProviderProps extends IdentityKitConfig {\n children: ReactNode\n}\n\nexport function IdentityKitProvider({\n children,\n rpcUrl,\n neynarApiKey,\n scryBaseUrl = 'https://thurin.id',\n}: IdentityKitProviderProps) {\n const config = useMemo(\n () => ({ rpcUrl, neynarApiKey, scryBaseUrl }),\n [rpcUrl, neynarApiKey, scryBaseUrl],\n )\n\n return (\n <IdentityKitContext.Provider value={config}>\n <WagmiDetector rpcUrl={rpcUrl}>\n {children}\n </WagmiDetector>\n </IdentityKitContext.Provider>\n )\n}\n","import { createContext, useContext } from 'react'\nimport type { IdentityKitConfig } from './core/types'\n\nconst defaultConfig: IdentityKitConfig = {\n scryBaseUrl: 'https://thurin.id',\n}\n\nexport const IdentityKitContext = createContext<IdentityKitConfig>(defaultConfig)\n\nexport function useIdentityKitConfig(): IdentityKitConfig {\n return useContext(IdentityKitContext)\n}\n","import { useEnsAddress, useEnsName, useEnsAvatar } from 'wagmi'\nimport { mainnet } from 'wagmi/chains'\nimport { normalize } from 'viem/ens'\nimport { useSignetClaims } from './useSignetClaims'\nimport { useEFPGraph } from './useEFPGraph'\nimport { usePGPProofs } from './usePGPProofs'\nimport type { ScryIdentity } from '../core/types'\n\nfunction isAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value)\n}\n\nfunction safeNormalize(name: string): string | undefined {\n try {\n return normalize(name)\n } catch {\n return undefined\n }\n}\n\nexport function useScryIdentity(ensOrAddress: string | undefined | null): ScryIdentity {\n const isAddr = ensOrAddress ? isAddress(ensOrAddress) : false\n const ensInput = ensOrAddress && !isAddr ? safeNormalize(ensOrAddress) : undefined\n\n // Resolve ENS → address\n const { data: resolvedAddress } = useEnsAddress({\n name: ensInput,\n chainId: mainnet.id,\n query: { enabled: !!ensInput },\n })\n\n const address = isAddr ? ensOrAddress! : resolvedAddress ?? null\n\n // Reverse resolve address → ENS\n const { data: ensName } = useEnsName({\n address: address as `0x${string}` | undefined,\n chainId: mainnet.id,\n query: { enabled: !!address },\n })\n\n const displayName = ensInput || ensName || null\n\n const { data: ensAvatar } = useEnsAvatar({\n name: displayName ? safeNormalize(displayName) : undefined,\n chainId: mainnet.id,\n query: { enabled: !!displayName },\n })\n\n // Signet claims\n const {\n claims,\n totalClaims,\n activeClaims,\n currentFingerprint,\n isLoading: claimsLoading,\n } = useSignetClaims(address)\n\n // PGP proofs (from current fingerprint)\n const {\n keyInfo: pgpKeyInfo,\n proofs,\n isLoading: proofsLoading,\n } = usePGPProofs(currentFingerprint)\n\n // EFP social graph\n const { efp, isLoading: efpLoading } = useEFPGraph(address)\n\n return {\n address,\n ensName: displayName,\n ensAvatar: ensAvatar ?? null,\n claims,\n totalClaims,\n activeClaims,\n currentFingerprint,\n pgpKeyInfo,\n proofs,\n efp,\n isLoading: claimsLoading || proofsLoading || efpLoading,\n error: null,\n }\n}\n","import { useReadContract, useReadContracts } from 'wagmi'\nimport { mainnet } from 'wagmi/chains'\nimport { useQuery } from '@tanstack/react-query'\nimport { createPublicClient, http, parseAbiItem } from 'viem'\nimport { REGISTRY_ADDRESS, REGISTRY_ABI, CONTRACT_DEPLOY_BLOCK } from '../core/contract'\nimport { getLogsChunked } from '../core/logs'\nimport { verifyAttestation } from '../core/pgp'\nimport { useIdentityKitConfig } from '../context'\nimport type { SignetClaim } from '../core/types'\n\nexport function useSignetClaims(address: string | undefined | null) {\n const config = useIdentityKitConfig()\n\n const { data: count, isLoading: countLoading } = useReadContract({\n address: REGISTRY_ADDRESS,\n abi: REGISTRY_ABI,\n functionName: 'attestationCount',\n args: address ? [address as `0x${string}`] : undefined,\n chainId: mainnet.id,\n query: { enabled: !!address },\n })\n\n const attestationCount = count ? Number(count) : 0\n\n const contracts = address\n ? Array.from({ length: attestationCount }, (_, i) => ({\n address: REGISTRY_ADDRESS as `0x${string}`,\n abi: REGISTRY_ABI,\n functionName: 'getAttestation' as const,\n args: [address as `0x${string}`, BigInt(i)] as const,\n chainId: mainnet.id,\n }))\n : []\n\n const { data: attestations, isLoading: attestationsLoading } = useReadContracts({\n contracts,\n query: { enabled: contracts.length > 0 },\n })\n\n // Fetch event logs for PGP data (signature + public key not available from contract reads)\n const { data: eventData } = useQuery({\n queryKey: ['signet-logs', address],\n queryFn: async () => {\n if (!address) return []\n const client = createPublicClient({\n chain: mainnet,\n transport: http(config.rpcUrl || 'https://ethereum-rpc.publicnode.com'),\n })\n const event = parseAbiItem(\n 'event Attested(address indexed ethAddress, string indexed fingerprintHash, string fingerprint, string pgpSignature, string pgpPublicKey, uint256 index, uint256 timestamp)',\n )\n return getLogsChunked(client, {\n address: REGISTRY_ADDRESS,\n event,\n args: { ethAddress: address as `0x${string}` },\n fromBlock: CONTRACT_DEPLOY_BLOCK,\n toBlock: 'latest',\n })\n },\n enabled: !!address && attestationCount > 0,\n staleTime: 300_000,\n })\n\n // Combine attestation data with event log data and verify\n const { data: claims, isLoading: verifyLoading } = useQuery({\n queryKey: ['signet-claims', address, attestationCount, !!eventData],\n queryFn: async (): Promise<SignetClaim[]> => {\n if (!attestations || !address) return []\n\n const results: SignetClaim[] = []\n\n for (let i = 0; i < attestations.length; i++) {\n const att = attestations[i]\n if (att.status !== 'success') continue\n\n const [fingerprint, createdAt, revoked] = att.result as [string, bigint, boolean]\n const log = (eventData || [])[i]\n\n const pgpSignature = log?.args?.pgpSignature ?? null\n const pgpPublicKey = log?.args?.pgpPublicKey ?? null\n const txHash = log?.transactionHash ?? null\n\n let verification = null\n if (pgpPublicKey && pgpSignature) {\n verification = await verifyAttestation({\n pgpPublicKey,\n pgpSignature,\n fingerprint,\n ethAddress: address,\n })\n }\n\n results.push({\n index: i,\n fingerprint,\n createdAt: Number(createdAt),\n revoked,\n pgpSignature,\n pgpPublicKey,\n txHash,\n verification,\n })\n }\n\n return results\n },\n enabled: !!attestations && attestations.length > 0,\n staleTime: 300_000,\n })\n\n const isLoading = countLoading || attestationsLoading || verifyLoading\n\n const activeClaims = (claims || []).filter((c) => !c.revoked)\n const currentFingerprint = activeClaims.length > 0\n ? activeClaims[activeClaims.length - 1].fingerprint\n : null\n\n return {\n claims: claims || [],\n totalClaims: attestationCount,\n activeClaims: activeClaims.length,\n currentFingerprint,\n isLoading,\n }\n}\n","export const REGISTRY_ADDRESS = '0xf7a45BC662A78a6fb417ED5f52b3766cbf13EbBb' as const\n\nexport const CONTRACT_DEPLOY_BLOCK = 24515891n\n\nexport const REGISTRY_ABI = [\n {\n name: 'Attested',\n type: 'event',\n inputs: [\n { name: 'ethAddress', type: 'address', indexed: true },\n { name: 'fingerprintHash', type: 'string', indexed: true },\n { name: 'fingerprint', type: 'string', indexed: false },\n { name: 'pgpSignature', type: 'string', indexed: false },\n { name: 'pgpPublicKey', type: 'string', indexed: false },\n { name: 'index', type: 'uint256', indexed: false },\n { name: 'timestamp', type: 'uint256', indexed: false },\n ],\n },\n {\n name: 'attestationCount',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'addr', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'getAttestation',\n type: 'function',\n stateMutability: 'view',\n inputs: [\n { name: 'addr', type: 'address' },\n { name: 'index', type: 'uint256' },\n ],\n outputs: [\n { name: 'fingerprint', type: 'string' },\n { name: 'createdAt', type: 'uint256' },\n { name: 'revoked', type: 'bool' },\n ],\n },\n] as const\n","import { type PublicClient } from 'viem'\nimport { CONTRACT_DEPLOY_BLOCK } from './contract'\n\nconst LOG_CHUNK_SIZE = 49999n\n\nexport async function getLogsChunked(\n client: PublicClient,\n params: {\n address?: `0x${string}`\n event?: any\n args?: Record<string, any>\n fromBlock?: bigint\n toBlock?: bigint | 'latest'\n },\n) {\n const latest = await client.getBlockNumber()\n const fromBlock = params.fromBlock ?? CONTRACT_DEPLOY_BLOCK\n const toBlock = params.toBlock === 'latest' || !params.toBlock ? latest : params.toBlock\n\n const allLogs: any[] = []\n\n for (let start = fromBlock; start <= toBlock; start += LOG_CHUNK_SIZE) {\n const end = start + LOG_CHUNK_SIZE - 1n > toBlock ? toBlock : start + LOG_CHUNK_SIZE - 1n\n const logs = await client.getLogs({\n ...params,\n fromBlock: start,\n toBlock: end,\n })\n allLogs.push(...logs)\n }\n\n return allLogs\n}\n","import type { PGPKeyInfo, PGPVerification } from './types'\n\nexport async function parsePgpKey(armoredKey: string): Promise<PGPKeyInfo | null> {\n try {\n const { readKey } = await import('openpgp')\n const key = await readKey({ armoredKey })\n const fingerprint = key.getFingerprint().toUpperCase()\n const userIDs = key.users.map((u: any) => u.userID?.userID).filter(Boolean)\n const algorithm = String(key.keyPacket.algorithm)\n const created = key.keyPacket.created?.toISOString() ?? null\n const expiration = await key.getExpirationTime()\n const expires =\n expiration && expiration !== Infinity\n ? new Date(expiration as number).toISOString()\n : null\n\n const notations: { name: string; value: string }[] = []\n const seen = new Set<string>()\n for (const user of key.users) {\n if (!(user as any).selfCertifications) continue\n for (const cert of (user as any).selfCertifications) {\n if (cert.rawNotations) {\n for (const n of cert.rawNotations) {\n const name =\n typeof n.name === 'string' ? n.name : new TextDecoder().decode(n.name)\n const value =\n n.value instanceof Uint8Array\n ? new TextDecoder().decode(n.value)\n : typeof n.value === 'string'\n ? n.value\n : null\n if (value) {\n const key = `${name}:${value}`\n if (!seen.has(key)) {\n seen.add(key)\n notations.push({ name, value })\n }\n }\n }\n }\n }\n }\n\n const subkeys = key.subkeys.map((sk: any) => ({\n algorithm: String(sk.keyPacket.algorithm),\n created: sk.keyPacket.created?.toISOString() ?? null,\n fingerprint: sk.getFingerprint().toUpperCase(),\n }))\n\n return { fingerprint, userIDs, algorithm, created, expires, notations, subkeys }\n } catch {\n return null\n }\n}\n\nexport async function verifyAttestation({\n pgpPublicKey,\n pgpSignature,\n fingerprint,\n ethAddress,\n}: {\n pgpPublicKey: string | null\n pgpSignature: string | null\n fingerprint: string\n ethAddress: string\n}): Promise<PGPVerification> {\n try {\n if (!pgpPublicKey || !pgpSignature) {\n return { verified: false, reason: 'Missing PGP data' }\n }\n\n const { readKey, readCleartextMessage, verify } = await import('openpgp')\n\n const publicKey = await readKey({ armoredKey: pgpPublicKey })\n const keyFingerprint = publicKey.getFingerprint().toUpperCase()\n if (keyFingerprint !== fingerprint.toUpperCase()) {\n return { verified: false, reason: 'Key fingerprint mismatch' }\n }\n\n const message = await readCleartextMessage({ cleartextMessage: pgpSignature })\n const { signatures } = await verify({ message, verificationKeys: publicKey })\n await signatures[0].verified\n\n const signedText = message.getText()\n if (!signedText.toLowerCase().includes(ethAddress.toLowerCase())) {\n return { verified: false, reason: 'Signed message does not contain ETH address' }\n }\n\n return { verified: true }\n } catch {\n return { verified: false, reason: 'Signature verification failed' }\n }\n}\n\nconst KEYSERVER_BASE = 'https://keys.openpgp.org/vks/v1'\n\nexport async function fetchKeyByFingerprint(fingerprint: string): Promise<string | null> {\n try {\n const resp = await fetch(`${KEYSERVER_BASE}/by-fingerprint/${fingerprint}`)\n if (!resp.ok) return null\n return await resp.text()\n } catch {\n return null\n }\n}\n\nexport async function fetchKeyByKeyId(keyId: string): Promise<string | null> {\n try {\n const resp = await fetch(`${KEYSERVER_BASE}/by-keyid/${keyId}`)\n if (!resp.ok) return null\n return await resp.text()\n } catch {\n return null\n }\n}\n","import { useQuery } from '@tanstack/react-query'\nimport { fetchEFPGraph } from '../core/efp'\nimport type { EFPGraph } from '../core/types'\n\nexport function useEFPGraph(address: string | undefined | null) {\n const { data, isLoading } = useQuery<EFPGraph | null>({\n queryKey: ['efp-graph', address],\n queryFn: () => fetchEFPGraph(address!),\n enabled: !!address,\n staleTime: 300_000,\n })\n\n return {\n efp: data ?? null,\n isLoading,\n }\n}\n","import type { EFPGraph } from './types'\n\nconst EFP_API = 'https://api.ethfollow.xyz/api/v1'\n\nexport async function fetchEFPGraph(address: string): Promise<EFPGraph | null> {\n try {\n const listsResp = await fetch(`${EFP_API}/users/${address}/lists`)\n if (!listsResp.ok) return null\n const listsData = await listsResp.json()\n const listId = listsData.primary_list\n if (!listId) return null\n\n const [followingResp, statsResp, top8Resp] = await Promise.all([\n fetch(`${EFP_API}/lists/${listId}/following?limit=100`),\n fetch(`${EFP_API}/users/${address}/stats`),\n fetch(`${EFP_API}/lists/${listId}/following?limit=8&tags=top8`),\n ])\n\n const followingData = followingResp.ok ? await followingResp.json() : { following: [] }\n const statsData = statsResp.ok ? await statsResp.json() : {}\n const top8Data = top8Resp.ok ? await top8Resp.json() : { following: [] }\n\n return {\n followers: parseInt(statsData.followers_count) || 0,\n following: (followingData.following || []).length,\n top8: (top8Data.following || []).map((f: any) => f.data),\n hasEfp: true,\n }\n } catch {\n return null\n }\n}\n","import { useQuery } from '@tanstack/react-query'\nimport { fetchKeyByFingerprint, parsePgpKey } from '../core/pgp'\nimport { identifyProof, verifyProof, displayUrl, proofHref, proofSecondaryHref } from '../core/proofs'\nimport { useIdentityKitConfig } from '../context'\nimport type { PGPKeyInfo, ProofResult } from '../core/types'\n\nexport function usePGPProofs(fingerprint: string | undefined | null) {\n const config = useIdentityKitConfig()\n\n const { data, isLoading } = useQuery({\n queryKey: ['pgp-proofs', fingerprint],\n queryFn: async (): Promise<{ keyInfo: PGPKeyInfo | null; proofs: ProofResult[] }> => {\n if (!fingerprint) return { keyInfo: null, proofs: [] }\n\n const armoredKey = await fetchKeyByFingerprint(fingerprint)\n if (!armoredKey) return { keyInfo: null, proofs: [] }\n\n const keyInfo = await parsePgpKey(armoredKey)\n if (!keyInfo) return { keyInfo: null, proofs: [] }\n\n // Identify proofs from notations\n const identified = keyInfo.notations\n .map((n) => identifyProof(n))\n .filter((p): p is NonNullable<typeof p> => p !== null)\n\n // Verify all proofs in parallel\n const proofs: ProofResult[] = await Promise.all(\n identified.map(async (proof) => {\n const result = await verifyProof(proof, fingerprint, config.neynarApiKey)\n return {\n provider: proof.provider,\n label: proof.label,\n url: proof.url,\n displayUrl: displayUrl(proof),\n href: proofHref(proof),\n secondaryHref: proofSecondaryHref(proof),\n status: result.verified ? 'verified' as const : 'unverified' as const,\n reason: result.reason,\n }\n }),\n )\n\n return { keyInfo, proofs }\n },\n enabled: !!fingerprint,\n staleTime: 300_000,\n })\n\n return {\n keyInfo: data?.keyInfo ?? null,\n proofs: data?.proofs ?? [],\n isLoading,\n }\n}\n","import type { Notation, Proof, PGPVerification } from './types'\n\nconst PROVIDERS: {\n provider: string\n label: string\n pattern: RegExp\n parse: (m: RegExpMatchArray) => Record<string, string>\n}[] = [\n {\n provider: 'github',\n label: 'GitHub',\n pattern: /^https:\\/\\/gist\\.github\\.com\\/([^/]+)\\/([a-f0-9]+)$/i,\n parse: (m) => ({ user: m[1], gistId: m[2] }),\n },\n {\n provider: 'dns',\n label: 'DNS',\n pattern: /^dns:([^?]+)\\?type=TXT$/i,\n parse: (m) => ({ domain: m[1] }),\n },\n {\n provider: 'farcaster',\n label: 'Farcaster',\n pattern: /^https:\\/\\/farcaster\\.xyz\\/([^/]+)\\/(0x[a-f0-9]+)$/i,\n parse: (m) => ({ user: m[1], castHash: m[2] }),\n },\n {\n provider: 'codeberg',\n label: 'Codeberg',\n pattern: /^https:\\/\\/codeberg\\.org\\/([^/]+)\\/([^/]+)$/i,\n parse: (m) => ({ user: m[1], repo: m[2] }),\n },\n {\n provider: 'mastodon',\n label: 'Mastodon',\n pattern: /^https:\\/\\/([^/]+)\\/@([^/]+)$/i,\n parse: (m) => ({ instance: m[1], user: m[2] }),\n },\n]\n\nexport function identifyProof(notation: Notation): Proof | null {\n if (notation.name !== 'proof@thurin.id') return null\n\n for (const { provider, label, pattern, parse } of PROVIDERS) {\n const m = notation.value.match(pattern)\n if (m) {\n return { provider, label, url: notation.value, ...parse(m) }\n }\n }\n\n return { provider: 'unknown', label: 'Unknown', url: notation.value }\n}\n\nexport function displayUrl(proof: Proof): string {\n if (proof.provider === 'dns') return proof.domain!\n if (proof.provider === 'github') return proof.user!\n if (proof.provider === 'farcaster') return `@${proof.user}`\n if (proof.provider === 'codeberg') return proof.user!\n if (proof.provider === 'mastodon') return `@${proof.user}@${proof.instance}`\n return proof.url\n}\n\nexport function proofHref(proof: Proof): string | null {\n if (proof.provider === 'dns') return `https://${proof.domain}`\n if (proof.provider === 'github') return `https://github.com/${proof.user}`\n if (proof.provider === 'farcaster') return `https://farcaster.xyz/${proof.user}`\n if (proof.provider === 'codeberg') return `https://codeberg.org/${proof.user}`\n if (proof.provider === 'mastodon') return `https://${proof.instance}/@${proof.user}`\n if (proof.url.startsWith('http')) return proof.url\n return null\n}\n\nexport function proofSecondaryHref(proof: Proof): string | null {\n if (proof.provider === 'github') return proof.url\n if (proof.provider === 'farcaster') return proof.url\n if (proof.provider === 'codeberg') return proof.url\n if (proof.provider === 'mastodon') return proof.url\n return null\n}\n\n// ─── Verification ──────────────────────────────────────────────────────────\n\nconst FPR_TOKEN = 'OPENPGP4FPR:'\n\nfunction containsFingerprint(text: string, fingerprint: string): boolean {\n return text.toUpperCase().includes(FPR_TOKEN + fingerprint.toUpperCase())\n}\n\nfunction containsFingerprintUrl(text: string, fingerprint: string): boolean {\n return text.toUpperCase().includes(fingerprint.toUpperCase())\n}\n\nasync function verifyGitHub(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`)\n if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` }\n const data = await resp.json()\n\n for (const file of Object.values(data.files || {}) as any[]) {\n if (file.content && containsFingerprint(file.content, fingerprint)) {\n return { verified: true }\n }\n }\n return { verified: false, reason: 'Fingerprint token not found in gist' }\n } catch (err: any) {\n return { verified: false, reason: `GitHub fetch failed: ${err.message}` }\n }\n}\n\nasync function verifyDNS(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(\n `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(proof.domain!)}&type=TXT`,\n { headers: { Accept: 'application/dns-json' } },\n )\n if (!resp.ok) return { verified: false, reason: `DNS query returned ${resp.status}` }\n const data = await resp.json()\n\n for (const answer of data.Answer || []) {\n if (answer.data && containsFingerprint(answer.data, fingerprint)) {\n return { verified: true }\n }\n }\n return { verified: false, reason: 'Fingerprint token not found in TXT records' }\n } catch (err: any) {\n return { verified: false, reason: `DNS fetch failed: ${err.message}` }\n }\n}\n\nconst NEYNAR_HUB = 'https://hub-api.neynar.com'\n\nasync function resolveFid(\n username: string,\n apiKey: string,\n): Promise<number | null> {\n const resp = await fetch(\n `${NEYNAR_HUB}/v1/userNameProofByName?name=${encodeURIComponent(username)}`,\n { headers: { 'x-api-key': apiKey } },\n )\n if (!resp.ok) return null\n const data = await resp.json()\n return data.fid ?? null\n}\n\nasync function verifyFarcaster(\n proof: Proof,\n fingerprint: string,\n neynarApiKey?: string,\n): Promise<PGPVerification> {\n if (!neynarApiKey) {\n return { verified: false, reason: 'Farcaster verification requires a Neynar API key' }\n }\n\n try {\n const fid = await resolveFid(proof.user!, neynarApiKey)\n if (!fid) return { verified: false, reason: `Could not resolve Farcaster user \"${proof.user}\"` }\n\n const headers = { 'x-api-key': neynarApiKey }\n let pageToken = ''\n for (let page = 0; page < 5; page++) {\n const url = `${NEYNAR_HUB}/v1/castsByFid?fid=${fid}&pageSize=100&reverse=true${pageToken ? `&pageToken=${pageToken}` : ''}`\n const resp = await fetch(url, { headers })\n if (!resp.ok) return { verified: false, reason: `Farcaster Hub returned ${resp.status}` }\n const data = await resp.json()\n\n for (const msg of data.messages || []) {\n if (msg.hash && msg.hash.startsWith(proof.castHash!)) {\n const text = msg.data?.castAddBody?.text || ''\n if (containsFingerprint(text, fingerprint)) {\n return { verified: true }\n }\n return { verified: false, reason: 'Cast found but fingerprint token not in text' }\n }\n }\n\n if (!data.nextPageToken) break\n pageToken = data.nextPageToken\n }\n\n return { verified: false, reason: 'Cast not found' }\n } catch (err: any) {\n return { verified: false, reason: `Farcaster fetch failed: ${err.message}` }\n }\n}\n\nasync function verifyCodeberg(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(\n `https://codeberg.org/api/v1/repos/${encodeURIComponent(proof.user!)}/${encodeURIComponent(proof.repo!)}`,\n )\n if (!resp.ok) return { verified: false, reason: `Codeberg API returned ${resp.status}` }\n const data = await resp.json()\n\n if (data.description && containsFingerprint(data.description, fingerprint)) {\n return { verified: true }\n }\n\n return { verified: false, reason: 'Fingerprint token not found in repo description' }\n } catch (err: any) {\n return { verified: false, reason: `Codeberg fetch failed: ${err.message}` }\n }\n}\n\nasync function verifyMastodon(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(\n `https://${proof.instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(proof.user!)}`,\n )\n if (!resp.ok) return { verified: false, reason: `Mastodon API returned ${resp.status}` }\n const data = await resp.json()\n\n const keyId = fingerprint.slice(-16)\n\n for (const field of data.fields || []) {\n const text = field.value.replace(/<[^>]*>/g, '')\n if (containsFingerprintUrl(text, fingerprint) || containsFingerprintUrl(text, keyId)) {\n return { verified: true }\n }\n }\n\n if (data.note) {\n const text = data.note.replace(/<[^>]*>/g, '')\n if (containsFingerprintUrl(text, fingerprint) || containsFingerprintUrl(text, keyId)) {\n return { verified: true }\n }\n }\n\n return { verified: false, reason: 'Fingerprint not found in profile fields or bio' }\n } catch (err: any) {\n return { verified: false, reason: `Mastodon fetch failed: ${err.message}` }\n }\n}\n\ntype Verifier = (proof: Proof, fingerprint: string, neynarApiKey?: string) => Promise<PGPVerification>\n\nconst verifiers: Record<string, Verifier> = {\n github: verifyGitHub,\n dns: verifyDNS,\n farcaster: verifyFarcaster,\n codeberg: verifyCodeberg,\n mastodon: verifyMastodon,\n}\n\nexport async function verifyProof(\n proof: Proof,\n fingerprint: string,\n neynarApiKey?: string,\n): Promise<PGPVerification> {\n const fn = verifiers[proof.provider]\n if (!fn) return { verified: false, reason: 'Unknown provider' }\n return fn(proof, fingerprint, neynarApiKey)\n}\n","import { useScryIdentity } from '../../hooks/useScryIdentity'\nimport { useIdentityKitConfig } from '../../context'\nimport type { Theme } from '../../core/types'\nimport '../../themes/index.css'\nimport './ScryCard.css'\n\nfunction ThurinLogo() {\n return (\n <svg className=\"scry-card-avatar-placeholder\" viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"var(--scry-surface-deep)\" />\n <path d=\"M25 80 Q25 25 50 25 Q75 25 75 50\" fill=\"none\" stroke=\"#7c9a3e\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n <path d=\"M33 75 Q33 35 50 35 Q67 35 67 52\" fill=\"none\" stroke=\"#7c9a3e\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n <path d=\"M41 70 Q41 45 50 45 Q59 45 59 55\" fill=\"none\" stroke=\"#c9a227\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n <path d=\"M50 65 L50 53\" fill=\"none\" stroke=\"#c9a227\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n </svg>\n )\n}\n\nexport interface ScryCardProps {\n ens?: string\n address?: string\n theme?: Theme\n}\n\nexport function ScryCard({ ens, address, theme = 'thurin' }: ScryCardProps) {\n const input = ens || address\n const identity = useScryIdentity(input)\n const config = useIdentityKitConfig()\n\n const scryUrl = identity.address\n ? `${config.scryBaseUrl}/eth/${identity.address}`\n : ens\n ? `${config.scryBaseUrl}/ens/${ens}`\n : null\n\n if (identity.isLoading) {\n return (\n <div className=\"scry-card\" data-scry-theme={theme}>\n <div className=\"scry-card-loading\">Loading identity...</div>\n </div>\n )\n }\n\n const displayAddress = identity.address || null\n\n const verifiedProofs = identity.proofs.filter((p) => p.status === 'verified').length\n const hasVerifiedPgp = identity.claims.some((c) => c.verification?.verified && !c.revoked)\n\n return (\n <div className=\"scry-card\" data-scry-theme={theme}>\n <a className=\"scry-card-header\" href={scryUrl || undefined} target=\"_blank\" rel=\"noopener noreferrer\">\n {identity.ensAvatar ? (\n <img\n className=\"scry-card-avatar\"\n src={identity.ensAvatar}\n alt={identity.ensName || ''}\n />\n ) : (\n <ThurinLogo />\n )}\n <div>\n {identity.ensName && (\n <div className=\"scry-card-name\">{identity.ensName}</div>\n )}\n {displayAddress && (\n <div className=\"scry-card-address\">{displayAddress}</div>\n )}\n </div>\n </a>\n\n <div className=\"scry-card-stats\">\n <div className=\"scry-card-stat\">\n <span className=\"scry-card-stat-value\">{identity.activeClaims}</span>\n <span className=\"scry-card-stat-label\">Seals</span>\n </div>\n <div className=\"scry-card-stat\">\n <span className=\"scry-card-stat-value\">{verifiedProofs}</span>\n <span className=\"scry-card-stat-label\">Proofs</span>\n </div>\n <div className=\"scry-card-stat\">\n <span className=\"scry-card-stat-value\">\n {identity.efp?.followers ?? 0}\n </span>\n <span className=\"scry-card-stat-label\">Followers</span>\n </div>\n </div>\n\n <div className=\"scry-card-badges\">\n {hasVerifiedPgp && (\n <span className=\"scry-card-badge scry-card-badge--verified\">PGP Verified</span>\n )}\n {identity.efp?.hasEfp && (\n <span className=\"scry-card-badge scry-card-badge--verified\">EFP</span>\n )}\n {identity.proofs.map((p, i) => (\n <span\n key={i}\n className={`scry-card-badge scry-card-badge--${p.status === 'verified' ? 'verified' : 'unverified'}`}\n >\n {p.label}\n </span>\n ))}\n </div>\n\n {scryUrl && (\n <a\n className=\"scry-card-link\"\n href={scryUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n View on Scry\n </a>\n )}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/benwoodall/Development/thurinlabs/identity-kit/dist/index.cjs","../src/provider.tsx","../src/context.ts","../src/hooks/useScryIdentity.ts","../src/hooks/useSignetClaims.ts","../src/core/contract.ts","../src/core/logs.ts","../src/core/pgp.ts","../src/hooks/useEFPGraph.ts","../src/core/efp.ts","../src/hooks/usePGPProofs.ts","../src/core/proofs.ts","../src/components/ScryCard/ScryCard.tsx"],"names":["mainnet","useQuery","jsx"],"mappings":"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","file":"/Users/benwoodall/Development/thurinlabs/identity-kit/dist/index.cjs","sourcesContent":[null,"import { type ReactNode, useMemo } from 'react'\nimport { WagmiProvider, createConfig, http, useConfig } from 'wagmi'\nimport { mainnet } from 'wagmi/chains'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { IdentityKitContext } from './context'\nimport type { IdentityKitConfig } from './core/types'\n\nconst defaultQueryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 60_000,\n retry: 1,\n },\n },\n})\n\nfunction createDefaultWagmiConfig(rpcUrl?: string) {\n return createConfig({\n chains: [mainnet],\n transports: {\n [mainnet.id]: http(rpcUrl || 'https://ethereum-rpc.publicnode.com'),\n },\n })\n}\n\nfunction WagmiDetector({ children, rpcUrl }: { children: ReactNode; rpcUrl?: string }) {\n // Try to use existing wagmi config\n let hasWagmi = false\n try {\n useConfig()\n hasWagmi = true\n } catch {\n // No WagmiProvider above us\n }\n\n const wagmiConfig = useMemo(() => createDefaultWagmiConfig(rpcUrl), [rpcUrl])\n\n if (hasWagmi) {\n return <>{children}</>\n }\n\n return (\n <WagmiProvider config={wagmiConfig}>\n <QueryClientProvider client={defaultQueryClient}>\n {children}\n </QueryClientProvider>\n </WagmiProvider>\n )\n}\n\nexport interface IdentityKitProviderProps extends IdentityKitConfig {\n children: ReactNode\n}\n\nexport function IdentityKitProvider({\n children,\n rpcUrl,\n neynarApiKey,\n scryBaseUrl = 'https://thurin.id',\n}: IdentityKitProviderProps) {\n const config = useMemo(\n () => ({ rpcUrl, neynarApiKey, scryBaseUrl }),\n [rpcUrl, neynarApiKey, scryBaseUrl],\n )\n\n return (\n <IdentityKitContext.Provider value={config}>\n <WagmiDetector rpcUrl={rpcUrl}>\n {children}\n </WagmiDetector>\n </IdentityKitContext.Provider>\n )\n}\n","import { createContext, useContext } from 'react'\nimport type { IdentityKitConfig } from './core/types'\n\nconst defaultConfig: IdentityKitConfig = {\n scryBaseUrl: 'https://thurin.id',\n}\n\nexport const IdentityKitContext = createContext<IdentityKitConfig>(defaultConfig)\n\nexport function useIdentityKitConfig(): IdentityKitConfig {\n return useContext(IdentityKitContext)\n}\n","import { useEnsAddress, useEnsName, useEnsAvatar } from 'wagmi'\nimport { mainnet } from 'wagmi/chains'\nimport { normalize } from 'viem/ens'\nimport { useSignetClaims } from './useSignetClaims'\nimport { useEFPGraph } from './useEFPGraph'\nimport { usePGPProofs } from './usePGPProofs'\nimport type { ScryIdentity } from '../core/types'\n\nfunction isAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value)\n}\n\nfunction safeNormalize(name: string): string | undefined {\n try {\n return normalize(name)\n } catch {\n return undefined\n }\n}\n\nexport function useScryIdentity(ensOrAddress: string | undefined | null): ScryIdentity {\n const isAddr = ensOrAddress ? isAddress(ensOrAddress) : false\n const ensInput = ensOrAddress && !isAddr ? safeNormalize(ensOrAddress) : undefined\n\n // Resolve ENS → address\n const { data: resolvedAddress } = useEnsAddress({\n name: ensInput,\n chainId: mainnet.id,\n query: { enabled: !!ensInput },\n })\n\n const address = isAddr ? ensOrAddress! : resolvedAddress ?? null\n\n // Reverse resolve address → ENS\n const { data: ensName } = useEnsName({\n address: address as `0x${string}` | undefined,\n chainId: mainnet.id,\n query: { enabled: !!address },\n })\n\n const displayName = ensInput || ensName || null\n\n const { data: ensAvatar } = useEnsAvatar({\n name: displayName ? safeNormalize(displayName) : undefined,\n chainId: mainnet.id,\n query: { enabled: !!displayName },\n })\n\n // Signet claims\n const {\n claims,\n totalClaims,\n activeClaims,\n currentFingerprint,\n isLoading: claimsLoading,\n } = useSignetClaims(address)\n\n // PGP proofs (from current fingerprint)\n const {\n keyInfo: pgpKeyInfo,\n proofs,\n isLoading: proofsLoading,\n } = usePGPProofs(currentFingerprint)\n\n // EFP social graph\n const { efp, isLoading: efpLoading } = useEFPGraph(address)\n\n return {\n address,\n ensName: displayName,\n ensAvatar: ensAvatar ?? null,\n claims,\n totalClaims,\n activeClaims,\n currentFingerprint,\n pgpKeyInfo,\n proofs,\n efp,\n isLoading: claimsLoading || proofsLoading || efpLoading,\n error: null,\n }\n}\n","import { useReadContract, useReadContracts } from 'wagmi'\nimport { mainnet } from 'wagmi/chains'\nimport { useQuery } from '@tanstack/react-query'\nimport { createPublicClient, http, parseAbiItem } from 'viem'\nimport { REGISTRY_ADDRESS, REGISTRY_ABI, CONTRACT_DEPLOY_BLOCK } from '../core/contract'\nimport { getLogsChunked } from '../core/logs'\nimport { verifyAttestation } from '../core/pgp'\nimport { useIdentityKitConfig } from '../context'\nimport type { SignetClaim } from '../core/types'\n\nexport function useSignetClaims(address: string | undefined | null) {\n const config = useIdentityKitConfig()\n\n const { data: count, isLoading: countLoading } = useReadContract({\n address: REGISTRY_ADDRESS,\n abi: REGISTRY_ABI,\n functionName: 'attestationCount',\n args: address ? [address as `0x${string}`] : undefined,\n chainId: mainnet.id,\n query: { enabled: !!address },\n })\n\n const attestationCount = count ? Number(count) : 0\n\n const contracts = address\n ? Array.from({ length: attestationCount }, (_, i) => ({\n address: REGISTRY_ADDRESS as `0x${string}`,\n abi: REGISTRY_ABI,\n functionName: 'getAttestation' as const,\n args: [address as `0x${string}`, BigInt(i)] as const,\n chainId: mainnet.id,\n }))\n : []\n\n const { data: attestations, isLoading: attestationsLoading } = useReadContracts({\n contracts,\n query: { enabled: contracts.length > 0 },\n })\n\n // Fetch event logs for PGP data (signature + public key not available from contract reads)\n const { data: eventData } = useQuery({\n queryKey: ['signet-logs', address],\n queryFn: async () => {\n if (!address) return []\n const client = createPublicClient({\n chain: mainnet,\n transport: http(config.rpcUrl || 'https://ethereum-rpc.publicnode.com'),\n })\n const event = parseAbiItem(\n 'event Attested(address indexed ethAddress, string indexed fingerprintHash, string fingerprint, string pgpSignature, string pgpPublicKey, uint256 index, uint256 timestamp)',\n )\n return getLogsChunked(client, {\n address: REGISTRY_ADDRESS,\n event,\n args: { ethAddress: address as `0x${string}` },\n fromBlock: CONTRACT_DEPLOY_BLOCK,\n toBlock: 'latest',\n })\n },\n enabled: !!address && attestationCount > 0,\n staleTime: 300_000,\n })\n\n // Combine attestation data with event log data and verify\n const { data: claims, isLoading: verifyLoading } = useQuery({\n queryKey: ['signet-claims', address, attestationCount, !!eventData],\n queryFn: async (): Promise<SignetClaim[]> => {\n if (!attestations || !address) return []\n\n const results: SignetClaim[] = []\n\n // Match each event log to its attestation by the on-chain index it\n // carries. The log array is block-ordered, not attestation-indexed, so\n // positional access can pair the wrong PGP signature/key with a claim,\n // which then fails verification.\n const logsByIndex: Record<number, any> = {}\n for (const l of eventData || []) logsByIndex[Number(l.args.index)] = l\n\n for (let i = 0; i < attestations.length; i++) {\n const att = attestations[i]\n if (att.status !== 'success') continue\n\n const [fingerprint, createdAt, revoked] = att.result as [string, bigint, boolean]\n const log = logsByIndex[i]\n\n const pgpSignature = log?.args?.pgpSignature ?? null\n const pgpPublicKey = log?.args?.pgpPublicKey ?? null\n const txHash = log?.transactionHash ?? null\n\n let verification = null\n if (pgpPublicKey && pgpSignature) {\n verification = await verifyAttestation({\n pgpPublicKey,\n pgpSignature,\n fingerprint,\n ethAddress: address,\n })\n }\n\n results.push({\n index: i,\n fingerprint,\n createdAt: Number(createdAt),\n revoked,\n pgpSignature,\n pgpPublicKey,\n txHash,\n verification,\n })\n }\n\n return results\n },\n enabled: !!attestations && attestations.length > 0,\n staleTime: 300_000,\n })\n\n const isLoading = countLoading || attestationsLoading || verifyLoading\n\n const activeClaims = (claims || []).filter((c) => !c.revoked)\n // The current identity is the latest claim that is both non-revoked and has\n // a verified signature binding the key to this address. A claim whose\n // signature has not verified must not surface the key's proofs or user data.\n const verifiedClaims = activeClaims.filter((c) => c.verification?.verified)\n const currentFingerprint = verifiedClaims.length > 0\n ? verifiedClaims[verifiedClaims.length - 1].fingerprint\n : null\n\n return {\n claims: claims || [],\n totalClaims: attestationCount,\n activeClaims: activeClaims.length,\n currentFingerprint,\n isLoading,\n }\n}\n","export const REGISTRY_ADDRESS = '0xf7a45BC662A78a6fb417ED5f52b3766cbf13EbBb' as const\n\nexport const CONTRACT_DEPLOY_BLOCK = 24515891n\n\nexport const REGISTRY_ABI = [\n {\n name: 'Attested',\n type: 'event',\n inputs: [\n { name: 'ethAddress', type: 'address', indexed: true },\n { name: 'fingerprintHash', type: 'string', indexed: true },\n { name: 'fingerprint', type: 'string', indexed: false },\n { name: 'pgpSignature', type: 'string', indexed: false },\n { name: 'pgpPublicKey', type: 'string', indexed: false },\n { name: 'index', type: 'uint256', indexed: false },\n { name: 'timestamp', type: 'uint256', indexed: false },\n ],\n },\n {\n name: 'attestationCount',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'addr', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'getAttestation',\n type: 'function',\n stateMutability: 'view',\n inputs: [\n { name: 'addr', type: 'address' },\n { name: 'index', type: 'uint256' },\n ],\n outputs: [\n { name: 'fingerprint', type: 'string' },\n { name: 'createdAt', type: 'uint256' },\n { name: 'revoked', type: 'bool' },\n ],\n },\n] as const\n","import { type PublicClient } from 'viem'\nimport { CONTRACT_DEPLOY_BLOCK } from './contract'\n\nconst LOG_CHUNK_SIZE = 49999n\n\nexport async function getLogsChunked(\n client: PublicClient,\n params: {\n address?: `0x${string}`\n event?: any\n args?: Record<string, any>\n fromBlock?: bigint\n toBlock?: bigint | 'latest'\n },\n) {\n const latest = await client.getBlockNumber()\n const fromBlock = params.fromBlock ?? CONTRACT_DEPLOY_BLOCK\n const toBlock = params.toBlock === 'latest' || !params.toBlock ? latest : params.toBlock\n\n const allLogs: any[] = []\n\n for (let start = fromBlock; start <= toBlock; start += LOG_CHUNK_SIZE) {\n const end = start + LOG_CHUNK_SIZE - 1n > toBlock ? toBlock : start + LOG_CHUNK_SIZE - 1n\n const logs = await client.getLogs({\n ...params,\n fromBlock: start,\n toBlock: end,\n })\n allLogs.push(...logs)\n }\n\n return allLogs\n}\n","import type { PGPKeyInfo, PGPVerification } from './types'\n\nexport async function parsePgpKey(armoredKey: string): Promise<PGPKeyInfo | null> {\n try {\n const { readKey } = await import('openpgp')\n const key = await readKey({ armoredKey })\n const fingerprint = key.getFingerprint().toUpperCase()\n const algorithm = String(key.keyPacket.algorithm)\n const created = key.keyPacket.created?.toISOString() ?? null\n const expiration = await key.getExpirationTime()\n const expires =\n expiration && expiration !== Infinity\n ? new Date(expiration as number).toISOString()\n : null\n\n // openpgp does not verify certification signatures when a key is parsed —\n // it groups them by issuer key ID only. A key's fingerprint also covers\n // only the primary key packet, not its user IDs or their certifications.\n // Each self-certification is therefore verified against the primary key\n // here before its user ID and notations are treated as authoritative.\n const userIDs: string[] = []\n const notations: { name: string; value: string }[] = []\n const seen = new Set<string>()\n for (const user of key.users) {\n const certs = (user as any).selfCertifications\n if (!certs || certs.length === 0) continue\n\n let userVerified = false\n for (const cert of certs) {\n try {\n await cert.verify(\n key.keyPacket,\n cert.signatureType,\n { userID: (user as any).userID, key: key.keyPacket },\n )\n } catch {\n continue // signature not made by this key — ignore this certification\n }\n userVerified = true\n\n if (cert.rawNotations) {\n for (const n of cert.rawNotations) {\n const name =\n typeof n.name === 'string' ? n.name : new TextDecoder().decode(n.name)\n const value =\n n.value instanceof Uint8Array\n ? new TextDecoder().decode(n.value)\n : typeof n.value === 'string'\n ? n.value\n : null\n if (value) {\n const dedupeKey = `${name}:${value}`\n if (!seen.has(dedupeKey)) {\n seen.add(dedupeKey)\n notations.push({ name, value })\n }\n }\n }\n }\n }\n\n if (userVerified) {\n const uid = (user as any).userID?.userID\n if (uid) userIDs.push(uid)\n }\n }\n\n const subkeys = key.subkeys.map((sk: any) => ({\n algorithm: String(sk.keyPacket.algorithm),\n created: sk.keyPacket.created?.toISOString() ?? null,\n fingerprint: sk.getFingerprint().toUpperCase(),\n }))\n\n return { fingerprint, userIDs, algorithm, created, expires, notations, subkeys }\n } catch {\n return null\n }\n}\n\nexport async function verifyAttestation({\n pgpPublicKey,\n pgpSignature,\n fingerprint,\n ethAddress,\n}: {\n pgpPublicKey: string | null\n pgpSignature: string | null\n fingerprint: string\n ethAddress: string\n}): Promise<PGPVerification> {\n try {\n if (!pgpPublicKey || !pgpSignature) {\n return { verified: false, reason: 'Missing PGP data' }\n }\n\n const { readKey, readCleartextMessage, verify } = await import('openpgp')\n\n const publicKey = await readKey({ armoredKey: pgpPublicKey })\n const keyFingerprint = publicKey.getFingerprint().toUpperCase()\n if (keyFingerprint !== fingerprint.toUpperCase()) {\n return { verified: false, reason: 'Key fingerprint mismatch' }\n }\n\n const message = await readCleartextMessage({ cleartextMessage: pgpSignature })\n const { signatures } = await verify({ message, verificationKeys: publicKey })\n await signatures[0].verified\n\n const signedText = message.getText()\n if (!signedText.toLowerCase().includes(ethAddress.toLowerCase())) {\n return { verified: false, reason: 'Signed message does not contain ETH address' }\n }\n\n return { verified: true }\n } catch {\n return { verified: false, reason: 'Signature verification failed' }\n }\n}\n\nconst KEYSERVER_BASE = 'https://keys.openpgp.org/vks/v1'\n\nexport async function fetchKeyByFingerprint(fingerprint: string): Promise<string | null> {\n try {\n const resp = await fetch(`${KEYSERVER_BASE}/by-fingerprint/${fingerprint}`)\n if (!resp.ok) return null\n return await resp.text()\n } catch {\n return null\n }\n}\n\nexport async function fetchKeyByKeyId(keyId: string): Promise<string | null> {\n try {\n const resp = await fetch(`${KEYSERVER_BASE}/by-keyid/${keyId}`)\n if (!resp.ok) return null\n return await resp.text()\n } catch {\n return null\n }\n}\n","import { useQuery } from '@tanstack/react-query'\nimport { fetchEFPGraph } from '../core/efp'\nimport type { EFPGraph } from '../core/types'\n\nexport function useEFPGraph(address: string | undefined | null) {\n const { data, isLoading } = useQuery<EFPGraph | null>({\n queryKey: ['efp-graph', address],\n queryFn: () => fetchEFPGraph(address!),\n enabled: !!address,\n staleTime: 300_000,\n })\n\n return {\n efp: data ?? null,\n isLoading,\n }\n}\n","import type { EFPGraph } from './types'\n\nconst EFP_API = 'https://api.ethfollow.xyz/api/v1'\n\nexport async function fetchEFPGraph(address: string): Promise<EFPGraph | null> {\n try {\n const listsResp = await fetch(`${EFP_API}/users/${address}/lists`)\n if (!listsResp.ok) return null\n const listsData = await listsResp.json()\n const listId = listsData.primary_list\n if (!listId) return null\n\n const [followingResp, statsResp, top8Resp] = await Promise.all([\n fetch(`${EFP_API}/lists/${listId}/following?limit=100`),\n fetch(`${EFP_API}/users/${address}/stats`),\n fetch(`${EFP_API}/lists/${listId}/following?limit=8&tags=top8`),\n ])\n\n const followingData = followingResp.ok ? await followingResp.json() : { following: [] }\n const statsData = statsResp.ok ? await statsResp.json() : {}\n const top8Data = top8Resp.ok ? await top8Resp.json() : { following: [] }\n\n return {\n followers: parseInt(statsData.followers_count) || 0,\n following: (followingData.following || []).length,\n top8: (top8Data.following || []).map((f: any) => f.data),\n hasEfp: true,\n }\n } catch {\n return null\n }\n}\n","import { useQuery } from '@tanstack/react-query'\nimport { fetchKeyByFingerprint, parsePgpKey } from '../core/pgp'\nimport { identifyProof, verifyProof, displayUrl, proofHref, proofSecondaryHref } from '../core/proofs'\nimport { useIdentityKitConfig } from '../context'\nimport type { PGPKeyInfo, ProofResult } from '../core/types'\n\nexport function usePGPProofs(fingerprint: string | undefined | null) {\n const config = useIdentityKitConfig()\n\n const { data, isLoading } = useQuery({\n queryKey: ['pgp-proofs', fingerprint],\n queryFn: async (): Promise<{ keyInfo: PGPKeyInfo | null; proofs: ProofResult[] }> => {\n if (!fingerprint) return { keyInfo: null, proofs: [] }\n\n const armoredKey = await fetchKeyByFingerprint(fingerprint)\n if (!armoredKey) return { keyInfo: null, proofs: [] }\n\n const keyInfo = await parsePgpKey(armoredKey)\n if (!keyInfo) return { keyInfo: null, proofs: [] }\n\n // Identify proofs from notations\n const identified = keyInfo.notations\n .map((n) => identifyProof(n))\n .filter((p): p is NonNullable<typeof p> => p !== null)\n\n // Verify all proofs in parallel\n const proofs: ProofResult[] = await Promise.all(\n identified.map(async (proof) => {\n const result = await verifyProof(proof, fingerprint, config.neynarApiKey)\n return {\n provider: proof.provider,\n label: proof.label,\n url: proof.url,\n displayUrl: displayUrl(proof),\n href: proofHref(proof),\n secondaryHref: proofSecondaryHref(proof),\n status: result.verified ? 'verified' as const : 'unverified' as const,\n reason: result.reason,\n }\n }),\n )\n\n return { keyInfo, proofs }\n },\n enabled: !!fingerprint,\n staleTime: 300_000,\n })\n\n return {\n keyInfo: data?.keyInfo ?? null,\n proofs: data?.proofs ?? [],\n isLoading,\n }\n}\n","import type { Notation, Proof, PGPVerification } from './types'\n\nconst PROVIDERS: {\n provider: string\n label: string\n pattern: RegExp\n parse: (m: RegExpMatchArray) => Record<string, string>\n}[] = [\n {\n provider: 'github',\n label: 'GitHub',\n pattern: /^https:\\/\\/gist\\.github\\.com\\/([^/]+)\\/([a-f0-9]+)$/i,\n parse: (m) => ({ user: m[1], gistId: m[2] }),\n },\n {\n provider: 'dns',\n label: 'DNS',\n // domain is restricted to a hostname (labels may include underscores for\n // records like _thurin.example.com); rejects slashes, userinfo, and spaces.\n pattern: /^dns:([a-z0-9._-]+)\\?type=TXT$/i,\n parse: (m) => ({ domain: m[1] }),\n },\n {\n provider: 'farcaster',\n label: 'Farcaster',\n pattern: /^https:\\/\\/farcaster\\.xyz\\/([^/]+)\\/(0x[a-f0-9]+)$/i,\n parse: (m) => ({ user: m[1], castHash: m[2] }),\n },\n {\n provider: 'codeberg',\n label: 'Codeberg',\n pattern: /^https:\\/\\/codeberg\\.org\\/([^/]+)\\/([^/]+)$/i,\n parse: (m) => ({ user: m[1], repo: m[2] }),\n },\n {\n provider: 'mastodon',\n label: 'Mastodon',\n // instance is restricted to a bare hostname (optional port) so a value\n // like \"mastodon.social@evil.com\" can't redirect the lookup elsewhere.\n pattern: /^https:\\/\\/([a-z0-9.-]+(?::\\d+)?)\\/@([^/@?#\\s]+)$/i,\n parse: (m) => ({ instance: m[1], user: m[2] }),\n },\n]\n\n// Thurin's own notation namespace plus the Keyoxide/Ariadne standard, which\n// Thurin is interoperable with — keys created with either are recognized.\nconst PROOF_NAMESPACES = new Set(['proof@thurin.id', 'proof@ariadne.id'])\n\nexport function identifyProof(notation: Notation): Proof | null {\n if (!PROOF_NAMESPACES.has(notation.name)) return null\n\n for (const { provider, label, pattern, parse } of PROVIDERS) {\n const m = notation.value.match(pattern)\n if (m) {\n return { provider, label, url: notation.value, ...parse(m) }\n }\n }\n\n return { provider: 'unknown', label: 'Unknown', url: notation.value }\n}\n\nexport function displayUrl(proof: Proof): string {\n if (proof.provider === 'dns') return proof.domain!\n if (proof.provider === 'github') return proof.user!\n if (proof.provider === 'farcaster') return `@${proof.user}`\n if (proof.provider === 'codeberg') return proof.user!\n if (proof.provider === 'mastodon') return `@${proof.user}@${proof.instance}`\n return proof.url\n}\n\nexport function proofHref(proof: Proof): string | null {\n if (proof.provider === 'dns') return `https://${proof.domain}`\n if (proof.provider === 'github') return `https://github.com/${proof.user}`\n if (proof.provider === 'farcaster') return `https://farcaster.xyz/${proof.user}`\n if (proof.provider === 'codeberg') return `https://codeberg.org/${proof.user}`\n if (proof.provider === 'mastodon') return `https://${proof.instance}/@${proof.user}`\n if (proof.url.startsWith('http')) return proof.url\n return null\n}\n\nexport function proofSecondaryHref(proof: Proof): string | null {\n if (proof.provider === 'github') return proof.url\n if (proof.provider === 'farcaster') return proof.url\n if (proof.provider === 'codeberg') return proof.url\n if (proof.provider === 'mastodon') return proof.url\n return null\n}\n\n// ─── Verification ──────────────────────────────────────────────────────────\n\nconst FPR_TOKEN = 'OPENPGP4FPR:'\n\nfunction containsFingerprint(text: string, fingerprint: string): boolean {\n return text.toUpperCase().includes(FPR_TOKEN + fingerprint.toUpperCase())\n}\n\nfunction containsFingerprintUrl(text: string, fingerprint: string): boolean {\n return text.toUpperCase().includes(fingerprint.toUpperCase())\n}\n\nasync function verifyGitHub(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`)\n if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` }\n const data = await resp.json()\n\n // A gist is identified solely by its globally-unique ID; the username in\n // the URL is not authoritative. Confirm the gist owner matches the claimed\n // account before trusting the token it contains.\n if (data.owner?.login?.toLowerCase() !== proof.user!.toLowerCase()) {\n return { verified: false, reason: 'Gist owner does not match claimed user' }\n }\n\n for (const file of Object.values(data.files || {}) as any[]) {\n if (file.content && containsFingerprint(file.content, fingerprint)) {\n return { verified: true }\n }\n }\n return { verified: false, reason: 'Fingerprint token not found in gist' }\n } catch (err: any) {\n return { verified: false, reason: `GitHub fetch failed: ${err.message}` }\n }\n}\n\nasync function verifyDNS(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(\n `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(proof.domain!)}&type=TXT`,\n { headers: { Accept: 'application/dns-json' } },\n )\n if (!resp.ok) return { verified: false, reason: `DNS query returned ${resp.status}` }\n const data = await resp.json()\n\n for (const answer of data.Answer || []) {\n // TXT records only (type 16) — a CNAME or other record in the resolution\n // chain must not be eligible to match the token.\n if (answer.type !== 16) continue\n if (answer.data && containsFingerprint(answer.data, fingerprint)) {\n return { verified: true }\n }\n }\n return { verified: false, reason: 'Fingerprint token not found in TXT records' }\n } catch (err: any) {\n return { verified: false, reason: `DNS fetch failed: ${err.message}` }\n }\n}\n\nconst NEYNAR_HUB = 'https://hub-api.neynar.com'\n\nasync function resolveFid(\n username: string,\n apiKey: string,\n): Promise<number | null> {\n const resp = await fetch(\n `${NEYNAR_HUB}/v1/userNameProofByName?name=${encodeURIComponent(username)}`,\n { headers: { 'x-api-key': apiKey } },\n )\n if (!resp.ok) return null\n const data = await resp.json()\n return data.fid ?? null\n}\n\nasync function verifyFarcaster(\n proof: Proof,\n fingerprint: string,\n neynarApiKey?: string,\n): Promise<PGPVerification> {\n if (!neynarApiKey) {\n return { verified: false, reason: 'Farcaster verification requires a Neynar API key' }\n }\n\n try {\n const fid = await resolveFid(proof.user!, neynarApiKey)\n if (!fid) return { verified: false, reason: `Could not resolve Farcaster user \"${proof.user}\"` }\n\n const headers = { 'x-api-key': neynarApiKey }\n let pageToken = ''\n for (let page = 0; page < 5; page++) {\n const url = `${NEYNAR_HUB}/v1/castsByFid?fid=${fid}&pageSize=100&reverse=true${pageToken ? `&pageToken=${pageToken}` : ''}`\n const resp = await fetch(url, { headers })\n if (!resp.ok) return { verified: false, reason: `Farcaster Hub returned ${resp.status}` }\n const data = await resp.json()\n\n for (const msg of data.messages || []) {\n if (msg.hash && msg.hash.startsWith(proof.castHash!)) {\n const text = msg.data?.castAddBody?.text || ''\n if (containsFingerprint(text, fingerprint)) {\n return { verified: true }\n }\n // A short cast-hash prefix can match multiple casts; keep scanning\n // rather than stopping at the first prefix match without the token.\n }\n }\n\n if (!data.nextPageToken) break\n pageToken = data.nextPageToken\n }\n\n return { verified: false, reason: 'Cast not found' }\n } catch (err: any) {\n return { verified: false, reason: `Farcaster fetch failed: ${err.message}` }\n }\n}\n\nasync function verifyCodeberg(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n const resp = await fetch(\n `https://codeberg.org/api/v1/repos/${encodeURIComponent(proof.user!)}/${encodeURIComponent(proof.repo!)}`,\n )\n if (!resp.ok) return { verified: false, reason: `Codeberg API returned ${resp.status}` }\n const data = await resp.json()\n\n if (data.description && containsFingerprint(data.description, fingerprint)) {\n return { verified: true }\n }\n\n return { verified: false, reason: 'Fingerprint token not found in repo description' }\n } catch (err: any) {\n return { verified: false, reason: `Codeberg fetch failed: ${err.message}` }\n }\n}\n\nasync function verifyMastodon(proof: Proof, fingerprint: string): Promise<PGPVerification> {\n try {\n // The instance host is user-supplied and interpolated into the request URL,\n // so restrict it to a bare hostname (optional port) before use — otherwise a\n // value such as \"mastodon.social@evil.com\" would send the lookup to another\n // server while still displaying as a legitimate handle.\n if (!proof.instance || !/^[a-z0-9.-]+(?::\\d+)?$/i.test(proof.instance)) {\n return { verified: false, reason: 'Invalid Mastodon instance host' }\n }\n const resp = await fetch(\n `https://${proof.instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(proof.user!)}`,\n )\n if (!resp.ok) return { verified: false, reason: `Mastodon API returned ${resp.status}` }\n const data = await resp.json()\n\n const keyId = fingerprint.slice(-16)\n\n for (const field of data.fields || []) {\n const text = field.value.replace(/<[^>]*>/g, '')\n if (containsFingerprintUrl(text, fingerprint) || containsFingerprintUrl(text, keyId)) {\n return { verified: true }\n }\n }\n\n if (data.note) {\n const text = data.note.replace(/<[^>]*>/g, '')\n if (containsFingerprintUrl(text, fingerprint) || containsFingerprintUrl(text, keyId)) {\n return { verified: true }\n }\n }\n\n return { verified: false, reason: 'Fingerprint not found in profile fields or bio' }\n } catch (err: any) {\n return { verified: false, reason: `Mastodon fetch failed: ${err.message}` }\n }\n}\n\ntype Verifier = (proof: Proof, fingerprint: string, neynarApiKey?: string) => Promise<PGPVerification>\n\nconst verifiers: Record<string, Verifier> = {\n github: verifyGitHub,\n dns: verifyDNS,\n farcaster: verifyFarcaster,\n codeberg: verifyCodeberg,\n mastodon: verifyMastodon,\n}\n\nexport async function verifyProof(\n proof: Proof,\n fingerprint: string,\n neynarApiKey?: string,\n): Promise<PGPVerification> {\n const fn = verifiers[proof.provider]\n if (!fn) return { verified: false, reason: 'Unknown provider' }\n return fn(proof, fingerprint, neynarApiKey)\n}\n","import { useScryIdentity } from '../../hooks/useScryIdentity'\nimport { useIdentityKitConfig } from '../../context'\nimport type { Theme } from '../../core/types'\nimport '../../themes/index.css'\nimport './ScryCard.css'\n\nfunction ThurinLogo() {\n return (\n <svg className=\"scry-card-avatar-placeholder\" viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"var(--scry-surface-deep)\" />\n <path d=\"M25 80 Q25 25 50 25 Q75 25 75 50\" fill=\"none\" stroke=\"#7c9a3e\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n <path d=\"M33 75 Q33 35 50 35 Q67 35 67 52\" fill=\"none\" stroke=\"#7c9a3e\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n <path d=\"M41 70 Q41 45 50 45 Q59 45 59 55\" fill=\"none\" stroke=\"#c9a227\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n <path d=\"M50 65 L50 53\" fill=\"none\" stroke=\"#c9a227\" strokeWidth=\"4\" strokeLinecap=\"round\"/>\n </svg>\n )\n}\n\nexport interface ScryCardProps {\n ens?: string\n address?: string\n theme?: Theme\n}\n\nexport function ScryCard({ ens, address, theme = 'thurin' }: ScryCardProps) {\n const input = ens || address\n const identity = useScryIdentity(input)\n const config = useIdentityKitConfig()\n\n const scryUrl = identity.address\n ? `${config.scryBaseUrl}/eth/${identity.address}`\n : ens\n ? `${config.scryBaseUrl}/ens/${ens}`\n : null\n\n if (identity.isLoading) {\n return (\n <div className=\"scry-card\" data-scry-theme={theme}>\n <div className=\"scry-card-loading\">Loading identity...</div>\n </div>\n )\n }\n\n const displayAddress = identity.address || null\n\n const verifiedProofs = identity.proofs.filter((p) => p.status === 'verified').length\n const hasVerifiedPgp = identity.claims.some((c) => c.verification?.verified && !c.revoked)\n\n return (\n <div className=\"scry-card\" data-scry-theme={theme}>\n <a className=\"scry-card-header\" href={scryUrl || undefined} target=\"_blank\" rel=\"noopener noreferrer\">\n {identity.ensAvatar ? (\n <img\n className=\"scry-card-avatar\"\n src={identity.ensAvatar}\n alt={identity.ensName || ''}\n />\n ) : (\n <ThurinLogo />\n )}\n <div>\n {identity.ensName && (\n <div className=\"scry-card-name\">{identity.ensName}</div>\n )}\n {displayAddress && (\n <div className=\"scry-card-address\">{displayAddress}</div>\n )}\n </div>\n </a>\n\n <div className=\"scry-card-stats\">\n <div className=\"scry-card-stat\">\n <span className=\"scry-card-stat-value\">{identity.activeClaims}</span>\n <span className=\"scry-card-stat-label\">Seals</span>\n </div>\n <div className=\"scry-card-stat\">\n <span className=\"scry-card-stat-value\">{verifiedProofs}</span>\n <span className=\"scry-card-stat-label\">Proofs</span>\n </div>\n <div className=\"scry-card-stat\">\n <span className=\"scry-card-stat-value\">\n {identity.efp?.followers ?? 0}\n </span>\n <span className=\"scry-card-stat-label\">Followers</span>\n </div>\n </div>\n\n <div className=\"scry-card-badges\">\n {hasVerifiedPgp && (\n <span className=\"scry-card-badge scry-card-badge--verified\">PGP Verified</span>\n )}\n {identity.efp?.hasEfp && (\n <span className=\"scry-card-badge scry-card-badge--verified\">EFP</span>\n )}\n {identity.proofs.map((p, i) => (\n <span\n key={i}\n className={`scry-card-badge scry-card-badge--${p.status === 'verified' ? 'verified' : 'unverified'}`}\n >\n {p.label}\n </span>\n ))}\n </div>\n\n {scryUrl && (\n <a\n className=\"scry-card-link\"\n href={scryUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n View on Scry\n </a>\n )}\n </div>\n )\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -134,30 +134,46 @@ async function parsePgpKey(armoredKey) {
|
|
|
134
134
|
const { readKey } = await import("openpgp");
|
|
135
135
|
const key = await readKey({ armoredKey });
|
|
136
136
|
const fingerprint = key.getFingerprint().toUpperCase();
|
|
137
|
-
const userIDs = key.users.map((u) => u.userID?.userID).filter(Boolean);
|
|
138
137
|
const algorithm = String(key.keyPacket.algorithm);
|
|
139
138
|
const created = key.keyPacket.created?.toISOString() ?? null;
|
|
140
139
|
const expiration = await key.getExpirationTime();
|
|
141
140
|
const expires = expiration && expiration !== Infinity ? new Date(expiration).toISOString() : null;
|
|
141
|
+
const userIDs = [];
|
|
142
142
|
const notations = [];
|
|
143
143
|
const seen = /* @__PURE__ */ new Set();
|
|
144
144
|
for (const user of key.users) {
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
const certs = user.selfCertifications;
|
|
146
|
+
if (!certs || certs.length === 0) continue;
|
|
147
|
+
let userVerified = false;
|
|
148
|
+
for (const cert of certs) {
|
|
149
|
+
try {
|
|
150
|
+
await cert.verify(
|
|
151
|
+
key.keyPacket,
|
|
152
|
+
cert.signatureType,
|
|
153
|
+
{ userID: user.userID, key: key.keyPacket }
|
|
154
|
+
);
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
userVerified = true;
|
|
147
159
|
if (cert.rawNotations) {
|
|
148
160
|
for (const n of cert.rawNotations) {
|
|
149
161
|
const name = typeof n.name === "string" ? n.name : new TextDecoder().decode(n.name);
|
|
150
162
|
const value = n.value instanceof Uint8Array ? new TextDecoder().decode(n.value) : typeof n.value === "string" ? n.value : null;
|
|
151
163
|
if (value) {
|
|
152
|
-
const
|
|
153
|
-
if (!seen.has(
|
|
154
|
-
seen.add(
|
|
164
|
+
const dedupeKey = `${name}:${value}`;
|
|
165
|
+
if (!seen.has(dedupeKey)) {
|
|
166
|
+
seen.add(dedupeKey);
|
|
155
167
|
notations.push({ name, value });
|
|
156
168
|
}
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
}
|
|
160
172
|
}
|
|
173
|
+
if (userVerified) {
|
|
174
|
+
const uid = user.userID?.userID;
|
|
175
|
+
if (uid) userIDs.push(uid);
|
|
176
|
+
}
|
|
161
177
|
}
|
|
162
178
|
const subkeys = key.subkeys.map((sk) => ({
|
|
163
179
|
algorithm: String(sk.keyPacket.algorithm),
|
|
@@ -267,11 +283,13 @@ function useSignetClaims(address) {
|
|
|
267
283
|
queryFn: async () => {
|
|
268
284
|
if (!attestations || !address) return [];
|
|
269
285
|
const results = [];
|
|
286
|
+
const logsByIndex = {};
|
|
287
|
+
for (const l of eventData || []) logsByIndex[Number(l.args.index)] = l;
|
|
270
288
|
for (let i = 0; i < attestations.length; i++) {
|
|
271
289
|
const att = attestations[i];
|
|
272
290
|
if (att.status !== "success") continue;
|
|
273
291
|
const [fingerprint, createdAt, revoked] = att.result;
|
|
274
|
-
const log =
|
|
292
|
+
const log = logsByIndex[i];
|
|
275
293
|
const pgpSignature = log?.args?.pgpSignature ?? null;
|
|
276
294
|
const pgpPublicKey = log?.args?.pgpPublicKey ?? null;
|
|
277
295
|
const txHash = log?.transactionHash ?? null;
|
|
@@ -302,7 +320,8 @@ function useSignetClaims(address) {
|
|
|
302
320
|
});
|
|
303
321
|
const isLoading = countLoading || attestationsLoading || verifyLoading;
|
|
304
322
|
const activeClaims = (claims || []).filter((c) => !c.revoked);
|
|
305
|
-
const
|
|
323
|
+
const verifiedClaims = activeClaims.filter((c) => c.verification?.verified);
|
|
324
|
+
const currentFingerprint = verifiedClaims.length > 0 ? verifiedClaims[verifiedClaims.length - 1].fingerprint : null;
|
|
306
325
|
return {
|
|
307
326
|
claims: claims || [],
|
|
308
327
|
totalClaims: attestationCount,
|
|
@@ -371,7 +390,9 @@ var PROVIDERS = [
|
|
|
371
390
|
{
|
|
372
391
|
provider: "dns",
|
|
373
392
|
label: "DNS",
|
|
374
|
-
|
|
393
|
+
// domain is restricted to a hostname (labels may include underscores for
|
|
394
|
+
// records like _thurin.example.com); rejects slashes, userinfo, and spaces.
|
|
395
|
+
pattern: /^dns:([a-z0-9._-]+)\?type=TXT$/i,
|
|
375
396
|
parse: (m) => ({ domain: m[1] })
|
|
376
397
|
},
|
|
377
398
|
{
|
|
@@ -389,12 +410,15 @@ var PROVIDERS = [
|
|
|
389
410
|
{
|
|
390
411
|
provider: "mastodon",
|
|
391
412
|
label: "Mastodon",
|
|
392
|
-
|
|
413
|
+
// instance is restricted to a bare hostname (optional port) so a value
|
|
414
|
+
// like "mastodon.social@evil.com" can't redirect the lookup elsewhere.
|
|
415
|
+
pattern: /^https:\/\/([a-z0-9.-]+(?::\d+)?)\/@([^/@?#\s]+)$/i,
|
|
393
416
|
parse: (m) => ({ instance: m[1], user: m[2] })
|
|
394
417
|
}
|
|
395
418
|
];
|
|
419
|
+
var PROOF_NAMESPACES = /* @__PURE__ */ new Set(["proof@thurin.id", "proof@ariadne.id"]);
|
|
396
420
|
function identifyProof(notation) {
|
|
397
|
-
if (notation.name
|
|
421
|
+
if (!PROOF_NAMESPACES.has(notation.name)) return null;
|
|
398
422
|
for (const { provider, label, pattern, parse } of PROVIDERS) {
|
|
399
423
|
const m = notation.value.match(pattern);
|
|
400
424
|
if (m) {
|
|
@@ -439,6 +463,9 @@ async function verifyGitHub(proof, fingerprint) {
|
|
|
439
463
|
const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`);
|
|
440
464
|
if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` };
|
|
441
465
|
const data = await resp.json();
|
|
466
|
+
if (data.owner?.login?.toLowerCase() !== proof.user.toLowerCase()) {
|
|
467
|
+
return { verified: false, reason: "Gist owner does not match claimed user" };
|
|
468
|
+
}
|
|
442
469
|
for (const file of Object.values(data.files || {})) {
|
|
443
470
|
if (file.content && containsFingerprint(file.content, fingerprint)) {
|
|
444
471
|
return { verified: true };
|
|
@@ -458,6 +485,7 @@ async function verifyDNS(proof, fingerprint) {
|
|
|
458
485
|
if (!resp.ok) return { verified: false, reason: `DNS query returned ${resp.status}` };
|
|
459
486
|
const data = await resp.json();
|
|
460
487
|
for (const answer of data.Answer || []) {
|
|
488
|
+
if (answer.type !== 16) continue;
|
|
461
489
|
if (answer.data && containsFingerprint(answer.data, fingerprint)) {
|
|
462
490
|
return { verified: true };
|
|
463
491
|
}
|
|
@@ -497,7 +525,6 @@ async function verifyFarcaster(proof, fingerprint, neynarApiKey) {
|
|
|
497
525
|
if (containsFingerprint(text, fingerprint)) {
|
|
498
526
|
return { verified: true };
|
|
499
527
|
}
|
|
500
|
-
return { verified: false, reason: "Cast found but fingerprint token not in text" };
|
|
501
528
|
}
|
|
502
529
|
}
|
|
503
530
|
if (!data.nextPageToken) break;
|
|
@@ -525,6 +552,9 @@ async function verifyCodeberg(proof, fingerprint) {
|
|
|
525
552
|
}
|
|
526
553
|
async function verifyMastodon(proof, fingerprint) {
|
|
527
554
|
try {
|
|
555
|
+
if (!proof.instance || !/^[a-z0-9.-]+(?::\d+)?$/i.test(proof.instance)) {
|
|
556
|
+
return { verified: false, reason: "Invalid Mastodon instance host" };
|
|
557
|
+
}
|
|
528
558
|
const resp = await fetch(
|
|
529
559
|
`https://${proof.instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(proof.user)}`
|
|
530
560
|
);
|