did-beta-bch 1.0.1 → 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 +14 -0
- package/PROTOCOL.md +214 -0
- package/README.md +134 -15
- package/dist/anchor.js +2 -0
- package/dist/binding.js +1 -0
- package/dist/credentials.d.ts +22 -1
- package/dist/credentials.js +79 -4
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/keygen.d.ts +13 -0
- package/dist/keygen.js +17 -0
- package/dist/nip89.d.ts +12 -0
- package/dist/nip89.js +18 -0
- package/dist/nip98.d.ts +33 -0
- package/dist/nip98.js +74 -0
- package/dist/verify.d.ts +24 -5
- package/dist/verify.js +85 -31
- package/package.json +23 -4
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
|
|
20
|
+
npm install did-beta-bch
|
|
11
21
|
```
|
|
12
22
|
|
|
13
|
-
##
|
|
23
|
+
## Core usage
|
|
14
24
|
|
|
15
25
|
### Pay by handle — one call
|
|
16
26
|
|
|
17
27
|
```ts
|
|
18
|
-
import { resolveAddress } from '
|
|
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 '
|
|
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
|
|
58
|
+
// Sign with BCH private key (recoverable compact sig)
|
|
46
59
|
const bchSig = signWithBchKey(message) // your wallet's signing function
|
|
47
60
|
|
|
48
|
-
const
|
|
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
|
-
###
|
|
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 '
|
|
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`
|
|
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]] : []),
|
package/dist/credentials.d.ts
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
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 =
|
|
7
|
+
export declare const CREDENTIAL_KIND = 30085;
|
|
8
8
|
export interface CredentialPayload {
|
|
9
9
|
subject: string;
|
|
10
10
|
type: string;
|
|
11
11
|
issuedAt: number;
|
|
12
|
+
rating?: 1 | 2 | 3 | 4 | 5;
|
|
13
|
+
comment?: string;
|
|
12
14
|
expiry?: number;
|
|
13
15
|
claims?: Record<string, string>;
|
|
14
16
|
}
|
|
@@ -25,3 +27,22 @@ export declare function issueCredential(subjectNpub: string, type: string, issue
|
|
|
25
27
|
}>;
|
|
26
28
|
/** Resolve all credentials issued to an npub */
|
|
27
29
|
export declare function resolveCredentials(npub: string, relays?: string[]): Promise<IssuedCredential[]>;
|
|
30
|
+
/** Issue a star rating (1–5) to an npub — usable by any wallet or app */
|
|
31
|
+
export declare function issueRating(subjectNpub: string, rating: 1 | 2 | 3 | 4 | 5, issuerPrivkey: Uint8Array, opts?: {
|
|
32
|
+
comment?: string;
|
|
33
|
+
txid?: string;
|
|
34
|
+
network?: string;
|
|
35
|
+
}, relays?: string[]): Promise<{
|
|
36
|
+
credential: IssuedCredential;
|
|
37
|
+
results: RelayResult[];
|
|
38
|
+
}>;
|
|
39
|
+
/** Revoke a credential by publishing a NIP-09 kind:5 deletion event */
|
|
40
|
+
export declare function revokeCredential(eventId: string, issuerPrivkey: Uint8Array, relays?: string[]): Promise<RelayResult[]>;
|
|
41
|
+
/** Compute a reputation score for an npub based on ratings and credentials */
|
|
42
|
+
export declare function scoreReputation(npub: string, relays?: string[]): Promise<{
|
|
43
|
+
credentialCount: number;
|
|
44
|
+
ratingCount: number;
|
|
45
|
+
averageRating: number | null;
|
|
46
|
+
uniqueIssuers: number;
|
|
47
|
+
score: number;
|
|
48
|
+
}>;
|
package/dist/credentials.js
CHANGED
|
@@ -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 },
|
|
@@ -49,7 +48,16 @@ export async function resolveCredentials(npub, relays = DEFAULT_RELAYS) {
|
|
|
49
48
|
"#t": ["cashlet-did-credential"],
|
|
50
49
|
limit: 50,
|
|
51
50
|
});
|
|
52
|
-
|
|
51
|
+
// Fetch revocations and filter them out
|
|
52
|
+
const revocations = await queryRelays(relays, {
|
|
53
|
+
kinds: [5],
|
|
54
|
+
authors: events.map(e => e.pubkey),
|
|
55
|
+
limit: 100,
|
|
56
|
+
});
|
|
57
|
+
const revokedIds = new Set(revocations.flatMap(e => e.tags.filter((t) => t[0] === "e").map((t) => t[1])));
|
|
58
|
+
return events
|
|
59
|
+
.filter(e => !revokedIds.has(e.id))
|
|
60
|
+
.map(e => {
|
|
53
61
|
try {
|
|
54
62
|
const payload = JSON.parse(e.content);
|
|
55
63
|
return { id: e.id, issuer: e.pubkey, payload, rawEvent: e };
|
|
@@ -59,3 +67,70 @@ export async function resolveCredentials(npub, relays = DEFAULT_RELAYS) {
|
|
|
59
67
|
}
|
|
60
68
|
}).filter(Boolean);
|
|
61
69
|
}
|
|
70
|
+
/** Issue a star rating (1–5) to an npub — usable by any wallet or app */
|
|
71
|
+
export async function issueRating(subjectNpub, rating, issuerPrivkey, opts = {}, relays = DEFAULT_RELAYS) {
|
|
72
|
+
const claims = {};
|
|
73
|
+
if (opts.txid)
|
|
74
|
+
claims.txid = opts.txid;
|
|
75
|
+
if (opts.network)
|
|
76
|
+
claims.network = opts.network;
|
|
77
|
+
const decoded = nip19.decode(subjectNpub);
|
|
78
|
+
if (decoded.type !== "npub")
|
|
79
|
+
throw new Error("Invalid npub");
|
|
80
|
+
const subjectPubkey = decoded.data;
|
|
81
|
+
const payload = {
|
|
82
|
+
subject: subjectPubkey,
|
|
83
|
+
type: "rating",
|
|
84
|
+
issuedAt: Math.floor(Date.now() / 1000),
|
|
85
|
+
rating,
|
|
86
|
+
...(opts.comment ? { comment: opts.comment } : {}),
|
|
87
|
+
...(Object.keys(claims).length ? { claims } : {}),
|
|
88
|
+
};
|
|
89
|
+
const template = {
|
|
90
|
+
kind: CREDENTIAL_KIND,
|
|
91
|
+
created_at: payload.issuedAt,
|
|
92
|
+
tags: [
|
|
93
|
+
["p", subjectPubkey],
|
|
94
|
+
["credential-type", "rating"],
|
|
95
|
+
["rating", String(rating)],
|
|
96
|
+
["t", "cashlet-did-credential"],
|
|
97
|
+
],
|
|
98
|
+
content: JSON.stringify(payload),
|
|
99
|
+
};
|
|
100
|
+
const signed = finalizeEvent(template, issuerPrivkey);
|
|
101
|
+
const results = await publishToRelays(relays, signed);
|
|
102
|
+
return {
|
|
103
|
+
credential: { id: signed.id, issuer: getPublicKey(issuerPrivkey), payload, rawEvent: signed },
|
|
104
|
+
results,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Revoke a credential by publishing a NIP-09 kind:5 deletion event */
|
|
108
|
+
export async function revokeCredential(eventId, issuerPrivkey, relays = DEFAULT_RELAYS) {
|
|
109
|
+
const template = {
|
|
110
|
+
kind: 5,
|
|
111
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
112
|
+
tags: [["e", eventId]],
|
|
113
|
+
content: "revoked",
|
|
114
|
+
};
|
|
115
|
+
const signed = finalizeEvent(template, issuerPrivkey);
|
|
116
|
+
return publishToRelays(relays, signed);
|
|
117
|
+
}
|
|
118
|
+
/** Compute a reputation score for an npub based on ratings and credentials */
|
|
119
|
+
export async function scoreReputation(npub, relays = DEFAULT_RELAYS) {
|
|
120
|
+
const credentials = await resolveCredentials(npub, relays);
|
|
121
|
+
const ratings = credentials.filter(c => c.payload.type === "rating" && c.payload.rating);
|
|
122
|
+
const uniqueIssuers = new Set(credentials.map(c => c.issuer)).size;
|
|
123
|
+
const averageRating = ratings.length
|
|
124
|
+
? ratings.reduce((sum, c) => sum + (c.payload.rating ?? 0), 0) / ratings.length
|
|
125
|
+
: null;
|
|
126
|
+
// Score: avg rating (0-50) + unique issuer bonus (0-50, caps at 10 issuers)
|
|
127
|
+
const ratingScore = averageRating ? (averageRating / 5) * 50 : 0;
|
|
128
|
+
const issuerScore = Math.min(uniqueIssuers, 10) * 5;
|
|
129
|
+
return {
|
|
130
|
+
credentialCount: credentials.length,
|
|
131
|
+
ratingCount: ratings.length,
|
|
132
|
+
averageRating: averageRating ? Math.round(averageRating * 10) / 10 : null,
|
|
133
|
+
uniqueIssuers,
|
|
134
|
+
score: Math.round(ratingScore + issuerScore),
|
|
135
|
+
};
|
|
136
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type CashletDidErrorCode = "NOT_FOUND" | "INVALID_SIG" | "INVALID_BINDING" | "RELAY_TIMEOUT" | "RELAY_ERROR" | "INVALID_NPub" | "CHAIN_BROKEN" | "INVALID_NIP05";
|
|
2
|
+
export declare class CashletDidError extends Error {
|
|
3
|
+
readonly code: CashletDidErrorCode;
|
|
4
|
+
constructor(code: CashletDidErrorCode, message: string);
|
|
5
|
+
}
|
package/dist/errors.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { buildDisclosure, verifyDisclosure, type Disclosure, } from "./disclosure.js";
|
|
2
2
|
export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
|
|
3
|
-
export { issueCredential, resolveCredentials, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
|
|
3
|
+
export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
|
|
4
4
|
export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, type Network, type CanonicalBinding, type IdentityBinding, type NostrEvent, buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
|
|
5
5
|
export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
|
|
6
|
-
export { resolveIdentity, resolveAddress, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
|
|
6
|
+
export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { buildDisclosure, verifyDisclosure, } from "./disclosure.js";
|
|
2
2
|
export { buildAnchorTx, ANCHOR_PREFIX, } from "./anchor.js";
|
|
3
|
-
export { issueCredential, resolveCredentials, CREDENTIAL_KIND, } from "./credentials.js";
|
|
3
|
+
export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, } from "./credentials.js";
|
|
4
4
|
export {
|
|
5
5
|
// Protocol constants
|
|
6
6
|
PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND,
|
|
@@ -11,4 +11,8 @@ export {
|
|
|
11
11
|
publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, } from "./relay.js";
|
|
12
12
|
export {
|
|
13
13
|
// Verification
|
|
14
|
-
resolveIdentity, resolveAddress, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
|
|
14
|
+
resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
|
|
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";
|
package/dist/keygen.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/nip89.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/nip98.d.ts
ADDED
|
@@ -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,26 +20,40 @@ 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
|
-
*
|
|
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...')
|
|
35
49
|
* if (address) wallet.sendTo(address, amount)
|
|
36
50
|
*/
|
|
37
51
|
export declare function resolveAddress(npub: string, relays?: string[]): Promise<string | null>;
|
|
38
|
-
/** Returns full binding history for an npub, newest first */
|
|
52
|
+
/** Returns full binding history for an npub, newest first, with cryptographic supersedes validation */
|
|
39
53
|
export declare function resolveIdentityChain(npub: string, relays?: string[]): Promise<ResolvedIdentity[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a BCH address from a NIP-05 handle (e.g. "alice@cashlet.app")
|
|
56
|
+
* Chains: NIP-05 → npub → cashlet-did binding → BCH address
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveByNip05(handle: string, relays?: string[]): Promise<string | null>;
|
|
40
59
|
export { bchSigningPayload, canonicalize, sha256hex, type CanonicalBinding, type IdentityBinding };
|
package/dist/verify.js
CHANGED
|
@@ -1,40 +1,51 @@
|
|
|
1
|
-
import { verifyEvent } from "nostr-tools";
|
|
1
|
+
import { verifyEvent, nip05 } from "nostr-tools";
|
|
2
2
|
import { secp256k1, hash256, encodeCashAddress, hash160 } from "@bitauth/libauth";
|
|
3
3
|
import { canonicalize, sha256hex, bchSigningPayload, BINDING_KIND, PROTOCOL_ID, } from "./binding.js";
|
|
4
4
|
import { queryRelays, DEFAULT_RELAYS } from "./relay.js";
|
|
5
|
+
import { CashletDidError } from "./errors.js";
|
|
5
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
|
+
*/
|
|
6
20
|
function bitcoinMsgHash(message) {
|
|
7
21
|
const enc = new TextEncoder();
|
|
8
22
|
const msgBytes = enc.encode(message);
|
|
9
23
|
const prefix = enc.encode("\x18Bitcoin Signed Message:\n");
|
|
10
|
-
|
|
11
|
-
return hash256(new Uint8Array([...prefix, ...lenByte, ...msgBytes]));
|
|
24
|
+
return hash256(new Uint8Array([...prefix, ...varint(msgBytes.length), ...msgBytes]));
|
|
12
25
|
}
|
|
13
26
|
function recoverBchAddress(sigHex, msgHash, expectedAddress) {
|
|
14
27
|
const sig = Uint8Array.from(sigHex.match(/.{2}/g).map(b => parseInt(b, 16)));
|
|
28
|
+
const addrPrefix = (expectedAddress?.split(":")[0] ?? "bitcoincash");
|
|
15
29
|
const tryRecover = (compact, id) => {
|
|
16
30
|
try {
|
|
17
31
|
const pub = secp256k1.recoverPublicKeyCompressed(compact, id, msgHash);
|
|
18
32
|
if (!(pub instanceof Uint8Array))
|
|
19
33
|
return null;
|
|
20
|
-
const
|
|
21
|
-
for (const type of ["p2pkhWithTokens", "p2pkh"]) {
|
|
34
|
+
for (const type of ["p2pkh", "p2pkhWithTokens"]) {
|
|
22
35
|
const r = encodeCashAddress({ prefix: addrPrefix, type, payload: hash160(pub) });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return r;
|
|
36
|
+
const addr = r?.address ?? (typeof r === "string" ? r : null);
|
|
37
|
+
if (addr && (!expectedAddress || addr === expectedAddress))
|
|
38
|
+
return addr;
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
catch { /* try next */ }
|
|
30
42
|
return null;
|
|
31
43
|
};
|
|
44
|
+
// 65-byte: recovery ID is encoded in byte 0 — use it directly, no brute-force
|
|
32
45
|
if (sig.length === 65) {
|
|
33
|
-
|
|
34
|
-
const addr = tryRecover(sig.slice(1), recoveryId);
|
|
35
|
-
if (addr)
|
|
36
|
-
return addr;
|
|
46
|
+
return tryRecover(sig.slice(1), (sig[0] & 0x03));
|
|
37
47
|
}
|
|
48
|
+
// 64-byte raw compact: try all recovery IDs
|
|
38
49
|
for (const id of [0, 1, 2, 3]) {
|
|
39
50
|
const addr = tryRecover(sig, id);
|
|
40
51
|
if (addr)
|
|
@@ -48,16 +59,19 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
|
|
|
48
59
|
if (decoded.type !== "npub")
|
|
49
60
|
return null;
|
|
50
61
|
const pubkey = decoded.data;
|
|
51
|
-
const events = await
|
|
52
|
-
kinds: [BINDING_KIND],
|
|
53
|
-
authors: [pubkey],
|
|
54
|
-
|
|
55
|
-
limit: 10,
|
|
56
|
-
});
|
|
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
|
+
]);
|
|
57
66
|
if (!events.length)
|
|
58
67
|
return null;
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
75
|
const bchTag = event.tags.find(t => t[0] === "bch");
|
|
62
76
|
const dTag = event.tags.find(t => t[0] === "d");
|
|
63
77
|
const bchProofTag = event.tags.find(t => t[0] === "bch-proof");
|
|
@@ -68,6 +82,8 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
|
|
|
68
82
|
createdAt = JSON.parse(event.content).createdAt ?? createdAt;
|
|
69
83
|
}
|
|
70
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;
|
|
71
87
|
return {
|
|
72
88
|
npub,
|
|
73
89
|
pubkey,
|
|
@@ -77,6 +93,7 @@ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
|
|
|
77
93
|
createdAt,
|
|
78
94
|
bchProofTag,
|
|
79
95
|
rawEvent: event,
|
|
96
|
+
...(anchoredAt !== undefined ? { anchoredAt } : {}),
|
|
80
97
|
};
|
|
81
98
|
}
|
|
82
99
|
/** Verify the BCH + Nostr signatures on a resolved identity */
|
|
@@ -106,24 +123,33 @@ export async function verifyIdentity(resolved) {
|
|
|
106
123
|
return { status: "malformed", bchValid: false, nostrValid, details: "Unexpected proof payload format" };
|
|
107
124
|
}
|
|
108
125
|
if (bchValid && nostrValid)
|
|
109
|
-
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 } : {}) };
|
|
110
127
|
if (!bchValid)
|
|
111
128
|
return { status: "invalid_bch_sig", bchValid, nostrValid, details: "BCH signature invalid" };
|
|
112
129
|
return { status: "invalid_nostr_sig", bchValid, nostrValid, details: "Nostr signature invalid" };
|
|
113
130
|
}
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
142
|
if (!proof.payload.includes(nonce))
|
|
117
143
|
return false;
|
|
118
|
-
if (!proof.payload.
|
|
144
|
+
if (audience !== undefined && !proof.payload.includes(audience))
|
|
119
145
|
return false;
|
|
120
146
|
const msgHash = bitcoinMsgHash(proof.payload);
|
|
121
|
-
|
|
122
|
-
return recovered === proof.address;
|
|
147
|
+
return recoverBchAddress(proof.signature, msgHash, proof.address) === proof.address;
|
|
123
148
|
}
|
|
124
149
|
/**
|
|
125
150
|
* Resolve a BCH address from an npub — the primary pay-by-handle entry point.
|
|
126
|
-
*
|
|
151
|
+
* Verifies both BCH and Nostr signatures before returning the address.
|
|
152
|
+
* Returns null if not found or if either signature is invalid.
|
|
127
153
|
*
|
|
128
154
|
* @example
|
|
129
155
|
* const address = await resolveAddress('npub1...')
|
|
@@ -131,9 +157,12 @@ export function verifyChallengeProof(proof, nonce) {
|
|
|
131
157
|
*/
|
|
132
158
|
export async function resolveAddress(npub, relays = DEFAULT_RELAYS) {
|
|
133
159
|
const identity = await resolveIdentity(npub, relays);
|
|
134
|
-
|
|
160
|
+
if (!identity)
|
|
161
|
+
return null;
|
|
162
|
+
const result = await verifyIdentity(identity);
|
|
163
|
+
return result.status === "verified" ? identity.bchAddress : null;
|
|
135
164
|
}
|
|
136
|
-
/** Returns full binding history for an npub, newest first */
|
|
165
|
+
/** Returns full binding history for an npub, newest first, with cryptographic supersedes validation */
|
|
137
166
|
export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
|
|
138
167
|
const decoded = nip19.decode(npub);
|
|
139
168
|
if (decoded.type !== "npub")
|
|
@@ -157,8 +186,33 @@ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
|
|
|
157
186
|
createdAt = JSON.parse(event.content).createdAt ?? createdAt;
|
|
158
187
|
}
|
|
159
188
|
catch { /* use event ts */ }
|
|
160
|
-
|
|
189
|
+
// Validate supersedes tag: the referenced event must exist and its d-tag must match claimedId
|
|
190
|
+
const supersedesTag = event.tags.find(t => t[0] === "supersedes");
|
|
191
|
+
if (supersedesTag) {
|
|
192
|
+
const claimedId = supersedesTag[1];
|
|
193
|
+
const [prev] = await queryRelays(relays, { "#d": [claimedId], kinds: [BINDING_KIND], limit: 1 });
|
|
194
|
+
if (prev) {
|
|
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`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
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 } : {}) });
|
|
161
204
|
}
|
|
162
205
|
return results;
|
|
163
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a BCH address from a NIP-05 handle (e.g. "alice@cashlet.app")
|
|
209
|
+
* Chains: NIP-05 → npub → cashlet-did binding → BCH address
|
|
210
|
+
*/
|
|
211
|
+
export async function resolveByNip05(handle, relays = DEFAULT_RELAYS) {
|
|
212
|
+
const profile = await nip05.queryProfile(handle);
|
|
213
|
+
if (!profile?.pubkey)
|
|
214
|
+
throw new CashletDidError("INVALID_NIP05", `NIP-05 handle not found: ${handle}`);
|
|
215
|
+
const npub = nip19.npubEncode(profile.pubkey);
|
|
216
|
+
return resolveAddress(npub, relays);
|
|
217
|
+
}
|
|
164
218
|
export { bchSigningPayload, canonicalize, sha256hex };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "did-beta-bch",
|
|
3
|
-
"version": "1.0
|
|
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": [
|
|
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": [
|
|
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": {
|
|
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": {
|