did-beta-bch 1.0.0 → 1.0.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.
@@ -9,6 +9,8 @@ export interface CredentialPayload {
9
9
  subject: string;
10
10
  type: string;
11
11
  issuedAt: number;
12
+ rating?: 1 | 2 | 3 | 4 | 5;
13
+ comment?: string;
12
14
  expiry?: number;
13
15
  claims?: Record<string, string>;
14
16
  }
@@ -25,3 +27,22 @@ export declare function issueCredential(subjectNpub: string, type: string, issue
25
27
  }>;
26
28
  /** Resolve all credentials issued to an npub */
27
29
  export declare function resolveCredentials(npub: string, relays?: string[]): Promise<IssuedCredential[]>;
30
+ /** Issue a star rating (1–5) to an npub — usable by any wallet or app */
31
+ export declare function issueRating(subjectNpub: string, rating: 1 | 2 | 3 | 4 | 5, issuerPrivkey: Uint8Array, opts?: {
32
+ comment?: string;
33
+ txid?: string;
34
+ network?: string;
35
+ }, relays?: string[]): Promise<{
36
+ credential: IssuedCredential;
37
+ results: RelayResult[];
38
+ }>;
39
+ /** Revoke a credential by publishing a NIP-09 kind:5 deletion event */
40
+ export declare function revokeCredential(eventId: string, issuerPrivkey: Uint8Array, relays?: string[]): Promise<RelayResult[]>;
41
+ /** Compute a reputation score for an npub based on ratings and credentials */
42
+ export declare function scoreReputation(npub: string, relays?: string[]): Promise<{
43
+ credentialCount: number;
44
+ ratingCount: number;
45
+ averageRating: number | null;
46
+ uniqueIssuers: number;
47
+ score: number;
48
+ }>;
@@ -49,7 +49,16 @@ export async function resolveCredentials(npub, relays = DEFAULT_RELAYS) {
49
49
  "#t": ["cashlet-did-credential"],
50
50
  limit: 50,
51
51
  });
52
- return events.map(e => {
52
+ // Fetch revocations and filter them out
53
+ const revocations = await queryRelays(relays, {
54
+ kinds: [5],
55
+ authors: events.map(e => e.pubkey),
56
+ limit: 100,
57
+ });
58
+ const revokedIds = new Set(revocations.flatMap(e => e.tags.filter((t) => t[0] === "e").map((t) => t[1])));
59
+ return events
60
+ .filter(e => !revokedIds.has(e.id))
61
+ .map(e => {
53
62
  try {
54
63
  const payload = JSON.parse(e.content);
55
64
  return { id: e.id, issuer: e.pubkey, payload, rawEvent: e };
@@ -59,3 +68,71 @@ export async function resolveCredentials(npub, relays = DEFAULT_RELAYS) {
59
68
  }
60
69
  }).filter(Boolean);
61
70
  }
71
+ /** Issue a star rating (1–5) to an npub — usable by any wallet or app */
72
+ export async function issueRating(subjectNpub, rating, issuerPrivkey, opts = {}, relays = DEFAULT_RELAYS) {
73
+ const claims = {};
74
+ if (opts.txid)
75
+ claims.txid = opts.txid;
76
+ if (opts.network)
77
+ claims.network = opts.network;
78
+ const decoded = nip19.decode(subjectNpub);
79
+ if (decoded.type !== "npub")
80
+ throw new Error("Invalid npub");
81
+ const subjectPubkey = decoded.data;
82
+ const payload = {
83
+ subject: subjectPubkey,
84
+ type: "rating",
85
+ issuedAt: Math.floor(Date.now() / 1000),
86
+ rating,
87
+ ...(opts.comment ? { comment: opts.comment } : {}),
88
+ ...(Object.keys(claims).length ? { claims } : {}),
89
+ };
90
+ const template = {
91
+ kind: CREDENTIAL_KIND,
92
+ created_at: payload.issuedAt,
93
+ tags: [
94
+ ["p", subjectPubkey],
95
+ ["credential-type", "rating"],
96
+ ["rating", String(rating)],
97
+ ["t", "cashlet-did-credential"],
98
+ ],
99
+ content: JSON.stringify(payload),
100
+ };
101
+ const signed = finalizeEvent(template, issuerPrivkey);
102
+ const results = await publishToRelays(relays, signed);
103
+ const { getPublicKey } = await import("nostr-tools");
104
+ return {
105
+ credential: { id: signed.id, issuer: getPublicKey(issuerPrivkey), payload, rawEvent: signed },
106
+ results,
107
+ };
108
+ }
109
+ /** Revoke a credential by publishing a NIP-09 kind:5 deletion event */
110
+ export async function revokeCredential(eventId, issuerPrivkey, relays = DEFAULT_RELAYS) {
111
+ const template = {
112
+ kind: 5,
113
+ created_at: Math.floor(Date.now() / 1000),
114
+ tags: [["e", eventId]],
115
+ content: "revoked",
116
+ };
117
+ const signed = finalizeEvent(template, issuerPrivkey);
118
+ return publishToRelays(relays, signed);
119
+ }
120
+ /** Compute a reputation score for an npub based on ratings and credentials */
121
+ export async function scoreReputation(npub, relays = DEFAULT_RELAYS) {
122
+ const credentials = await resolveCredentials(npub, relays);
123
+ const ratings = credentials.filter(c => c.payload.type === "rating" && c.payload.rating);
124
+ const uniqueIssuers = new Set(credentials.map(c => c.issuer)).size;
125
+ const averageRating = ratings.length
126
+ ? ratings.reduce((sum, c) => sum + (c.payload.rating ?? 0), 0) / ratings.length
127
+ : null;
128
+ // Score: avg rating (0-50) + unique issuer bonus (0-50, caps at 10 issuers)
129
+ const ratingScore = averageRating ? (averageRating / 5) * 50 : 0;
130
+ const issuerScore = Math.min(uniqueIssuers, 10) * 5;
131
+ return {
132
+ credentialCount: credentials.length,
133
+ ratingCount: ratings.length,
134
+ averageRating: averageRating ? Math.round(averageRating * 10) / 10 : null,
135
+ uniqueIssuers,
136
+ score: Math.round(ratingScore + issuerScore),
137
+ };
138
+ }
@@ -0,0 +1,5 @@
1
+ export type CashletDidErrorCode = "NOT_FOUND" | "INVALID_SIG" | "INVALID_BINDING" | "RELAY_TIMEOUT" | "RELAY_ERROR" | "INVALID_NPub" | "CHAIN_BROKEN" | "INVALID_NIP05";
2
+ export declare class CashletDidError extends Error {
3
+ readonly code: CashletDidErrorCode;
4
+ constructor(code: CashletDidErrorCode, message: string);
5
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,7 @@
1
+ export class CashletDidError extends Error {
2
+ constructor(code, message) {
3
+ super(message);
4
+ this.code = code;
5
+ this.name = "CashletDidError";
6
+ }
7
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { buildDisclosure, verifyDisclosure, type Disclosure, } from "./disclosure.js";
2
2
  export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
3
- export { issueCredential, resolveCredentials, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
3
+ export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
4
4
  export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, type Network, type CanonicalBinding, type IdentityBinding, type NostrEvent, buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
5
5
  export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
6
- export { resolveIdentity, resolveAddress, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
6
+ export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
7
+ export { CashletDidError, type CashletDidErrorCode } from "./errors.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { buildDisclosure, verifyDisclosure, } from "./disclosure.js";
2
2
  export { buildAnchorTx, ANCHOR_PREFIX, } from "./anchor.js";
3
- export { issueCredential, resolveCredentials, CREDENTIAL_KIND, } from "./credentials.js";
3
+ export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, } from "./credentials.js";
4
4
  export {
5
5
  // Protocol constants
6
6
  PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND,
@@ -11,4 +11,5 @@ export {
11
11
  publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, } from "./relay.js";
12
12
  export {
13
13
  // Verification
14
- resolveIdentity, resolveAddress, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
14
+ resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
15
+ export { CashletDidError } from "./errors.js";
package/dist/verify.d.ts CHANGED
@@ -35,6 +35,11 @@ export declare function verifyChallengeProof(proof: {
35
35
  * if (address) wallet.sendTo(address, amount)
36
36
  */
37
37
  export declare function resolveAddress(npub: string, relays?: string[]): Promise<string | null>;
38
- /** Returns full binding history for an npub, newest first */
38
+ /** Returns full binding history for an npub, newest first, with cryptographic supersedes validation */
39
39
  export declare function resolveIdentityChain(npub: string, relays?: string[]): Promise<ResolvedIdentity[]>;
40
+ /**
41
+ * Resolve a BCH address from a NIP-05 handle (e.g. "alice@cashlet.app")
42
+ * Chains: NIP-05 → npub → cashlet-did binding → BCH address
43
+ */
44
+ export declare function resolveByNip05(handle: string, relays?: string[]): Promise<string | null>;
40
45
  export { bchSigningPayload, canonicalize, sha256hex, type CanonicalBinding, type IdentityBinding };
package/dist/verify.js CHANGED
@@ -1,7 +1,8 @@
1
- import { verifyEvent } from "nostr-tools";
1
+ import { verifyEvent, nip05 } from "nostr-tools";
2
2
  import { secp256k1, hash256, encodeCashAddress, hash160 } from "@bitauth/libauth";
3
3
  import { canonicalize, sha256hex, bchSigningPayload, BINDING_KIND, PROTOCOL_ID, } from "./binding.js";
4
4
  import { queryRelays, DEFAULT_RELAYS } from "./relay.js";
5
+ import { CashletDidError } from "./errors.js";
5
6
  import { nip19 } from "nostr-tools";
6
7
  function bitcoinMsgHash(message) {
7
8
  const enc = new TextEncoder();
@@ -10,15 +11,16 @@ function bitcoinMsgHash(message) {
10
11
  const lenByte = new Uint8Array([msgBytes.length]);
11
12
  return hash256(new Uint8Array([...prefix, ...lenByte, ...msgBytes]));
12
13
  }
13
- function recoverBchAddress(sigHex, msgHash) {
14
+ function recoverBchAddress(sigHex, msgHash, expectedAddress) {
14
15
  const sig = Uint8Array.from(sigHex.match(/.{2}/g).map(b => parseInt(b, 16)));
15
16
  const tryRecover = (compact, id) => {
16
17
  try {
17
18
  const pub = secp256k1.recoverPublicKeyCompressed(compact, id, msgHash);
18
19
  if (!(pub instanceof Uint8Array))
19
20
  return null;
21
+ const addrPrefix = (expectedAddress?.split(":")[0] ?? "bitcoincash");
20
22
  for (const type of ["p2pkhWithTokens", "p2pkh"]) {
21
- const r = encodeCashAddress({ prefix: "bitcoincash", type, payload: hash160(pub) });
23
+ const r = encodeCashAddress({ prefix: addrPrefix, type, payload: hash160(pub) });
22
24
  if (r?.address)
23
25
  return r.address;
24
26
  if (typeof r === "string")
@@ -29,7 +31,8 @@ function recoverBchAddress(sigHex, msgHash) {
29
31
  return null;
30
32
  };
31
33
  if (sig.length === 65) {
32
- const addr = tryRecover(sig.slice(1), sig[0]);
34
+ const recoveryId = (sig[0] & 0x03);
35
+ const addr = tryRecover(sig.slice(1), recoveryId);
33
36
  if (addr)
34
37
  return addr;
35
38
  }
@@ -97,7 +100,7 @@ export async function verifyIdentity(resolved) {
97
100
  }
98
101
  // 2. BCH signature recovery
99
102
  const msgHash = bitcoinMsgHash(message);
100
- const recovered = recoverBchAddress(sigHex, msgHash);
103
+ const recovered = recoverBchAddress(sigHex, msgHash, bchAddress);
101
104
  const bchValid = recovered === bchAddress;
102
105
  // 3. Payload integrity
103
106
  if (!message.startsWith(`${PROTOCOL_ID}/bch-proof/`)) {
@@ -116,7 +119,7 @@ export function verifyChallengeProof(proof, nonce) {
116
119
  if (!proof.payload.startsWith(`${PROTOCOL_ID}/challenge`))
117
120
  return false;
118
121
  const msgHash = bitcoinMsgHash(proof.payload);
119
- const recovered = recoverBchAddress(proof.signature, msgHash);
122
+ const recovered = recoverBchAddress(proof.signature, msgHash, proof.address);
120
123
  return recovered === proof.address;
121
124
  }
122
125
  /**
@@ -131,7 +134,7 @@ export async function resolveAddress(npub, relays = DEFAULT_RELAYS) {
131
134
  const identity = await resolveIdentity(npub, relays);
132
135
  return identity?.bchAddress ?? null;
133
136
  }
134
- /** Returns full binding history for an npub, newest first */
137
+ /** Returns full binding history for an npub, newest first, with cryptographic supersedes validation */
135
138
  export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
136
139
  const decoded = nip19.decode(npub);
137
140
  if (decoded.type !== "npub")
@@ -155,8 +158,40 @@ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
155
158
  createdAt = JSON.parse(event.content).createdAt ?? createdAt;
156
159
  }
157
160
  catch { /* use event ts */ }
161
+ // Validate supersedes tag cryptographically
162
+ const supersedesTag = event.tags.find(t => t[0] === "supersedes");
163
+ if (supersedesTag) {
164
+ const claimedId = supersedesTag[1];
165
+ // Fetch the referenced event and verify its bindingId matches
166
+ const [prev] = await queryRelays(relays, { ids: [claimedId], limit: 1 });
167
+ if (prev) {
168
+ try {
169
+ const prevContent = JSON.parse(prev.content);
170
+ const actualId = await sha256hex(canonicalize(prevContent));
171
+ if (actualId !== claimedId) {
172
+ throw new CashletDidError("CHAIN_BROKEN", `supersedes tag ${claimedId} does not match referenced event content hash`);
173
+ }
174
+ }
175
+ catch (e) {
176
+ if (e instanceof CashletDidError)
177
+ throw e;
178
+ // content not canonical JSON — skip validation but keep event
179
+ }
180
+ }
181
+ }
158
182
  results.push({ npub, pubkey, bchAddress: bchTag[1], network: bchTag[2] ?? "mainnet", bindingId: dTag[1], createdAt, bchProofTag, rawEvent: event });
159
183
  }
160
184
  return results;
161
185
  }
186
+ /**
187
+ * Resolve a BCH address from a NIP-05 handle (e.g. "alice@cashlet.app")
188
+ * Chains: NIP-05 → npub → cashlet-did binding → BCH address
189
+ */
190
+ export async function resolveByNip05(handle, relays = DEFAULT_RELAYS) {
191
+ const profile = await nip05.queryProfile(handle);
192
+ if (!profile?.pubkey)
193
+ throw new CashletDidError("INVALID_NIP05", `NIP-05 handle not found: ${handle}`);
194
+ const npub = nip19.npubEncode(profile.pubkey);
195
+ return resolveAddress(npub, relays);
196
+ }
162
197
  export { bchSigningPayload, canonicalize, sha256hex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "did-beta-bch",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",