did-beta-bch 1.2.0 → 2.0.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 +25 -2
- package/PROTOCOL.md +146 -116
- package/README.md +76 -20
- package/dist/anchor.d.ts +2 -1
- package/dist/anchor.js +20 -7
- package/dist/binding.d.ts +5 -11
- package/dist/binding.js +19 -4
- package/dist/credentials.d.ts +8 -2
- package/dist/credentials.js +59 -13
- package/dist/disclosure.d.ts +8 -4
- package/dist/disclosure.js +18 -6
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/nip98.d.ts +2 -0
- package/dist/nip98.js +25 -5
- package/dist/relay.js +10 -6
- package/dist/signing.d.ts +16 -0
- package/dist/signing.js +30 -0
- package/dist/verify.d.ts +12 -13
- package/dist/verify.js +327 -98
- package/package.json +4 -2
package/dist/anchor.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BCH on-chain identity commitment via OP_RETURN.
|
|
3
|
-
*
|
|
3
|
+
* Builds a transaction committing to a bindingId; it does not broadcast or verify confirmation.
|
|
4
4
|
*/
|
|
5
5
|
export const ANCHOR_PREFIX = "cashlet-did:";
|
|
6
6
|
const OP_RETURN = 0x6a;
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const MIN_FEE_SATS = 500;
|
|
8
|
+
function pushData(data) {
|
|
9
|
+
if (data.length <= 0x4b)
|
|
10
|
+
return new Uint8Array([data.length, ...data]);
|
|
11
|
+
if (data.length <= 0xff)
|
|
12
|
+
return new Uint8Array([0x4c, data.length, ...data]);
|
|
13
|
+
if (data.length <= 0xffff)
|
|
14
|
+
return new Uint8Array([0x4d, data.length & 0xff, data.length >>> 8, ...data]);
|
|
15
|
+
throw new Error("Anchor data is too large for a script push");
|
|
16
|
+
}
|
|
9
17
|
/** Build an OP_RETURN tx anchoring a bindingId to the BCH chain */
|
|
10
18
|
export async function buildAnchorTx(bindingId, utxo) {
|
|
11
|
-
if (
|
|
12
|
-
throw new Error(
|
|
19
|
+
if (!/^[0-9a-f]{64}$/.test(bindingId))
|
|
20
|
+
throw new Error("bindingId must be 64 lowercase hex characters");
|
|
21
|
+
const feeSats = utxo.feeSats ?? MIN_FEE_SATS;
|
|
22
|
+
if (feeSats < MIN_FEE_SATS)
|
|
23
|
+
throw new Error(`feeSats ${feeSats} below minimum ${MIN_FEE_SATS}`);
|
|
24
|
+
if (utxo.valueSats <= feeSats)
|
|
25
|
+
throw new Error(`UTXO value ${utxo.valueSats} sats too small to cover fee ${feeSats} sats`);
|
|
13
26
|
const { encodeTransaction, generateTransaction, hexToBin, binToHex, walletTemplateP2pkhNonHd, importWalletTemplate, walletTemplateToCompilerBch } = await import("@bitauth/libauth");
|
|
14
27
|
const data = ANCHOR_PREFIX + bindingId;
|
|
15
28
|
const dataBytes = new TextEncoder().encode(data);
|
|
16
29
|
// OP_RETURN <push> <data>
|
|
17
|
-
const opReturnScript = new Uint8Array([OP_RETURN,
|
|
30
|
+
const opReturnScript = new Uint8Array([OP_RETURN, ...pushData(dataBytes)]);
|
|
18
31
|
const template = importWalletTemplate(walletTemplateP2pkhNonHd);
|
|
19
32
|
if (typeof template === "string")
|
|
20
33
|
throw new Error(template);
|
|
@@ -38,7 +51,7 @@ export async function buildAnchorTx(bindingId, utxo) {
|
|
|
38
51
|
// change back to sender minus fee
|
|
39
52
|
{
|
|
40
53
|
lockingBytecode: utxo.lockingBytecode,
|
|
41
|
-
valueSatoshis: BigInt(utxo.valueSats -
|
|
54
|
+
valueSatoshis: BigInt(utxo.valueSats - feeSats),
|
|
42
55
|
},
|
|
43
56
|
],
|
|
44
57
|
version: 2,
|
package/dist/binding.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type BchMessageProof } from "./signing.js";
|
|
1
2
|
export declare const PROTOCOL_ID = "cashlet-did";
|
|
2
|
-
export declare const PROTOCOL_VERSION = "
|
|
3
|
+
export declare const PROTOCOL_VERSION = "2.0.0";
|
|
3
4
|
export declare const BINDING_KIND = 30078;
|
|
4
5
|
export type Network = "mainnet" | "testnet" | "chipnet";
|
|
5
6
|
export interface CanonicalBinding {
|
|
@@ -16,7 +17,7 @@ export interface CanonicalBinding {
|
|
|
16
17
|
};
|
|
17
18
|
createdAt: number;
|
|
18
19
|
nonce: string;
|
|
19
|
-
purpose: "identity-binding-
|
|
20
|
+
purpose: "identity-binding-v2";
|
|
20
21
|
previousBindingId?: string;
|
|
21
22
|
anchor?: {
|
|
22
23
|
txid: string;
|
|
@@ -25,11 +26,7 @@ export interface CanonicalBinding {
|
|
|
25
26
|
}
|
|
26
27
|
export interface IdentityBinding {
|
|
27
28
|
binding: CanonicalBinding;
|
|
28
|
-
bchProof:
|
|
29
|
-
message: string;
|
|
30
|
-
signature: string;
|
|
31
|
-
pubkeyHex?: string;
|
|
32
|
-
};
|
|
29
|
+
bchProof: BchMessageProof;
|
|
33
30
|
nostrProof: {
|
|
34
31
|
event: NostrEvent;
|
|
35
32
|
};
|
|
@@ -48,10 +45,7 @@ export declare function canonicalize(v: unknown): string;
|
|
|
48
45
|
export declare function sha256hex(s: string): Promise<string>;
|
|
49
46
|
export declare function buildBinding(bchAddress: string, network: Network, nostrPubkey: string, paymentCode?: string, previousBindingId?: string): Promise<CanonicalBinding>;
|
|
50
47
|
export declare function bchSigningPayload(binding: CanonicalBinding): string;
|
|
51
|
-
export declare function nostrEventTemplate(binding: CanonicalBinding, bchProof?: {
|
|
52
|
-
message: string;
|
|
53
|
-
signature: string;
|
|
54
|
-
}): {
|
|
48
|
+
export declare function nostrEventTemplate(binding: CanonicalBinding, bchProof?: BchMessageProof): {
|
|
55
49
|
kind: number;
|
|
56
50
|
created_at: number;
|
|
57
51
|
tags: string[][];
|
package/dist/binding.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { BCH_PROOF_FORMAT, normalizeBchMessageSignature, } from "./signing.js";
|
|
1
2
|
export const PROTOCOL_ID = "cashlet-did";
|
|
2
|
-
export const PROTOCOL_VERSION = "
|
|
3
|
+
export const PROTOCOL_VERSION = "2.0.0";
|
|
3
4
|
export const BINDING_KIND = 30078;
|
|
4
5
|
export function canonicalize(v) {
|
|
5
6
|
if (v === null || typeof v !== "object")
|
|
@@ -21,6 +22,15 @@ function randomHex(bytes) {
|
|
|
21
22
|
return Array.from(arr).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
22
23
|
}
|
|
23
24
|
export async function buildBinding(bchAddress, network, nostrPubkey, paymentCode, previousBindingId) {
|
|
25
|
+
const expectedPrefix = network === "mainnet" ? "bitcoincash" : "bchtest";
|
|
26
|
+
if (!["mainnet", "testnet", "chipnet"].includes(network))
|
|
27
|
+
throw new Error(`Invalid BCH network: ${network}`);
|
|
28
|
+
if (!bchAddress.startsWith(`${expectedPrefix}:`) || bchAddress.split(":")[1]?.length < 20)
|
|
29
|
+
throw new Error(`Invalid BCH address: ${bchAddress}`);
|
|
30
|
+
if (!/^[0-9a-f]{64}$/.test(nostrPubkey))
|
|
31
|
+
throw new Error(`Invalid Nostr pubkey: ${nostrPubkey}`);
|
|
32
|
+
if (previousBindingId !== undefined && !/^[0-9a-f]{64}$/.test(previousBindingId))
|
|
33
|
+
throw new Error(`Invalid previous binding ID: ${previousBindingId}`);
|
|
24
34
|
const partial = {
|
|
25
35
|
protocol: PROTOCOL_ID,
|
|
26
36
|
version: PROTOCOL_VERSION,
|
|
@@ -28,7 +38,7 @@ export async function buildBinding(bchAddress, network, nostrPubkey, paymentCode
|
|
|
28
38
|
nostr: { pubkey: nostrPubkey },
|
|
29
39
|
createdAt: Math.floor(Date.now() / 1000),
|
|
30
40
|
nonce: randomHex(32),
|
|
31
|
-
purpose: "identity-binding-
|
|
41
|
+
purpose: "identity-binding-v2",
|
|
32
42
|
...(previousBindingId ? { previousBindingId } : {}),
|
|
33
43
|
};
|
|
34
44
|
const bindingId = await sha256hex(canonicalize(partial));
|
|
@@ -46,9 +56,14 @@ export function bchSigningPayload(binding) {
|
|
|
46
56
|
});
|
|
47
57
|
}
|
|
48
58
|
export function nostrEventTemplate(binding, bchProof) {
|
|
59
|
+
if (bchProof) {
|
|
60
|
+
if (bchProof.message !== bchSigningPayload(binding))
|
|
61
|
+
throw new Error("BCH proof message does not match the binding");
|
|
62
|
+
normalizeBchMessageSignature(bchProof.signatureBase64);
|
|
63
|
+
}
|
|
49
64
|
return {
|
|
50
65
|
kind: BINDING_KIND,
|
|
51
|
-
created_at:
|
|
66
|
+
created_at: binding.createdAt,
|
|
52
67
|
tags: [
|
|
53
68
|
["d", binding.bindingId],
|
|
54
69
|
["bch", binding.bch.address, binding.bch.network],
|
|
@@ -57,7 +72,7 @@ export function nostrEventTemplate(binding, bchProof) {
|
|
|
57
72
|
["t", PROTOCOL_ID],
|
|
58
73
|
...(binding.previousBindingId ? [["supersedes", binding.previousBindingId]] : []),
|
|
59
74
|
...(binding.anchor ? [["bch-anchor", binding.anchor.txid, ...(binding.anchor.blockHeight ? [String(binding.anchor.blockHeight)] : [])]] : []),
|
|
60
|
-
...(bchProof ? [["bch-proof", bchProof.message, bchProof.
|
|
75
|
+
...(bchProof ? [["bch-proof", BCH_PROOF_FORMAT, bchProof.message, bchProof.signatureBase64]] : []),
|
|
61
76
|
],
|
|
62
77
|
content: canonicalize({
|
|
63
78
|
protocol: PROTOCOL_ID,
|
package/dist/credentials.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Verifiable Credentials — third-party attestations anchored to a Nostr identity.
|
|
3
|
-
* Issuer signs a Nostr event (kind
|
|
3
|
+
* Issuer signs a Nostr event (kind 30085) 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
6
|
import type { RelayResult } from "./relay.js";
|
|
@@ -38,7 +38,13 @@ export declare function issueRating(subjectNpub: string, rating: 1 | 2 | 3 | 4 |
|
|
|
38
38
|
}>;
|
|
39
39
|
/** Revoke a credential by publishing a NIP-09 kind:5 deletion event */
|
|
40
40
|
export declare function revokeCredential(eventId: string, issuerPrivkey: Uint8Array, relays?: string[]): Promise<RelayResult[]>;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Compute a reputation score (0–100) for an npub.
|
|
43
|
+
* Formula: rating score (0–50) + issuer diversity bonus (0–50).
|
|
44
|
+
* rating score = (averageRating / 5) * 50
|
|
45
|
+
* issuer bonus = min(uniqueIssuers, 10) * 5
|
|
46
|
+
* Pass a custom `weight` function to override the formula.
|
|
47
|
+
*/
|
|
42
48
|
export declare function scoreReputation(npub: string, relays?: string[]): Promise<{
|
|
43
49
|
credentialCount: number;
|
|
44
50
|
ratingCount: number;
|
package/dist/credentials.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Verifiable Credentials — third-party attestations anchored to a Nostr identity.
|
|
3
|
-
* Issuer signs a Nostr event (kind
|
|
3
|
+
* Issuer signs a Nostr event (kind 30085) 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, getPublicKey } from "nostr-tools";
|
|
6
|
+
import { finalizeEvent, nip19, getPublicKey, verifyEvent } from "nostr-tools";
|
|
7
7
|
import { queryRelays, publishToRelays, DEFAULT_RELAYS } from "./relay.js";
|
|
8
|
-
export const CREDENTIAL_KIND = 30085;
|
|
8
|
+
export const CREDENTIAL_KIND = 30085;
|
|
9
9
|
/** Issue a credential — issuer signs a Nostr event attesting a claim about subjectNpub */
|
|
10
10
|
export async function issueCredential(subjectNpub, type, issuerPrivkey, claims, relays = DEFAULT_RELAYS) {
|
|
11
11
|
const decoded = nip19.decode(subjectNpub);
|
|
@@ -48,24 +48,64 @@ export async function resolveCredentials(npub, relays = DEFAULT_RELAYS) {
|
|
|
48
48
|
"#t": ["cashlet-did-credential"],
|
|
49
49
|
limit: 50,
|
|
50
50
|
});
|
|
51
|
-
// Fetch revocations
|
|
51
|
+
// Fetch revocations; only a valid event from the credential issuer can revoke it.
|
|
52
52
|
const revocations = await queryRelays(relays, {
|
|
53
53
|
kinds: [5],
|
|
54
54
|
authors: events.map(e => e.pubkey),
|
|
55
55
|
limit: 100,
|
|
56
56
|
});
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
.filter(e => !revokedIds.has(e.id))
|
|
60
|
-
.map(e => {
|
|
57
|
+
const revokedByIssuer = new Map();
|
|
58
|
+
for (const revocation of revocations) {
|
|
61
59
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if (!verifyEvent(revocation) || revocation.kind !== 5)
|
|
61
|
+
continue;
|
|
64
62
|
}
|
|
65
63
|
catch {
|
|
66
|
-
|
|
64
|
+
continue;
|
|
67
65
|
}
|
|
68
|
-
|
|
66
|
+
const revokedIds = revokedByIssuer.get(revocation.pubkey) ?? new Set();
|
|
67
|
+
for (const tag of revocation.tags)
|
|
68
|
+
if (tag[0] === "e" && tag[1])
|
|
69
|
+
revokedIds.add(tag[1]);
|
|
70
|
+
revokedByIssuer.set(revocation.pubkey, revokedIds);
|
|
71
|
+
}
|
|
72
|
+
const now = Math.floor(Date.now() / 1000);
|
|
73
|
+
const credentials = [];
|
|
74
|
+
for (const event of events) {
|
|
75
|
+
if (revokedByIssuer.get(event.pubkey)?.has(event.id))
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
if (!verifyEvent(event) || event.kind !== CREDENTIAL_KIND)
|
|
79
|
+
continue;
|
|
80
|
+
const parsed = JSON.parse(event.content);
|
|
81
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
82
|
+
continue;
|
|
83
|
+
const payload = parsed;
|
|
84
|
+
const subjectTag = event.tags.find(tag => tag[0] === "p");
|
|
85
|
+
const typeTag = event.tags.find(tag => tag[0] === "credential-type");
|
|
86
|
+
const markerTag = event.tags.find(tag => tag[0] === "t");
|
|
87
|
+
if (payload.subject !== pubkey ||
|
|
88
|
+
typeof payload.type !== "string" || payload.type.length === 0 ||
|
|
89
|
+
!Number.isSafeInteger(payload.issuedAt) ||
|
|
90
|
+
payload.issuedAt !== event.created_at ||
|
|
91
|
+
subjectTag?.[1] !== pubkey ||
|
|
92
|
+
typeTag?.[1] !== payload.type ||
|
|
93
|
+
markerTag?.[1] !== "cashlet-did-credential" ||
|
|
94
|
+
(payload.expiry !== undefined && (!Number.isSafeInteger(payload.expiry) || payload.expiry <= now)) ||
|
|
95
|
+
(payload.rating !== undefined && (!Number.isInteger(payload.rating) || payload.rating < 1 || payload.rating > 5)) ||
|
|
96
|
+
(payload.comment !== undefined && typeof payload.comment !== "string") ||
|
|
97
|
+
(payload.claims !== undefined && (typeof payload.claims !== "object" ||
|
|
98
|
+
payload.claims === null ||
|
|
99
|
+
Array.isArray(payload.claims) ||
|
|
100
|
+
Object.values(payload.claims).some(value => typeof value !== "string"))))
|
|
101
|
+
continue;
|
|
102
|
+
credentials.push({ id: event.id, issuer: event.pubkey, payload, rawEvent: event });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return credentials;
|
|
69
109
|
}
|
|
70
110
|
/** Issue a star rating (1–5) to an npub — usable by any wallet or app */
|
|
71
111
|
export async function issueRating(subjectNpub, rating, issuerPrivkey, opts = {}, relays = DEFAULT_RELAYS) {
|
|
@@ -115,7 +155,13 @@ export async function revokeCredential(eventId, issuerPrivkey, relays = DEFAULT_
|
|
|
115
155
|
const signed = finalizeEvent(template, issuerPrivkey);
|
|
116
156
|
return publishToRelays(relays, signed);
|
|
117
157
|
}
|
|
118
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Compute a reputation score (0–100) for an npub.
|
|
160
|
+
* Formula: rating score (0–50) + issuer diversity bonus (0–50).
|
|
161
|
+
* rating score = (averageRating / 5) * 50
|
|
162
|
+
* issuer bonus = min(uniqueIssuers, 10) * 5
|
|
163
|
+
* Pass a custom `weight` function to override the formula.
|
|
164
|
+
*/
|
|
119
165
|
export async function scoreReputation(npub, relays = DEFAULT_RELAYS) {
|
|
120
166
|
const credentials = await resolveCredentials(npub, relays);
|
|
121
167
|
const ratings = credentials.filter(c => c.payload.type === "rating" && c.payload.rating);
|
package/dist/disclosure.d.ts
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
* // verifier calls verifyDisclosure(revealed, commitments)
|
|
11
11
|
*/
|
|
12
12
|
export interface Disclosure {
|
|
13
|
-
/** SHA-256 hashes of each attribute —
|
|
13
|
+
/** Salted SHA-256 hashes of each attribute — keep each salt private until reveal */
|
|
14
14
|
commitments: Record<string, string>;
|
|
15
|
-
/** Reveal selected
|
|
16
|
-
reveal: (keys: string[]) => Record<string,
|
|
15
|
+
/** Reveal selected values with their salts — share these only for requested attributes */
|
|
16
|
+
reveal: (keys: string[]) => Record<string, DisclosureReveal>;
|
|
17
|
+
}
|
|
18
|
+
export interface DisclosureReveal {
|
|
19
|
+
value: string;
|
|
20
|
+
salt: string;
|
|
17
21
|
}
|
|
18
22
|
/** Build a disclosure object from a set of attributes */
|
|
19
23
|
export declare function buildDisclosure(attrs: Record<string, string>): Promise<Disclosure>;
|
|
@@ -21,7 +25,7 @@ export declare function buildDisclosure(attrs: Record<string, string>): Promise<
|
|
|
21
25
|
* Verify that revealed attributes match their published commitments.
|
|
22
26
|
* Returns the set of verified attribute keys.
|
|
23
27
|
*/
|
|
24
|
-
export declare function verifyDisclosure(revealed: Record<string,
|
|
28
|
+
export declare function verifyDisclosure(revealed: Record<string, DisclosureReveal>, commitments: Record<string, string>): Promise<{
|
|
25
29
|
verified: string[];
|
|
26
30
|
failed: string[];
|
|
27
31
|
}>;
|
package/dist/disclosure.js
CHANGED
|
@@ -9,20 +9,27 @@
|
|
|
9
9
|
* const revealed = d.reveal(["merchant"])
|
|
10
10
|
* // verifier calls verifyDisclosure(revealed, commitments)
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
const
|
|
12
|
+
function randomSalt() {
|
|
13
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
14
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
}
|
|
16
|
+
async function hashAttr(key, value, salt) {
|
|
17
|
+
const data = new TextEncoder().encode(`cashlet-did/attr/v2\n${JSON.stringify([key, value, salt])}`);
|
|
14
18
|
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
15
19
|
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
16
20
|
}
|
|
17
21
|
/** Build a disclosure object from a set of attributes */
|
|
18
22
|
export async function buildDisclosure(attrs) {
|
|
19
23
|
const commitments = {};
|
|
24
|
+
const salts = {};
|
|
20
25
|
for (const [k, v] of Object.entries(attrs)) {
|
|
21
|
-
|
|
26
|
+
const salt = randomSalt();
|
|
27
|
+
salts[k] = salt;
|
|
28
|
+
commitments[k] = await hashAttr(k, v, salt);
|
|
22
29
|
}
|
|
23
30
|
return {
|
|
24
31
|
commitments,
|
|
25
|
-
reveal: (keys) => Object.fromEntries(keys.filter(k => k in attrs).map(k => [k, attrs[k]])),
|
|
32
|
+
reveal: (keys) => Object.fromEntries(keys.filter(k => k in attrs).map(k => [k, { value: attrs[k], salt: salts[k] }])),
|
|
26
33
|
};
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
@@ -34,11 +41,16 @@ export async function verifyDisclosure(revealed, commitments) {
|
|
|
34
41
|
const failed = [];
|
|
35
42
|
for (const [k, v] of Object.entries(revealed)) {
|
|
36
43
|
const expected = commitments[k];
|
|
37
|
-
if (!expected
|
|
44
|
+
if (!expected ||
|
|
45
|
+
!v ||
|
|
46
|
+
typeof v !== "object" ||
|
|
47
|
+
typeof v.value !== "string" ||
|
|
48
|
+
typeof v.salt !== "string" ||
|
|
49
|
+
!/^[0-9a-f]{64}$/.test(v.salt)) {
|
|
38
50
|
failed.push(k);
|
|
39
51
|
continue;
|
|
40
52
|
}
|
|
41
|
-
const actual = await hashAttr(k, v);
|
|
53
|
+
const actual = await hashAttr(k, v.value, v.salt);
|
|
42
54
|
(actual === expected ? verified : failed).push(k);
|
|
43
55
|
}
|
|
44
56
|
return { verified, failed };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export { buildDisclosure, verifyDisclosure, type Disclosure, } from "./disclosure.js";
|
|
1
|
+
export { buildDisclosure, verifyDisclosure, type Disclosure, type DisclosureReveal, } from "./disclosure.js";
|
|
2
2
|
export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
|
|
3
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
|
+
export { BCH_PROOF_FORMAT, normalizeBchMessageSignature, signBchMessage, type BchMessageProof, type BchMessageSigner, type BchProofTag, } from "./signing.js";
|
|
5
6
|
export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
|
|
6
|
-
export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
|
|
7
|
+
export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, type BCHAnchorClaim, } from "./verify.js";
|
|
7
8
|
export { CashletDidError, type CashletDidErrorCode } from "./errors.js";
|
|
8
9
|
export { deriveNostrKeypair, type NostrKeypair, } from "./keygen.js";
|
|
9
10
|
export { buildHttpAuthEventAsync, verifyHttpAuth, type HttpAuthEventTemplate, type HttpAuthResult, } from "./nip98.js";
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,13 @@ export {
|
|
|
6
6
|
PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND,
|
|
7
7
|
// Binding construction
|
|
8
8
|
buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
|
|
9
|
+
export { BCH_PROOF_FORMAT, normalizeBchMessageSignature, signBchMessage, } from "./signing.js";
|
|
9
10
|
export {
|
|
10
11
|
// Relay communication
|
|
11
12
|
publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, } from "./relay.js";
|
|
12
13
|
export {
|
|
13
14
|
// Verification
|
|
14
|
-
resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
|
|
15
|
+
resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
|
|
15
16
|
export { CashletDidError } from "./errors.js";
|
|
16
17
|
export { deriveNostrKeypair, } from "./keygen.js";
|
|
17
18
|
export { buildHttpAuthEventAsync, verifyHttpAuth, } from "./nip98.js";
|
package/dist/nip98.d.ts
CHANGED
|
@@ -24,10 +24,12 @@ export interface HttpAuthResult {
|
|
|
24
24
|
* @param authHeader - value of the Authorization header (e.g. "Nostr <base64>")
|
|
25
25
|
* @param url - the full request URL (must match event tag exactly)
|
|
26
26
|
* @param method - HTTP method (must match event tag exactly)
|
|
27
|
+
* @param opts.body - actual request body, or `null` for a request without a body; required
|
|
27
28
|
* @param opts.verifyBch - if true, resolve + verify the signer's BCH binding
|
|
28
29
|
* @param opts.relays - relay list for BCH binding resolution
|
|
29
30
|
*/
|
|
30
31
|
export declare function verifyHttpAuth(authHeader: string, url: string, method: string, opts?: {
|
|
32
|
+
body?: Uint8Array | null;
|
|
31
33
|
verifyBch?: boolean;
|
|
32
34
|
relays?: string[];
|
|
33
35
|
}): Promise<HttpAuthResult>;
|
package/dist/nip98.js
CHANGED
|
@@ -2,6 +2,7 @@ import { verifyEvent } from "nostr-tools";
|
|
|
2
2
|
import { resolveIdentity, verifyIdentity } from "./verify.js";
|
|
3
3
|
const HTTP_AUTH_KIND = 27235;
|
|
4
4
|
const HTTP_AUTH_TTL = 60; // seconds — NIP-98 spec
|
|
5
|
+
const HTTP_AUTH_CLOCK_SKEW = 60;
|
|
5
6
|
/**
|
|
6
7
|
* Build a NIP-98 HTTP Auth event template.
|
|
7
8
|
* Pass a body Uint8Array to include its SHA-256 hash as a `payload` tag.
|
|
@@ -14,12 +15,14 @@ export async function buildHttpAuthEventAsync(url, method, body) {
|
|
|
14
15
|
["method", method.toUpperCase()],
|
|
15
16
|
];
|
|
16
17
|
if (body) {
|
|
17
|
-
|
|
18
|
-
const hash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
19
|
-
tags.push(["payload", hash]);
|
|
18
|
+
tags.push(["payload", await sha256Hex(body)]);
|
|
20
19
|
}
|
|
21
20
|
return { kind: HTTP_AUTH_KIND, created_at: Math.floor(Date.now() / 1000), tags, content: "" };
|
|
22
21
|
}
|
|
22
|
+
async function sha256Hex(data) {
|
|
23
|
+
const hashBuf = await crypto.subtle.digest("SHA-256", data);
|
|
24
|
+
return Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
25
|
+
}
|
|
23
26
|
/**
|
|
24
27
|
* Verify a NIP-98 Authorization header on the server side.
|
|
25
28
|
* Optionally resolves the signer's BCH binding — proving both Nostr + BCH ownership.
|
|
@@ -27,6 +30,7 @@ export async function buildHttpAuthEventAsync(url, method, body) {
|
|
|
27
30
|
* @param authHeader - value of the Authorization header (e.g. "Nostr <base64>")
|
|
28
31
|
* @param url - the full request URL (must match event tag exactly)
|
|
29
32
|
* @param method - HTTP method (must match event tag exactly)
|
|
33
|
+
* @param opts.body - actual request body, or `null` for a request without a body; required
|
|
30
34
|
* @param opts.verifyBch - if true, resolve + verify the signer's BCH binding
|
|
31
35
|
* @param opts.relays - relay list for BCH binding resolution
|
|
32
36
|
*/
|
|
@@ -47,9 +51,12 @@ export async function verifyHttpAuth(authHeader, url, method, opts = {}) {
|
|
|
47
51
|
// 2. Kind
|
|
48
52
|
if (event.kind !== HTTP_AUTH_KIND)
|
|
49
53
|
return { valid: false, error: "Wrong event kind" };
|
|
50
|
-
// 3. TTL
|
|
51
|
-
|
|
54
|
+
// 3. TTL and clock skew
|
|
55
|
+
const now = Math.floor(Date.now() / 1000);
|
|
56
|
+
if (!Number.isSafeInteger(event.created_at) || now - event.created_at > HTTP_AUTH_TTL)
|
|
52
57
|
return { valid: false, error: "Event expired" };
|
|
58
|
+
if (event.created_at - now > HTTP_AUTH_CLOCK_SKEW)
|
|
59
|
+
return { valid: false, error: "Event timestamp is in the future" };
|
|
53
60
|
// 4. URL + method tags
|
|
54
61
|
const uTag = event.tags.find((t) => t[0] === "u");
|
|
55
62
|
const mTag = event.tags.find((t) => t[0] === "method");
|
|
@@ -57,6 +64,19 @@ export async function verifyHttpAuth(authHeader, url, method, opts = {}) {
|
|
|
57
64
|
return { valid: false, error: "URL mismatch" };
|
|
58
65
|
if (mTag?.[1] !== method.toUpperCase())
|
|
59
66
|
return { valid: false, error: "Method mismatch" };
|
|
67
|
+
if (opts.body === undefined)
|
|
68
|
+
return { valid: false, error: "Request body must be provided (use null when absent)" };
|
|
69
|
+
const payloadTags = event.tags.filter((t) => t[0] === "payload");
|
|
70
|
+
if (opts.body === null) {
|
|
71
|
+
if (payloadTags.length)
|
|
72
|
+
return { valid: false, error: "Unexpected payload hash for bodyless request" };
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (payloadTags.length !== 1 || typeof payloadTags[0][1] !== "string")
|
|
76
|
+
return { valid: false, error: "Missing or duplicate payload hash" };
|
|
77
|
+
if (payloadTags[0][1] !== await sha256Hex(opts.body))
|
|
78
|
+
return { valid: false, error: "Request body hash mismatch" };
|
|
79
|
+
}
|
|
60
80
|
const result = { valid: true, pubkey: event.pubkey };
|
|
61
81
|
// 5. Optionally resolve + verify BCH binding
|
|
62
82
|
if (opts.verifyBch) {
|
package/dist/relay.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const CONNECT_TIMEOUT_MS = 5000;
|
|
2
|
+
const RESPONSE_TIMEOUT_MS = 8000;
|
|
3
|
+
const MAX_EVENTS_PER_RELAY = 200;
|
|
2
4
|
function openRelay(url) {
|
|
3
5
|
return new Promise((resolve, reject) => {
|
|
4
6
|
const ws = new WebSocket(url);
|
|
5
|
-
const t = setTimeout(() => { ws.close(); reject(new Error("connect timeout")); },
|
|
7
|
+
const t = setTimeout(() => { ws.close(); reject(new Error("connect timeout")); }, CONNECT_TIMEOUT_MS);
|
|
6
8
|
ws.onopen = () => { clearTimeout(t); resolve(ws); };
|
|
7
9
|
ws.onerror = () => { clearTimeout(t); reject(new Error("connect error")); };
|
|
8
10
|
});
|
|
@@ -11,7 +13,7 @@ export async function publishToRelay(url, event) {
|
|
|
11
13
|
try {
|
|
12
14
|
const ws = await openRelay(url);
|
|
13
15
|
return await new Promise((resolve) => {
|
|
14
|
-
const t = setTimeout(() => { ws.close(); resolve({ relay: url, ok: false, error: "publish timeout" }); },
|
|
16
|
+
const t = setTimeout(() => { ws.close(); resolve({ relay: url, ok: false, error: "publish timeout" }); }, RESPONSE_TIMEOUT_MS);
|
|
15
17
|
ws.onmessage = (e) => {
|
|
16
18
|
clearTimeout(t);
|
|
17
19
|
ws.close();
|
|
@@ -42,12 +44,14 @@ export async function queryRelay(url, filter) {
|
|
|
42
44
|
return await new Promise((resolve) => {
|
|
43
45
|
const events = [];
|
|
44
46
|
const subId = Math.random().toString(36).slice(2);
|
|
45
|
-
const t = setTimeout(() => { ws.close(); resolve(events); },
|
|
47
|
+
const t = setTimeout(() => { ws.close(); resolve(events); }, RESPONSE_TIMEOUT_MS);
|
|
46
48
|
ws.onmessage = (e) => {
|
|
47
49
|
try {
|
|
48
50
|
const msg = JSON.parse(e.data);
|
|
49
|
-
if (msg[0] === "EVENT" && msg[1] === subId)
|
|
50
|
-
events.
|
|
51
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
52
|
+
if (events.length < MAX_EVENTS_PER_RELAY)
|
|
53
|
+
events.push(msg[2]);
|
|
54
|
+
}
|
|
51
55
|
if (msg[0] === "EOSE" && msg[1] === subId) {
|
|
52
56
|
clearTimeout(t);
|
|
53
57
|
ws.close();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const BCH_PROOF_FORMAT = "bitcoin-message/compact-base64";
|
|
2
|
+
export type BchProofTag = ["bch-proof", typeof BCH_PROOF_FORMAT, string, string];
|
|
3
|
+
export interface BchMessageProof {
|
|
4
|
+
message: string;
|
|
5
|
+
signatureBase64: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BchMessageSigner {
|
|
8
|
+
signMessage(message: string): Promise<unknown> | unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validate and return a wallet's standard Bitcoin compact-message signature.
|
|
12
|
+
* The header must encode a compressed public key and recovery ID 0–3.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeBchMessageSignature(signature: unknown): string;
|
|
15
|
+
/** Pass exact UTF-8 message text to a transport adapter and validate its response. */
|
|
16
|
+
export declare function signBchMessage(signer: BchMessageSigner, message: string): Promise<string>;
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const BCH_PROOF_FORMAT = "bitcoin-message/compact-base64";
|
|
2
|
+
/**
|
|
3
|
+
* Validate and return a wallet's standard Bitcoin compact-message signature.
|
|
4
|
+
* The header must encode a compressed public key and recovery ID 0–3.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeBchMessageSignature(signature) {
|
|
7
|
+
if (typeof signature !== "string" || !/^(?:[A-Za-z0-9+/]{4}){21}[A-Za-z0-9+/]{3}=$/.test(signature))
|
|
8
|
+
throw new Error("BCH message signature must be a canonical 65-byte Base64 compact signature");
|
|
9
|
+
let decoded;
|
|
10
|
+
try {
|
|
11
|
+
decoded = atob(signature);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error("BCH message signature is not valid Base64");
|
|
15
|
+
}
|
|
16
|
+
if (decoded.length !== 65 || btoa(decoded) !== signature)
|
|
17
|
+
throw new Error("BCH message signature must be canonical Base64 for exactly 65 bytes");
|
|
18
|
+
const header = decoded.charCodeAt(0);
|
|
19
|
+
if (header < 31 || header > 34)
|
|
20
|
+
throw new Error("BCH message signature must use a compressed-key header (31–34)");
|
|
21
|
+
return signature;
|
|
22
|
+
}
|
|
23
|
+
/** Pass exact UTF-8 message text to a transport adapter and validate its response. */
|
|
24
|
+
export async function signBchMessage(signer, message) {
|
|
25
|
+
if (!signer || typeof signer.signMessage !== "function")
|
|
26
|
+
throw new TypeError("A BCH message signer with signMessage(message) is required");
|
|
27
|
+
if (typeof message !== "string")
|
|
28
|
+
throw new TypeError("BCH message must be a string");
|
|
29
|
+
return normalizeBchMessageSignature(await signer.signMessage(message));
|
|
30
|
+
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { canonicalize, sha256hex, bchSigningPayload, type CanonicalBinding, type IdentityBinding, type NostrEvent } from "./binding.js";
|
|
2
|
+
import { type BchProofTag } from "./signing.js";
|
|
2
3
|
export type VerifyStatus = "verified" | "invalid_bch_sig" | "invalid_nostr_sig" | "id_mismatch" | "malformed" | "expired";
|
|
3
4
|
export interface VerifyResult {
|
|
4
5
|
status: VerifyStatus;
|
|
5
6
|
bchValid: boolean;
|
|
6
7
|
nostrValid: boolean;
|
|
7
8
|
details: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
anchoredAt?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface BCHAnchorClaim {
|
|
11
|
+
txid: string;
|
|
12
|
+
blockHeight?: number;
|
|
13
13
|
}
|
|
14
14
|
export interface ResolvedIdentity {
|
|
15
15
|
npub: string;
|
|
@@ -18,20 +18,19 @@ export interface ResolvedIdentity {
|
|
|
18
18
|
network: string;
|
|
19
19
|
bindingId: string;
|
|
20
20
|
createdAt: number;
|
|
21
|
-
bchProofTag:
|
|
21
|
+
bchProofTag: BchProofTag;
|
|
22
22
|
rawEvent: NostrEvent;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
* cannot be significantly backdated (median-time-past rule). */
|
|
26
|
-
anchoredAt?: number;
|
|
23
|
+
/** Unverified metadata claimed by the event; this library does not verify chain inclusion. */
|
|
24
|
+
anchorClaim?: BCHAnchorClaim;
|
|
27
25
|
}
|
|
28
26
|
/** Resolve a BCH address from an npub via Nostr relays */
|
|
29
27
|
export declare function resolveIdentity(npub: string, relays?: string[]): Promise<ResolvedIdentity | null>;
|
|
30
28
|
/** Verify the BCH + Nostr signatures on a resolved identity */
|
|
31
29
|
export declare function verifyIdentity(resolved: ResolvedIdentity): Promise<VerifyResult>;
|
|
30
|
+
/** Build the exact versioned challenge message that a BCH wallet must sign. */
|
|
31
|
+
export declare function bchChallengePayload(nonce: string, audience?: string): string;
|
|
32
32
|
/** Verify a challenge-response proof.
|
|
33
|
-
*
|
|
34
|
-
* Matches the SIGN_CHALLENGE format used by the Cashlet extension and any conforming client.
|
|
33
|
+
* The wallet signs the exact text produced by `bchChallengePayload`.
|
|
35
34
|
* @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
|
|
36
35
|
*/
|
|
37
36
|
export declare function verifyChallengeProof(proof: {
|
|
@@ -49,7 +48,7 @@ export declare function verifyChallengeProof(proof: {
|
|
|
49
48
|
* if (address) wallet.sendTo(address, amount)
|
|
50
49
|
*/
|
|
51
50
|
export declare function resolveAddress(npub: string, relays?: string[]): Promise<string | null>;
|
|
52
|
-
/** Returns
|
|
51
|
+
/** Returns verified binding events newest first, following and validating each supersedes reference. */
|
|
53
52
|
export declare function resolveIdentityChain(npub: string, relays?: string[]): Promise<ResolvedIdentity[]>;
|
|
54
53
|
/**
|
|
55
54
|
* Resolve a BCH address from a NIP-05 handle (e.g. "alice@cashlet.app")
|