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.
- package/dist/credentials.d.ts +21 -0
- package/dist/credentials.js +78 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +42 -7
- package/package.json +1 -1
package/dist/credentials.d.ts
CHANGED
|
@@ -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
|
+
}>;
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
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:
|
|
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
|
|
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 };
|