@orangecheck/sdk 1.11.0 → 1.13.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/README.md CHANGED
@@ -145,6 +145,40 @@ Now the gate doesn't have to trust the client — the address it sees has been c
145
145
 
146
146
  ---
147
147
 
148
+ ## Bonds: one coin backing one account
149
+
150
+ An attestation publishes an address. A bond keeps it private. The holder signs
151
+ once to back one subject with one unspent coin, and whoever relies on the bond
152
+ sees only floors (at least N sats, at least D days) and a nullifier that is the
153
+ same for that coin at that site and unrelated anywhere else. The registry keys
154
+ the nullifier with its own secret, so nobody can find the coin by hashing
155
+ outpoints. Not to be confused with an attestation's `bond:` line (an amount at
156
+ a public address) or `@orangecheck/pledge-core`'s `verifyBond`. Spec:
157
+ [`BOND.md`](https://github.com/orangecheck/oc-attest-protocol/blob/main/BOND.md).
158
+
159
+ ```ts
160
+ import { bondFloors, bondNullifier, esploraSource, issueBondMessage, verifyBond } from '@orangecheck/sdk';
161
+
162
+ // Registry: hand the holder a message naming their coin and your subject.
163
+ const { message } = issueBondMessage({ address, outpoint: `${txid}:${vout}`, subject: didOc });
164
+
165
+ // The holder signs it with BIP-322 in their wallet, then you verify the
166
+ // signature and the coin: unspent, confirmed, 6+ deep, at that address.
167
+ const r = await verifyBond({ message, signature, subject: didOc, source: esploraSource() });
168
+ if (r.ok) {
169
+ const age = r.bond.tip.height - r.bond.confirmed_height + 1;
170
+ // NULLIFIER_KEY: your registry's secret, 32+ bytes, kept for good.
171
+ const nullifier = bondNullifier(NULLIFIER_KEY, audience, r.bond.outpoint);
172
+ const claim = { ...bondFloors(r.bond.sats, age), nullifier };
173
+ }
174
+ ```
175
+
176
+ `checkBondCoin` re-checks a stored bond's coin later; a spent coin ends the
177
+ bond. Ban a nullifier and you've banned the coin: the holder has to move it to
178
+ get past the ban, and moving it restarts its age.
179
+
180
+ ---
181
+
148
182
  ## Creating an attestation (issuer side)
149
183
 
150
184
  If you're building a wallet, a Nostr client, or a settings page where users should be able to _create_ a proof, use:
@@ -248,6 +282,13 @@ createAttestation(options: CreateAttestationOptions): Promise<AttestationEnvelop
248
282
  issueChallenge(options): Challenge
249
283
  verifyChallenge(options): Promise<VerifyChallengeResult>
250
284
 
285
+ // Bonds (BOND.md).
286
+ issueBondMessage(options): { message, nonce, expiresAt }
287
+ verifyBond(options): Promise<BondResult>
288
+ checkBondCoin(options): Promise<...>
289
+ bondNullifier(key, audience, outpoint): string
290
+ bondFloors(sats, ageBlocks): { sats_floor, days_floor }
291
+
251
292
  // Building blocks.
252
293
  buildCanonicalMessage(...)
253
294
  generateAttestationId(msg: string): Promise<string>
@@ -1,4 +1,4 @@
1
- import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-D9Tb1roj.mjs';
1
+ import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface CreateAttestationOptions {
4
4
  message: string;
@@ -1,4 +1,4 @@
1
- import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-D9Tb1roj.js';
1
+ import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface CreateAttestationOptions {
4
4
  message: string;
@@ -1,4 +1,4 @@
1
- import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-D9Tb1roj.mjs';
1
+ import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface CanonicalInput {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-D9Tb1roj.js';
1
+ import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface CanonicalInput {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { S as Scheme } from './types-D9Tb1roj.mjs';
1
+ import { S as Scheme } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface IssueChallengeOptions {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { S as Scheme } from './types-D9Tb1roj.js';
1
+ import { S as Scheme } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface IssueChallengeOptions {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { c as NostrEvent, I as IdentityBinding } from './types-D9Tb1roj.mjs';
1
+ import { c as NostrEvent, I as IdentityBinding } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface DnsVerificationResult {
4
4
  verified: boolean;
@@ -1,4 +1,4 @@
1
- import { c as NostrEvent, I as IdentityBinding } from './types-D9Tb1roj.js';
1
+ import { c as NostrEvent, I as IdentityBinding } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface DnsVerificationResult {
4
4
  verified: boolean;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-D9Tb1roj.mjs';
2
- export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-D9Tb1roj.mjs';
1
+ import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BBRMCgMo.mjs';
2
+ export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BBRMCgMo.mjs';
3
3
  export { verify } from './verify.mjs';
4
4
  export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.mjs';
5
5
  export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.mjs';
@@ -36,6 +36,111 @@ declare function check(params: CheckParams): Promise<CheckResult>;
36
36
  declare function nostrPubkeyToHex(value: string): string | null;
37
37
  declare function nostrIdentifierForms(identifier: string): string[];
38
38
 
39
+ declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
40
+ declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
41
+ declare const BOND_MIN_CONFIRMATIONS = 6;
42
+ declare const BOND_MAX_WINDOW_SECONDS = 3600;
43
+ interface BondFields {
44
+ address: string;
45
+ outpoint: string;
46
+ subject: string;
47
+ nonce: string;
48
+ issued_at: string;
49
+ expires_at: string;
50
+ }
51
+ type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'bond_window_too_long' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
52
+ interface BondCoin {
53
+ sats: number;
54
+ scriptPubKey: string;
55
+ confirmedHeight: number | null;
56
+ spent: boolean;
57
+ }
58
+ interface ChainTip {
59
+ height: number;
60
+ hash: string;
61
+ }
62
+ interface BondChainSource {
63
+ tip(): Promise<ChainTip>;
64
+ coin(outpoint: string): Promise<BondCoin | null>;
65
+ }
66
+ interface VerifiedBond {
67
+ bond_id: string;
68
+ subject: string;
69
+ address: string;
70
+ outpoint: string;
71
+ sats: number;
72
+ confirmed_height: number;
73
+ tip: ChainTip;
74
+ }
75
+ type BondResult = {
76
+ ok: true;
77
+ bond: VerifiedBond;
78
+ } | {
79
+ ok: false;
80
+ reason: BondReason;
81
+ };
82
+ declare function buildBondMessage(fields: BondFields): string;
83
+ declare function issueBondMessage(opts: {
84
+ address: string;
85
+ outpoint: string;
86
+ subject: string;
87
+ ttlSeconds?: number;
88
+ now?: Date;
89
+ }): {
90
+ message: string;
91
+ nonce: string;
92
+ expiresAt: number;
93
+ };
94
+ declare function parseBondMessage(message: string): BondFields | null;
95
+ declare function bondId(message: string): string;
96
+ declare function bondNullifier(key: Uint8Array, audience: string, outpoint: string): string;
97
+ declare function bondFloors(sats: number, ageBlocks: number): {
98
+ sats_floor: number;
99
+ days_floor: number;
100
+ };
101
+ declare function verifyBondSignature(opts: {
102
+ message: string;
103
+ signature: string;
104
+ subject?: string;
105
+ now?: Date;
106
+ skewSeconds?: number;
107
+ maxWindowSeconds?: number;
108
+ }): {
109
+ ok: true;
110
+ fields: BondFields;
111
+ bond_id: string;
112
+ } | {
113
+ ok: false;
114
+ reason: BondReason;
115
+ };
116
+ declare function checkBondCoin(opts: {
117
+ address: string;
118
+ outpoint: string;
119
+ source: BondChainSource;
120
+ minConfirmations?: number;
121
+ }): Promise<{
122
+ ok: true;
123
+ sats: number;
124
+ confirmed_height: number;
125
+ tip: ChainTip;
126
+ } | {
127
+ ok: false;
128
+ reason: BondReason;
129
+ }>;
130
+ declare function verifyBond(opts: {
131
+ message: string;
132
+ signature: string;
133
+ source: BondChainSource;
134
+ subject?: string;
135
+ now?: Date;
136
+ maxWindowSeconds?: number;
137
+ minConfirmations?: number;
138
+ }): Promise<BondResult>;
139
+ declare function esploraSource(base?: string, opts?: {
140
+ timeoutMs?: number;
141
+ fetch?: typeof fetch;
142
+ }): BondChainSource;
143
+
39
144
  declare function isNip07Available(): boolean;
40
145
  declare function getNostrPublicKey(): Promise<string | null>;
41
146
  declare function signNostrEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent | null>;
@@ -53,4 +158,4 @@ interface StatusMeta {
53
158
  }
54
159
  declare const STATUS_META: Record<StatusCode, StatusMeta>;
55
160
 
56
- export { type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, VerifyOptions, check, getNip07Info, getNostrPublicKey, isNip07Available, nostrIdentifierForms, nostrPubkeyToHex, signNostrEvent };
161
+ export { BOND_DAYS_FLOORS, BOND_MAX_WINDOW_SECONDS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, type BondChainSource, type BondCoin, type BondFields, type BondReason, type BondResult, type ChainTip, type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, type VerifiedBond, VerifyOptions, bondFloors, bondId, bondNullifier, buildBondMessage, check, checkBondCoin, esploraSource, getNip07Info, getNostrPublicKey, isNip07Available, issueBondMessage, nostrIdentifierForms, nostrPubkeyToHex, parseBondMessage, signNostrEvent, verifyBond, verifyBondSignature };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-D9Tb1roj.js';
2
- export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-D9Tb1roj.js';
1
+ import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BBRMCgMo.js';
2
+ export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BBRMCgMo.js';
3
3
  export { verify } from './verify.js';
4
4
  export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.js';
5
5
  export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.js';
@@ -36,6 +36,111 @@ declare function check(params: CheckParams): Promise<CheckResult>;
36
36
  declare function nostrPubkeyToHex(value: string): string | null;
37
37
  declare function nostrIdentifierForms(identifier: string): string[];
38
38
 
39
+ declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
40
+ declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
41
+ declare const BOND_MIN_CONFIRMATIONS = 6;
42
+ declare const BOND_MAX_WINDOW_SECONDS = 3600;
43
+ interface BondFields {
44
+ address: string;
45
+ outpoint: string;
46
+ subject: string;
47
+ nonce: string;
48
+ issued_at: string;
49
+ expires_at: string;
50
+ }
51
+ type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'bond_window_too_long' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
52
+ interface BondCoin {
53
+ sats: number;
54
+ scriptPubKey: string;
55
+ confirmedHeight: number | null;
56
+ spent: boolean;
57
+ }
58
+ interface ChainTip {
59
+ height: number;
60
+ hash: string;
61
+ }
62
+ interface BondChainSource {
63
+ tip(): Promise<ChainTip>;
64
+ coin(outpoint: string): Promise<BondCoin | null>;
65
+ }
66
+ interface VerifiedBond {
67
+ bond_id: string;
68
+ subject: string;
69
+ address: string;
70
+ outpoint: string;
71
+ sats: number;
72
+ confirmed_height: number;
73
+ tip: ChainTip;
74
+ }
75
+ type BondResult = {
76
+ ok: true;
77
+ bond: VerifiedBond;
78
+ } | {
79
+ ok: false;
80
+ reason: BondReason;
81
+ };
82
+ declare function buildBondMessage(fields: BondFields): string;
83
+ declare function issueBondMessage(opts: {
84
+ address: string;
85
+ outpoint: string;
86
+ subject: string;
87
+ ttlSeconds?: number;
88
+ now?: Date;
89
+ }): {
90
+ message: string;
91
+ nonce: string;
92
+ expiresAt: number;
93
+ };
94
+ declare function parseBondMessage(message: string): BondFields | null;
95
+ declare function bondId(message: string): string;
96
+ declare function bondNullifier(key: Uint8Array, audience: string, outpoint: string): string;
97
+ declare function bondFloors(sats: number, ageBlocks: number): {
98
+ sats_floor: number;
99
+ days_floor: number;
100
+ };
101
+ declare function verifyBondSignature(opts: {
102
+ message: string;
103
+ signature: string;
104
+ subject?: string;
105
+ now?: Date;
106
+ skewSeconds?: number;
107
+ maxWindowSeconds?: number;
108
+ }): {
109
+ ok: true;
110
+ fields: BondFields;
111
+ bond_id: string;
112
+ } | {
113
+ ok: false;
114
+ reason: BondReason;
115
+ };
116
+ declare function checkBondCoin(opts: {
117
+ address: string;
118
+ outpoint: string;
119
+ source: BondChainSource;
120
+ minConfirmations?: number;
121
+ }): Promise<{
122
+ ok: true;
123
+ sats: number;
124
+ confirmed_height: number;
125
+ tip: ChainTip;
126
+ } | {
127
+ ok: false;
128
+ reason: BondReason;
129
+ }>;
130
+ declare function verifyBond(opts: {
131
+ message: string;
132
+ signature: string;
133
+ source: BondChainSource;
134
+ subject?: string;
135
+ now?: Date;
136
+ maxWindowSeconds?: number;
137
+ minConfirmations?: number;
138
+ }): Promise<BondResult>;
139
+ declare function esploraSource(base?: string, opts?: {
140
+ timeoutMs?: number;
141
+ fetch?: typeof fetch;
142
+ }): BondChainSource;
143
+
39
144
  declare function isNip07Available(): boolean;
40
145
  declare function getNostrPublicKey(): Promise<string | null>;
41
146
  declare function signNostrEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent | null>;
@@ -53,4 +158,4 @@ interface StatusMeta {
53
158
  }
54
159
  declare const STATUS_META: Record<StatusCode, StatusMeta>;
55
160
 
56
- export { type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, VerifyOptions, check, getNip07Info, getNostrPublicKey, isNip07Available, nostrIdentifierForms, nostrPubkeyToHex, signNostrEvent };
161
+ export { BOND_DAYS_FLOORS, BOND_MAX_WINDOW_SECONDS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, type BondChainSource, type BondCoin, type BondFields, type BondReason, type BondResult, type ChainTip, type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, type VerifiedBond, VerifyOptions, bondFloors, bondId, bondNullifier, buildBondMessage, check, checkBondCoin, esploraSource, getNip07Info, getNostrPublicKey, isNip07Available, issueBondMessage, nostrIdentifierForms, nostrPubkeyToHex, parseBondMessage, signNostrEvent, verifyBond, verifyBondSignature };
package/dist/index.js CHANGED
@@ -3,8 +3,10 @@
3
3
  var base = require('@scure/base');
4
4
  var buffer = require('buffer');
5
5
  var bip322 = require('@orangecheck/bip322');
6
- var secp256k1_js = require('@noble/curves/secp256k1.js');
6
+ var hmac_js = require('@noble/hashes/hmac.js');
7
7
  var sha2_js = require('@noble/hashes/sha2.js');
8
+ var utils_js = require('@noble/hashes/utils.js');
9
+ var secp256k1_js = require('@noble/curves/secp256k1.js');
8
10
 
9
11
  // src/canonical.ts
10
12
  function toLowerHex(bytes) {
@@ -1026,7 +1028,7 @@ async function getAddressUtxos(address, network, opts) {
1026
1028
  log4.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
1027
1029
  throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
1028
1030
  }
1029
- function computeMetrics(utxos, bondAmount) {
1031
+ function computeMetrics(utxos, bondAmount, coverSats) {
1030
1032
  const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
1031
1033
  if (confirmedUtxos.length === 0) {
1032
1034
  return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
@@ -1059,6 +1061,23 @@ function computeMetrics(utxos, bondAmount) {
1059
1061
  };
1060
1062
  }
1061
1063
  const sats_bonded = confirmed_balance;
1064
+ if (coverSats !== void 0 && coverSats > 0) {
1065
+ const sorted = [...confirmedUtxos].sort(
1066
+ (a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)
1067
+ );
1068
+ let sum = 0;
1069
+ let youngestTime = 0;
1070
+ for (const utxo of sorted) {
1071
+ sum += utxo.value;
1072
+ youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
1073
+ if (sum >= coverSats) break;
1074
+ }
1075
+ const days_unspent2 = Math.max(
1076
+ 0,
1077
+ Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
1078
+ );
1079
+ return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
1080
+ }
1062
1081
  const oldestTime = Math.min(
1063
1082
  ...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
1064
1083
  );
@@ -1122,7 +1141,8 @@ async function verify(input, opts = {}) {
1122
1141
  });
1123
1142
  const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
1124
1143
  utxos,
1125
- parsed.bond
1144
+ parsed.bond,
1145
+ opts.coverSats
1126
1146
  );
1127
1147
  if (parsed.bond !== void 0 && parsed.bond > 0) {
1128
1148
  if (confirmed_balance < parsed.bond) {
@@ -1227,7 +1247,7 @@ async function check(params) {
1227
1247
  sig: env.signature,
1228
1248
  scheme: env.scheme
1229
1249
  },
1230
- verifyOptions
1250
+ { ...verifyOptions, coverSats: minSats > 0 ? minSats : void 0 }
1231
1251
  );
1232
1252
  const sats = outcome.metrics?.sats_bonded ?? 0;
1233
1253
  const days = outcome.metrics?.days_unspent ?? 0;
@@ -1431,6 +1451,181 @@ async function verifyChallenge(opts) {
1431
1451
  ...purpose !== void 0 ? { purpose } : {}
1432
1452
  };
1433
1453
  }
1454
+ var HEADER = "orangecheck-bond";
1455
+ var ACK = "I back this subject with this coin. It stays in my wallet.";
1456
+ var FIELDS = ["address", "outpoint", "subject", "nonce", "issued_at", "expires_at"];
1457
+ var BOND_SATS_FLOORS = [1e3, 1e4, 1e5, 1e6, 1e7];
1458
+ var BOND_DAYS_FLOORS = [7, 30, 90, 365];
1459
+ var BOND_MIN_CONFIRMATIONS = 6;
1460
+ var BOND_MAX_WINDOW_SECONDS = 3600;
1461
+ var OUTPOINT_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]{0,9})$/;
1462
+ var NONCE_RE = /^[0-9a-f]{32}$/;
1463
+ var TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
1464
+ var SUBJECT_RE = /^[\x21-\x7e]{1,256}$/;
1465
+ function iso2(d) {
1466
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
1467
+ }
1468
+ function instant(s) {
1469
+ const t = Date.parse(s);
1470
+ return TIME_RE.test(s) && Number.isFinite(t) && iso2(new Date(t)) === s;
1471
+ }
1472
+ function randomNonce2() {
1473
+ const b = new Uint8Array(16);
1474
+ globalThis.crypto.getRandomValues(b);
1475
+ return utils_js.bytesToHex(b);
1476
+ }
1477
+ function wellFormed(f) {
1478
+ return bip322.decodeAddress(f.address) !== null && OUTPOINT_RE.test(f.outpoint) && SUBJECT_RE.test(f.subject) && NONCE_RE.test(f.nonce) && instant(f.issued_at) && instant(f.expires_at) && Date.parse(f.expires_at) > Date.parse(f.issued_at);
1479
+ }
1480
+ function buildBondMessage(fields) {
1481
+ if (!wellFormed(fields)) throw new Error("bond fields are malformed");
1482
+ return [HEADER, ...FIELDS.map((k) => `${k}: ${fields[k]}`), `ack: ${ACK}`].join("\n") + "\n";
1483
+ }
1484
+ function issueBondMessage(opts) {
1485
+ const now = opts.now ?? /* @__PURE__ */ new Date();
1486
+ const expires = new Date(now.getTime() + (opts.ttlSeconds ?? 600) * 1e3);
1487
+ const nonce = randomNonce2();
1488
+ const message = buildBondMessage({
1489
+ address: opts.address,
1490
+ outpoint: opts.outpoint,
1491
+ subject: opts.subject,
1492
+ nonce,
1493
+ issued_at: iso2(now),
1494
+ expires_at: iso2(expires)
1495
+ });
1496
+ return { message, nonce, expiresAt: expires.getTime() };
1497
+ }
1498
+ function parseBondMessage(message) {
1499
+ if (!message.endsWith("\n") || message.includes("\r")) return null;
1500
+ const lines = message.slice(0, -1).split("\n");
1501
+ if (lines.length !== FIELDS.length + 2 || lines[0] !== HEADER) return null;
1502
+ if (lines[lines.length - 1] !== `ack: ${ACK}`) return null;
1503
+ const out = {};
1504
+ for (const [i, key] of FIELDS.entries()) {
1505
+ const line = lines[i + 1];
1506
+ const prefix = `${key}: `;
1507
+ if (!line.startsWith(prefix)) return null;
1508
+ out[key] = line.slice(prefix.length);
1509
+ }
1510
+ const fields = out;
1511
+ return wellFormed(fields) && buildBondMessage(fields) === message ? fields : null;
1512
+ }
1513
+ function bondId(message) {
1514
+ return utils_js.bytesToHex(sha2_js.sha256(utils_js.utf8ToBytes(message)));
1515
+ }
1516
+ function bondNullifier(key, audience, outpoint) {
1517
+ if (key.length < 32) throw new Error("bond nullifier key must be at least 32 bytes");
1518
+ const message = utils_js.utf8ToBytes(`orangecheck-bond-nullifier
1519
+ ${audience}
1520
+ ${outpoint}`);
1521
+ return utils_js.bytesToHex(hmac_js.hmac(sha2_js.sha256, key, message));
1522
+ }
1523
+ function bondFloors(sats, ageBlocks) {
1524
+ const days = Math.floor(ageBlocks / 144);
1525
+ const floor = (ladder, v) => ladder.filter((f) => v >= f).reduce((a, b) => Math.max(a, b), 0);
1526
+ return { sats_floor: floor(BOND_SATS_FLOORS, sats), days_floor: floor(BOND_DAYS_FLOORS, days) };
1527
+ }
1528
+ function verifyBondSignature(opts) {
1529
+ const fields = parseBondMessage(opts.message);
1530
+ if (!fields) return { ok: false, reason: "bond_malformed" };
1531
+ if (opts.subject !== void 0 && fields.subject !== opts.subject) {
1532
+ return { ok: false, reason: "bond_subject_mismatch" };
1533
+ }
1534
+ const window2 = Date.parse(fields.expires_at) - Date.parse(fields.issued_at);
1535
+ if (window2 > (opts.maxWindowSeconds ?? BOND_MAX_WINDOW_SECONDS) * 1e3) {
1536
+ return { ok: false, reason: "bond_window_too_long" };
1537
+ }
1538
+ const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
1539
+ const skew = (opts.skewSeconds ?? 60) * 1e3;
1540
+ if (Date.parse(fields.issued_at) - skew > now) return { ok: false, reason: "bond_not_yet_valid" };
1541
+ if (Date.parse(fields.expires_at) + skew < now) return { ok: false, reason: "bond_expired" };
1542
+ if (!bip322.verifySignature(fields.address, opts.message, opts.signature)) {
1543
+ return { ok: false, reason: "sig_invalid" };
1544
+ }
1545
+ return { ok: true, fields, bond_id: bondId(opts.message) };
1546
+ }
1547
+ async function checkBondCoin(opts) {
1548
+ const decoded = bip322.decodeAddress(opts.address);
1549
+ if (!decoded || !OUTPOINT_RE.test(opts.outpoint)) return { ok: false, reason: "bond_malformed" };
1550
+ let coin;
1551
+ let tip;
1552
+ try {
1553
+ [coin, tip] = await Promise.all([opts.source.coin(opts.outpoint), opts.source.tip()]);
1554
+ } catch {
1555
+ return { ok: false, reason: "chain_unavailable" };
1556
+ }
1557
+ if (!coin) return { ok: false, reason: "coin_not_found" };
1558
+ if (coin.scriptPubKey !== utils_js.bytesToHex(decoded.scriptPubKey)) {
1559
+ return { ok: false, reason: "coin_address_mismatch" };
1560
+ }
1561
+ if (coin.confirmedHeight === null) return { ok: false, reason: "coin_unconfirmed" };
1562
+ if (coin.spent) return { ok: false, reason: "coin_spent" };
1563
+ const confirmations = tip.height - coin.confirmedHeight + 1;
1564
+ if (confirmations < (opts.minConfirmations ?? BOND_MIN_CONFIRMATIONS)) {
1565
+ return { ok: false, reason: "coin_too_recent" };
1566
+ }
1567
+ return { ok: true, sats: coin.sats, confirmed_height: coin.confirmedHeight, tip };
1568
+ }
1569
+ async function verifyBond(opts) {
1570
+ const sig = verifyBondSignature(opts);
1571
+ if (!sig.ok) return sig;
1572
+ const coin = await checkBondCoin({
1573
+ address: sig.fields.address,
1574
+ outpoint: sig.fields.outpoint,
1575
+ source: opts.source,
1576
+ minConfirmations: opts.minConfirmations
1577
+ });
1578
+ if (!coin.ok) return coin;
1579
+ return {
1580
+ ok: true,
1581
+ bond: {
1582
+ bond_id: sig.bond_id,
1583
+ subject: sig.fields.subject,
1584
+ address: sig.fields.address,
1585
+ outpoint: sig.fields.outpoint,
1586
+ sats: coin.sats,
1587
+ confirmed_height: coin.confirmed_height,
1588
+ tip: coin.tip
1589
+ }
1590
+ };
1591
+ }
1592
+ function esploraSource(base = "https://mempool.space/api", opts = {}) {
1593
+ const get = async (path, missingOk = false) => {
1594
+ const r = await (opts.fetch ?? fetch)(`${base}${path}`, {
1595
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
1596
+ });
1597
+ if (!r.ok && !(missingOk && r.status === 404)) {
1598
+ throw new Error(`esplora ${path}: HTTP ${r.status}`);
1599
+ }
1600
+ return r;
1601
+ };
1602
+ return {
1603
+ async tip() {
1604
+ const [h, hash] = await Promise.all([get("/blocks/tip/height"), get("/blocks/tip/hash")]);
1605
+ const tip = { height: Number(await h.text()), hash: (await hash.text()).trim() };
1606
+ if (!Number.isSafeInteger(tip.height) || !/^[0-9a-f]{64}$/.test(tip.hash)) {
1607
+ throw new Error("esplora tip: malformed");
1608
+ }
1609
+ return tip;
1610
+ },
1611
+ async coin(outpoint) {
1612
+ const [txid, voutStr] = outpoint.split(":");
1613
+ const vout = Number(voutStr);
1614
+ const txr = await get(`/tx/${txid}`, true);
1615
+ if (txr.status === 404) return null;
1616
+ const tx = await txr.json();
1617
+ const out = tx.vout[vout];
1618
+ if (!out) return null;
1619
+ const spend = await (await get(`/tx/${txid}/outspend/${vout}`)).json();
1620
+ return {
1621
+ sats: out.value,
1622
+ scriptPubKey: out.scriptpubkey.toLowerCase(),
1623
+ confirmedHeight: tx.status.confirmed ? tx.status.block_height ?? null : null,
1624
+ spent: spend.spent
1625
+ };
1626
+ }
1627
+ };
1628
+ }
1434
1629
 
1435
1630
  // src/utils/rate-limiter.ts
1436
1631
  var RateLimitError = class extends Error {
@@ -2220,17 +2415,27 @@ var STATUS_META = {
2220
2415
  }
2221
2416
  };
2222
2417
 
2418
+ exports.BOND_DAYS_FLOORS = BOND_DAYS_FLOORS;
2419
+ exports.BOND_MAX_WINDOW_SECONDS = BOND_MAX_WINDOW_SECONDS;
2420
+ exports.BOND_MIN_CONFIRMATIONS = BOND_MIN_CONFIRMATIONS;
2421
+ exports.BOND_SATS_FLOORS = BOND_SATS_FLOORS;
2223
2422
  exports.DEFAULT_RELAYS = DEFAULT_RELAYS;
2224
2423
  exports.FANOUT_DEADLINE_MS = FANOUT_DEADLINE_MS;
2225
2424
  exports.STATUS_META = STATUS_META;
2425
+ exports.bondFloors = bondFloors;
2426
+ exports.bondId = bondId;
2427
+ exports.bondNullifier = bondNullifier;
2428
+ exports.buildBondMessage = buildBondMessage;
2226
2429
  exports.buildCanonicalMessage = buildCanonicalMessage;
2227
2430
  exports.check = check;
2431
+ exports.checkBondCoin = checkBondCoin;
2228
2432
  exports.computeAllScores = computeAllScores;
2229
2433
  exports.computeScore = computeScore;
2230
2434
  exports.createAttestation = createAttestation;
2231
2435
  exports.createAttestationEnvelope = createAttestationEnvelope;
2232
2436
  exports.createAttestationEvent = createAttestationEvent;
2233
2437
  exports.discoverAttestations = discoverAttestations;
2438
+ exports.esploraSource = esploraSource;
2234
2439
  exports.extractAttestationIdFromUrl = extractAttestationIdFromUrl;
2235
2440
  exports.formatIdentities = formatIdentities;
2236
2441
  exports.formatIdentitiesForDisplay = formatIdentitiesForDisplay;
@@ -2260,10 +2465,12 @@ exports.isNip07Available = isNip07Available;
2260
2465
  exports.isNostrIdentityVerified = isNostrIdentityVerified;
2261
2466
  exports.isValidDomain = isValidDomain;
2262
2467
  exports.isValidTweetUrl = isValidTweetUrl;
2468
+ exports.issueBondMessage = issueBondMessage;
2263
2469
  exports.issueChallenge = issueChallenge;
2264
2470
  exports.nostrIdentifierForms = nostrIdentifierForms;
2265
2471
  exports.nostrPubkeyToHex = nostrPubkeyToHex;
2266
2472
  exports.parseAttestationFromEvent = parseAttestationFromEvent;
2473
+ exports.parseBondMessage = parseBondMessage;
2267
2474
  exports.parseIdentities = parseIdentities;
2268
2475
  exports.publishAttestation = publishAttestation;
2269
2476
  exports.publishToRelays = publishToRelays;
@@ -2277,6 +2484,8 @@ exports.supportsAutomaticVerification = supportsAutomaticVerification;
2277
2484
  exports.verify = verify;
2278
2485
  exports.verifyAllIdentities = verifyAllIdentities;
2279
2486
  exports.verifyAttestationById = verifyAttestationById;
2487
+ exports.verifyBond = verifyBond;
2488
+ exports.verifyBondSignature = verifyBondSignature;
2280
2489
  exports.verifyChallenge = verifyChallenge;
2281
2490
  exports.verifyDnsIdentity = verifyDnsIdentity;
2282
2491
  exports.verifyGitHubIdentity = verifyGitHubIdentity;