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 +28 -0
- package/PROTOCOL.md +14 -5
- package/README.md +9 -2
- package/dist/anchor-client.js +19 -1
- package/dist/anchor-verification.d.ts +1 -0
- package/dist/anchor-verification.js +13 -4
- package/dist/bip47.js +16 -2
- package/dist/credentials.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/nip98.js +6 -4
- package/dist/relay.js +17 -2
- package/dist/verify.d.ts +7 -2
- package/dist/verify.js +44 -27
- package/package.json +3 -5
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
|
|
145
|
-
atomically consume it only after validating both signatures.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
package/dist/anchor-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
|
@@ -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,
|
|
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(
|
|
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
|
|
114
|
+
publicKey,
|
|
101
115
|
chainCode: payload.slice(35, 67),
|
|
102
116
|
};
|
|
103
117
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
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 => {
|
|
74
|
-
|
|
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;
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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": ">=
|
|
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
|
}
|