did-beta-bch 3.1.1 → 3.1.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/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to `cashlet-did` will be documented here.
4
4
 
5
+ ## [3.1.2] — 2026-11-07
6
+
7
+ ### Fixed
8
+ - `verifyBchAnchorProof`: corrected the ASERT height calculation and added a
9
+ configurable `maxCheckpointAgeSeconds` (default 14 days) plus checkpoint
10
+ timestamp validation.
11
+ - `resolveIdentity` and `queryRelays`: require valid Nostr signatures before
12
+ trusting or deduplicating events; revocations now require a valid kind `5`
13
+ event authored by the binding key.
14
+ - BIP47 payment codes now validate the checksum, flags, reserved bytes, and
15
+ public key.
16
+ - Credential and rating events now include unique NIP-33 `d` tags.
17
+ - Electrum client rejects malformed JSON responses and broadcast txid
18
+ mismatches.
19
+ - NIP-98 verification rejects duplicate or malformed `u` and `method` tags.
20
+ - `verifyChallengeProof` now requires `issuedAt`; missing, stale, or
21
+ future-dated challenges are rejected.
22
+ - Fixed a syntax error in `demo/verifier.mjs`.
23
+
24
+ ### Changed
25
+ - Requires Node.js 22+ for the built-in WebSocket transport.
26
+ - Anchor verification defaults to a 14-day maximum checkpoint age; refresh the
27
+ trusted checkpoint and review it out-of-band.
28
+
29
+ ### Breaking
30
+ - TypeScript callers of `verifyChallengeProof` must pass `issuedAt` as the
31
+ third argument (or `audience, issuedAt` for audience-bound challenges).
32
+
5
33
  ## [2.0.0] — 2026-11-03
6
34
 
7
35
  ### Breaking
package/PROTOCOL.md CHANGED
@@ -141,8 +141,11 @@ of these tags: `u` (verification endpoint), `method` (`POST`), `audience`,
141
141
  Both proofs MUST match the same verified binding's BCH address and Nostr
142
142
  pubkey, nonce, audience, endpoint, issue time, and expiration.
143
143
 
144
- The server MUST issue an unpredictable, one-time challenge, persist it, and
145
- atomically consume it only after validating both signatures. Browser-local
144
+ The server MUST issue an unpredictable, one-time challenge, persist its issue
145
+ time, and atomically consume it only after validating both signatures.
146
+ `verifyChallengeProof` requires the server-issued `issuedAt` and rejects a
147
+ challenge older than five minutes or dated more than 60 seconds in the future.
148
+ Browser-local
146
149
  challenge state alone is not server-enforced authentication.
147
150
  `verifyDualChallengeProof` checks cryptographic and context binding; the
148
151
  application is responsible for durable single-use challenge state.
@@ -179,9 +182,15 @@ The exported `BCH_MAINNET_CHECKPOINT` is a fixed mainnet trust root. The result
179
182
  is **checkpointed SPV**, not full-node validation: proof sources are untrusted,
180
183
  the checkpoint must not be fetched from the same endpoint, and the proof does
181
184
  not establish that a supplied branch is the globally highest-work branch.
182
- There is no chipnet checkpoint or proof-fetch adapter in this package, so
183
- chipnet anchors remain unverifiable unless an application supplies an
184
- independently reviewed checkpoint and consensus configuration.
185
+ `fetchAnchorProof` retrieves headers from an Electrum endpoint, but the
186
+ checkpoint itself must be supplied and reviewed out-of-band. Verification
187
+ rejects checkpoints older than `maxCheckpointAgeSeconds` (default 14 days), so
188
+ applications must refresh the trust root regularly.
189
+
190
+ `BCH_CHIPNET_CHECKPOINT` and `BCH_CHIPNET_ASERT_PARAMETERS` are provided for
191
+ chipnet testing, but chipnet may reset. After a reset, callers must supply a
192
+ new checkpoint derived from the post-reset chain; do not treat the bundled
193
+ chipnet constants as a long-lived trust root.
185
194
 
186
195
  ## 9. Other helpers
187
196
 
package/README.md CHANGED
@@ -134,7 +134,9 @@ const valid = verifyDualChallengeProof(
134
134
  ```
135
135
 
136
136
  Challenge issuance and one-time consumption belong on a server with durable,
137
- atomic storage. Do not treat browser-only challenge state as authentication.
137
+ atomic storage. `verifyChallengeProof` requires the server-issued `issuedAt`
138
+ and rejects challenges older than five minutes. Do not treat browser-only
139
+ challenge state as authentication.
138
140
 
139
141
  ### Key rotation
140
142
 
@@ -216,10 +218,15 @@ const result = verifyBchAnchorProof(proof, bindingId, {
216
218
  checkpoint: BCH_MAINNET_CHECKPOINT,
217
219
  asert: BCH_MAINNET_ASERT_PARAMETERS,
218
220
  minConfirmations: 6,
221
+ maxCheckpointAgeSeconds: 14 * 24 * 60 * 60, // default; update the checkpoint regularly
219
222
  })
220
223
  ```
221
224
 
222
- `wsTransport` works in both browser and Node.js (Node 22+ has native WebSocket). For Node < 22 or TCP, implement the three-method `ElectrumTransport` interface.
225
+ Checkpoints age out. The default `maxCheckpointAgeSeconds` is 14 days, which
226
+ matches the 2,048-header proof window at 144 BCH blocks/day; applications should
227
+ refresh the trusted checkpoint and review it out-of-band.
228
+
229
+ `wsTransport` works in both browser and Node.js. Node 22+ is required for the built-in WebSocket client; for other runtimes or TCP, implement the three-method `ElectrumTransport` interface.
223
230
 
224
231
  Chipnet uses `BCH_CHIPNET_CHECKPOINT` and `BCH_CHIPNET_ASERT_PARAMETERS`. Note chipnet is a test network that may reset; the checkpoint is valid for the current chain instance only.
225
232
 
@@ -3,6 +3,7 @@
3
3
  * SPV proofs. Works in Node.js (TCP) and browsers (WebSocket) via a pluggable
4
4
  * transport. Default transports are provided for each environment.
5
5
  */
6
+ import { hash256, hexToBin, binToHex } from "@bitauth/libauth";
6
7
  export const ELECTRUM_MAINNET_TCP = "electroncash.dk:50001";
7
8
  export const ELECTRUM_MAINNET_WS = "wss://electroncash.dk:50004";
8
9
  export const ELECTRUM_CHIPNET_TCP = "chipnet.imaginary.cash:50001";
@@ -15,7 +16,16 @@ async function electrumCall(transport, requests, timeoutMs = 15000) {
15
16
  transport.onLine(line => {
16
17
  if (!line.trim())
17
18
  return;
18
- const msg = JSON.parse(line);
19
+ let msg;
20
+ try {
21
+ msg = JSON.parse(line);
22
+ }
23
+ catch {
24
+ clearTimeout(timer);
25
+ transport.close();
26
+ reject(new Error("Malformed Electrum response"));
27
+ return;
28
+ }
19
29
  if (msg.error) {
20
30
  clearTimeout(timer);
21
31
  transport.close();
@@ -65,6 +75,14 @@ export async function broadcastAnchorTx(txHex, transport) {
65
75
  const txid = results.get(1);
66
76
  if (typeof txid !== "string" || !/^[0-9a-f]{64}$/.test(txid))
67
77
  throw new Error(`Unexpected broadcast response: ${JSON.stringify(txid)}`);
78
+ if (!/^(?:[0-9a-f]{2})+$/i.test(txHex))
79
+ throw new Error("Invalid transaction hex");
80
+ const raw = hexToBin(txHex);
81
+ if (typeof raw === "string")
82
+ throw new Error("Invalid transaction hex");
83
+ const expectedTxid = binToHex(Uint8Array.from(hash256(raw)).reverse());
84
+ if (txid !== expectedTxid)
85
+ throw new Error(`Broadcast txid does not match transaction: expected ${expectedTxid}, got ${txid}`);
68
86
  return txid;
69
87
  }
70
88
  /**
@@ -23,6 +23,7 @@ export interface BchAnchorVerificationOptions {
23
23
  asert: BchAsertParameters;
24
24
  minConfirmations?: number;
25
25
  maxHeaders?: number;
26
+ maxCheckpointAgeSeconds?: number;
26
27
  now?: number;
27
28
  }
28
29
  export interface BchAnchorVerificationResult {
@@ -2,6 +2,7 @@ import { decodeTransaction, hash256, hexToBin } from "@bitauth/libauth";
2
2
  import { ANCHOR_PREFIX } from "./anchor.js";
3
3
  const HEADER_BYTES = 80;
4
4
  const MAX_PROOF_HEADERS = 2048;
5
+ const DEFAULT_MAX_CHECKPOINT_AGE_SECS = 14 * 24 * 60 * 60;
5
6
  const TARGET_SPACING_SECS = 600n;
6
7
  const ASERT_HALF_LIFE_SECS = 172800n;
7
8
  const ASERT_RADIX = 1n << 16n;
@@ -62,10 +63,10 @@ function targetToCompact(target) {
62
63
  }
63
64
  return ((size << 24) | compact) >>> 0;
64
65
  }
65
- function asertTarget(parameters, previousHeight, previousTime) {
66
+ function asertTarget(parameters, candidateHeight, previousTime) {
66
67
  const anchorTarget = compactToTarget(parameters.anchorBits);
67
68
  const timeDifference = BigInt(previousTime - parameters.anchorPreviousBlockTime);
68
- const heightDifference = BigInt(previousHeight - parameters.anchorHeight);
69
+ const heightDifference = BigInt(candidateHeight - parameters.anchorHeight);
69
70
  if (heightDifference < 0n)
70
71
  throw new Error("Checkpoint predates the ASERT anchor");
71
72
  const exponent = ((timeDifference - TARGET_SPACING_SECS * heightDifference) * ASERT_RADIX) /
@@ -88,7 +89,7 @@ function expectedBits(parameters, previousHeight, previous, candidateTime) {
88
89
  if (parameters.allowMinDifficultyBlocks &&
89
90
  candidateTime > previous.timestamp + Number(2n * TARGET_SPACING_SECS))
90
91
  return parameters.powLimitBits;
91
- return targetToCompact(asertTarget(parameters, previousHeight, previous.timestamp));
92
+ return targetToCompact(asertTarget(parameters, previousHeight + 1, previous.timestamp));
92
93
  }
93
94
  function assertProofOfWork(header, powLimit) {
94
95
  const target = compactToTarget(header.bits);
@@ -146,10 +147,16 @@ export function verifyBchAnchorProof(proof, bindingId, options) {
146
147
  return result("invalid", "Invalid binding ID");
147
148
  const minConfirmations = options.minConfirmations ?? 6;
148
149
  const maxHeaders = options.maxHeaders ?? MAX_PROOF_HEADERS;
150
+ const maxCheckpointAge = options.maxCheckpointAgeSeconds ?? DEFAULT_MAX_CHECKPOINT_AGE_SECS;
151
+ const now = options.now ?? Math.floor(Date.now() / 1000);
149
152
  if (!Number.isSafeInteger(minConfirmations) || minConfirmations < 1)
150
153
  return result("invalid", "Minimum confirmations must be a positive integer");
151
154
  if (!Number.isSafeInteger(maxHeaders) || maxHeaders < 1)
152
155
  return result("invalid", "Maximum header count must be a positive integer");
156
+ if (!Number.isSafeInteger(maxCheckpointAge) || maxCheckpointAge < 1)
157
+ return result("invalid", "Maximum checkpoint age must be a positive integer");
158
+ if (!Number.isSafeInteger(now))
159
+ return result("invalid", "Current time must be a safe integer");
153
160
  if (!proof || !Array.isArray(proof.headers) || proof.headers.length === 0)
154
161
  return result("unverifiable", "Header chain proof is missing");
155
162
  if (proof.headers.length > maxHeaders)
@@ -164,8 +171,11 @@ export function verifyBchAnchorProof(proof, bindingId, options) {
164
171
  checkpointHeader.hashHex !== checkpoint.hash.toLowerCase() ||
165
172
  !Array.isArray(checkpoint.recentTimestamps) ||
166
173
  checkpoint.recentTimestamps.length !== 11 ||
174
+ !checkpoint.recentTimestamps.every(timestamp => Number.isSafeInteger(timestamp)) ||
167
175
  checkpoint.recentTimestamps[checkpoint.recentTimestamps.length - 1] !== checkpointHeader.timestamp)
168
176
  return result("unverifiable", "Invalid or unsupported trusted checkpoint");
177
+ if (now - checkpointHeader.timestamp > maxCheckpointAge)
178
+ return result("unverifiable", "Checkpoint is older than the maximum allowed age");
169
179
  if (!Number.isSafeInteger(proof.blockHeight) || proof.blockHeight <= checkpoint.height)
170
180
  return result("unverifiable", "Anchor predates the configured trusted checkpoint");
171
181
  if (!Number.isSafeInteger(proof.blockHeight) ||
@@ -180,7 +190,6 @@ export function verifyBchAnchorProof(proof, bindingId, options) {
180
190
  let timestamps = checkpoint.recentTimestamps.slice(-11);
181
191
  let inclusionHeader;
182
192
  const powLimit = compactToTarget(options.asert.powLimitBits);
183
- const now = options.now ?? Math.floor(Date.now() / 1000);
184
193
  for (let offset = 0; offset < proof.headers.length; offset++) {
185
194
  const height = checkpoint.height + offset + 1;
186
195
  const header = parseHeader(proof.headers[offset]);
package/dist/bip47.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * BIP47 v1 reusable payment code helpers.
3
3
  * Notification transactions and scanning are wallet-specific and out of scope.
4
4
  */
5
- import { secp256k1, hash160, encodeCashAddress, deriveHdPublicNodeChild, deriveHdPrivateNodeChild, deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode, } from "@bitauth/libauth";
5
+ import { secp256k1, sha256, hash160, encodeCashAddress, deriveHdPublicNodeChild, deriveHdPrivateNodeChild, deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode, } from "@bitauth/libauth";
6
6
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
7
7
  // BIP47 payment code: [0x47][0x01][0x00][pubkey 33][chaincode 32][reserved 13][checksum 4] = 85 bytes
8
8
  const PC_VERSION = 0x47;
@@ -66,6 +66,9 @@ async function sha256Once(data) {
66
66
  async function paymentCodeChecksum(versionAndPayload) {
67
67
  return (await sha256Once(await sha256Once(versionAndPayload))).slice(0, 4);
68
68
  }
69
+ function paymentCodeChecksumSync(versionAndPayload) {
70
+ return sha256.hash(sha256.hash(versionAndPayload)).slice(0, 4);
71
+ }
69
72
  /**
70
73
  * Encode a BIP47 v1 payment code from a public key and chain code.
71
74
  */
@@ -96,8 +99,19 @@ export function parsePaymentCode(paymentCode) {
96
99
  const payload = decoded.slice(1, 1 + PC_PAYLOAD_BYTES);
97
100
  if (payload[0] !== 0x01)
98
101
  throw new Error(`Unsupported payment code payload version: ${payload[0]}`);
102
+ if (payload[1] !== 0x00)
103
+ throw new Error(`Unsupported payment code features: 0x${payload[1].toString(16)}`);
104
+ if (!payload.slice(67, PC_PAYLOAD_BYTES).every(byte => byte === 0))
105
+ throw new Error("Invalid payment code reserved bytes");
106
+ const checksum = decoded.slice(PC_PAYLOAD_BYTES + 1);
107
+ const expected = paymentCodeChecksumSync(decoded.slice(0, PC_PAYLOAD_BYTES + 1));
108
+ if (checksum.length !== 4 || !checksum.every((byte, index) => byte === expected[index]))
109
+ throw new Error("Invalid payment code checksum");
110
+ const publicKey = payload.slice(2, 35);
111
+ if (!secp256k1.validatePublicKey(publicKey))
112
+ throw new Error("Invalid payment code public key");
99
113
  return {
100
- publicKey: payload.slice(2, 35),
114
+ publicKey,
101
115
  chainCode: payload.slice(35, 67),
102
116
  };
103
117
  }
@@ -6,6 +6,10 @@
6
6
  import { finalizeEvent, nip19, getPublicKey, verifyEvent } from "nostr-tools";
7
7
  import { queryRelays, publishToRelays, DEFAULT_RELAYS } from "./relay.js";
8
8
  export const CREDENTIAL_KIND = 30085;
9
+ function credentialAddressTag(subjectPubkey, type, issuedAt) {
10
+ const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, "0")).join("");
11
+ return `${type}:${subjectPubkey}:${issuedAt}:${nonce}`;
12
+ }
9
13
  /** Issue a credential — issuer signs a Nostr event attesting a claim about subjectNpub */
10
14
  export async function issueCredential(subjectNpub, type, issuerPrivkey, claims, relays = DEFAULT_RELAYS) {
11
15
  const decoded = nip19.decode(subjectNpub);
@@ -22,6 +26,7 @@ export async function issueCredential(subjectNpub, type, issuerPrivkey, claims,
22
26
  kind: CREDENTIAL_KIND,
23
27
  created_at: payload.issuedAt,
24
28
  tags: [
29
+ ["d", credentialAddressTag(subjectPubkey, type, payload.issuedAt)],
25
30
  ["p", subjectPubkey],
26
31
  ["credential-type", type],
27
32
  ["t", "cashlet-did-credential"],
@@ -130,6 +135,7 @@ export async function issueRating(subjectNpub, rating, issuerPrivkey, opts = {},
130
135
  kind: CREDENTIAL_KIND,
131
136
  created_at: payload.issuedAt,
132
137
  tags: [
138
+ ["d", credentialAddressTag(subjectPubkey, "rating", payload.issuedAt)],
133
139
  ["p", subjectPubkey],
134
140
  ["credential-type", "rating"],
135
141
  ["rating", String(rating)],
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { buildDisclosure, verifyDisclosure, type Disclosure, type DisclosureReveal, } from "./disclosure.js";
2
- export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, type PaymentCode, type EncodedPaymentCode, } from "./bip47.js";
2
+ export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, deriveHdPath, deriveHdPrivateNodeFromBip39Mnemonic, type PaymentCode, type EncodedPaymentCode, } from "./bip47.js";
3
3
  export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
4
4
  export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS, BCH_CHIPNET_CHECKPOINT, type BchSpvCheckpoint, type BchAsertParameters, type BchAnchorProof, type BchAnchorVerificationOptions, type BchAnchorVerificationResult, } from "./anchor-verification.js";
5
5
  export { broadcastAnchorTx, fetchAnchorProof, wsTransport, ELECTRUM_MAINNET_TCP, ELECTRUM_MAINNET_WS, ELECTRUM_CHIPNET_TCP, ELECTRUM_CHIPNET_WS, type ElectrumTransport, } from "./anchor-client.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { buildDisclosure, verifyDisclosure, } from "./disclosure.js";
2
- export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, } from "./bip47.js";
2
+ export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, deriveHdPath, deriveHdPrivateNodeFromBip39Mnemonic, } from "./bip47.js";
3
3
  export { buildAnchorTx, ANCHOR_PREFIX, } from "./anchor.js";
4
4
  export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS, BCH_CHIPNET_CHECKPOINT, } from "./anchor-verification.js";
5
5
  export { broadcastAnchorTx, fetchAnchorProof, wsTransport, ELECTRUM_MAINNET_TCP, ELECTRUM_MAINNET_WS, ELECTRUM_CHIPNET_TCP, ELECTRUM_CHIPNET_WS, } from "./anchor-client.js";
package/dist/nip98.js CHANGED
@@ -58,11 +58,13 @@ export async function verifyHttpAuth(authHeader, url, method, opts = {}) {
58
58
  if (event.created_at - now > HTTP_AUTH_CLOCK_SKEW)
59
59
  return { valid: false, error: "Event timestamp is in the future" };
60
60
  // 4. URL + method tags
61
- const uTag = event.tags.find((t) => t[0] === "u");
62
- const mTag = event.tags.find((t) => t[0] === "method");
63
- if (uTag?.[1] !== url)
61
+ if (!Array.isArray(event.tags))
62
+ return { valid: false, error: "Malformed tags" };
63
+ const uTags = event.tags.filter((t) => t[0] === "u");
64
+ const mTags = event.tags.filter((t) => t[0] === "method");
65
+ if (uTags.length !== 1 || uTags[0].length !== 2 || uTags[0][1] !== url)
64
66
  return { valid: false, error: "URL mismatch" };
65
- if (mTag?.[1] !== method.toUpperCase())
67
+ if (mTags.length !== 1 || mTags[0].length !== 2 || mTags[0][1] !== method.toUpperCase())
66
68
  return { valid: false, error: "Method mismatch" };
67
69
  if (opts.body === undefined)
68
70
  return { valid: false, error: "Request body must be provided (use null when absent)" };
package/dist/relay.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { verifyEvent } from "nostr-tools";
1
2
  const CONNECT_TIMEOUT_MS = 5000;
2
3
  const RESPONSE_TIMEOUT_MS = 8000;
3
4
  const MAX_EVENTS_PER_RELAY = 200;
@@ -67,11 +68,25 @@ export async function queryRelay(url, filter) {
67
68
  return [];
68
69
  }
69
70
  }
71
+ function isValidEvent(event) {
72
+ if (!event || typeof event.id !== "string")
73
+ return false;
74
+ try {
75
+ return verifyEvent(event);
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
70
81
  export async function queryRelays(relays, filter) {
71
82
  const results = await Promise.all(relays.map(r => queryRelay(r, filter)));
72
83
  const seen = new Set();
73
- return results.flat().filter(e => { if (seen.has(e.id))
74
- return false; seen.add(e.id); return true; });
84
+ return results.flat().filter(e => {
85
+ if (!isValidEvent(e) || seen.has(e.id))
86
+ return false;
87
+ seen.add(e.id);
88
+ return true;
89
+ });
75
90
  }
76
91
  export const DEFAULT_RELAYS = [
77
92
  "wss://nos.lol",
package/dist/verify.d.ts CHANGED
@@ -66,13 +66,18 @@ export declare function verifyDualChallengeProof(proof: DualChallengeProof, expe
66
66
  }): boolean;
67
67
  /** Verify a challenge-response proof.
68
68
  * The wallet signs the exact text produced by `bchChallengePayload`.
69
- * @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
69
+ * @param issuedAt - unix timestamp when the challenge was issued; required, rejects proofs older than 5 minutes.
70
70
  */
71
71
  export declare function verifyChallengeProof(proof: {
72
72
  signature: string;
73
73
  payload: string;
74
74
  address: string;
75
- }, nonce: string, audience?: string, issuedAt?: number): boolean;
75
+ }, nonce: string, issuedAt: number): boolean;
76
+ export declare function verifyChallengeProof(proof: {
77
+ signature: string;
78
+ payload: string;
79
+ address: string;
80
+ }, nonce: string, audience: string | undefined, issuedAt: number): boolean;
76
81
  /**
77
82
  * Resolve a BCH address from an npub — the primary pay-by-handle entry point.
78
83
  * Verifies both BCH and Nostr signatures before returning the address.
package/dist/verify.js CHANGED
@@ -105,6 +105,31 @@ function decodeNpub(npub) {
105
105
  return null;
106
106
  }
107
107
  }
108
+ function collectVerifiedRevocationIds(revocations, pubkey) {
109
+ const revokedIds = new Set();
110
+ for (const revocation of revocations) {
111
+ if (!revocation ||
112
+ revocation.kind !== 5 ||
113
+ revocation.pubkey !== pubkey ||
114
+ !Array.isArray(revocation.tags))
115
+ continue;
116
+ try {
117
+ if (!verifyEvent(revocation))
118
+ continue;
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ for (const tag of revocation.tags) {
124
+ if (Array.isArray(tag) &&
125
+ tag[0] === "e" &&
126
+ typeof tag[1] === "string" &&
127
+ /^[0-9a-f]{64}$/.test(tag[1]))
128
+ revokedIds.add(tag[1]);
129
+ }
130
+ }
131
+ return revokedIds;
132
+ }
108
133
  /** Resolve a BCH address from an npub via Nostr relays */
109
134
  export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
110
135
  const pubkey = decodeNpub(npub);
@@ -116,17 +141,20 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
116
141
  ]);
117
142
  if (!events.length)
118
143
  return null;
119
- const revokedIds = new Set(revocations.flatMap(e => Array.isArray(e.tags)
120
- ? e.tags.filter(t => Array.isArray(t) && t[0] === "e" && typeof t[1] === "string").map(t => t[1])
121
- : []));
122
- // Pick latest non-revoked event
123
- const resolvedEvents = events
144
+ const candidates = events
124
145
  .filter(e => e && e.pubkey === pubkey && Array.isArray(e.tags) && typeof e.content === "string")
125
- .filter(e => !revokedIds.has(e.id))
126
146
  .map(event => resolvedIdentityFromEvent(event))
127
- .filter((identity) => identity !== null)
147
+ .filter((identity) => identity !== null);
148
+ const verified = [];
149
+ for (const candidate of candidates) {
150
+ if ((await verifyIdentity(candidate)).status === "verified")
151
+ verified.push(candidate);
152
+ }
153
+ const revokedIds = collectVerifiedRevocationIds(revocations, pubkey);
154
+ const [latest] = verified
155
+ .filter(identity => !revokedIds.has(identity.rawEvent.id))
128
156
  .sort((a, b) => b.rawEvent.created_at - a.rawEvent.created_at);
129
- return resolvedEvents[0] ? { ...resolvedEvents[0], npub } : null;
157
+ return latest ? { ...latest, npub } : null;
130
158
  }
131
159
  /** Verify the BCH + Nostr signatures on a resolved identity */
132
160
  export async function verifyIdentity(resolved) {
@@ -348,21 +376,19 @@ export function verifyDualChallengeProof(proof, expected) {
348
376
  return false;
349
377
  }
350
378
  }
351
- /** Verify a challenge-response proof.
352
- * The wallet signs the exact text produced by `bchChallengePayload`.
353
- * @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
354
- */
355
- export function verifyChallengeProof(proof, nonce, audience, issuedAt) {
379
+ export function verifyChallengeProof(proof, nonce, audienceOrIssuedAt, issuedAtArg) {
380
+ const audience = typeof audienceOrIssuedAt === "number" ? undefined : audienceOrIssuedAt;
381
+ const issuedAt = typeof audienceOrIssuedAt === "number" ? audienceOrIssuedAt : issuedAtArg;
356
382
  if (!proof ||
357
383
  typeof proof.signature !== "string" ||
358
384
  typeof proof.payload !== "string" ||
359
385
  typeof proof.address !== "string" ||
360
386
  typeof nonce !== "string")
361
387
  return false;
362
- if (issuedAt !== undefined &&
363
- (!Number.isSafeInteger(issuedAt) ||
364
- issuedAt > Math.floor(Date.now() / 1000) + 60 ||
365
- Math.floor(Date.now() / 1000) - issuedAt > CHALLENGE_TTL_SECS))
388
+ if (issuedAt === undefined ||
389
+ !Number.isSafeInteger(issuedAt) ||
390
+ issuedAt > Math.floor(Date.now() / 1000) + 60 ||
391
+ Math.floor(Date.now() / 1000) - issuedAt > CHALLENGE_TTL_SECS)
366
392
  return false;
367
393
  let expectedPayload;
368
394
  try {
@@ -555,16 +581,7 @@ async function isBindingRevoked(event, relays) {
555
581
  authors: [event.pubkey],
556
582
  limit: 100,
557
583
  });
558
- return revocations.some(revocation => {
559
- try {
560
- return verifyEvent(revocation) &&
561
- revocation.pubkey === event.pubkey &&
562
- revocation.tags.some(tag => tag[0] === "e" && tag[1] === event.id);
563
- }
564
- catch {
565
- return false;
566
- }
567
- });
584
+ return collectVerifiedRevocationIds(revocations, event.pubkey).has(event.id);
568
585
  }
569
586
  /**
570
587
  * Resolve a BCH address from a NIP-05 handle (e.g. "alice@cashlet.app")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "did-beta-bch",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,8 +18,7 @@
18
18
  "PROTOCOL.md"
19
19
  ],
20
20
  "scripts": {
21
- "build": "tsc && npm run build:demo",
22
- "build:demo": "esbuild demo/client.js --bundle --format=esm --platform=browser --target=es2022 --outfile=demo/bundle.js",
21
+ "build": "tsc",
23
22
  "test": "node --test test/cashlet-did.test.mjs",
24
23
  "typecheck": "tsc --noEmit",
25
24
  "prepublishOnly": "npm run build"
@@ -37,7 +36,7 @@
37
36
  "author": "",
38
37
  "license": "MIT",
39
38
  "engines": {
40
- "node": ">=18"
39
+ "node": ">=22"
41
40
  },
42
41
  "dependencies": {
43
42
  "@bitauth/libauth": "^3.1.0-next.8",
@@ -46,7 +45,6 @@
46
45
  "nostr-tools": "^2.0.0"
47
46
  },
48
47
  "devDependencies": {
49
- "esbuild": "^0.28.2",
50
48
  "typescript": "^5.4.0"
51
49
  }
52
50
  }