did-beta-bch 1.0.1 → 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();
@@ -133,7 +134,7 @@ export async function resolveAddress(npub, relays = DEFAULT_RELAYS) {
133
134
  const identity = await resolveIdentity(npub, relays);
134
135
  return identity?.bchAddress ?? null;
135
136
  }
136
- /** Returns full binding history for an npub, newest first */
137
+ /** Returns full binding history for an npub, newest first, with cryptographic supersedes validation */
137
138
  export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
138
139
  const decoded = nip19.decode(npub);
139
140
  if (decoded.type !== "npub")
@@ -157,8 +158,40 @@ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
157
158
  createdAt = JSON.parse(event.content).createdAt ?? createdAt;
158
159
  }
159
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
+ }
160
182
  results.push({ npub, pubkey, bchAddress: bchTag[1], network: bchTag[2] ?? "mainnet", bindingId: dTag[1], createdAt, bchProofTag, rawEvent: event });
161
183
  }
162
184
  return results;
163
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
+ }
164
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.1",
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",