@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 CHANGED
@@ -1,6 +1,12 @@
1
1
  # @thurinlabs/identity-kit
2
2
 
3
- React SDK for embedding Thurin identity data. Drop-in components and hooks for displaying on-chain identity claims, PGP verification, social proofs, and EFP social graph data.
3
+ The shared library for Thurin identity — the single source of truth for looking up and **verifying** on-chain identity claims, PGP key proofs, social proofs, and EFP social graph data. It powers both the [Scry](https://thurin.id) explorer and the embeddable `ScryCard`, so a "verified" result means the same thing everywhere.
4
+
5
+ Three layers — use whichever fits:
6
+
7
+ - **Core** — framework-agnostic functions (verify proofs, parse PGP keys, fetch EFP/claims). No React required.
8
+ - **Hooks** — thin React wrappers around the core.
9
+ - **ScryCard** — a drop-in identity card UI built on the hooks.
4
10
 
5
11
  A [Thurin Labs](https://thurin.id) project.
6
12
 
@@ -115,6 +121,55 @@ const { keyInfo, proofs, isLoading } = usePGPProofs('03E53D807CE38C...')
115
121
  // proofs[].provider, proofs[].status, proofs[].displayUrl
116
122
  ```
117
123
 
124
+ ## Core utilities (no React)
125
+
126
+ The verification and data logic is exported as plain functions — no React, no provider. This is the layer the Scry explorer and the hooks both build on; use it directly when you need the validated data behind your own UI.
127
+
128
+ ### Proofs
129
+
130
+ ```ts
131
+ import { identifyProof, verifyProof, displayUrl, proofHref, proofSecondaryHref } from '@thurinlabs/identity-kit'
132
+
133
+ const proof = identifyProof({ name: 'proof@thurin.id', value: 'https://gist.github.com/alice/abc123' })
134
+ // → { provider: 'github', label: 'GitHub', user: 'alice', gistId: 'abc123', url }
135
+
136
+ const result = await verifyProof(proof, fingerprint, neynarApiKey /* only needed for Farcaster */)
137
+ // → { verified: boolean, reason?: string }
138
+ ```
139
+
140
+ `verifyProof` performs the real check per provider — including confirming the GitHub gist is **owned** by the claimed user, so a proof can't be forged by pointing at someone else's gist ID. `displayUrl` / `proofHref` / `proofSecondaryHref` build the display string and links.
141
+
142
+ ### PGP
143
+
144
+ ```ts
145
+ import { parsePgpKey, verifyAttestation, fetchKeyByFingerprint, fetchKeyByKeyId } from '@thurinlabs/identity-kit'
146
+
147
+ const keyInfo = await parsePgpKey(armoredKey)
148
+ // → { fingerprint, userIDs, algorithm, created, expires, notations, subkeys } | null
149
+
150
+ const verification = await verifyAttestation({ pgpPublicKey, pgpSignature, fingerprint, ethAddress })
151
+ // → { verified: boolean, reason?: string }
152
+
153
+ const armored = await fetchKeyByFingerprint(fingerprint) // from keys.openpgp.org
154
+ ```
155
+
156
+ ### EFP & claims
157
+
158
+ ```ts
159
+ import { fetchEFPGraph } from '@thurinlabs/identity-kit'
160
+
161
+ const graph = await fetchEFPGraph(address)
162
+ // → { followers, following, top8: string[], hasEfp } | null
163
+ ```
164
+
165
+ ### Contract constants
166
+
167
+ ```ts
168
+ import { REGISTRY_ADDRESS, REGISTRY_ABI, CONTRACT_DEPLOY_BLOCK } from '@thurinlabs/identity-kit'
169
+ ```
170
+
171
+ Note `REGISTRY_ABI` here is read-only (events + `attestationCount` + `getAttestation`). Apps that write claims (the Signet flow) need their own ABI with the `attest`/`revoke` functions.
172
+
118
173
  ## Themes
119
174
 
120
175
  Three built-in themes: `thurin`, `dark`, `light`. All styles are scoped under `[data-scry-theme]` with `scry-` prefixed class names to avoid conflicts with your app's styles.
@@ -39353,30 +39353,46 @@ ${prettyStateOverride(stateOverride)}`;
39353
39353
  const { readKey } = await Promise.resolve().then(() => (init_openpgp_min(), openpgp_min_exports));
39354
39354
  const key = await readKey({ armoredKey });
39355
39355
  const fingerprint = key.getFingerprint().toUpperCase();
39356
- const userIDs = key.users.map((u2) => u2.userID?.userID).filter(Boolean);
39357
39356
  const algorithm = String(key.keyPacket.algorithm);
39358
39357
  const created = key.keyPacket.created?.toISOString() ?? null;
39359
39358
  const expiration = await key.getExpirationTime();
39360
39359
  const expires = expiration && expiration !== Infinity ? new Date(expiration).toISOString() : null;
39360
+ const userIDs = [];
39361
39361
  const notations = [];
39362
39362
  const seen = /* @__PURE__ */ new Set();
39363
39363
  for (const user of key.users) {
39364
- if (!user.selfCertifications) continue;
39365
- for (const cert of user.selfCertifications) {
39364
+ const certs = user.selfCertifications;
39365
+ if (!certs || certs.length === 0) continue;
39366
+ let userVerified = false;
39367
+ for (const cert of certs) {
39368
+ try {
39369
+ await cert.verify(
39370
+ key.keyPacket,
39371
+ cert.signatureType,
39372
+ { userID: user.userID, key: key.keyPacket }
39373
+ );
39374
+ } catch {
39375
+ continue;
39376
+ }
39377
+ userVerified = true;
39366
39378
  if (cert.rawNotations) {
39367
39379
  for (const n2 of cert.rawNotations) {
39368
39380
  const name = typeof n2.name === "string" ? n2.name : new TextDecoder().decode(n2.name);
39369
39381
  const value = n2.value instanceof Uint8Array ? new TextDecoder().decode(n2.value) : typeof n2.value === "string" ? n2.value : null;
39370
39382
  if (value) {
39371
- const key2 = `${name}:${value}`;
39372
- if (!seen.has(key2)) {
39373
- seen.add(key2);
39383
+ const dedupeKey = `${name}:${value}`;
39384
+ if (!seen.has(dedupeKey)) {
39385
+ seen.add(dedupeKey);
39374
39386
  notations.push({ name, value });
39375
39387
  }
39376
39388
  }
39377
39389
  }
39378
39390
  }
39379
39391
  }
39392
+ if (userVerified) {
39393
+ const uid3 = user.userID?.userID;
39394
+ if (uid3) userIDs.push(uid3);
39395
+ }
39380
39396
  }
39381
39397
  const subkeys = key.subkeys.map((sk) => ({
39382
39398
  algorithm: String(sk.keyPacket.algorithm),
@@ -39477,11 +39493,13 @@ ${prettyStateOverride(stateOverride)}`;
39477
39493
  queryFn: async () => {
39478
39494
  if (!attestations || !address) return [];
39479
39495
  const results = [];
39496
+ const logsByIndex = {};
39497
+ for (const l2 of eventData || []) logsByIndex[Number(l2.args.index)] = l2;
39480
39498
  for (let i2 = 0; i2 < attestations.length; i2++) {
39481
39499
  const att = attestations[i2];
39482
39500
  if (att.status !== "success") continue;
39483
39501
  const [fingerprint, createdAt, revoked] = att.result;
39484
- const log = (eventData || [])[i2];
39502
+ const log = logsByIndex[i2];
39485
39503
  const pgpSignature = log?.args?.pgpSignature ?? null;
39486
39504
  const pgpPublicKey = log?.args?.pgpPublicKey ?? null;
39487
39505
  const txHash = log?.transactionHash ?? null;
@@ -39512,7 +39530,8 @@ ${prettyStateOverride(stateOverride)}`;
39512
39530
  });
39513
39531
  const isLoading = countLoading || attestationsLoading || verifyLoading;
39514
39532
  const activeClaims = (claims || []).filter((c2) => !c2.revoked);
39515
- const currentFingerprint = activeClaims.length > 0 ? activeClaims[activeClaims.length - 1].fingerprint : null;
39533
+ const verifiedClaims = activeClaims.filter((c2) => c2.verification?.verified);
39534
+ const currentFingerprint = verifiedClaims.length > 0 ? verifiedClaims[verifiedClaims.length - 1].fingerprint : null;
39516
39535
  return {
39517
39536
  claims: claims || [],
39518
39537
  totalClaims: attestationCount,
@@ -39575,7 +39594,9 @@ ${prettyStateOverride(stateOverride)}`;
39575
39594
  {
39576
39595
  provider: "dns",
39577
39596
  label: "DNS",
39578
- pattern: /^dns:([^?]+)\?type=TXT$/i,
39597
+ // domain is restricted to a hostname (labels may include underscores for
39598
+ // records like _thurin.example.com); rejects slashes, userinfo, and spaces.
39599
+ pattern: /^dns:([a-z0-9._-]+)\?type=TXT$/i,
39579
39600
  parse: (m2) => ({ domain: m2[1] })
39580
39601
  },
39581
39602
  {
@@ -39593,12 +39614,15 @@ ${prettyStateOverride(stateOverride)}`;
39593
39614
  {
39594
39615
  provider: "mastodon",
39595
39616
  label: "Mastodon",
39596
- pattern: /^https:\/\/([^/]+)\/@([^/]+)$/i,
39617
+ // instance is restricted to a bare hostname (optional port) so a value
39618
+ // like "mastodon.social@evil.com" can't redirect the lookup elsewhere.
39619
+ pattern: /^https:\/\/([a-z0-9.-]+(?::\d+)?)\/@([^/@?#\s]+)$/i,
39597
39620
  parse: (m2) => ({ instance: m2[1], user: m2[2] })
39598
39621
  }
39599
39622
  ];
39623
+ var PROOF_NAMESPACES = /* @__PURE__ */ new Set(["proof@thurin.id", "proof@ariadne.id"]);
39600
39624
  function identifyProof(notation) {
39601
- if (notation.name !== "proof@thurin.id") return null;
39625
+ if (!PROOF_NAMESPACES.has(notation.name)) return null;
39602
39626
  for (const { provider, label, pattern, parse } of PROVIDERS) {
39603
39627
  const m2 = notation.value.match(pattern);
39604
39628
  if (m2) {
@@ -39643,6 +39667,9 @@ ${prettyStateOverride(stateOverride)}`;
39643
39667
  const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`);
39644
39668
  if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` };
39645
39669
  const data = await resp.json();
39670
+ if (data.owner?.login?.toLowerCase() !== proof.user.toLowerCase()) {
39671
+ return { verified: false, reason: "Gist owner does not match claimed user" };
39672
+ }
39646
39673
  for (const file of Object.values(data.files || {})) {
39647
39674
  if (file.content && containsFingerprint(file.content, fingerprint)) {
39648
39675
  return { verified: true };
@@ -39662,6 +39689,7 @@ ${prettyStateOverride(stateOverride)}`;
39662
39689
  if (!resp.ok) return { verified: false, reason: `DNS query returned ${resp.status}` };
39663
39690
  const data = await resp.json();
39664
39691
  for (const answer of data.Answer || []) {
39692
+ if (answer.type !== 16) continue;
39665
39693
  if (answer.data && containsFingerprint(answer.data, fingerprint)) {
39666
39694
  return { verified: true };
39667
39695
  }
@@ -39701,7 +39729,6 @@ ${prettyStateOverride(stateOverride)}`;
39701
39729
  if (containsFingerprint(text, fingerprint)) {
39702
39730
  return { verified: true };
39703
39731
  }
39704
- return { verified: false, reason: "Cast found but fingerprint token not in text" };
39705
39732
  }
39706
39733
  }
39707
39734
  if (!data.nextPageToken) break;
@@ -39729,6 +39756,9 @@ ${prettyStateOverride(stateOverride)}`;
39729
39756
  }
39730
39757
  async function verifyMastodon(proof, fingerprint) {
39731
39758
  try {
39759
+ if (!proof.instance || !/^[a-z0-9.-]+(?::\d+)?$/i.test(proof.instance)) {
39760
+ return { verified: false, reason: "Invalid Mastodon instance host" };
39761
+ }
39732
39762
  const resp = await fetch(
39733
39763
  `https://${proof.instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(proof.user)}`
39734
39764
  );