@thurinlabs/identity-kit 0.7.0 → 0.7.2

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.
@@ -168,6 +223,7 @@ npm test
168
223
  - [Scry](https://thurin.id) — Identity explorer
169
224
  - [Signet](https://thurin.id/signet) — Create identity claims
170
225
  - [Documentation](https://docs.thurin.id)
226
+ - [GitHub](https://github.com/thurinlabs/identity-kit)
171
227
  - [Codeberg](https://codeberg.org/thurinlabs/identity-kit)
172
228
 
173
229
  ## License
@@ -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),
@@ -39512,7 +39528,8 @@ ${prettyStateOverride(stateOverride)}`;
39512
39528
  });
39513
39529
  const isLoading = countLoading || attestationsLoading || verifyLoading;
39514
39530
  const activeClaims = (claims || []).filter((c2) => !c2.revoked);
39515
- const currentFingerprint = activeClaims.length > 0 ? activeClaims[activeClaims.length - 1].fingerprint : null;
39531
+ const verifiedClaims = activeClaims.filter((c2) => c2.verification?.verified);
39532
+ const currentFingerprint = verifiedClaims.length > 0 ? verifiedClaims[verifiedClaims.length - 1].fingerprint : null;
39516
39533
  return {
39517
39534
  claims: claims || [],
39518
39535
  totalClaims: attestationCount,
@@ -39575,7 +39592,9 @@ ${prettyStateOverride(stateOverride)}`;
39575
39592
  {
39576
39593
  provider: "dns",
39577
39594
  label: "DNS",
39578
- pattern: /^dns:([^?]+)\?type=TXT$/i,
39595
+ // domain is restricted to a hostname (labels may include underscores for
39596
+ // records like _thurin.example.com); rejects slashes, userinfo, and spaces.
39597
+ pattern: /^dns:([a-z0-9._-]+)\?type=TXT$/i,
39579
39598
  parse: (m2) => ({ domain: m2[1] })
39580
39599
  },
39581
39600
  {
@@ -39593,7 +39612,9 @@ ${prettyStateOverride(stateOverride)}`;
39593
39612
  {
39594
39613
  provider: "mastodon",
39595
39614
  label: "Mastodon",
39596
- pattern: /^https:\/\/([^/]+)\/@([^/]+)$/i,
39615
+ // instance is restricted to a bare hostname (optional port) so a value
39616
+ // like "mastodon.social@evil.com" can't redirect the lookup elsewhere.
39617
+ pattern: /^https:\/\/([a-z0-9.-]+(?::\d+)?)\/@([^/@?#\s]+)$/i,
39597
39618
  parse: (m2) => ({ instance: m2[1], user: m2[2] })
39598
39619
  }
39599
39620
  ];
@@ -39643,6 +39664,9 @@ ${prettyStateOverride(stateOverride)}`;
39643
39664
  const resp = await fetch(`https://api.github.com/gists/${proof.gistId}`);
39644
39665
  if (!resp.ok) return { verified: false, reason: `GitHub API returned ${resp.status}` };
39645
39666
  const data = await resp.json();
39667
+ if (data.owner?.login?.toLowerCase() !== proof.user.toLowerCase()) {
39668
+ return { verified: false, reason: "Gist owner does not match claimed user" };
39669
+ }
39646
39670
  for (const file of Object.values(data.files || {})) {
39647
39671
  if (file.content && containsFingerprint(file.content, fingerprint)) {
39648
39672
  return { verified: true };
@@ -39662,6 +39686,7 @@ ${prettyStateOverride(stateOverride)}`;
39662
39686
  if (!resp.ok) return { verified: false, reason: `DNS query returned ${resp.status}` };
39663
39687
  const data = await resp.json();
39664
39688
  for (const answer of data.Answer || []) {
39689
+ if (answer.type !== 16) continue;
39665
39690
  if (answer.data && containsFingerprint(answer.data, fingerprint)) {
39666
39691
  return { verified: true };
39667
39692
  }
@@ -39701,7 +39726,6 @@ ${prettyStateOverride(stateOverride)}`;
39701
39726
  if (containsFingerprint(text, fingerprint)) {
39702
39727
  return { verified: true };
39703
39728
  }
39704
- return { verified: false, reason: "Cast found but fingerprint token not in text" };
39705
39729
  }
39706
39730
  }
39707
39731
  if (!data.nextPageToken) break;
@@ -39729,6 +39753,9 @@ ${prettyStateOverride(stateOverride)}`;
39729
39753
  }
39730
39754
  async function verifyMastodon(proof, fingerprint) {
39731
39755
  try {
39756
+ if (!proof.instance || !/^[a-z0-9.-]+(?::\d+)?$/i.test(proof.instance)) {
39757
+ return { verified: false, reason: "Invalid Mastodon instance host" };
39758
+ }
39732
39759
  const resp = await fetch(
39733
39760
  `https://${proof.instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(proof.user)}`
39734
39761
  );