@thurinlabs/identity-kit 1.0.2 → 1.0.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thurin Labs LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -264,6 +264,10 @@ The card talks directly to Ethereum and each proof platform — no intermediary,
264
264
  | Codeberg | Repository description |
265
265
  | Mastodon | Profile metadata |
266
266
 
267
+ ## How a claim is verified
268
+
269
+ `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).
270
+
267
271
  ## Key algorithms
268
272
 
269
273
  Any curve openpgp.js can compute is accepted: Ed25519, Cv25519, NIST P-256/384/521, brainpool, RSA, and **secp256k1**. openpgp.js rejects secp256k1 by default because RFC 9580 does not list it; identity-kit turns that rejection off, since it is a compatibility rule, not a security one. A secp256k1 PGP key doubles as an Ethereum key (the address is derived from the same public point), so anything that can sign with the PGP key can sign Ethereum transactions: hold such a key if you like, but do not fund its derived address. In Node, openpgp.js needs the `eckey-utils` package for this curve; identity-kit depends on it, so `npm install` brings it in. The browser build needs nothing extra.
@@ -31148,7 +31148,9 @@ ${prettyStateOverride(stateOverride)}`;
31148
31148
  }
31149
31149
 
31150
31150
  // src/core/pgp.ts
31151
- var PGP_CONFIG = { rejectCurves: /* @__PURE__ */ new Set() };
31151
+ function pgpConfig(openpgp) {
31152
+ return { ...openpgp.config, rejectCurves: /* @__PURE__ */ new Set() };
31153
+ }
31152
31154
  function algorithmName(keyOrSubkey) {
31153
31155
  try {
31154
31156
  const info = keyOrSubkey.getAlgorithmInfo();
@@ -31170,7 +31172,9 @@ ${prettyStateOverride(stateOverride)}`;
31170
31172
  }
31171
31173
  async function parsePgpKey(armoredKey) {
31172
31174
  try {
31173
- const { readKey } = await Promise.resolve().then(() => (init_openpgp_min(), openpgp_min_exports));
31175
+ const openpgp = await Promise.resolve().then(() => (init_openpgp_min(), openpgp_min_exports));
31176
+ const { readKey } = openpgp;
31177
+ const config = pgpConfig(openpgp);
31174
31178
  const key = await readKey({ armoredKey });
31175
31179
  const fingerprint = key.getFingerprint().toUpperCase();
31176
31180
  const algorithm = algorithmName(key);
@@ -31191,7 +31195,10 @@ ${prettyStateOverride(stateOverride)}`;
31191
31195
  cert.signatureType,
31192
31196
  { userID: user.userID, key: key.keyPacket },
31193
31197
  void 0,
31194
- PGP_CONFIG
31198
+ // date: now
31199
+ void 0,
31200
+ // detached
31201
+ config
31195
31202
  );
31196
31203
  } catch {
31197
31204
  continue;
@@ -31235,15 +31242,28 @@ ${prettyStateOverride(stateOverride)}`;
31235
31242
  if (!pgpPublicKey || !pgpSignature) {
31236
31243
  return { verified: false, reason: "Missing PGP data" };
31237
31244
  }
31238
- const { readKey, readCleartextMessage, verify } = await Promise.resolve().then(() => (init_openpgp_min(), openpgp_min_exports));
31245
+ const openpgp = await Promise.resolve().then(() => (init_openpgp_min(), openpgp_min_exports));
31246
+ const { readKey, readCleartextMessage, LiteralDataPacket } = openpgp;
31247
+ const config = pgpConfig(openpgp);
31239
31248
  const publicKey = await readKey({ armoredKey: pgpPublicKey });
31240
31249
  const keyFingerprint = publicKey.getFingerprint().toUpperCase();
31241
31250
  if (keyFingerprint !== fingerprint.toUpperCase()) {
31242
31251
  return { verified: false, reason: "Key fingerprint mismatch" };
31243
31252
  }
31244
31253
  const message = await readCleartextMessage({ cleartextMessage: pgpSignature });
31245
- const { signatures } = await verify({ message, verificationKeys: publicKey, config: PGP_CONFIG });
31246
- await signatures[0].verified;
31254
+ const msg = message;
31255
+ const now = /* @__PURE__ */ new Date();
31256
+ let signed2 = false;
31257
+ for (const sig of msg.signature.packets) {
31258
+ if (!publicKey.getKeys(sig.issuerKeyID).length) continue;
31259
+ const signingKey = await publicKey.getSigningKey(sig.issuerKeyID, now, void 0, config);
31260
+ const literal = new LiteralDataPacket();
31261
+ literal.setText(msg.text ?? message.getText().replace(/\r?\n/g, "\r\n"));
31262
+ await sig.verify(signingKey.keyPacket, sig.signatureType, literal, now, true, config);
31263
+ signed2 = true;
31264
+ break;
31265
+ }
31266
+ if (!signed2) return { verified: false, reason: "Message is not signed by this key" };
31247
31267
  const signedText = message.getText();
31248
31268
  if (!signedText.toLowerCase().includes(ethAddress.toLowerCase())) {
31249
31269
  return { verified: false, reason: "Signed message does not contain ETH address" };
@@ -31333,25 +31353,28 @@ ${prettyStateOverride(stateOverride)}`;
31333
31353
  var EFP_API = "https://api.ethfollow.xyz/api/v1";
31334
31354
  async function fetchEFPGraph(address) {
31335
31355
  try {
31336
- const listsResp = await fetch(`${EFP_API}/users/${address}/lists`);
31337
- if (!listsResp.ok) return null;
31338
- const listsData = await listsResp.json();
31339
- const listId = listsData.primary_list;
31340
- if (!listId) return null;
31341
- const [followingResp, statsResp, top8Resp] = await Promise.all([
31342
- fetch(`${EFP_API}/lists/${listId}/following?limit=100`),
31356
+ const [statsResp, listsResp] = await Promise.all([
31343
31357
  fetch(`${EFP_API}/users/${address}/stats`),
31344
- fetch(`${EFP_API}/lists/${listId}/following?limit=8&tags=top8`)
31358
+ fetch(`${EFP_API}/users/${address}/lists`)
31345
31359
  ]);
31346
- const followingData = followingResp.ok ? await followingResp.json() : { following: [] };
31360
+ if (!statsResp.ok && !listsResp.ok) return null;
31347
31361
  const statsData = statsResp.ok ? await statsResp.json() : {};
31348
- const top8Data = top8Resp.ok ? await top8Resp.json() : { following: [] };
31349
- return {
31350
- followers: parseInt(statsData.followers_count) || 0,
31351
- following: (followingData.following || []).length,
31352
- top8: (top8Data.following || []).map((f2) => f2.data),
31353
- hasEfp: true
31354
- };
31362
+ const listsData = listsResp.ok ? await listsResp.json() : {};
31363
+ const followers = parseInt(statsData.followers_count) || 0;
31364
+ const listId = listsData.primary_list;
31365
+ let following = 0;
31366
+ let top8 = [];
31367
+ if (listId) {
31368
+ const [followingResp, top8Resp] = await Promise.all([
31369
+ fetch(`${EFP_API}/lists/${listId}/following?limit=100`),
31370
+ fetch(`${EFP_API}/lists/${listId}/following?limit=8&tags=top8`)
31371
+ ]);
31372
+ const followingData = followingResp.ok ? await followingResp.json() : { following: [] };
31373
+ const top8Data = top8Resp.ok ? await top8Resp.json() : { following: [] };
31374
+ following = (followingData.following || []).length;
31375
+ top8 = (top8Data.following || []).map((f2) => f2.data);
31376
+ }
31377
+ return { followers, following, top8, hasEfp: !!listId || followers > 0 };
31355
31378
  } catch {
31356
31379
  return null;
31357
31380
  }