@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/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.
|
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),
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
);
|