did-beta-bch 1.3.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/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 = "1.0.0";
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-v1";
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 = "1.0.0";
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,11 +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) {
24
- // Validate cashaddr format — must have prefix and be non-empty after colon
25
- if (!bchAddress.includes(":") || bchAddress.split(":")[1].length < 20)
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)
26
29
  throw new Error(`Invalid BCH address: ${bchAddress}`);
27
30
  if (!/^[0-9a-f]{64}$/.test(nostrPubkey))
28
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}`);
29
34
  const partial = {
30
35
  protocol: PROTOCOL_ID,
31
36
  version: PROTOCOL_VERSION,
@@ -33,7 +38,7 @@ export async function buildBinding(bchAddress, network, nostrPubkey, paymentCode
33
38
  nostr: { pubkey: nostrPubkey },
34
39
  createdAt: Math.floor(Date.now() / 1000),
35
40
  nonce: randomHex(32),
36
- purpose: "identity-binding-v1",
41
+ purpose: "identity-binding-v2",
37
42
  ...(previousBindingId ? { previousBindingId } : {}),
38
43
  };
39
44
  const bindingId = await sha256hex(canonicalize(partial));
@@ -51,9 +56,14 @@ export function bchSigningPayload(binding) {
51
56
  });
52
57
  }
53
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
+ }
54
64
  return {
55
65
  kind: BINDING_KIND,
56
- created_at: Math.floor(Date.now() / 1000),
66
+ created_at: binding.createdAt,
57
67
  tags: [
58
68
  ["d", binding.bindingId],
59
69
  ["bch", binding.bch.address, binding.bch.network],
@@ -62,7 +72,7 @@ export function nostrEventTemplate(binding, bchProof) {
62
72
  ["t", PROTOCOL_ID],
63
73
  ...(binding.previousBindingId ? [["supersedes", binding.previousBindingId]] : []),
64
74
  ...(binding.anchor ? [["bch-anchor", binding.anchor.txid, ...(binding.anchor.blockHeight ? [String(binding.anchor.blockHeight)] : [])]] : []),
65
- ...(bchProof ? [["bch-proof", bchProof.message, bchProof.signature]] : []),
75
+ ...(bchProof ? [["bch-proof", BCH_PROOF_FORMAT, bchProof.message, bchProof.signatureBase64]] : []),
66
76
  ],
67
77
  content: canonicalize({
68
78
  protocol: PROTOCOL_ID,
@@ -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 30079) attesting a claim about a subject npub.
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";
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Verifiable Credentials — third-party attestations anchored to a Nostr identity.
3
- * Issuer signs a Nostr event (kind 30079) attesting a claim about a subject npub.
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
8
  export const CREDENTIAL_KIND = 30085;
9
9
  /** Issue a credential — issuer signs a Nostr event attesting a claim about 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 and filter them out
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 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 => {
57
+ const revokedByIssuer = new Map();
58
+ for (const revocation of revocations) {
61
59
  try {
62
- const payload = JSON.parse(e.content);
63
- return { id: e.id, issuer: e.pubkey, payload, rawEvent: e };
60
+ if (!verifyEvent(revocation) || revocation.kind !== 5)
61
+ continue;
64
62
  }
65
63
  catch {
66
- return null;
64
+ continue;
67
65
  }
68
- }).filter(Boolean);
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) {
@@ -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 — publish these in the binding event */
13
+ /** Salted SHA-256 hashes of each attribute — keep each salt private until reveal */
14
14
  commitments: Record<string, string>;
15
- /** Reveal selected attribute values — share with verifier */
16
- reveal: (keys: string[]) => Record<string, 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, string>, commitments: Record<string, string>): Promise<{
28
+ export declare function verifyDisclosure(revealed: Record<string, DisclosureReveal>, commitments: Record<string, string>): Promise<{
25
29
  verified: string[];
26
30
  failed: string[];
27
31
  }>;
@@ -9,20 +9,27 @@
9
9
  * const revealed = d.reveal(["merchant"])
10
10
  * // verifier calls verifyDisclosure(revealed, commitments)
11
11
  */
12
- async function hashAttr(key, value) {
13
- const data = new TextEncoder().encode(`cashlet-did/attr/${key}:${value}`);
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
- commitments[k] = await hashAttr(k, v);
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
- 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]);
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
- if (Math.floor(Date.now() / 1000) - event.created_at > HTTP_AUTH_TTL)
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) {
@@ -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>;
@@ -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
- /** 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;
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: string[];
21
+ bchProofTag: BchProofTag;
22
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;
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
- * 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.
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 full binding history for an npub, newest first, with cryptographic supersedes validation */
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")