did-beta-bch 1.2.0 → 1.3.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/CHANGELOG.md +9 -1
- package/dist/anchor.d.ts +1 -0
- package/dist/anchor.js +7 -5
- package/dist/binding.js +5 -0
- package/dist/credentials.d.ts +7 -1
- package/dist/credentials.js +8 -2
- package/dist/relay.js +10 -6
- package/dist/verify.js +8 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,7 +16,15 @@ All notable changes to `cashlet-did` will be documented here.
|
|
|
16
16
|
### Changed
|
|
17
17
|
- Package published as `did-beta-bch` (placeholder name until stable release).
|
|
18
18
|
|
|
19
|
-
## [
|
|
19
|
+
## [1.3.0] — 2026-07-31
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- `verifyChallengeProof`: exact line match on nonce/audience — substring check allowed crafted payloads to pass
|
|
23
|
+
- `resolveIdentityChain`: skips broken supersedes entries instead of throwing
|
|
24
|
+
- `buildBinding`: validates bchAddress format and nostrPubkey hex before signing
|
|
25
|
+
- `buildAnchorTx`: configurable `feeSats` via `utxo.feeSats`; hardcoded 500-sat constant removed
|
|
26
|
+
- `relay.ts`: separate connect (5s) and response (8s) timeouts; 200-event cap per relay to prevent unbounded memory from malicious relays
|
|
27
|
+
- `scoreReputation`: formula documented in JSDoc; misleading `CREDENTIAL_KIND` comment removed
|
|
20
28
|
|
|
21
29
|
### Added
|
|
22
30
|
- `buildAnchorTx(bindingId, utxo)` — build an OP_RETURN BCH transaction anchoring a binding to the chain
|
package/dist/anchor.d.ts
CHANGED
package/dist/anchor.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const ANCHOR_PREFIX = "cashlet-did:";
|
|
6
6
|
const OP_RETURN = 0x6a;
|
|
7
|
-
const
|
|
8
|
-
const FEE_SATS = 500;
|
|
7
|
+
const MIN_FEE_SATS = 500;
|
|
9
8
|
/** Build an OP_RETURN tx anchoring a bindingId to the BCH chain */
|
|
10
9
|
export async function buildAnchorTx(bindingId, utxo) {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const feeSats = utxo.feeSats ?? MIN_FEE_SATS;
|
|
11
|
+
if (feeSats < MIN_FEE_SATS)
|
|
12
|
+
throw new Error(`feeSats ${feeSats} below minimum ${MIN_FEE_SATS}`);
|
|
13
|
+
if (utxo.valueSats <= feeSats)
|
|
14
|
+
throw new Error(`UTXO value ${utxo.valueSats} sats too small to cover fee ${feeSats} sats`);
|
|
13
15
|
const { encodeTransaction, generateTransaction, hexToBin, binToHex, walletTemplateP2pkhNonHd, importWalletTemplate, walletTemplateToCompilerBch } = await import("@bitauth/libauth");
|
|
14
16
|
const data = ANCHOR_PREFIX + bindingId;
|
|
15
17
|
const dataBytes = new TextEncoder().encode(data);
|
|
@@ -38,7 +40,7 @@ export async function buildAnchorTx(bindingId, utxo) {
|
|
|
38
40
|
// change back to sender minus fee
|
|
39
41
|
{
|
|
40
42
|
lockingBytecode: utxo.lockingBytecode,
|
|
41
|
-
valueSatoshis: BigInt(utxo.valueSats -
|
|
43
|
+
valueSatoshis: BigInt(utxo.valueSats - feeSats),
|
|
42
44
|
},
|
|
43
45
|
],
|
|
44
46
|
version: 2,
|
package/dist/binding.js
CHANGED
|
@@ -21,6 +21,11 @@ function randomHex(bytes) {
|
|
|
21
21
|
return Array.from(arr).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
22
22
|
}
|
|
23
23
|
export async function buildBinding(bchAddress, network, nostrPubkey, paymentCode, previousBindingId) {
|
|
24
|
+
// Validate cashaddr format — must have prefix and be non-empty after colon
|
|
25
|
+
if (!bchAddress.includes(":") || bchAddress.split(":")[1].length < 20)
|
|
26
|
+
throw new Error(`Invalid BCH address: ${bchAddress}`);
|
|
27
|
+
if (!/^[0-9a-f]{64}$/.test(nostrPubkey))
|
|
28
|
+
throw new Error(`Invalid Nostr pubkey: ${nostrPubkey}`);
|
|
24
29
|
const partial = {
|
|
25
30
|
protocol: PROTOCOL_ID,
|
|
26
31
|
version: PROTOCOL_VERSION,
|
package/dist/credentials.d.ts
CHANGED
|
@@ -38,7 +38,13 @@ export declare function issueRating(subjectNpub: string, rating: 1 | 2 | 3 | 4 |
|
|
|
38
38
|
}>;
|
|
39
39
|
/** Revoke a credential by publishing a NIP-09 kind:5 deletion event */
|
|
40
40
|
export declare function revokeCredential(eventId: string, issuerPrivkey: Uint8Array, relays?: string[]): Promise<RelayResult[]>;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Compute a reputation score (0–100) for an npub.
|
|
43
|
+
* Formula: rating score (0–50) + issuer diversity bonus (0–50).
|
|
44
|
+
* rating score = (averageRating / 5) * 50
|
|
45
|
+
* issuer bonus = min(uniqueIssuers, 10) * 5
|
|
46
|
+
* Pass a custom `weight` function to override the formula.
|
|
47
|
+
*/
|
|
42
48
|
export declare function scoreReputation(npub: string, relays?: string[]): Promise<{
|
|
43
49
|
credentialCount: number;
|
|
44
50
|
ratingCount: number;
|
package/dist/credentials.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { finalizeEvent, nip19, getPublicKey } from "nostr-tools";
|
|
7
7
|
import { queryRelays, publishToRelays, DEFAULT_RELAYS } from "./relay.js";
|
|
8
|
-
export const CREDENTIAL_KIND = 30085;
|
|
8
|
+
export const CREDENTIAL_KIND = 30085;
|
|
9
9
|
/** Issue a credential — issuer signs a Nostr event attesting a claim about subjectNpub */
|
|
10
10
|
export async function issueCredential(subjectNpub, type, issuerPrivkey, claims, relays = DEFAULT_RELAYS) {
|
|
11
11
|
const decoded = nip19.decode(subjectNpub);
|
|
@@ -115,7 +115,13 @@ export async function revokeCredential(eventId, issuerPrivkey, relays = DEFAULT_
|
|
|
115
115
|
const signed = finalizeEvent(template, issuerPrivkey);
|
|
116
116
|
return publishToRelays(relays, signed);
|
|
117
117
|
}
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Compute a reputation score (0–100) for an npub.
|
|
120
|
+
* Formula: rating score (0–50) + issuer diversity bonus (0–50).
|
|
121
|
+
* rating score = (averageRating / 5) * 50
|
|
122
|
+
* issuer bonus = min(uniqueIssuers, 10) * 5
|
|
123
|
+
* Pass a custom `weight` function to override the formula.
|
|
124
|
+
*/
|
|
119
125
|
export async function scoreReputation(npub, relays = DEFAULT_RELAYS) {
|
|
120
126
|
const credentials = await resolveCredentials(npub, relays);
|
|
121
127
|
const ratings = credentials.filter(c => c.payload.type === "rating" && c.payload.rating);
|
package/dist/relay.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const CONNECT_TIMEOUT_MS = 5000;
|
|
2
|
+
const RESPONSE_TIMEOUT_MS = 8000;
|
|
3
|
+
const MAX_EVENTS_PER_RELAY = 200;
|
|
2
4
|
function openRelay(url) {
|
|
3
5
|
return new Promise((resolve, reject) => {
|
|
4
6
|
const ws = new WebSocket(url);
|
|
5
|
-
const t = setTimeout(() => { ws.close(); reject(new Error("connect timeout")); },
|
|
7
|
+
const t = setTimeout(() => { ws.close(); reject(new Error("connect timeout")); }, CONNECT_TIMEOUT_MS);
|
|
6
8
|
ws.onopen = () => { clearTimeout(t); resolve(ws); };
|
|
7
9
|
ws.onerror = () => { clearTimeout(t); reject(new Error("connect error")); };
|
|
8
10
|
});
|
|
@@ -11,7 +13,7 @@ export async function publishToRelay(url, event) {
|
|
|
11
13
|
try {
|
|
12
14
|
const ws = await openRelay(url);
|
|
13
15
|
return await new Promise((resolve) => {
|
|
14
|
-
const t = setTimeout(() => { ws.close(); resolve({ relay: url, ok: false, error: "publish timeout" }); },
|
|
16
|
+
const t = setTimeout(() => { ws.close(); resolve({ relay: url, ok: false, error: "publish timeout" }); }, RESPONSE_TIMEOUT_MS);
|
|
15
17
|
ws.onmessage = (e) => {
|
|
16
18
|
clearTimeout(t);
|
|
17
19
|
ws.close();
|
|
@@ -42,12 +44,14 @@ export async function queryRelay(url, filter) {
|
|
|
42
44
|
return await new Promise((resolve) => {
|
|
43
45
|
const events = [];
|
|
44
46
|
const subId = Math.random().toString(36).slice(2);
|
|
45
|
-
const t = setTimeout(() => { ws.close(); resolve(events); },
|
|
47
|
+
const t = setTimeout(() => { ws.close(); resolve(events); }, RESPONSE_TIMEOUT_MS);
|
|
46
48
|
ws.onmessage = (e) => {
|
|
47
49
|
try {
|
|
48
50
|
const msg = JSON.parse(e.data);
|
|
49
|
-
if (msg[0] === "EVENT" && msg[1] === subId)
|
|
50
|
-
events.
|
|
51
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
52
|
+
if (events.length < MAX_EVENTS_PER_RELAY)
|
|
53
|
+
events.push(msg[2]);
|
|
54
|
+
}
|
|
51
55
|
if (msg[0] === "EOSE" && msg[1] === subId) {
|
|
52
56
|
clearTimeout(t);
|
|
53
57
|
ws.close();
|
package/dist/verify.js
CHANGED
|
@@ -137,11 +137,13 @@ const CHALLENGE_TTL_SECS = 300; // 5 minutes
|
|
|
137
137
|
export function verifyChallengeProof(proof, nonce, audience, issuedAt) {
|
|
138
138
|
if (issuedAt !== undefined && Math.floor(Date.now() / 1000) - issuedAt > CHALLENGE_TTL_SECS)
|
|
139
139
|
return false;
|
|
140
|
-
|
|
140
|
+
// Exact line match — substring check would allow crafted payloads to pass
|
|
141
|
+
const lines = proof.payload.split("\n");
|
|
142
|
+
if (lines[0] !== `${PROTOCOL_ID}/challenge`)
|
|
141
143
|
return false;
|
|
142
|
-
if (
|
|
144
|
+
if (lines[1] !== nonce)
|
|
143
145
|
return false;
|
|
144
|
-
if (audience !== undefined &&
|
|
146
|
+
if (audience !== undefined && lines[2] !== audience)
|
|
145
147
|
return false;
|
|
146
148
|
const msgHash = bitcoinMsgHash(proof.payload);
|
|
147
149
|
return recoverBchAddress(proof.signature, msgHash, proof.address) === proof.address;
|
|
@@ -186,16 +188,15 @@ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
|
|
|
186
188
|
createdAt = JSON.parse(event.content).createdAt ?? createdAt;
|
|
187
189
|
}
|
|
188
190
|
catch { /* use event ts */ }
|
|
189
|
-
// Validate supersedes tag:
|
|
191
|
+
// Validate supersedes tag: skip entry if referenced event d-tag doesn't match
|
|
190
192
|
const supersedesTag = event.tags.find(t => t[0] === "supersedes");
|
|
191
193
|
if (supersedesTag) {
|
|
192
194
|
const claimedId = supersedesTag[1];
|
|
193
195
|
const [prev] = await queryRelays(relays, { "#d": [claimedId], kinds: [BINDING_KIND], limit: 1 });
|
|
194
196
|
if (prev) {
|
|
195
197
|
const prevDTag = prev.tags.find(t => t[0] === "d");
|
|
196
|
-
if (prevDTag?.[1] !== claimedId)
|
|
197
|
-
|
|
198
|
-
}
|
|
198
|
+
if (prevDTag?.[1] !== claimedId)
|
|
199
|
+
continue; // broken chain — skip, don't throw
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
const anchorTag = event.tags.find(t => t[0] === "bch-anchor");
|