@orangecheck/sdk 1.12.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
@@ -148,9 +148,12 @@ Now the gate doesn't have to trust the client — the address it sees has been c
148
148
  ## Bonds: one coin backing one account
149
149
 
150
150
  An attestation publishes an address. A bond keeps it private. The holder signs
151
- once to pledge one unspent coin to one subject, and whoever relies on the bond
151
+ once to back one subject with one unspent coin, and whoever relies on the bond
152
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. Spec:
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:
154
157
  [`BOND.md`](https://github.com/orangecheck/oc-attest-protocol/blob/main/BOND.md).
155
158
 
156
159
  ```ts
@@ -164,7 +167,9 @@ const { message } = issueBondMessage({ address, outpoint: `${txid}:${vout}`, sub
164
167
  const r = await verifyBond({ message, signature, subject: didOc, source: esploraSource() });
165
168
  if (r.ok) {
166
169
  const age = r.bond.tip.height - r.bond.confirmed_height + 1;
167
- const claim = { ...bondFloors(r.bond.sats, age), nullifier: bondNullifier(audience, r.bond.outpoint) };
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 };
168
173
  }
169
174
  ```
170
175
 
@@ -281,7 +286,7 @@ verifyChallenge(options): Promise<VerifyChallengeResult>
281
286
  issueBondMessage(options): { message, nonce, expiresAt }
282
287
  verifyBond(options): Promise<BondResult>
283
288
  checkBondCoin(options): Promise<...>
284
- bondNullifier(audience, outpoint): string
289
+ bondNullifier(key, audience, outpoint): string
285
290
  bondFloors(sats, ageBlocks): { sats_floor, days_floor }
286
291
 
287
292
  // Building blocks.
package/dist/index.d.mts CHANGED
@@ -39,6 +39,7 @@ declare function nostrIdentifierForms(identifier: string): string[];
39
39
  declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
40
40
  declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
41
41
  declare const BOND_MIN_CONFIRMATIONS = 6;
42
+ declare const BOND_MAX_WINDOW_SECONDS = 3600;
42
43
  interface BondFields {
43
44
  address: string;
44
45
  outpoint: string;
@@ -47,7 +48,7 @@ interface BondFields {
47
48
  issued_at: string;
48
49
  expires_at: string;
49
50
  }
50
- type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
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';
51
52
  interface BondCoin {
52
53
  sats: number;
53
54
  scriptPubKey: string;
@@ -92,7 +93,7 @@ declare function issueBondMessage(opts: {
92
93
  };
93
94
  declare function parseBondMessage(message: string): BondFields | null;
94
95
  declare function bondId(message: string): string;
95
- declare function bondNullifier(audience: string, outpoint: string): string;
96
+ declare function bondNullifier(key: Uint8Array, audience: string, outpoint: string): string;
96
97
  declare function bondFloors(sats: number, ageBlocks: number): {
97
98
  sats_floor: number;
98
99
  days_floor: number;
@@ -103,6 +104,7 @@ declare function verifyBondSignature(opts: {
103
104
  subject?: string;
104
105
  now?: Date;
105
106
  skewSeconds?: number;
107
+ maxWindowSeconds?: number;
106
108
  }): {
107
109
  ok: true;
108
110
  fields: BondFields;
@@ -131,6 +133,7 @@ declare function verifyBond(opts: {
131
133
  source: BondChainSource;
132
134
  subject?: string;
133
135
  now?: Date;
136
+ maxWindowSeconds?: number;
134
137
  minConfirmations?: number;
135
138
  }): Promise<BondResult>;
136
139
  declare function esploraSource(base?: string, opts?: {
@@ -155,4 +158,4 @@ interface StatusMeta {
155
158
  }
156
159
  declare const STATUS_META: Record<StatusCode, StatusMeta>;
157
160
 
158
- export { BOND_DAYS_FLOORS, 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 };
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
@@ -39,6 +39,7 @@ declare function nostrIdentifierForms(identifier: string): string[];
39
39
  declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
40
40
  declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
41
41
  declare const BOND_MIN_CONFIRMATIONS = 6;
42
+ declare const BOND_MAX_WINDOW_SECONDS = 3600;
42
43
  interface BondFields {
43
44
  address: string;
44
45
  outpoint: string;
@@ -47,7 +48,7 @@ interface BondFields {
47
48
  issued_at: string;
48
49
  expires_at: string;
49
50
  }
50
- type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
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';
51
52
  interface BondCoin {
52
53
  sats: number;
53
54
  scriptPubKey: string;
@@ -92,7 +93,7 @@ declare function issueBondMessage(opts: {
92
93
  };
93
94
  declare function parseBondMessage(message: string): BondFields | null;
94
95
  declare function bondId(message: string): string;
95
- declare function bondNullifier(audience: string, outpoint: string): string;
96
+ declare function bondNullifier(key: Uint8Array, audience: string, outpoint: string): string;
96
97
  declare function bondFloors(sats: number, ageBlocks: number): {
97
98
  sats_floor: number;
98
99
  days_floor: number;
@@ -103,6 +104,7 @@ declare function verifyBondSignature(opts: {
103
104
  subject?: string;
104
105
  now?: Date;
105
106
  skewSeconds?: number;
107
+ maxWindowSeconds?: number;
106
108
  }): {
107
109
  ok: true;
108
110
  fields: BondFields;
@@ -131,6 +133,7 @@ declare function verifyBond(opts: {
131
133
  source: BondChainSource;
132
134
  subject?: string;
133
135
  now?: Date;
136
+ maxWindowSeconds?: number;
134
137
  minConfirmations?: number;
135
138
  }): Promise<BondResult>;
136
139
  declare function esploraSource(base?: string, opts?: {
@@ -155,4 +158,4 @@ interface StatusMeta {
155
158
  }
156
159
  declare const STATUS_META: Record<StatusCode, StatusMeta>;
157
160
 
158
- export { BOND_DAYS_FLOORS, 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 };
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,6 +3,7 @@
3
3
  var base = require('@scure/base');
4
4
  var buffer = require('buffer');
5
5
  var bip322 = require('@orangecheck/bip322');
6
+ var hmac_js = require('@noble/hashes/hmac.js');
6
7
  var sha2_js = require('@noble/hashes/sha2.js');
7
8
  var utils_js = require('@noble/hashes/utils.js');
8
9
  var secp256k1_js = require('@noble/curves/secp256k1.js');
@@ -1451,11 +1452,12 @@ async function verifyChallenge(opts) {
1451
1452
  };
1452
1453
  }
1453
1454
  var HEADER = "orangecheck-bond";
1454
- var ACK = "I pledge this coin to back this subject. It stays in my wallet.";
1455
+ var ACK = "I back this subject with this coin. It stays in my wallet.";
1455
1456
  var FIELDS = ["address", "outpoint", "subject", "nonce", "issued_at", "expires_at"];
1456
1457
  var BOND_SATS_FLOORS = [1e3, 1e4, 1e5, 1e6, 1e7];
1457
1458
  var BOND_DAYS_FLOORS = [7, 30, 90, 365];
1458
1459
  var BOND_MIN_CONFIRMATIONS = 6;
1460
+ var BOND_MAX_WINDOW_SECONDS = 3600;
1459
1461
  var OUTPOINT_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]{0,9})$/;
1460
1462
  var NONCE_RE = /^[0-9a-f]{32}$/;
1461
1463
  var TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
@@ -1463,13 +1465,17 @@ var SUBJECT_RE = /^[\x21-\x7e]{1,256}$/;
1463
1465
  function iso2(d) {
1464
1466
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");
1465
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
+ }
1466
1472
  function randomNonce2() {
1467
1473
  const b = new Uint8Array(16);
1468
1474
  globalThis.crypto.getRandomValues(b);
1469
1475
  return utils_js.bytesToHex(b);
1470
1476
  }
1471
1477
  function wellFormed(f) {
1472
- return bip322.decodeAddress(f.address) !== null && OUTPOINT_RE.test(f.outpoint) && SUBJECT_RE.test(f.subject) && NONCE_RE.test(f.nonce) && TIME_RE.test(f.issued_at) && TIME_RE.test(f.expires_at) && Date.parse(f.expires_at) > Date.parse(f.issued_at);
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);
1473
1479
  }
1474
1480
  function buildBondMessage(fields) {
1475
1481
  if (!wellFormed(fields)) throw new Error("bond fields are malformed");
@@ -1507,10 +1513,12 @@ function parseBondMessage(message) {
1507
1513
  function bondId(message) {
1508
1514
  return utils_js.bytesToHex(sha2_js.sha256(utils_js.utf8ToBytes(message)));
1509
1515
  }
1510
- function bondNullifier(audience, outpoint) {
1511
- return utils_js.bytesToHex(sha2_js.sha256(utils_js.utf8ToBytes(`orangecheck-bond-nullifier
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
1512
1519
  ${audience}
1513
- ${outpoint}`)));
1520
+ ${outpoint}`);
1521
+ return utils_js.bytesToHex(hmac_js.hmac(sha2_js.sha256, key, message));
1514
1522
  }
1515
1523
  function bondFloors(sats, ageBlocks) {
1516
1524
  const days = Math.floor(ageBlocks / 144);
@@ -1523,6 +1531,10 @@ function verifyBondSignature(opts) {
1523
1531
  if (opts.subject !== void 0 && fields.subject !== opts.subject) {
1524
1532
  return { ok: false, reason: "bond_subject_mismatch" };
1525
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
+ }
1526
1538
  const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
1527
1539
  const skew = (opts.skewSeconds ?? 60) * 1e3;
1528
1540
  if (Date.parse(fields.issued_at) - skew > now) return { ok: false, reason: "bond_not_yet_valid" };
@@ -1578,22 +1590,28 @@ async function verifyBond(opts) {
1578
1590
  };
1579
1591
  }
1580
1592
  function esploraSource(base = "https://mempool.space/api", opts = {}) {
1581
- const get = async (path) => {
1593
+ const get = async (path, missingOk = false) => {
1582
1594
  const r = await (opts.fetch ?? fetch)(`${base}${path}`, {
1583
1595
  signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
1584
1596
  });
1585
- if (!r.ok && r.status !== 404) throw new Error(`esplora ${path}: HTTP ${r.status}`);
1597
+ if (!r.ok && !(missingOk && r.status === 404)) {
1598
+ throw new Error(`esplora ${path}: HTTP ${r.status}`);
1599
+ }
1586
1600
  return r;
1587
1601
  };
1588
1602
  return {
1589
1603
  async tip() {
1590
1604
  const [h, hash] = await Promise.all([get("/blocks/tip/height"), get("/blocks/tip/hash")]);
1591
- return { height: Number(await h.text()), hash: (await hash.text()).trim() };
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;
1592
1610
  },
1593
1611
  async coin(outpoint) {
1594
1612
  const [txid, voutStr] = outpoint.split(":");
1595
1613
  const vout = Number(voutStr);
1596
- const txr = await get(`/tx/${txid}`);
1614
+ const txr = await get(`/tx/${txid}`, true);
1597
1615
  if (txr.status === 404) return null;
1598
1616
  const tx = await txr.json();
1599
1617
  const out = tx.vout[vout];
@@ -2398,6 +2416,7 @@ var STATUS_META = {
2398
2416
  };
2399
2417
 
2400
2418
  exports.BOND_DAYS_FLOORS = BOND_DAYS_FLOORS;
2419
+ exports.BOND_MAX_WINDOW_SECONDS = BOND_MAX_WINDOW_SECONDS;
2401
2420
  exports.BOND_MIN_CONFIRMATIONS = BOND_MIN_CONFIRMATIONS;
2402
2421
  exports.BOND_SATS_FLOORS = BOND_SATS_FLOORS;
2403
2422
  exports.DEFAULT_RELAYS = DEFAULT_RELAYS;