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 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
- ## [Unreleased]
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
@@ -9,6 +9,7 @@ export interface AnchorUtxo {
9
9
  valueSats: number;
10
10
  lockingBytecode: Uint8Array;
11
11
  privateKey: Uint8Array;
12
+ feeSats?: number;
12
13
  }
13
14
  export interface AnchorResult {
14
15
  txHex: string;
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 DUST_SATS = 1000; // anchor output value (burned)
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
- if (utxo.valueSats <= FEE_SATS)
12
- throw new Error(`UTXO value ${utxo.valueSats} sats too small to cover fee ${FEE_SATS} sats`);
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 - FEE_SATS),
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,
@@ -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
- /** Compute a reputation score for an npub based on ratings and credentials */
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;
@@ -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; // kind 30079 is reserved for revocation (DID-PLAN)
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
- /** Compute a reputation score for an npub based on ratings and credentials */
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 TIMEOUT_MS = 8000;
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")); }, TIMEOUT_MS);
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" }); }, TIMEOUT_MS);
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); }, TIMEOUT_MS);
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.push(msg[2]);
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
- if (!proof.payload.startsWith(`${PROTOCOL_ID}/challenge\n`))
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 (!proof.payload.includes(nonce))
144
+ if (lines[1] !== nonce)
143
145
  return false;
144
- if (audience !== undefined && !proof.payload.includes(audience))
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: the referenced event must exist and its d-tag must match claimedId
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
- throw new CashletDidError("CHAIN_BROKEN", `supersedes tag ${claimedId} does not match referenced event d-tag`);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "did-beta-bch",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",