@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 +57 -1
- package/dist/embed.global.js +37 -10
- package/dist/embed.global.js.map +1 -1
- package/dist/index.cjs +50 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +37 -10
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# @thurinlabs/identity-kit
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/embed.global.js
CHANGED
|
@@ -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
|
-
|
|
39365
|
-
|
|
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
|
|
39372
|
-
if (!seen.has(
|
|
39373
|
-
seen.add(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|