did-beta-bch 1.0.2 → 1.2.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
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to `cashlet-did` will be documented here.
4
4
 
5
+ ## [1.2.0] — 2026-07-31
6
+
7
+ ### Fixed
8
+ - **Breaking:** Bitcoin signed message hash now uses correct compact-size (varint) length prefix.
9
+ Previously used a single truncated byte, making all published bindings unverifiable for payloads
10
+ ≥256 bytes. Bindings published with ≤1.1.0 must be re-published.
11
+
12
+ ### Added
13
+ - `PROTOCOL.md` — standalone normative spec; third parties can implement compatible verifiers
14
+ without depending on this library.
15
+
16
+ ### Changed
17
+ - Package published as `did-beta-bch` (placeholder name until stable release).
18
+
5
19
  ## [Unreleased]
6
20
 
7
21
  ### Added
package/PROTOCOL.md ADDED
@@ -0,0 +1,214 @@
1
+ # Cashlet DID Protocol Specification
2
+
3
+ Version: `1.0.0`
4
+ Status: Draft
5
+
6
+ This document is the normative protocol specification. Any BCH wallet or Nostr
7
+ client can implement compatibility by following this spec — no dependency on the
8
+ `cashlet-did` library is required.
9
+
10
+ ---
11
+
12
+ ## 1. Identifiers
13
+
14
+ | Constant | Value |
15
+ |---|---|
16
+ | Protocol ID | `cashlet-did` |
17
+ | Protocol version | `1.0.0` |
18
+ | Binding event kind | `30078` (NIP-78 addressable data) |
19
+ | Credential event kind | `30085` |
20
+ | Revocation event kind | `5` (NIP-09) |
21
+ | Nostr derivation path | `m/44'/1237'/<account>'/0/0` (NIP-06) |
22
+
23
+ ---
24
+
25
+ ## 2. Canonical binding record
26
+
27
+ A binding record is a JSON object with **sorted keys** (no whitespace) used as
28
+ the input to the binding ID hash. Undefined/absent fields are omitted.
29
+
30
+ ```json
31
+ {
32
+ "bch": {
33
+ "address": "<cashaddr>",
34
+ "network": "mainnet" | "testnet" | "chipnet",
35
+ "paymentCode": "<BIP47 payment code>" // optional
36
+ },
37
+ "createdAt": <unix seconds>,
38
+ "nostr": { "pubkey": "<32-byte hex>" },
39
+ "nonce": "<64 hex chars of random bytes>",
40
+ "previousBindingId": "<hex>", // optional, key rotation
41
+ "protocol": "cashlet-did",
42
+ "purpose": "identity-binding-v1",
43
+ "version": "1.0.0"
44
+ }
45
+ ```
46
+
47
+ **Binding ID** = `SHA-256(canonicalize(record_without_bindingId))` as lowercase hex.
48
+
49
+ `canonicalize` means: recursively sort object keys alphabetically, no spaces,
50
+ omit keys whose value is `undefined`. Arrays preserve order.
51
+
52
+ ---
53
+
54
+ ## 3. BCH signing payload
55
+
56
+ ```
57
+ cashlet-did/bch-proof/v1.0.0\n<canonicalized subset>
58
+ ```
59
+
60
+ The canonicalized subset contains exactly these keys (sorted):
61
+
62
+ ```json
63
+ {
64
+ "bchAddress": "<cashaddr>",
65
+ "bchNetwork": "mainnet" | "testnet" | "chipnet",
66
+ "bindingId": "<hex>",
67
+ "createdAt": <unix seconds>,
68
+ "nonce": "<hex>",
69
+ "nostrPubkey": "<hex>",
70
+ "purpose": "identity-binding-v1"
71
+ }
72
+ ```
73
+
74
+ ### BCH signature format
75
+
76
+ Bitcoin signed message format:
77
+
78
+ ```
79
+ SHA256d( "\x18Bitcoin Signed Message:\n" + varint(len(payload_bytes)) + payload_bytes )
80
+ ```
81
+
82
+ `varint` is Bitcoin compact-size encoding:
83
+ - `n < 0xfd` → 1 byte `[n]`
84
+ - `0xfd ≤ n ≤ 0xffff` → 3 bytes `[0xfd, n & 0xff, n >> 8]`
85
+
86
+ The signature is a **65-byte recoverable compact signature**:
87
+ `[recovery_id_byte (0–3)] + [64-byte compact sig]`, hex-encoded.
88
+
89
+ > ⚠️ Signing payloads are typically ~400 bytes. A single-byte length prefix
90
+ > (truncating to `n & 0xff`) is **wrong** and will produce unverifiable signatures.
91
+
92
+ ---
93
+
94
+ ## 4. Nostr event (kind 30078)
95
+
96
+ ```json
97
+ {
98
+ "kind": 30078,
99
+ "pubkey": "<nostr pubkey hex>",
100
+ "created_at": <unix seconds>,
101
+ "tags": [
102
+ ["d", "<bindingId>"],
103
+ ["bch", "<cashaddr>", "<network>"],
104
+ ["bch-proof", "<signing payload>", "<65-byte sig hex>"],
105
+ ["protocol", "cashlet-did", "1.0.0"],
106
+ ["t", "cashlet-did"],
107
+ ["bch-payment-code", "<BIP47 code>"], // optional
108
+ ["supersedes", "<previous bindingId>"], // optional, key rotation
109
+ ["bch-anchor", "<txid>", "<block timestamp unix>"] // optional
110
+ ],
111
+ "content": "<canonicalized binding JSON>",
112
+ "id": "<NIP-01 event id>",
113
+ "sig": "<Schnorr sig>"
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 5. Discovery (NIP-01 filter)
120
+
121
+ ```json
122
+ { "kinds": [30078], "authors": ["<pubkey hex>"], "#t": ["cashlet-did"] }
123
+ ```
124
+
125
+ Query any public Nostr relay. Default relays: `wss://nos.lol`,
126
+ `wss://relay.damus.io`, `wss://relay.nostr.band`.
127
+
128
+ Revocations are kind `5` events from the same author with `["e", "<event id>"]`
129
+ tags. Filter revoked events before selecting the latest binding.
130
+
131
+ ---
132
+
133
+ ## 6. Verification algorithm
134
+
135
+ 1. Fetch binding event(s) via NIP-01 filter above.
136
+ 2. Fetch kind `5` revocations from the same author; discard revoked events.
137
+ 3. Select the event with the highest `created_at` among non-revoked results.
138
+ 4. Verify the **Nostr event signature** (NIP-01 Schnorr).
139
+ 5. Reconstruct the signing payload from the `bch-proof` tag (index 1).
140
+ 6. Compute `SHA256d` of the Bitcoin signed message over that payload (§3).
141
+ 7. Recover the BCH public key from the 65-byte signature + hash.
142
+ 8. Derive the P2PKH address from the recovered key; compare to the `bch` tag.
143
+ 9. Status is `verified` iff both signatures are valid.
144
+
145
+ ---
146
+
147
+ ## 7. Challenge-response authentication
148
+
149
+ Payload format:
150
+
151
+ ```
152
+ cashlet-did/challenge\n<nonce>\n<audience>
153
+ ```
154
+
155
+ - `nonce`: random hex string issued by the verifier
156
+ - `audience`: verifier's URL or identifier (optional but recommended)
157
+
158
+ The wallet signs this payload using the same Bitcoin signed message format (§3).
159
+ The verifier recovers the BCH address and checks it matches the binding.
160
+
161
+ Implementations **should** enforce a 5-minute TTL on issued challenges.
162
+
163
+ ---
164
+
165
+ ## 8. Key rotation
166
+
167
+ Set `previousBindingId` in the new binding record and add a
168
+ `["supersedes", "<previousBindingId>"]` tag to the Nostr event.
169
+
170
+ Resolvers can reconstruct the full rotation chain by following `supersedes` tags
171
+ backwards. The newest non-revoked binding is the authoritative identity.
172
+
173
+ ---
174
+
175
+ ## 9. Selective disclosure
176
+
177
+ Each attribute is committed as:
178
+
179
+ ```
180
+ SHA-256( "cashlet-did/attr/<key>:<value>" )
181
+ ```
182
+
183
+ Commitments are published in the binding (e.g. as extra tags or in content).
184
+ To verify a revealed attribute, recompute the hash and compare to the commitment.
185
+
186
+ ---
187
+
188
+ ## 10. On-chain commitment (OP_RETURN)
189
+
190
+ Embed the binding ID in a BCH transaction output:
191
+
192
+ ```
193
+ OP_RETURN <"cashlet-did:" + bindingId>
194
+ ```
195
+
196
+ The confirmed block's `time` field provides a consensus-enforced timestamp
197
+ accurate to ~1 block (~10 minutes). It cannot be significantly backdated
198
+ (median-time-past rule) and can drift at most 2 hours forward (miner clock
199
+ skew allowance). Prefer `anchoredAt` over the self-reported `createdAt`.
200
+
201
+ ---
202
+
203
+ ## 11. NIP-05 handle resolution
204
+
205
+ `user@domain` → `GET https://domain/.well-known/nostr.json?name=user`
206
+ → extract `pubkey` → resolve binding via §5.
207
+
208
+ ---
209
+
210
+ ## 12. NIP-98 HTTP Auth
211
+
212
+ Build a kind `27235` event per NIP-98. The `verifyBch: true` option on the
213
+ server additionally resolves the signer's cashlet-did binding, proving BCH
214
+ address ownership in the same request.
package/README.md CHANGED
@@ -1,21 +1,31 @@
1
1
  # cashlet-did
2
2
 
3
- A standalone, framework-agnostic library implementing the **Cashlet DID protocol** — a decentralised identity layer that binds Bitcoin Cash (BCH) addresses to Nostr public keys.
3
+ A standalone, framework-agnostic library implementing the **Cashlet DID protocol** — a decentralised identity layer that cryptographically binds Bitcoin Cash (BCH) addresses to Nostr public keys.
4
4
 
5
5
  Any BCH wallet or Nostr client can use this library to publish, resolve, and verify BCH–Nostr identity bindings without depending on the Cashlet extension.
6
6
 
7
+ ## Real-world use cases
8
+
9
+ **BCH wallet** — call `buildBinding` + `publishToRelays` once when a user sets up their identity. Anyone can then pay them by Nostr handle (`resolveAddress('npub1...')`) instead of exchanging addresses. Users never see the protocol.
10
+
11
+ **Nostr client** — call `verifyIdentity` when rendering a profile to show a cryptographically-proven "verified BCH address" badge. No issuing authority — the proof is in the signature.
12
+
13
+ **Merchant checkout** — call `resolveByNip05('alice@shop.com')` at payment time. No QR codes, no address copy-paste. When the merchant rotates keys, all payers automatically resolve the new address.
14
+
15
+ **Passwordless login** — drop `verifyHttpAuth` into any web server. A NIP-07 browser extension (Alby, nos2x) signs the request transparently. The server gets a verified Nostr pubkey + BCH address — no OAuth, no email, no password database.
16
+
7
17
  ## Install
8
18
 
9
19
  ```sh
10
- npm install cashlet-did
20
+ npm install did-beta-bch
11
21
  ```
12
22
 
13
- ## Usage
23
+ ## Core usage
14
24
 
15
25
  ### Pay by handle — one call
16
26
 
17
27
  ```ts
18
- import { resolveAddress } from 'cashlet-did'
28
+ import { resolveAddress } from 'did-beta-bch'
19
29
 
20
30
  const address = await resolveAddress('npub1...')
21
31
  // Returns a token-aware bitcoincash:z... address
@@ -25,55 +35,164 @@ if (address) wallet.sendTo(address, amount)
25
35
 
26
36
  ### Resolve + verify
27
37
 
38
+ ```ts
39
+ import { resolveIdentity, verifyIdentity } from 'did-beta-bch'
40
+
28
41
  const identity = await resolveIdentity('npub1...')
29
42
  if (identity) {
30
- console.log(identity.bchAddress) // bitcoincash:z...
31
43
  const result = await verifyIdentity(identity)
32
44
  console.log(result.status) // "verified"
45
+ console.log(identity.bchAddress) // bitcoincash:z...
33
46
  }
34
47
  ```
35
48
 
36
49
  ### Build and publish a binding (wallet integration)
37
50
 
38
51
  ```ts
39
- import { buildBinding, bchSigningPayload, nostrEventTemplate, publishToRelays, DEFAULT_RELAYS } from 'cashlet-did'
52
+ import { buildBinding, bchSigningPayload, nostrEventTemplate, publishToRelays, DEFAULT_RELAYS } from 'did-beta-bch'
40
53
  import { finalizeEvent } from 'nostr-tools'
41
54
 
42
55
  const binding = await buildBinding(bchAddress, 'mainnet', nostrPubkeyHex)
43
56
  const message = bchSigningPayload(binding)
44
57
 
45
- // Sign message with BCH private key (recoverable compact sig)
58
+ // Sign with BCH private key (recoverable compact sig)
46
59
  const bchSig = signWithBchKey(message) // your wallet's signing function
47
60
 
48
- const template = nostrEventTemplate(binding, { message, signature: bchSig })
49
- const event = finalizeEvent(template, nostrPrivkey)
61
+ const event = finalizeEvent(nostrEventTemplate(binding, { message, signature: bchSig }), nostrPrivkey)
50
62
  await publishToRelays(DEFAULT_RELAYS, event)
51
63
  ```
52
64
 
53
- ### Verify a challenge-response proof
65
+ ### Key derivation (NIP-06)
66
+
67
+ ```ts
68
+ import { deriveNostrKeypair } from 'did-beta-bch'
69
+
70
+ const { privkey, pubkey, npub, nsec } = deriveNostrKeypair(mnemonic)
71
+ // account index for multiple independent identities from one seed
72
+ const social = deriveNostrKeypair(mnemonic, 1)
73
+ ```
74
+
75
+ ### Challenge-response authentication
54
76
 
55
77
  ```ts
56
- import { verifyChallengeProof } from 'cashlet-did'
78
+ import { verifyChallengeProof } from 'did-beta-bch'
57
79
 
80
+ // Payload format: `cashlet-did/challenge\n<nonce>\n<audience>`
81
+ // issuedAt enforces 5-minute TTL
58
82
  const valid = verifyChallengeProof(
59
83
  { signature, payload, address },
60
- nonce
84
+ nonce,
85
+ audience,
86
+ issuedAt, // unix seconds — optional, rejects if > 5 min old
87
+ )
88
+ ```
89
+
90
+ ### Key rotation
91
+
92
+ ```ts
93
+ import { buildBinding, resolveIdentityChain } from 'did-beta-bch'
94
+
95
+ // Rotate: reference the old binding
96
+ const newBinding = await buildBinding(newAddress, 'mainnet', newNostrPub, undefined, oldBindingId)
97
+
98
+ // Resolve full rotation history, newest first
99
+ const chain = await resolveIdentityChain('npub1...')
100
+ ```
101
+
102
+ ### Verifiable credentials + reputation
103
+
104
+ ```ts
105
+ import { issueCredential, resolveCredentials, issueRating, scoreReputation } from 'did-beta-bch'
106
+
107
+ // Issue a credential (issuer signs with their Nostr key)
108
+ const { credential } = await issueCredential(subjectNpub, 'merchant', issuerPrivkey, { since: '2024' })
109
+
110
+ // Fetch + score reputation for an npub
111
+ const creds = await resolveCredentials('npub1...')
112
+ const score = await scoreReputation('npub1...')
113
+ ```
114
+
115
+ ### Selective disclosure
116
+
117
+ ```ts
118
+ import { buildDisclosure, verifyDisclosure } from 'did-beta-bch'
119
+
120
+ // Commit to attributes — publish commitments in binding
121
+ const d = await buildDisclosure({ merchant: 'true', country: 'AU', kyc: 'passed' })
122
+
123
+ // Later: reveal only what's needed
124
+ const revealed = d.reveal(['merchant'])
125
+ const { verified, failed } = await verifyDisclosure(revealed, d.commitments)
126
+ ```
127
+
128
+ ### BCH-anchored identity commitment
129
+
130
+ ```ts
131
+ import { buildAnchorTx } from 'did-beta-bch'
132
+
133
+ // Embed bindingId in an OP_RETURN transaction for on-chain commitment
134
+ const { txHex } = await buildAnchorTx(bindingId, { txid, vout, valueSats, lockingBytecode, privateKey })
135
+ ```
136
+
137
+ ### NIP-05 handle resolution
138
+
139
+ ```ts
140
+ import { resolveByNip05 } from 'did-beta-bch'
141
+
142
+ // alice@cashlet.com → npub → verified BCH address
143
+ const address = await resolveByNip05('alice@cashlet.com')
144
+ if (address) wallet.sendTo(address, amount)
145
+ ```
146
+
147
+ ### NIP-98 HTTP Auth (server-side)
148
+
149
+ ```ts
150
+ import { buildHttpAuthEventAsync, verifyHttpAuth } from 'did-beta-bch'
151
+ import { finalizeEvent } from 'nostr-tools'
152
+
153
+ // Client: build + sign auth event
154
+ const template = await buildHttpAuthEventAsync('https://api.example.com/me', 'GET')
155
+ const event = finalizeEvent(template, nostrPrivkey)
156
+ const header = 'Nostr ' + btoa(JSON.stringify(event))
157
+ // → send as Authorization header
158
+
159
+ // Server: verify — proves Nostr identity + optionally BCH address ownership
160
+ const result = await verifyHttpAuth(header, 'https://api.example.com/me', 'GET', { verifyBch: true })
161
+ if (result.valid) {
162
+ console.log(result.pubkey) // verified Nostr pubkey
163
+ console.log(result.bchAddress) // verified BCH address (if verifyBch: true)
164
+ }
165
+ ```
166
+
167
+ ### NIP-89 App handler (advertise cashlet-did support)
168
+
169
+ ```ts
170
+ import { buildAppHandlerEvent, publishToRelays, DEFAULT_RELAYS } from 'did-beta-bch'
171
+ import { finalizeEvent } from 'nostr-tools'
172
+
173
+ // Advertise that your app handles cashlet-did (kind 30078) events
174
+ const event = finalizeEvent(
175
+ buildAppHandlerEvent('My App', 'https://myapp.com', 'BCH + Nostr identity'),
176
+ nostrPrivkey,
61
177
  )
178
+ await publishToRelays(DEFAULT_RELAYS, event)
62
179
  ```
63
180
 
64
181
  ## Protocol
65
182
 
66
- Bindings are published as Nostr events of kind `30078` with the following tags:
183
+ Bindings are published as Nostr events of kind `30078` (NIP-78 addressable data):
67
184
 
68
185
  | Tag | Value |
69
186
  |---|---|
70
187
  | `d` | binding ID (SHA-256 of canonical binding JSON) |
71
188
  | `bch` | BCH address, network |
72
- | `bch-proof` | signing payload, recoverable compact signature |
189
+ | `bch-proof` | signing payload, 65-byte recoverable compact signature |
190
+ | `bch-payment-code` | BIP47 payment code (optional, enables fresh address per payment) |
73
191
  | `protocol` | `cashlet-did`, version |
192
+ | `supersedes` | previous binding ID (key rotation) |
74
193
  | `t` | `cashlet-did` |
75
194
 
76
- The BCH signature uses Bitcoin signed message format (`\x18Bitcoin Signed Message:\n`) with a 65-byte recoverable compact signature (recovery ID byte + 64-byte compact sig).
195
+ The BCH signature uses Bitcoin signed message format (`\x18Bitcoin Signed Message:\n`) with a 65-byte recoverable compact signature (recovery ID byte + 64-byte compact sig). Revocation uses NIP-09 kind `5` deletion events.
77
196
 
78
197
  ## Relays
79
198
 
package/dist/anchor.js CHANGED
@@ -8,6 +8,8 @@ const DUST_SATS = 1000; // anchor output value (burned)
8
8
  const FEE_SATS = 500;
9
9
  /** Build an OP_RETURN tx anchoring a bindingId to the BCH chain */
10
10
  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`);
11
13
  const { encodeTransaction, generateTransaction, hexToBin, binToHex, walletTemplateP2pkhNonHd, importWalletTemplate, walletTemplateToCompilerBch } = await import("@bitauth/libauth");
12
14
  const data = ANCHOR_PREFIX + bindingId;
13
15
  const dataBytes = new TextEncoder().encode(data);
package/dist/binding.js CHANGED
@@ -52,6 +52,7 @@ export function nostrEventTemplate(binding, bchProof) {
52
52
  tags: [
53
53
  ["d", binding.bindingId],
54
54
  ["bch", binding.bch.address, binding.bch.network],
55
+ ...(binding.bch.paymentCode ? [["bch-payment-code", binding.bch.paymentCode]] : []),
55
56
  ["protocol", PROTOCOL_ID, PROTOCOL_VERSION],
56
57
  ["t", PROTOCOL_ID],
57
58
  ...(binding.previousBindingId ? [["supersedes", binding.previousBindingId]] : []),
@@ -4,7 +4,7 @@
4
4
  * Subject's binding event links to received credentials via ["credential", eventId, issuerPubkey, type] tag.
5
5
  */
6
6
  import type { RelayResult } from "./relay.js";
7
- export declare const CREDENTIAL_KIND = 30079;
7
+ export declare const CREDENTIAL_KIND = 30085;
8
8
  export interface CredentialPayload {
9
9
  subject: string;
10
10
  type: string;
@@ -3,9 +3,9 @@
3
3
  * Issuer signs a Nostr event (kind 30079) attesting a claim about a subject npub.
4
4
  * Subject's binding event links to received credentials via ["credential", eventId, issuerPubkey, type] tag.
5
5
  */
6
- import { finalizeEvent, nip19 } from "nostr-tools";
6
+ import { finalizeEvent, nip19, getPublicKey } from "nostr-tools";
7
7
  import { queryRelays, publishToRelays, DEFAULT_RELAYS } from "./relay.js";
8
- export const CREDENTIAL_KIND = 30079;
8
+ export const CREDENTIAL_KIND = 30085; // kind 30079 is reserved for revocation (DID-PLAN)
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);
@@ -30,7 +30,6 @@ export async function issueCredential(subjectNpub, type, issuerPrivkey, claims,
30
30
  };
31
31
  const signed = finalizeEvent(template, issuerPrivkey);
32
32
  const results = await publishToRelays(relays, signed);
33
- const { getPublicKey } = await import("nostr-tools");
34
33
  const issuerPubkey = getPublicKey(issuerPrivkey);
35
34
  return {
36
35
  credential: { id: signed.id, issuer: issuerPubkey, payload, rawEvent: signed },
@@ -100,7 +99,6 @@ export async function issueRating(subjectNpub, rating, issuerPrivkey, opts = {},
100
99
  };
101
100
  const signed = finalizeEvent(template, issuerPrivkey);
102
101
  const results = await publishToRelays(relays, signed);
103
- const { getPublicKey } = await import("nostr-tools");
104
102
  return {
105
103
  credential: { id: signed.id, issuer: getPublicKey(issuerPrivkey), payload, rawEvent: signed },
106
104
  results,
package/dist/index.d.ts CHANGED
@@ -5,3 +5,6 @@ export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, type Network, type Canonic
5
5
  export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
6
6
  export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
7
7
  export { CashletDidError, type CashletDidErrorCode } from "./errors.js";
8
+ export { deriveNostrKeypair, type NostrKeypair, } from "./keygen.js";
9
+ export { buildHttpAuthEventAsync, verifyHttpAuth, type HttpAuthEventTemplate, type HttpAuthResult, } from "./nip98.js";
10
+ export { buildAppHandlerEvent, type AppHandlerEventTemplate, } from "./nip89.js";
package/dist/index.js CHANGED
@@ -13,3 +13,6 @@ export {
13
13
  // Verification
14
14
  resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
15
15
  export { CashletDidError } from "./errors.js";
16
+ export { deriveNostrKeypair, } from "./keygen.js";
17
+ export { buildHttpAuthEventAsync, verifyHttpAuth, } from "./nip98.js";
18
+ export { buildAppHandlerEvent, } from "./nip89.js";
@@ -0,0 +1,13 @@
1
+ export interface NostrKeypair {
2
+ privkey: Uint8Array;
3
+ pubkey: string;
4
+ npub: string;
5
+ nsec: string;
6
+ account: number;
7
+ }
8
+ /**
9
+ * Derive a Nostr keypair from a BIP39 mnemonic using NIP-06 path.
10
+ * Path: m/44'/1237'/<account>'/0/0
11
+ * account=0 is the default identity; higher indices produce independent keypairs.
12
+ */
13
+ export declare function deriveNostrKeypair(mnemonic: string, account?: number): NostrKeypair;
package/dist/keygen.js ADDED
@@ -0,0 +1,17 @@
1
+ import { HDKey } from "@scure/bip32";
2
+ import { mnemonicToSeedSync } from "@scure/bip39";
3
+ import { getPublicKey, nip19 } from "nostr-tools";
4
+ /**
5
+ * Derive a Nostr keypair from a BIP39 mnemonic using NIP-06 path.
6
+ * Path: m/44'/1237'/<account>'/0/0
7
+ * account=0 is the default identity; higher indices produce independent keypairs.
8
+ */
9
+ export function deriveNostrKeypair(mnemonic, account = 0) {
10
+ const seed = mnemonicToSeedSync(mnemonic.trim());
11
+ const child = HDKey.fromMasterSeed(seed).derive(`m/44'/1237'/${account}'/0/0`);
12
+ if (!child.privateKey)
13
+ throw new Error("Nostr key derivation failed");
14
+ const privkey = child.privateKey;
15
+ const pubkey = getPublicKey(privkey);
16
+ return { privkey, pubkey, npub: nip19.npubEncode(pubkey), nsec: nip19.nsecEncode(privkey), account };
17
+ }
@@ -0,0 +1,12 @@
1
+ export interface AppHandlerEventTemplate {
2
+ kind: 31990;
3
+ created_at: number;
4
+ tags: string[][];
5
+ content: string;
6
+ }
7
+ /**
8
+ * Build a NIP-89 App Handler event template advertising support for cashlet-did (kind 30078).
9
+ * Nostr clients use this to route users to compatible apps.
10
+ * Caller must finalizeEvent(template, nostrPrivkey) then publishToRelays.
11
+ */
12
+ export declare function buildAppHandlerEvent(appName: string, appUrl: string, about?: string): AppHandlerEventTemplate;
package/dist/nip89.js ADDED
@@ -0,0 +1,18 @@
1
+ import { BINDING_KIND, PROTOCOL_ID } from "./binding.js";
2
+ const APP_HANDLER_KIND = 31990;
3
+ /**
4
+ * Build a NIP-89 App Handler event template advertising support for cashlet-did (kind 30078).
5
+ * Nostr clients use this to route users to compatible apps.
6
+ * Caller must finalizeEvent(template, nostrPrivkey) then publishToRelays.
7
+ */
8
+ export function buildAppHandlerEvent(appName, appUrl, about) {
9
+ return {
10
+ kind: APP_HANDLER_KIND,
11
+ created_at: Math.floor(Date.now() / 1000),
12
+ tags: [
13
+ ["k", String(BINDING_KIND)],
14
+ ["d", PROTOCOL_ID],
15
+ ],
16
+ content: JSON.stringify({ name: appName, url: appUrl, ...(about ? { about } : {}) }),
17
+ };
18
+ }
@@ -0,0 +1,33 @@
1
+ export interface HttpAuthEventTemplate {
2
+ kind: 27235;
3
+ created_at: number;
4
+ tags: string[][];
5
+ content: string;
6
+ }
7
+ /**
8
+ * Build a NIP-98 HTTP Auth event template.
9
+ * Pass a body Uint8Array to include its SHA-256 hash as a `payload` tag.
10
+ * Caller must finalizeEvent(template, nostrPrivkey) before sending.
11
+ * Send as: Authorization: Nostr <base64(JSON.stringify(signedEvent))>
12
+ */
13
+ export declare function buildHttpAuthEventAsync(url: string, method: string, body?: Uint8Array): Promise<HttpAuthEventTemplate>;
14
+ export interface HttpAuthResult {
15
+ valid: boolean;
16
+ pubkey?: string;
17
+ bchAddress?: string;
18
+ error?: string;
19
+ }
20
+ /**
21
+ * Verify a NIP-98 Authorization header on the server side.
22
+ * Optionally resolves the signer's BCH binding — proving both Nostr + BCH ownership.
23
+ *
24
+ * @param authHeader - value of the Authorization header (e.g. "Nostr <base64>")
25
+ * @param url - the full request URL (must match event tag exactly)
26
+ * @param method - HTTP method (must match event tag exactly)
27
+ * @param opts.verifyBch - if true, resolve + verify the signer's BCH binding
28
+ * @param opts.relays - relay list for BCH binding resolution
29
+ */
30
+ export declare function verifyHttpAuth(authHeader: string, url: string, method: string, opts?: {
31
+ verifyBch?: boolean;
32
+ relays?: string[];
33
+ }): Promise<HttpAuthResult>;
package/dist/nip98.js ADDED
@@ -0,0 +1,74 @@
1
+ import { verifyEvent } from "nostr-tools";
2
+ import { resolveIdentity, verifyIdentity } from "./verify.js";
3
+ const HTTP_AUTH_KIND = 27235;
4
+ const HTTP_AUTH_TTL = 60; // seconds — NIP-98 spec
5
+ /**
6
+ * Build a NIP-98 HTTP Auth event template.
7
+ * Pass a body Uint8Array to include its SHA-256 hash as a `payload` tag.
8
+ * Caller must finalizeEvent(template, nostrPrivkey) before sending.
9
+ * Send as: Authorization: Nostr <base64(JSON.stringify(signedEvent))>
10
+ */
11
+ export async function buildHttpAuthEventAsync(url, method, body) {
12
+ const tags = [
13
+ ["u", url],
14
+ ["method", method.toUpperCase()],
15
+ ];
16
+ if (body) {
17
+ const hashBuf = await crypto.subtle.digest("SHA-256", body);
18
+ const hash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, "0")).join("");
19
+ tags.push(["payload", hash]);
20
+ }
21
+ return { kind: HTTP_AUTH_KIND, created_at: Math.floor(Date.now() / 1000), tags, content: "" };
22
+ }
23
+ /**
24
+ * Verify a NIP-98 Authorization header on the server side.
25
+ * Optionally resolves the signer's BCH binding — proving both Nostr + BCH ownership.
26
+ *
27
+ * @param authHeader - value of the Authorization header (e.g. "Nostr <base64>")
28
+ * @param url - the full request URL (must match event tag exactly)
29
+ * @param method - HTTP method (must match event tag exactly)
30
+ * @param opts.verifyBch - if true, resolve + verify the signer's BCH binding
31
+ * @param opts.relays - relay list for BCH binding resolution
32
+ */
33
+ export async function verifyHttpAuth(authHeader, url, method, opts = {}) {
34
+ if (!authHeader.startsWith("Nostr "))
35
+ return { valid: false, error: "Missing Nostr auth header" };
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ let event;
38
+ try {
39
+ event = JSON.parse(atob(authHeader.slice(6)));
40
+ }
41
+ catch {
42
+ return { valid: false, error: "Invalid base64 event" };
43
+ }
44
+ // 1. Nostr event signature
45
+ if (!verifyEvent(event))
46
+ return { valid: false, error: "Invalid Nostr signature" };
47
+ // 2. Kind
48
+ if (event.kind !== HTTP_AUTH_KIND)
49
+ return { valid: false, error: "Wrong event kind" };
50
+ // 3. TTL
51
+ if (Math.floor(Date.now() / 1000) - event.created_at > HTTP_AUTH_TTL)
52
+ return { valid: false, error: "Event expired" };
53
+ // 4. URL + method tags
54
+ const uTag = event.tags.find((t) => t[0] === "u");
55
+ const mTag = event.tags.find((t) => t[0] === "method");
56
+ if (uTag?.[1] !== url)
57
+ return { valid: false, error: "URL mismatch" };
58
+ if (mTag?.[1] !== method.toUpperCase())
59
+ return { valid: false, error: "Method mismatch" };
60
+ const result = { valid: true, pubkey: event.pubkey };
61
+ // 5. Optionally resolve + verify BCH binding
62
+ if (opts.verifyBch) {
63
+ const { nip19 } = await import("nostr-tools");
64
+ const npub = nip19.npubEncode(event.pubkey);
65
+ const identity = await resolveIdentity(npub, opts.relays);
66
+ if (!identity)
67
+ return { valid: false, error: "No BCH binding found for pubkey" };
68
+ const verify = await verifyIdentity(identity);
69
+ if (verify.status !== "verified")
70
+ return { valid: false, error: `BCH binding ${verify.status}` };
71
+ result.bchAddress = identity.bchAddress;
72
+ }
73
+ return result;
74
+ }
package/dist/verify.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { canonicalize, sha256hex, bchSigningPayload, type CanonicalBinding, type IdentityBinding, type NostrEvent } from "./binding.js";
2
- export type VerifyStatus = "verified" | "invalid_bch_sig" | "invalid_nostr_sig" | "id_mismatch" | "malformed";
2
+ export type VerifyStatus = "verified" | "invalid_bch_sig" | "invalid_nostr_sig" | "id_mismatch" | "malformed" | "expired";
3
3
  export interface VerifyResult {
4
4
  status: VerifyStatus;
5
5
  bchValid: boolean;
6
6
  nostrValid: boolean;
7
7
  details: string;
8
+ /** BCH block timestamp (unix seconds) from the bch-anchor tag, if present.
9
+ * Accurate to ~1 block (~10 min): must exceed median of last 11 blocks so
10
+ * it cannot be significantly backdated, and can be at most 2 hours ahead of
11
+ * network time. Use this instead of the self-reported `createdAt`. */
12
+ anchoredAt?: number;
8
13
  }
9
14
  export interface ResolvedIdentity {
10
15
  npub: string;
@@ -15,20 +20,29 @@ export interface ResolvedIdentity {
15
20
  createdAt: number;
16
21
  bchProofTag: string[];
17
22
  rawEvent: NostrEvent;
23
+ /** BCH block timestamp (unix seconds) from the bch-anchor tag, if present.
24
+ * More trustworthy than `createdAt` — accurate to ~1 block (~10 min),
25
+ * cannot be significantly backdated (median-time-past rule). */
26
+ anchoredAt?: number;
18
27
  }
19
28
  /** Resolve a BCH address from an npub via Nostr relays */
20
29
  export declare function resolveIdentity(npub: string, relays?: string[]): Promise<ResolvedIdentity | null>;
21
30
  /** Verify the BCH + Nostr signatures on a resolved identity */
22
31
  export declare function verifyIdentity(resolved: ResolvedIdentity): Promise<VerifyResult>;
23
- /** Verify a challenge-response proof */
32
+ /** Verify a challenge-response proof.
33
+ * Payload must be: `cashlet-did/challenge\n<nonce>\n<audience>`
34
+ * Matches the SIGN_CHALLENGE format used by the Cashlet extension and any conforming client.
35
+ * @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
36
+ */
24
37
  export declare function verifyChallengeProof(proof: {
25
38
  signature: string;
26
39
  payload: string;
27
40
  address: string;
28
- }, nonce: string): boolean;
41
+ }, nonce: string, audience?: string, issuedAt?: number): boolean;
29
42
  /**
30
43
  * Resolve a BCH address from an npub — the primary pay-by-handle entry point.
31
- * Returns the verified BCH address string, or null if not found.
44
+ * Verifies both BCH and Nostr signatures before returning the address.
45
+ * Returns null if not found or if either signature is invalid.
32
46
  *
33
47
  * @example
34
48
  * const address = await resolveAddress('npub1...')
package/dist/verify.js CHANGED
@@ -4,38 +4,48 @@ import { canonicalize, sha256hex, bchSigningPayload, BINDING_KIND, PROTOCOL_ID,
4
4
  import { queryRelays, DEFAULT_RELAYS } from "./relay.js";
5
5
  import { CashletDidError } from "./errors.js";
6
6
  import { nip19 } from "nostr-tools";
7
+ /** Encode n as a Bitcoin-style varint (compact size). */
8
+ function varint(n) {
9
+ if (n < 0xfd)
10
+ return new Uint8Array([n]);
11
+ if (n <= 0xffff)
12
+ return new Uint8Array([0xfd, n & 0xff, (n >> 8) & 0xff]);
13
+ return new Uint8Array([0xfe, n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]);
14
+ }
15
+ /**
16
+ * Compute the double-SHA256 hash used by Bitcoin signed messages.
17
+ * Format: SHA256d("\x18Bitcoin Signed Message:\n" + varint(len) + message)
18
+ * Uses proper varint encoding — required for messages longer than 252 bytes.
19
+ */
7
20
  function bitcoinMsgHash(message) {
8
21
  const enc = new TextEncoder();
9
22
  const msgBytes = enc.encode(message);
10
23
  const prefix = enc.encode("\x18Bitcoin Signed Message:\n");
11
- const lenByte = new Uint8Array([msgBytes.length]);
12
- return hash256(new Uint8Array([...prefix, ...lenByte, ...msgBytes]));
24
+ return hash256(new Uint8Array([...prefix, ...varint(msgBytes.length), ...msgBytes]));
13
25
  }
14
26
  function recoverBchAddress(sigHex, msgHash, expectedAddress) {
15
27
  const sig = Uint8Array.from(sigHex.match(/.{2}/g).map(b => parseInt(b, 16)));
28
+ const addrPrefix = (expectedAddress?.split(":")[0] ?? "bitcoincash");
16
29
  const tryRecover = (compact, id) => {
17
30
  try {
18
31
  const pub = secp256k1.recoverPublicKeyCompressed(compact, id, msgHash);
19
32
  if (!(pub instanceof Uint8Array))
20
33
  return null;
21
- const addrPrefix = (expectedAddress?.split(":")[0] ?? "bitcoincash");
22
- for (const type of ["p2pkhWithTokens", "p2pkh"]) {
34
+ for (const type of ["p2pkh", "p2pkhWithTokens"]) {
23
35
  const r = encodeCashAddress({ prefix: addrPrefix, type, payload: hash160(pub) });
24
- if (r?.address)
25
- return r.address;
26
- if (typeof r === "string")
27
- return r;
36
+ const addr = r?.address ?? (typeof r === "string" ? r : null);
37
+ if (addr && (!expectedAddress || addr === expectedAddress))
38
+ return addr;
28
39
  }
29
40
  }
30
41
  catch { /* try next */ }
31
42
  return null;
32
43
  };
44
+ // 65-byte: recovery ID is encoded in byte 0 — use it directly, no brute-force
33
45
  if (sig.length === 65) {
34
- const recoveryId = (sig[0] & 0x03);
35
- const addr = tryRecover(sig.slice(1), recoveryId);
36
- if (addr)
37
- return addr;
46
+ return tryRecover(sig.slice(1), (sig[0] & 0x03));
38
47
  }
48
+ // 64-byte raw compact: try all recovery IDs
39
49
  for (const id of [0, 1, 2, 3]) {
40
50
  const addr = tryRecover(sig, id);
41
51
  if (addr)
@@ -49,16 +59,19 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
49
59
  if (decoded.type !== "npub")
50
60
  return null;
51
61
  const pubkey = decoded.data;
52
- const events = await queryRelays(relays, {
53
- kinds: [BINDING_KIND],
54
- authors: [pubkey],
55
- "#t": [PROTOCOL_ID],
56
- limit: 10,
57
- });
62
+ const [events, revocations] = await Promise.all([
63
+ queryRelays(relays, { kinds: [BINDING_KIND], authors: [pubkey], "#t": [PROTOCOL_ID], limit: 10 }),
64
+ queryRelays(relays, { kinds: [5], authors: [pubkey], limit: 50 }),
65
+ ]);
58
66
  if (!events.length)
59
67
  return null;
60
- // Pick latest
61
- const event = events.sort((a, b) => b.created_at - a.created_at)[0];
68
+ const revokedIds = new Set(revocations.flatMap(e => e.tags.filter(t => t[0] === "e").map(t => t[1])));
69
+ // Pick latest non-revoked event
70
+ const event = events
71
+ .filter(e => !revokedIds.has(e.id))
72
+ .sort((a, b) => b.created_at - a.created_at)[0];
73
+ if (!event)
74
+ return null;
62
75
  const bchTag = event.tags.find(t => t[0] === "bch");
63
76
  const dTag = event.tags.find(t => t[0] === "d");
64
77
  const bchProofTag = event.tags.find(t => t[0] === "bch-proof");
@@ -69,6 +82,8 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
69
82
  createdAt = JSON.parse(event.content).createdAt ?? createdAt;
70
83
  }
71
84
  catch { /* use event ts */ }
85
+ const anchorTag = event.tags.find(t => t[0] === "bch-anchor");
86
+ const anchoredAt = anchorTag?.[2] ? Number(anchorTag[2]) : undefined;
72
87
  return {
73
88
  npub,
74
89
  pubkey,
@@ -78,6 +93,7 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
78
93
  createdAt,
79
94
  bchProofTag,
80
95
  rawEvent: event,
96
+ ...(anchoredAt !== undefined ? { anchoredAt } : {}),
81
97
  };
82
98
  }
83
99
  /** Verify the BCH + Nostr signatures on a resolved identity */
@@ -107,24 +123,33 @@ export async function verifyIdentity(resolved) {
107
123
  return { status: "malformed", bchValid: false, nostrValid, details: "Unexpected proof payload format" };
108
124
  }
109
125
  if (bchValid && nostrValid)
110
- return { status: "verified", bchValid, nostrValid, details: "Both proofs valid" };
126
+ return { status: "verified", bchValid, nostrValid, details: "Both proofs valid", ...(resolved.anchoredAt !== undefined ? { anchoredAt: resolved.anchoredAt } : {}) };
111
127
  if (!bchValid)
112
128
  return { status: "invalid_bch_sig", bchValid, nostrValid, details: "BCH signature invalid" };
113
129
  return { status: "invalid_nostr_sig", bchValid, nostrValid, details: "Nostr signature invalid" };
114
130
  }
115
- /** Verify a challenge-response proof */
116
- export function verifyChallengeProof(proof, nonce) {
131
+ const CHALLENGE_TTL_SECS = 300; // 5 minutes
132
+ /** Verify a challenge-response proof.
133
+ * Payload must be: `cashlet-did/challenge\n<nonce>\n<audience>`
134
+ * Matches the SIGN_CHALLENGE format used by the Cashlet extension and any conforming client.
135
+ * @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
136
+ */
137
+ export function verifyChallengeProof(proof, nonce, audience, issuedAt) {
138
+ if (issuedAt !== undefined && Math.floor(Date.now() / 1000) - issuedAt > CHALLENGE_TTL_SECS)
139
+ return false;
140
+ if (!proof.payload.startsWith(`${PROTOCOL_ID}/challenge\n`))
141
+ return false;
117
142
  if (!proof.payload.includes(nonce))
118
143
  return false;
119
- if (!proof.payload.startsWith(`${PROTOCOL_ID}/challenge`))
144
+ if (audience !== undefined && !proof.payload.includes(audience))
120
145
  return false;
121
146
  const msgHash = bitcoinMsgHash(proof.payload);
122
- const recovered = recoverBchAddress(proof.signature, msgHash, proof.address);
123
- return recovered === proof.address;
147
+ return recoverBchAddress(proof.signature, msgHash, proof.address) === proof.address;
124
148
  }
125
149
  /**
126
150
  * Resolve a BCH address from an npub — the primary pay-by-handle entry point.
127
- * Returns the verified BCH address string, or null if not found.
151
+ * Verifies both BCH and Nostr signatures before returning the address.
152
+ * Returns null if not found or if either signature is invalid.
128
153
  *
129
154
  * @example
130
155
  * const address = await resolveAddress('npub1...')
@@ -132,7 +157,10 @@ export function verifyChallengeProof(proof, nonce) {
132
157
  */
133
158
  export async function resolveAddress(npub, relays = DEFAULT_RELAYS) {
134
159
  const identity = await resolveIdentity(npub, relays);
135
- return identity?.bchAddress ?? null;
160
+ if (!identity)
161
+ return null;
162
+ const result = await verifyIdentity(identity);
163
+ return result.status === "verified" ? identity.bchAddress : null;
136
164
  }
137
165
  /** Returns full binding history for an npub, newest first, with cryptographic supersedes validation */
138
166
  export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
@@ -158,28 +186,21 @@ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
158
186
  createdAt = JSON.parse(event.content).createdAt ?? createdAt;
159
187
  }
160
188
  catch { /* use event ts */ }
161
- // Validate supersedes tag cryptographically
189
+ // Validate supersedes tag: the referenced event must exist and its d-tag must match claimedId
162
190
  const supersedesTag = event.tags.find(t => t[0] === "supersedes");
163
191
  if (supersedesTag) {
164
192
  const claimedId = supersedesTag[1];
165
- // Fetch the referenced event and verify its bindingId matches
166
- const [prev] = await queryRelays(relays, { ids: [claimedId], limit: 1 });
193
+ const [prev] = await queryRelays(relays, { "#d": [claimedId], kinds: [BINDING_KIND], limit: 1 });
167
194
  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
195
+ 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`);
179
198
  }
180
199
  }
181
200
  }
182
- results.push({ npub, pubkey, bchAddress: bchTag[1], network: bchTag[2] ?? "mainnet", bindingId: dTag[1], createdAt, bchProofTag, rawEvent: event });
201
+ const anchorTag = event.tags.find(t => t[0] === "bch-anchor");
202
+ const anchoredAt = anchorTag?.[2] ? Number(anchorTag[2]) : undefined;
203
+ results.push({ npub, pubkey, bchAddress: bchTag[1], network: bchTag[2] ?? "mainnet", bindingId: dTag[1], createdAt, bchProofTag, rawEvent: event, ...(anchoredAt !== undefined ? { anchoredAt } : {}) });
183
204
  }
184
205
  return results;
185
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "did-beta-bch",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,18 +11,37 @@
11
11
  "types": "./dist/index.d.ts"
12
12
  }
13
13
  },
14
- "files": ["dist", "README.md", "CHANGELOG.md"],
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "PROTOCOL.md"
19
+ ],
15
20
  "scripts": {
16
21
  "build": "tsc",
17
22
  "typecheck": "tsc --noEmit",
23
+ "test": "node --test test/cashlet-did.test.mjs",
18
24
  "prepublishOnly": "npm run build"
19
25
  },
20
- "keywords": ["bch", "bitcoin-cash", "nostr", "did", "decentralised-identity", "cashaddr", "nip-06", "nip-07"],
26
+ "keywords": [
27
+ "bch",
28
+ "bitcoin-cash",
29
+ "nostr",
30
+ "did",
31
+ "decentralised-identity",
32
+ "cashaddr",
33
+ "nip-06",
34
+ "nip-07"
35
+ ],
21
36
  "author": "",
22
37
  "license": "MIT",
23
- "engines": { "node": ">=18" },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
24
41
  "dependencies": {
25
42
  "@bitauth/libauth": "^3.1.0-next.8",
43
+ "@scure/bip32": "^2.4.0",
44
+ "@scure/bip39": "^2.4.0",
26
45
  "nostr-tools": "^2.0.0"
27
46
  },
28
47
  "devDependencies": {