@thurinlabs/identity-kit 1.4.0 → 2.0.0
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 +44 -384
- package/dist/{chunk-ZAQ2EPF5.js → chunk-AAJWERE2.js} +2455 -1343
- package/dist/{chunk-M2OJDB7I.cjs → chunk-DJGCYNJN.cjs} +2460 -1348
- package/dist/core.cjs +44 -3
- package/dist/core.d.cts +1563 -903
- package/dist/core.d.ts +1563 -903
- package/dist/core.js +65 -24
- package/dist/index.cjs +25 -459
- package/dist/index.d.cts +1 -83
- package/dist/index.d.ts +1 -83
- package/dist/index.js +65 -499
- package/package.json +5 -22
- package/dist/chunk-M2OJDB7I.cjs.map +0 -1
- package/dist/chunk-ZAQ2EPF5.js.map +0 -1
- package/dist/core.cjs.map +0 -1
- package/dist/core.js.map +0 -1
- package/dist/embed.global.js +0 -32213
- package/dist/embed.global.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -185
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,408 +1,78 @@
|
|
|
1
1
|
# @thurinlabs/identity-kit
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The library behind [Thurin.id](https://thurin.id), the Thurin CLI, and the share cards. It reads PGP claims from the PGPRegistry contract, checks them with openpgp.js, and checks the proofs on the key. One core, so "verified" means the same thing everywhere.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
- **ThurinCard** — a drop-in identity card UI built on the hooks.
|
|
10
|
-
|
|
11
|
-
A [Thurin Labs](https://thurin.id) project.
|
|
12
|
-
|
|
13
|
-
## Install
|
|
5
|
+
Full reference: [docs.thurin.id/#/sdk](https://docs.thurin.id/#/sdk).
|
|
14
6
|
|
|
15
7
|
```bash
|
|
16
8
|
npm install @thurinlabs/identity-kit
|
|
17
9
|
```
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
```tsx
|
|
24
|
-
import { IdentityKitProvider, ThurinCard } from '@thurinlabs/identity-kit'
|
|
25
|
-
import '@thurinlabs/identity-kit/styles'
|
|
26
|
-
|
|
27
|
-
function App() {
|
|
28
|
-
return (
|
|
29
|
-
<IdentityKitProvider>
|
|
30
|
-
<ThurinCard ens="vitalik.eth" theme="thurin" />
|
|
31
|
-
</IdentityKitProvider>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## ThurinCard
|
|
37
|
-
|
|
38
|
-
A self-contained identity card that fetches and displays all available identity data.
|
|
39
|
-
|
|
40
|
-
```tsx
|
|
41
|
-
<ThurinCard ens="vitalik.eth" theme="thurin" />
|
|
42
|
-
<ThurinCard address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" theme="dark" />
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**Props:**
|
|
46
|
-
|
|
47
|
-
| Prop | Type | Default | Description |
|
|
48
|
-
|------|------|---------|-------------|
|
|
49
|
-
| `ens` | `string` | — | ENS name to look up |
|
|
50
|
-
| `address` | `string` | — | ETH address to look up |
|
|
51
|
-
| `theme` | `'thurin' \| 'dark' \| 'light'` | `'thurin'` | Visual theme |
|
|
52
|
-
|
|
53
|
-
**Displays:** ENS avatar, name, address, on-chain attestation count, verified proof count, EFP follower count, proof provider badges, and a link to the full [thurin.id](https://thurin.id) profile.
|
|
54
|
-
|
|
55
|
-
## Provider
|
|
56
|
-
|
|
57
|
-
Wrap your app (or just the part using identity-kit) in `IdentityKitProvider`. If you already have a `WagmiProvider`, the SDK detects it and uses your existing config.
|
|
58
|
-
|
|
59
|
-
```tsx
|
|
60
|
-
// Zero config — public RPC and a public Farcaster node, no keys
|
|
61
|
-
<IdentityKitProvider>
|
|
62
|
-
<ThurinCard ens="vitalik.eth" />
|
|
63
|
-
</IdentityKitProvider>
|
|
64
|
-
|
|
65
|
-
// With options
|
|
66
|
-
<IdentityKitProvider
|
|
67
|
-
rpcUrl="https://your-node.example"
|
|
68
|
-
farcasterHub="https://your-farcaster-node.example"
|
|
69
|
-
baseUrl="https://thurin.id"
|
|
70
|
-
>
|
|
71
|
-
<ThurinCard ens="vitalik.eth" />
|
|
72
|
-
</IdentityKitProvider>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
| Prop | Type | Default | Description |
|
|
76
|
-
|------|------|---------|-------------|
|
|
77
|
-
| `rpcUrl` | `string` | publicnode | Ethereum RPC endpoint. Any RPC works — v2 needs only `eth_call` |
|
|
78
|
-
| `farcasterHub` | `string` | Hypersnap public node | A Farcaster node's HTTP API for Farcaster proofs. The default, Quilibrium's `haatz.quilibrium.com`, needs no key; it sees the visitor's IP and which account was checked (1.3.7) |
|
|
79
|
-
| `neynarApiKey` | `string` | — | Optional: read Farcaster through Neynar's hub instead. Not needed since 1.3.7 |
|
|
80
|
-
| `baseUrl` | `string` | `https://thurin.id` | Base URL for "View on Thurin" links |
|
|
81
|
-
| `network` | `'mainnet' \| 'sepolia' \| 'local'` | `'mainnet'` | Which chain to read the PGPRegistry on (`local` = a running anvil) |
|
|
82
|
-
| `registryAddress` | `string` | v2 address | Override the registry contract address |
|
|
83
|
-
|
|
84
|
-
## Hooks
|
|
85
|
-
|
|
86
|
-
For custom UI, use the hooks directly instead of `ThurinCard`.
|
|
87
|
-
|
|
88
|
-
### useThurinIdentity
|
|
89
|
-
|
|
90
|
-
Combined identity data — ENS, on-chain attestations, PGP proofs, and EFP social graph.
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
const identity = useThurinIdentity('vitalik.eth')
|
|
94
|
-
// or
|
|
95
|
-
const identity = useThurinIdentity('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Returns: `ThurinIdentity` with `address`, `ensName`, `ensAvatar`, `claims`, `totalClaims`, `activeClaims`, `currentFingerprint`, `pgpKeyInfo`, `proofs`, `efp`, `isLoading`, `error`.
|
|
99
|
-
|
|
100
|
-
When the identity can't be shown, `error` is a plain `Error` safe to display and `errorKind` says why: `'rpc'` (the RPC didn't answer, so nothing is known), `'not-found'` (the ENS name has no address), or `'read'` (the RPC answers but a registry read failed); `retry()` re-runs the lookups. `ThurinCard` shows these instead of zeros, and a follower count EFP didn't answer for shows `–` (1.3.4).
|
|
101
|
-
|
|
102
|
-
`ensAvatar` is set only when loading it can't reveal the viewer to the name's owner: IPFS, Arweave, inline data, a content-addressed NFT, or `euc.li` (the ENS app's upload host). IPFS images load through `ipfs.filebase.io`, falling back to Pinata's public gateway if that fails (`IPFS_GATEWAYS`, `avatarFallbacks()`; 1.3.6). A plain `https://` avatar on the owner's own server is left out, since loading it would hand that server every viewer's IP (1.3.3; earlier versions loaded any avatar). The rule is `avatarUrl()` in the core; `useSafeAvatar(name, chainId)` is the hook.
|
|
103
|
-
|
|
104
|
-
### useAttestations
|
|
105
|
-
|
|
106
|
-
On-chain attestation data from the PGPRegistry v2 contract — the owner's history plus the stored signature and key for each claim, read with plain contract calls (no event logs), each verified off-chain.
|
|
107
|
-
|
|
108
|
-
```tsx
|
|
109
|
-
const { claims, totalClaims, activeClaims, currentFingerprint, isLoading } =
|
|
110
|
-
useAttestations('0xd8dA...')
|
|
111
|
-
// claims[].{ index, fingerprint, createdAt, revoked, revokedAt, messageVersion, pgpSignature, pgpPublicKey, verification }
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### useEFPGraph
|
|
115
|
-
|
|
116
|
-
EFP (Ethereum Follow Protocol) social graph data.
|
|
117
|
-
|
|
118
|
-
```tsx
|
|
119
|
-
const { efp, isLoading } = useEFPGraph('0xd8dA...')
|
|
120
|
-
// efp.followers, efp.following, efp.top8, efp.hasEfp
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### usePGPProofs
|
|
124
|
-
|
|
125
|
-
PGP key info and verified social proofs, read from the key stored in the identity's on-chain attestation. No keyserver is consulted.
|
|
126
|
-
|
|
127
|
-
```tsx
|
|
128
|
-
const { keyInfo, proofs, isLoading } = usePGPProofs(fingerprint, attestation.pgpPublicKey)
|
|
129
|
-
// proofs[].provider, proofs[].status, proofs[].displayUrl
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
`useThurinIdentity` wires this up for you from the current attestation.
|
|
133
|
-
|
|
134
|
-
### useEnsHint
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
const { state, record, reason, isLoading } = useEnsHint('ben.thurinlabs.eth', identity.currentFingerprint)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
The name's `id.thurin` text record against the key the registry verifies for its address. `state` is `match`, `unset`, or `mismatch` (with a `reason`). The record is a discovery hint an ENS profile can show; the trust is in the claim. See [Point your ENS name at your claim](https://docs.thurin.id/#/guides/ens-record).
|
|
141
|
-
|
|
142
|
-
### useRecords
|
|
143
|
-
|
|
144
|
-
```tsx
|
|
145
|
-
const { records, isLoading } = useRecords(identity.address, claimIndex)
|
|
146
|
-
// records: [{ kind: 'thurin.railgun', text, valid, data: { type: 'railgun', address } }, ...]
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The records on one claim for the kinds an identity page shows (`IDENTITY_KINDS`), read in one multicall and parsed. Kinds with no record are left out. See the [records reference](https://docs.thurin.id/#/records).
|
|
150
|
-
|
|
151
|
-
## `@thurinlabs/identity-kit/core` — no React
|
|
152
|
-
|
|
153
|
-
Everything under "Core utilities" below is also published as its own entry point with no React, wagmi, or DOM dependency, for Node and worker consumers (the Thurin CLI and the share-card service use it):
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
import { verifyAttestation, parsePgpKey, getRegistry, chainFor } from '@thurinlabs/identity-kit/core'
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Runtime dependencies of this entry: `openpgp` (bundled dependency), `viem` (peer), and in Node `eckey-utils` (dependency) for secp256k1 keys.
|
|
160
|
-
|
|
161
|
-
## Core utilities (no React)
|
|
162
|
-
|
|
163
|
-
The verification and data logic is exported as plain functions — no React, no provider. This is the layer the thurin.id explorer and the hooks both build on; use it directly when you need the validated data behind your own UI.
|
|
164
|
-
|
|
165
|
-
### Proofs
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
import { identifyProof, verifyProof, displayUrl, proofHref, proofSecondaryHref } from '@thurinlabs/identity-kit'
|
|
169
|
-
|
|
170
|
-
const proof = identifyProof({ name: 'proof@thurin.id', value: 'https://gist.github.com/alice/abc123' })
|
|
171
|
-
// → { provider: 'github', label: 'GitHub', user: 'alice', gistId: 'abc123', url }
|
|
172
|
-
|
|
173
|
-
const result = await verifyProof(proof, fingerprint) // options: { farcasterHub?, neynarApiKey? }
|
|
174
|
-
// → { verified: boolean, reason?: string }
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
`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.
|
|
178
|
-
|
|
179
|
-
### PGP
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
import { parsePgpKey, verifyAttestation, stripEmailUserIDs, hasEmailUserID } from '@thurinlabs/identity-kit'
|
|
183
|
-
|
|
184
|
-
const keyInfo = await parsePgpKey(armoredKey)
|
|
185
|
-
// → { fingerprint, userIDs, algorithm, created, expires, notations, subkeys } | null
|
|
186
|
-
|
|
187
|
-
const verification = await verifyAttestation({ pgpPublicKey, pgpSignature, fingerprint, ethAddress })
|
|
188
|
-
// → { verified, kind, at?, revocationReason?, signingKey?, expiresAt?, algorithm?, reason? }
|
|
189
|
-
// kind: 'verified' | 'expired' | 'signing-key-expired' | 'revoked' | 'compromised'
|
|
190
|
-
// | 'signing-key-revoked' | 'unsupported' | 'bad-signature' (1.4.0)
|
|
191
|
-
|
|
192
|
-
// Prepare a key for publishing: drop every user ID that contains an email address.
|
|
193
|
-
const stripped = await stripEmailUserIDs(armoredKey)
|
|
194
|
-
// → { armored, kept: ['thurin'], removed: ['Alice <alice@example.com>'] } | null (null = nothing would remain)
|
|
195
|
-
|
|
196
|
-
await hasEmailUserID(armoredKey) // → true if any user ID contains an @
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
**Published identity.** An attestation stores the armored key on-chain, permanently and publicly. Since 0.9.0 the intended shape is a key whose only user ID is a non-email one (usually the name already on the key, without the email), carrying the `proof@thurin.id` notations. `stripEmailUserIDs` produces that from a normal export; the stripped key still verifies (`verifyAttestation` needs at least one self-certified user ID, so a key with none is rejected) and keeps the notations on the user ID it retains. Proofs are then read from the on-chain key, never from a keyserver.
|
|
200
|
-
|
|
201
|
-
### Why a claim does or doesn't count (1.4.0)
|
|
202
|
-
|
|
203
|
-
`verifyAttestation` returns a `kind` along with `verified`, so a page can say why in plain words instead of showing a library error. Checked in this order: the key revoked (`compromised` when the owner's reason was compromise), the key expired, the signing subkey revoked, the signing subkey expired; then an algorithm openpgp.js refuses (`unsupported`, e.g. DSA); anything else is `bad-signature`. `at` is the date that goes with it. A verified result carries `expiresAt`: the earlier of the key's and the signing subkey's expiry.
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
import { claimCheckText, expiresSoon, expiresSoonText, claimFates, claimFateText, CLAIM_CHECK_LABEL } from '@thurinlabs/identity-kit'
|
|
207
|
-
|
|
208
|
-
claimCheckText(verification)
|
|
209
|
-
// → { kind: 'expired', label: 'key expired',
|
|
210
|
-
// sentence: 'The key on this claim expired on Mar 5, 2029, so the claim no longer counts.',
|
|
211
|
-
// fix: 'Extend the key, then Update key. No new signature needed.' } // `fix` is for the owner
|
|
212
|
-
|
|
213
|
-
const soon = expiresSoon(verification) // within 30 days → { days, at } | null
|
|
214
|
-
if (soon) expiresSoonText(soon) // 'Key expires in 12 days (Mar 6, 2027).'
|
|
215
|
-
|
|
216
|
-
// Revoked or replaced: reattest revokes and attests in one transaction, so a claim revoked the
|
|
217
|
-
// same second a newer one was created was replaced by it.
|
|
218
|
-
const fates = claimFates(attestations) // Map<index, { state: 'active' | 'revoked' | 'replaced', at?, by? }>
|
|
219
|
-
claimFateText(fates.get(1)!) // 'Replaced by claim #2 on Oct 3, 2026.'
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Dates are formatted in UTC ("Mar 5, 2029") so every viewer sees the same day; pass your own formatter as the second argument.
|
|
223
|
-
|
|
224
|
-
Nothing in the kit talks to a keyserver: keys come from the registry, and `thurin keyserver` / keys.thurin.id serve them over HKP for gpg.
|
|
225
|
-
|
|
226
|
-
### EFP & claims
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
import { fetchEFPGraph } from '@thurinlabs/identity-kit'
|
|
230
|
-
|
|
231
|
-
const graph = await fetchEFPGraph(address)
|
|
232
|
-
// → { followers, following, top8: string[], hasEfp } | null
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Contract
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
import { REGISTRY_ADDRESS, REGISTRY_ABI, NETWORKS, getRegistry } from '@thurinlabs/identity-kit'
|
|
239
|
-
|
|
240
|
-
getRegistry('sepolia') // → { chainId: 11155111, address, deployBlock, explorerUrl, defaultRpcUrl }
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
`REGISTRY_ABI` is the complete v2 ABI (reads and writes), so apps that publish claims use the same one. The v2 registry is deployed with CREATE2 and has the same address on every network.
|
|
244
|
-
|
|
245
|
-
### Records
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
import { fetchRecords, parseRecord, encodeRecord, kindName, IDENTITY_KINDS, KNOWN_KINDS } from '@thurinlabs/identity-kit/core'
|
|
249
|
-
|
|
250
|
-
const records = await fetchRecords(publicClient, REGISTRY_ADDRESS, REGISTRY_ABI, owner, claimIndex) // ParsedRecord[] for IDENTITY_KINDS
|
|
251
|
-
const one = await parseRecord('thurin.canary', text) // { valid, reason?, data: { type: 'canary', date, statement, clearsigned } }
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
A record is one value per claim per kind, up to 1 KB, set only by the owner. `parseRecord` never throws: a value that does not fit its kind comes back with `valid: false` and a reason. Kinds Thurin defines: `thurin.railgun`, `thurin.security`, `thurin.successor`, `thurin.affiliation`, `thurin.canary`, `thurin.private`, `thurin.disclosure` (shown on identity pages) and `thurin.pointer` (the Thurin Labs release list). Anyone can use reverse-dot names of their own.
|
|
255
|
-
|
|
256
|
-
### ENS record (`id.thurin`)
|
|
257
|
-
|
|
258
|
-
```ts
|
|
259
|
-
import { ensHintFor, ensHintValue, ensHintWrite, fetchEnsHint, ENS_HINT_KEY } from '@thurinlabs/identity-kit/core'
|
|
260
|
-
|
|
261
|
-
const hint = await fetchEnsHint(publicClient, 'ben.thurinlabs.eth', verifiedFingerprint) // { state: 'match' | 'unset' | 'mismatch', record, fingerprint, expected, reason? }
|
|
262
|
-
const call = ensHintWrite('ben.thurinlabs.eth', verifiedFingerprint) // { abi, functionName: 'setText', args: [namehash, 'id.thurin', 'FPR…'] }
|
|
263
|
-
const resolver = await publicClient.getEnsResolver({ name: call.name }) // look it up at write time; ENSv2 resolvers are per account
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
`ensHintFor(record, fingerprint)` is the pure comparison; `ensHintValue` is the bare uppercase form Thurin writes.
|
|
267
|
-
|
|
268
|
-
### Avatars
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
import { avatarUrl, parseNftAvatar, nftAvatarImage } from '@thurinlabs/identity-kit/core'
|
|
272
|
-
|
|
273
|
-
avatarUrl('ipfs://Qm…') // 'https://ipfs.filebase.io/ipfs/Qm…' (avatarFallbacks() → Pinata)
|
|
274
|
-
avatarUrl('https://euc.li/vitalik.eth') // allowed: ENS Labs' host, not the owner's
|
|
275
|
-
avatarUrl('https://example.com/me.png') // null: the owner's server would see every viewer
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### Fingerprints and key IDs
|
|
11
|
+
Runs in Node, workers, and browsers. The only peer dependency is `viem`. `@thurinlabs/identity-kit/core` is the same entry, for older imports.
|
|
279
12
|
|
|
280
|
-
|
|
13
|
+
## Read an address
|
|
281
14
|
|
|
282
15
|
```ts
|
|
283
|
-
import {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
bytesToFingerprint('0x6e00…7fe7') // → '6e0053911942a889426c1866e34d9266098f7fe7'
|
|
287
|
-
fingerprintHash(fp) // → keccak256 of the raw bytes (addressesFor arg)
|
|
288
|
-
keyIdOf(fp) // → '0xe34d9266098f7fe7' (fingerprintsForKeyId arg; v4 = last 8 bytes, v6 = first 8, per RFC 9580)
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Authorized writes (EIP-712)
|
|
292
|
-
|
|
293
|
-
Every write has a `…For` twin that anyone can submit with the owner's signature — for cold wallets, a CLI, or a sponsor. The helpers build exactly the typed data the contract verifies:
|
|
16
|
+
import { createPublicClient, http } from 'viem'
|
|
17
|
+
import { mainnet } from 'viem/chains'
|
|
18
|
+
import { readClaims, keyStanding, claimCheckText } from '@thurinlabs/identity-kit'
|
|
294
19
|
|
|
295
|
-
|
|
296
|
-
import { attestTypedData, authorizationDigest } from '@thurinlabs/identity-kit'
|
|
297
|
-
import { signTypedData } from '@wagmi/core'
|
|
20
|
+
const client = createPublicClient({ chain: mainnet, transport: http('https://ethereum.publicnode.com'), batch: { multicall: true } })
|
|
298
21
|
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// anyone: attestFor(owner, fingerprintBytes, sigBytes, keyBytes, deadline, signature)
|
|
22
|
+
const claims = await readClaims(client, '0x539C7e1E454296Dc150B95a0acCC05bCa3b33538')
|
|
23
|
+
const { kind, claim } = keyStanding(claims)
|
|
24
|
+
// kind: verified · not-counted (active claims, none verify) · inactive (only ended claims) · none
|
|
25
|
+
if (kind === 'verified') console.log(claim.fingerprint, claim.pgpPublicKey)
|
|
26
|
+
if (kind === 'not-counted' && claim.verification) console.log(claimCheckText(claim.verification).label)
|
|
305
27
|
```
|
|
306
28
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
## Themes
|
|
310
|
-
|
|
311
|
-
Three built-in themes: `thurin`, `dark`, `light`. All styles are scoped under `[data-thurin-theme]` with `thurin-` prefixed class names to avoid conflicts with your app's styles.
|
|
29
|
+
`readClaims` returns every claim, oldest first, and reads and verifies the newest 50 (`{ limit }` changes that; older ones come back with `verification: null`). `keyStanding` picks the newest active claim that verifies. `findOwners(client, { fingerprint })` or `{ keyId }` goes the other way: every address that ever claimed a key. ENS is left to you: resolve the name with viem first. `batch: { multicall: true }` makes the reads one request.
|
|
312
30
|
|
|
313
|
-
|
|
31
|
+
In React, wrap it in whatever you use for data, for example:
|
|
314
32
|
|
|
315
33
|
```tsx
|
|
316
|
-
|
|
34
|
+
const { data: claims } = useQuery({ queryKey: ['claims', address], queryFn: () => readClaims(client, address) })
|
|
317
35
|
```
|
|
318
36
|
|
|
319
|
-
|
|
37
|
+
`verifyAttestation` checks that the key has the claimed fingerprint, that the signature is over exactly `I control the Ethereum address: <lowercase address>`, and that the key is valid today, as gpg judges it. When a claim doesn't count, `kind` says why (`expired`, `revoked`, `compromised`, `unsupported`, `bad-signature`, …) and `claimCheckText` turns it into words.
|
|
320
38
|
|
|
321
|
-
|
|
39
|
+
`REGISTRY_ADDRESS` is `0xFa6956c11163517249f8A67F5560a4406B519451`, the same on Ethereum mainnet and Sepolia; `getRegistry(network)` gives each network's chain id, explorer, and default RPC. `REGISTRY_ABI` is the whole contract, writes included.
|
|
322
40
|
|
|
323
|
-
|
|
41
|
+
**Is a key compromised?** Ask the contract: `keyStatus(owner, fingerprint)`. Don't read it off the newest claim, and never count "compromised" across owners: anyone can claim any fingerprint and mark it under their own address.
|
|
324
42
|
|
|
325
|
-
|
|
326
|
-
<div
|
|
327
|
-
data-thurin-card="bendoubleu.eth"
|
|
328
|
-
data-theme="thurin"
|
|
329
|
-
data-rpc-url="https://your-node.example"
|
|
330
|
-
></div>
|
|
331
|
-
|
|
332
|
-
<script src="https://cdn.jsdelivr.net/npm/@thurinlabs/identity-kit@0/dist/embed.global.js"></script>
|
|
333
|
-
```
|
|
43
|
+
The rest, all covered in the [docs](https://docs.thurin.id/#/sdk):
|
|
334
44
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
| `data-network` | Optional. `sepolia` or `local` instead of mainnet. |
|
|
344
|
-
| `data-registry-address` | Optional. Override the registry contract address. |
|
|
45
|
+
- **Proofs:** `identifyProof`, `verifyProof` (GitHub, DNS, Farcaster, Codeberg, Mastodon; a GitHub or Codeberg proof must belong to the account in its URL).
|
|
46
|
+
- **Claim history:** `claimFates`, `claimFateText`, `expiresSoon`, `expiresSoonText`.
|
|
47
|
+
- **Records:** `kindName`, `checkKindName`, `checkRecordValue`, `fetchRecords`, `pickRecords`, `pageRecords`, `parseRecord`, and the `thurin.releases` helpers.
|
|
48
|
+
- **Permissions:** `attestTypedData`, `reattestTypedData`, `updateKeyTypedData`, `revokeTypedData`, `setRecordTypedData`, `markCompromisedTypedData`, for the registry's `…For` writes. To mark an already revoked claim compromised, sign `markCompromisedTypedData` alone.
|
|
49
|
+
- **Keys:** `parsePgpKey`, `leanKey`, `claimSignature`, `sshKeys` (SSH keys as `authorized_keys` lines), `stripEmailUserIDs`, fingerprint and key-ID helpers.
|
|
50
|
+
- **Encrypt:** `encryptionKeyFor` (only the claim that counts, with a valid encryption subkey; says when the key arrived in the last 7 days), `encryptTo` (hides the recipient by default), `encryptRefusalText`, `keyChangedText`.
|
|
51
|
+
- **ENS:** `fetchEnsHint`, `ensHintWrite` for the `id.thurin` record.
|
|
52
|
+
- **Avatars:** `avatarUrl`, `avatarFallbacks`, only from places that can't see the viewer: IPFS, Arweave, inline data, a content-addressed NFT, or `euc.li`.
|
|
345
53
|
|
|
346
|
-
|
|
54
|
+
## A card for your README
|
|
347
55
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
| Provider | Proof Method |
|
|
351
|
-
|----------|-------------|
|
|
352
|
-
| GitHub | Public gist, or a repository description (the form an organisation can use) |
|
|
353
|
-
| DNS | TXT record |
|
|
354
|
-
| Farcaster | Public cast (read from a public Farcaster node; no key) |
|
|
355
|
-
| Codeberg | Repository description |
|
|
356
|
-
| Mastodon | Profile metadata |
|
|
357
|
-
|
|
358
|
-
## How a claim is verified
|
|
359
|
-
|
|
360
|
-
`verifyAttestation` checks three things: the stored key's fingerprint is the one on the claim; the stored clearsigned statement was made by that key (or one of its bound signing subkeys) and has not been altered; and the statement names the claim's address. Key validity is judged **now**, the way gpg does it: the key must currently be bound, unrevoked, and unexpired. It is deliberately not judged at the instant the signature was made, which is openpgp.js's default. That default rejects a perfectly good claim whenever the key's newest self-certification postdates the attest signature, which is exactly what happens when you add a proof after attesting and export with `export-minimal` (1.0.3).
|
|
56
|
+
No library needed: thurin.id draws an image of any identity (name, key, and whether it's verified), `https://thurin.id/card/ens/<name>.png`. See [the docs](https://docs.thurin.id/#/sdk?id=readme-card).
|
|
361
57
|
|
|
362
58
|
## Key algorithms
|
|
363
59
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
## Migrating from 0.9.x
|
|
60
|
+
Anything openpgp.js can verify: Ed25519, Cv25519, NIST P-256/384/521, brainpool, RSA, and secp256k1, which openpgp.js refuses by default and the kit allows. A secp256k1 PGP key is also an Ethereum key, so don't fund its address. In Node, secp256k1 needs `eckey-utils`, which the kit installs.
|
|
367
61
|
|
|
368
|
-
|
|
62
|
+
## Upgrading from 1.x
|
|
369
63
|
|
|
370
|
-
|
|
371
|
-
|-------|-------|
|
|
372
|
-
| `REGISTRY_ABI` (v1, reads only) | v2 ABI, reads + writes |
|
|
373
|
-
| `CONTRACT_DEPLOY_BLOCK` | removed (no log scans) |
|
|
374
|
-
| `Attestation.txHash` | removed; `revokedAt` and `messageVersion` added |
|
|
375
|
-
| `rpcUrl` needed `eth_getLogs` | any RPC |
|
|
376
|
-
| — | `local` network, `registryAddress` prop / `data-registry-address` |
|
|
377
|
-
| — | fingerprint helpers, EIP-712 authorization helpers |
|
|
64
|
+
2.0.0 reads PGPRegistry v3. Claims store the key and signature as raw bytes.
|
|
378
65
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
|
384
|
-
|
|
385
|
-
| `
|
|
386
|
-
|
|
|
387
|
-
|
|
|
388
|
-
|
|
389
|
-
`useThurinIdentity`, `ThurinCard`, and the embed need no changes; they pass the attestation's key through automatically.
|
|
390
|
-
|
|
391
|
-
## Migrating from 0.7.x
|
|
392
|
-
|
|
393
|
-
0.8.0 collapses the Scry / Signet sub-brands into Thurin. Renames only — no behaviour changed, and existing on-chain attestations verify exactly as before.
|
|
394
|
-
|
|
395
|
-
| 0.7.x | 0.8.0 |
|
|
396
|
-
|-------|-------|
|
|
397
|
-
| `ScryCard` / `ScryCardProps` | `ThurinCard` / `ThurinCardProps` |
|
|
398
|
-
| `useScryIdentity` | `useThurinIdentity` |
|
|
399
|
-
| `useSignetClaims` | `useAttestations` |
|
|
400
|
-
| `ScryIdentity` (type) | `ThurinIdentity` |
|
|
401
|
-
| `SignetClaim` (type) | `Attestation` |
|
|
402
|
-
| `scryBaseUrl` (provider prop) | `baseUrl` |
|
|
403
|
-
| `data-scry-card` (embed) | `data-thurin-card` |
|
|
404
|
-
| `[data-scry-theme]`, `.scry-*`, `--scry-*` | `[data-thurin-theme]`, `.thurin-*`, `--thurin-*` |
|
|
405
|
-
| `ScryEmbed` (IIFE global) | `ThurinEmbed` |
|
|
66
|
+
| 1.x | 2.0.0 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `attestationsOf`, `getPayload` | `claimsOf`, `keyBytes`, `signatureBytes`; `readClaims` does it for you |
|
|
69
|
+
| `Attestation` | adds `state`, `replacedBy`, `revokeReason`; `messageVersion` 1 = detached, 0 = clearsigned |
|
|
70
|
+
| typed data `pgpSignature`, `pgpPublicKey` | `signature`, `key`; `Reattest` adds `keepRecords`, `Revoke` adds `reason`, `SetRecord` takes text; new `MarkCompromised` |
|
|
71
|
+
| `encodeRecord`, `decodeRecord`, `bytes32` kinds | text records listed by `recordsOf`; `checkKindName`, `checkRecordValue` |
|
|
72
|
+
| `RegistryDeployment.deployBlock` | removed |
|
|
73
|
+
| `IdentityKitProvider` and the hooks (`useThurinIdentity`, `useAttestations`, …) | removed; `readClaims`, `keyStanding`, `findOwners` in any framework |
|
|
74
|
+
| `ThurinCard`, the embed script, `/styles`, `Theme`, the `baseUrl` prop | removed; use the [card image](https://docs.thurin.id/#/sdk?id=readme-card) |
|
|
75
|
+
| a statement containing the address verified | the signed text must be exactly the statement |
|
|
406
76
|
|
|
407
77
|
## Development
|
|
408
78
|
|
|
@@ -412,14 +82,4 @@ npm run build
|
|
|
412
82
|
npm test
|
|
413
83
|
```
|
|
414
84
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
- [Thurin](https://thurin.id) — Identity explorer
|
|
418
|
-
- [Attest](https://thurin.id/attest) — Create identity claims
|
|
419
|
-
- [Documentation](https://docs.thurin.id)
|
|
420
|
-
- [GitHub](https://github.com/thurinlabs/identity-kit)
|
|
421
|
-
- [Codeberg](https://codeberg.org/thurinlabs/identity-kit)
|
|
422
|
-
|
|
423
|
-
## License
|
|
424
|
-
|
|
425
|
-
MIT
|
|
85
|
+
[GitHub](https://github.com/thurinlabs/identity-kit) · [Codeberg](https://codeberg.org/thurinlabs/identity-kit) · MIT
|