@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 +9 -4
- package/dist/index.d.mts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +28 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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) &&
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|