bsv-x402 0.3.0 → 0.5.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/index.cjs +214 -340
- package/dist/index.d.cts +11 -20
- package/dist/index.d.ts +11 -20
- package/dist/index.js +214 -340
- package/package.json +7 -3
package/dist/index.cjs
CHANGED
|
@@ -53,12 +53,15 @@ function parseBrc105Challenge(response) {
|
|
|
53
53
|
if (!Number.isFinite(satoshisRequired) || !Number.isInteger(satoshisRequired) || satoshisRequired <= 0) {
|
|
54
54
|
throw new Error("BRC-105: satoshis-required must be a positive integer");
|
|
55
55
|
}
|
|
56
|
-
const
|
|
56
|
+
const authIdentityKey = response.headers.get("x-bsv-auth-identity-key") || null;
|
|
57
|
+
const paymentIdentityKey = response.headers.get("x-bsv-payment-identity-key") || null;
|
|
58
|
+
const authenticated = authIdentityKey !== null && authIdentityKey.length > 0;
|
|
59
|
+
const serverIdentityKey = authIdentityKey || paymentIdentityKey;
|
|
57
60
|
if (serverIdentityKey === null || serverIdentityKey.length === 0) {
|
|
58
|
-
throw new Error("BRC-105: missing
|
|
61
|
+
throw new Error("BRC-105: missing identity key (expected x-bsv-auth-identity-key or x-bsv-payment-identity-key)");
|
|
59
62
|
}
|
|
60
63
|
if (!/^0[23][0-9a-fA-F]{64}$/.test(serverIdentityKey)) {
|
|
61
|
-
throw new Error("BRC-105:
|
|
64
|
+
throw new Error("BRC-105: identity key must be a 33-byte compressed public key (hex)");
|
|
62
65
|
}
|
|
63
66
|
const derivationPrefix = response.headers.get("x-bsv-payment-derivation-prefix");
|
|
64
67
|
if (derivationPrefix === null || derivationPrefix.length === 0) {
|
|
@@ -68,7 +71,8 @@ function parseBrc105Challenge(response) {
|
|
|
68
71
|
version,
|
|
69
72
|
satoshisRequired,
|
|
70
73
|
serverIdentityKey,
|
|
71
|
-
derivationPrefix
|
|
74
|
+
derivationPrefix,
|
|
75
|
+
authenticated
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -516,7 +520,8 @@ async function createDerivationSuffix(wallet) {
|
|
|
516
520
|
const nonceBytes = [...firstHalf, ...hmac];
|
|
517
521
|
return numberArrayToBase64(nonceBytes);
|
|
518
522
|
}
|
|
519
|
-
async function constructBrc105Proof(challenge, wallet) {
|
|
523
|
+
async function constructBrc105Proof(challenge, wallet, origin) {
|
|
524
|
+
const { publicKey: clientIdentityKey } = await wallet.getPublicKey({ identityKey: true });
|
|
520
525
|
const derivationSuffix = await createDerivationSuffix(wallet);
|
|
521
526
|
const keyID = `${challenge.derivationPrefix} ${derivationSuffix}`;
|
|
522
527
|
const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
|
|
@@ -525,12 +530,13 @@ async function constructBrc105Proof(challenge, wallet) {
|
|
|
525
530
|
counterparty: challenge.serverIdentityKey
|
|
526
531
|
});
|
|
527
532
|
const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
|
|
533
|
+
const description = origin ? `Payment for request to ${origin}` : "BRC-105 payment";
|
|
528
534
|
const result = await wallet.createAction({
|
|
529
|
-
description
|
|
535
|
+
description,
|
|
530
536
|
outputs: [{
|
|
531
537
|
satoshis: challenge.satoshisRequired,
|
|
532
538
|
lockingScript,
|
|
533
|
-
|
|
539
|
+
outputDescription: "HTTP request payment",
|
|
534
540
|
customInstructions: JSON.stringify({
|
|
535
541
|
derivationPrefix: challenge.derivationPrefix,
|
|
536
542
|
derivationSuffix,
|
|
@@ -553,6 +559,7 @@ async function constructBrc105Proof(challenge, wallet) {
|
|
|
553
559
|
derivationPrefix: challenge.derivationPrefix,
|
|
554
560
|
derivationSuffix,
|
|
555
561
|
transaction: transactionBase64,
|
|
562
|
+
clientIdentityKey,
|
|
556
563
|
txid: result.txid
|
|
557
564
|
};
|
|
558
565
|
}
|
|
@@ -589,6 +596,171 @@ function parseChallenge(header) {
|
|
|
589
596
|
};
|
|
590
597
|
}
|
|
591
598
|
|
|
599
|
+
// src/x402-fetch.ts
|
|
600
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
601
|
+
function base58DecodeCheck(address) {
|
|
602
|
+
let leadingZeros = 0;
|
|
603
|
+
for (const c of address) {
|
|
604
|
+
if (c === "1") leadingZeros++;
|
|
605
|
+
else break;
|
|
606
|
+
}
|
|
607
|
+
let n = BigInt(0);
|
|
608
|
+
for (const c of address) {
|
|
609
|
+
const i = BASE58_ALPHABET.indexOf(c);
|
|
610
|
+
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
611
|
+
n = n * 58n + BigInt(i);
|
|
612
|
+
}
|
|
613
|
+
const hexFromBigint = n === 0n ? "" : n.toString(16);
|
|
614
|
+
const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
|
|
615
|
+
const bigintBytes = [];
|
|
616
|
+
for (let i = 0; i < paddedHex.length; i += 2) {
|
|
617
|
+
bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
|
|
618
|
+
}
|
|
619
|
+
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
620
|
+
allBytes.set(bigintBytes, leadingZeros);
|
|
621
|
+
if (allBytes.length !== 25) {
|
|
622
|
+
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
623
|
+
}
|
|
624
|
+
const body = allBytes.slice(0, 21);
|
|
625
|
+
const checksum = allBytes.slice(21);
|
|
626
|
+
const version = allBytes[0];
|
|
627
|
+
if (version !== 0 && version !== 111) {
|
|
628
|
+
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
629
|
+
}
|
|
630
|
+
return { version, payload: body.slice(1) };
|
|
631
|
+
}
|
|
632
|
+
function payeeAddressToLockingScript(address) {
|
|
633
|
+
const { payload } = base58DecodeCheck(address);
|
|
634
|
+
if (payload.length !== 20) {
|
|
635
|
+
throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
|
|
636
|
+
}
|
|
637
|
+
const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
638
|
+
return `76a914${pubkeyHash}88ac`;
|
|
639
|
+
}
|
|
640
|
+
async function defaultConstructProof(challenge) {
|
|
641
|
+
const cwi = globalThis.CWI;
|
|
642
|
+
if (!cwi || typeof cwi.createAction !== "function") {
|
|
643
|
+
throw new Error(
|
|
644
|
+
"No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const result = await cwi.createAction({
|
|
648
|
+
description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
|
|
649
|
+
outputs: [{
|
|
650
|
+
satoshis: challenge.amount,
|
|
651
|
+
lockingScript: payeeAddressToLockingScript(challenge.payee),
|
|
652
|
+
description: `Payment to ${challenge.payee}`
|
|
653
|
+
}],
|
|
654
|
+
labels: ["x402-payment"],
|
|
655
|
+
options: {
|
|
656
|
+
returnTXIDOnly: false,
|
|
657
|
+
noSend: false
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
if (!result || !result.txid) {
|
|
661
|
+
throw new Error("Wallet declined payment or returned invalid result");
|
|
662
|
+
}
|
|
663
|
+
if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
|
|
664
|
+
throw new Error("Wallet did not return raw transaction");
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
txid: result.txid,
|
|
668
|
+
rawTx: result.rawTx
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function createX402Fetch(config = {}) {
|
|
672
|
+
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
673
|
+
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
674
|
+
const brc105Wallet = config.brc105Wallet;
|
|
675
|
+
return async function x402Fetch2(input, init) {
|
|
676
|
+
const response = await fetch(input, init);
|
|
677
|
+
if (response.status !== 402) return response;
|
|
678
|
+
const origin = extractOrigin(input);
|
|
679
|
+
const challengeHeader = response.headers.get("X402-Challenge");
|
|
680
|
+
if (challengeHeader) {
|
|
681
|
+
let challenge;
|
|
682
|
+
try {
|
|
683
|
+
challenge = parseChallenge(challengeHeader);
|
|
684
|
+
} catch {
|
|
685
|
+
return response;
|
|
686
|
+
}
|
|
687
|
+
let proof;
|
|
688
|
+
try {
|
|
689
|
+
proof = await constructProof(challenge);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
console.error("[x402] Proof construction failed (x402):", err);
|
|
692
|
+
config.onProofError?.(err, "x402");
|
|
693
|
+
return response;
|
|
694
|
+
}
|
|
695
|
+
const headers = new Headers(init?.headers);
|
|
696
|
+
headers.set("X402-Proof", JSON.stringify(proof));
|
|
697
|
+
return fetch(input, { ...init, headers });
|
|
698
|
+
}
|
|
699
|
+
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
700
|
+
if (brc105Version) {
|
|
701
|
+
if (!brc105ProofConstructor && !brc105Wallet) return response;
|
|
702
|
+
let brc105Challenge;
|
|
703
|
+
try {
|
|
704
|
+
brc105Challenge = parseBrc105Challenge(response);
|
|
705
|
+
} catch {
|
|
706
|
+
return response;
|
|
707
|
+
}
|
|
708
|
+
let proof;
|
|
709
|
+
try {
|
|
710
|
+
if (brc105ProofConstructor) {
|
|
711
|
+
proof = await brc105ProofConstructor(brc105Challenge);
|
|
712
|
+
} else {
|
|
713
|
+
proof = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
714
|
+
}
|
|
715
|
+
} catch (err) {
|
|
716
|
+
console.error("[x402] Proof construction failed (brc105):", err);
|
|
717
|
+
config.onProofError?.(err, "brc105");
|
|
718
|
+
return response;
|
|
719
|
+
}
|
|
720
|
+
const headers = new Headers(init?.headers);
|
|
721
|
+
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
722
|
+
headers.set("x-bsv-auth-identity-key", proof.clientIdentityKey);
|
|
723
|
+
return fetch(input, { ...init, headers });
|
|
724
|
+
}
|
|
725
|
+
return response;
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
var singleton;
|
|
729
|
+
async function x402Fetch(input, init) {
|
|
730
|
+
if (!singleton) singleton = createX402Fetch();
|
|
731
|
+
return singleton(input, init);
|
|
732
|
+
}
|
|
733
|
+
function resolveRelativeUrl(url) {
|
|
734
|
+
const loc = globalThis.location;
|
|
735
|
+
if (loc?.href) {
|
|
736
|
+
return new URL(url, loc.href).origin;
|
|
737
|
+
}
|
|
738
|
+
return "unknown";
|
|
739
|
+
}
|
|
740
|
+
function extractOrigin(input) {
|
|
741
|
+
if (input instanceof URL) return input.origin;
|
|
742
|
+
if (typeof input === "string") {
|
|
743
|
+
try {
|
|
744
|
+
return new URL(input).origin;
|
|
745
|
+
} catch {
|
|
746
|
+
try {
|
|
747
|
+
return resolveRelativeUrl(input);
|
|
748
|
+
} catch {
|
|
749
|
+
return "unknown";
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
return new URL(input.url).origin;
|
|
755
|
+
} catch {
|
|
756
|
+
try {
|
|
757
|
+
return resolveRelativeUrl(input.url);
|
|
758
|
+
} catch {
|
|
759
|
+
return "unknown";
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
592
764
|
// src/limits.ts
|
|
593
765
|
var BFG_DAILY_CEILING_SATOSHIS = 1e10;
|
|
594
766
|
var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
|
|
@@ -890,41 +1062,6 @@ var RateLimiter = class {
|
|
|
890
1062
|
}
|
|
891
1063
|
};
|
|
892
1064
|
|
|
893
|
-
// src/site-policy.ts
|
|
894
|
-
var defaultSitePrompt = async (origin) => {
|
|
895
|
-
if (typeof globalThis.confirm !== "function") return "global";
|
|
896
|
-
const allow = globalThis.confirm(
|
|
897
|
-
`First payment to ${origin}.
|
|
898
|
-
|
|
899
|
-
Use your global spending limits for this site?
|
|
900
|
-
|
|
901
|
-
OK = Use global limits
|
|
902
|
-
Cancel = Block this site`
|
|
903
|
-
);
|
|
904
|
-
return allow ? "global" : "block";
|
|
905
|
-
};
|
|
906
|
-
async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
|
|
907
|
-
const existing = limits.sitePolicies[origin];
|
|
908
|
-
if (existing) return existing;
|
|
909
|
-
if (!limits.requirePerSitePrompt) {
|
|
910
|
-
return { origin, action: "global" };
|
|
911
|
-
}
|
|
912
|
-
if (limits.require2fa.onNewSiteApproval) {
|
|
913
|
-
if (!twoFactorProvider) {
|
|
914
|
-
return { origin, action: "block" };
|
|
915
|
-
}
|
|
916
|
-
const verified = await twoFactorProvider.verify({
|
|
917
|
-
type: "new-site-approval",
|
|
918
|
-
origin
|
|
919
|
-
});
|
|
920
|
-
if (!verified) {
|
|
921
|
-
return { origin, action: "block" };
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const action = await promptFn(origin);
|
|
925
|
-
return { origin, action };
|
|
926
|
-
}
|
|
927
|
-
|
|
928
1065
|
// src/storage.ts
|
|
929
1066
|
var STATE_KEY = "x402:limit-state";
|
|
930
1067
|
var POLICIES_KEY = "x402:site-policies";
|
|
@@ -999,304 +1136,6 @@ var LocalStorageAdapter = class {
|
|
|
999
1136
|
}
|
|
1000
1137
|
};
|
|
1001
1138
|
|
|
1002
|
-
// src/x402-fetch.ts
|
|
1003
|
-
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
1004
|
-
function base58DecodeCheck(address) {
|
|
1005
|
-
let leadingZeros = 0;
|
|
1006
|
-
for (const c of address) {
|
|
1007
|
-
if (c === "1") leadingZeros++;
|
|
1008
|
-
else break;
|
|
1009
|
-
}
|
|
1010
|
-
let n = BigInt(0);
|
|
1011
|
-
for (const c of address) {
|
|
1012
|
-
const i = BASE58_ALPHABET.indexOf(c);
|
|
1013
|
-
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
1014
|
-
n = n * 58n + BigInt(i);
|
|
1015
|
-
}
|
|
1016
|
-
const hexFromBigint = n === 0n ? "" : n.toString(16);
|
|
1017
|
-
const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
|
|
1018
|
-
const bigintBytes = [];
|
|
1019
|
-
for (let i = 0; i < paddedHex.length; i += 2) {
|
|
1020
|
-
bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
|
|
1021
|
-
}
|
|
1022
|
-
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
1023
|
-
allBytes.set(bigintBytes, leadingZeros);
|
|
1024
|
-
if (allBytes.length !== 25) {
|
|
1025
|
-
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
1026
|
-
}
|
|
1027
|
-
const body = allBytes.slice(0, 21);
|
|
1028
|
-
const checksum = allBytes.slice(21);
|
|
1029
|
-
const version = allBytes[0];
|
|
1030
|
-
if (version !== 0 && version !== 111) {
|
|
1031
|
-
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
1032
|
-
}
|
|
1033
|
-
return { version, payload: body.slice(1) };
|
|
1034
|
-
}
|
|
1035
|
-
function payeeAddressToLockingScript(address) {
|
|
1036
|
-
const { payload } = base58DecodeCheck(address);
|
|
1037
|
-
if (payload.length !== 20) {
|
|
1038
|
-
throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
|
|
1039
|
-
}
|
|
1040
|
-
const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1041
|
-
return `76a914${pubkeyHash}88ac`;
|
|
1042
|
-
}
|
|
1043
|
-
async function defaultConstructProof(challenge) {
|
|
1044
|
-
const cwi = globalThis.CWI;
|
|
1045
|
-
if (!cwi || typeof cwi.createAction !== "function") {
|
|
1046
|
-
throw new Error(
|
|
1047
|
-
"No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
|
|
1048
|
-
);
|
|
1049
|
-
}
|
|
1050
|
-
const result = await cwi.createAction({
|
|
1051
|
-
description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
|
|
1052
|
-
outputs: [{
|
|
1053
|
-
satoshis: challenge.amount,
|
|
1054
|
-
lockingScript: payeeAddressToLockingScript(challenge.payee),
|
|
1055
|
-
description: `Payment to ${challenge.payee}`
|
|
1056
|
-
}],
|
|
1057
|
-
labels: ["x402-payment"],
|
|
1058
|
-
options: {
|
|
1059
|
-
returnTXIDOnly: false,
|
|
1060
|
-
noSend: false
|
|
1061
|
-
}
|
|
1062
|
-
});
|
|
1063
|
-
if (!result || !result.txid) {
|
|
1064
|
-
throw new Error("Wallet declined payment or returned invalid result");
|
|
1065
|
-
}
|
|
1066
|
-
if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
|
|
1067
|
-
throw new Error("Wallet did not return raw transaction");
|
|
1068
|
-
}
|
|
1069
|
-
return {
|
|
1070
|
-
txid: result.txid,
|
|
1071
|
-
rawTx: result.rawTx
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
function createMutex() {
|
|
1075
|
-
let chain = Promise.resolve();
|
|
1076
|
-
return (fn) => {
|
|
1077
|
-
const result = chain.then(fn, fn);
|
|
1078
|
-
chain = result.then(() => {
|
|
1079
|
-
}, () => {
|
|
1080
|
-
});
|
|
1081
|
-
return result;
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
function createX402Fetch(config = {}) {
|
|
1085
|
-
const tier = config.tier ?? "Hey, Not Too Rough";
|
|
1086
|
-
const mode = config.mode ?? "interactive";
|
|
1087
|
-
if (tier === "Nightmare!" && config.nightmareConfirmation !== "NIGHTMARE") {
|
|
1088
|
-
throw new Error('Nightmare! tier requires nightmareConfirmation: "NIGHTMARE"');
|
|
1089
|
-
}
|
|
1090
|
-
const limits = resolveSpendLimits(tier, mode, config.limits);
|
|
1091
|
-
const storage = config.storage ?? new LocalStorageAdapter();
|
|
1092
|
-
const twoFactor = config.twoFactorProvider;
|
|
1093
|
-
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
1094
|
-
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
1095
|
-
const brc105Wallet = config.brc105Wallet;
|
|
1096
|
-
const nowFn = config.now ?? Date.now;
|
|
1097
|
-
const mutex = createMutex();
|
|
1098
|
-
const needs2fa = limits.require2fa;
|
|
1099
|
-
if (!twoFactor && (needs2fa.onCircuitBreakerReset || needs2fa.onHighValueTx || needs2fa.onNewSiteApproval || needs2fa.onTierChange)) {
|
|
1100
|
-
console.warn("x402: tier requires 2FA but no twoFactorProvider configured \u2014 2FA-gated actions will be blocked");
|
|
1101
|
-
}
|
|
1102
|
-
let limiter;
|
|
1103
|
-
let initialised = false;
|
|
1104
|
-
async function ensureInitialised() {
|
|
1105
|
-
if (limiter && initialised) return limiter;
|
|
1106
|
-
const state = await storage.load();
|
|
1107
|
-
limiter = new RateLimiter(limits, state ?? void 0, nowFn);
|
|
1108
|
-
const policies = await storage.loadSitePolicies();
|
|
1109
|
-
Object.assign(limits.sitePolicies, policies);
|
|
1110
|
-
initialised = true;
|
|
1111
|
-
return limiter;
|
|
1112
|
-
}
|
|
1113
|
-
async function persist(rl) {
|
|
1114
|
-
await storage.save(rl.getState());
|
|
1115
|
-
await storage.saveSitePolicies(limits.sitePolicies);
|
|
1116
|
-
}
|
|
1117
|
-
async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
|
|
1118
|
-
return mutex(async () => {
|
|
1119
|
-
const rl = await ensureInitialised();
|
|
1120
|
-
const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
|
|
1121
|
-
if (sitePolicy.action === "block") return originalResponse;
|
|
1122
|
-
if (!limits.sitePolicies[origin]) {
|
|
1123
|
-
limits.sitePolicies[origin] = sitePolicy;
|
|
1124
|
-
await storage.saveSitePolicies(limits.sitePolicies);
|
|
1125
|
-
}
|
|
1126
|
-
const spendCheckable = { amount, origin, protocol };
|
|
1127
|
-
const result = rl.check(spendCheckable, origin);
|
|
1128
|
-
if (result.action === "block") {
|
|
1129
|
-
if (result.severity === "trip") {
|
|
1130
|
-
rl.trip();
|
|
1131
|
-
await persist(rl);
|
|
1132
|
-
config.onLimitReached?.(result.reason);
|
|
1133
|
-
return originalResponse;
|
|
1134
|
-
}
|
|
1135
|
-
if (result.severity === "window" && twoFactor) {
|
|
1136
|
-
config.onLimitReached?.(result.reason);
|
|
1137
|
-
const override = await twoFactor.verify({
|
|
1138
|
-
type: "limit-override",
|
|
1139
|
-
amount,
|
|
1140
|
-
origin,
|
|
1141
|
-
reason: result.reason
|
|
1142
|
-
});
|
|
1143
|
-
if (override) {
|
|
1144
|
-
} else {
|
|
1145
|
-
return originalResponse;
|
|
1146
|
-
}
|
|
1147
|
-
} else {
|
|
1148
|
-
config.onLimitReached?.(result.reason);
|
|
1149
|
-
return originalResponse;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
if (result.action === "yellow-light") {
|
|
1153
|
-
const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
|
|
1154
|
-
if (!proceed) return originalResponse;
|
|
1155
|
-
}
|
|
1156
|
-
if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
|
|
1157
|
-
if (!twoFactor) return originalResponse;
|
|
1158
|
-
const verified = await twoFactor.verify({
|
|
1159
|
-
type: "high-value-tx",
|
|
1160
|
-
amount,
|
|
1161
|
-
origin
|
|
1162
|
-
});
|
|
1163
|
-
if (!verified) return originalResponse;
|
|
1164
|
-
}
|
|
1165
|
-
let proof;
|
|
1166
|
-
try {
|
|
1167
|
-
proof = await buildProof();
|
|
1168
|
-
} catch {
|
|
1169
|
-
return originalResponse;
|
|
1170
|
-
}
|
|
1171
|
-
rl.record(makeLedgerEntry(proof));
|
|
1172
|
-
await persist(rl);
|
|
1173
|
-
return retryWithProof(proof);
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
const fetchFn = async function x402Fetch2(input, init) {
|
|
1177
|
-
const response = await fetch(input, init);
|
|
1178
|
-
if (response.status !== 402) return response;
|
|
1179
|
-
const origin = extractOrigin(input);
|
|
1180
|
-
const challengeHeader = response.headers.get("X402-Challenge");
|
|
1181
|
-
if (challengeHeader) {
|
|
1182
|
-
let challenge;
|
|
1183
|
-
try {
|
|
1184
|
-
challenge = parseChallenge(challengeHeader);
|
|
1185
|
-
} catch {
|
|
1186
|
-
return response;
|
|
1187
|
-
}
|
|
1188
|
-
return handlePaymentFlow(
|
|
1189
|
-
response,
|
|
1190
|
-
input,
|
|
1191
|
-
init,
|
|
1192
|
-
origin,
|
|
1193
|
-
challenge.amount,
|
|
1194
|
-
"x402",
|
|
1195
|
-
async () => constructProof(challenge),
|
|
1196
|
-
(proof) => {
|
|
1197
|
-
const headers = new Headers(init?.headers);
|
|
1198
|
-
headers.set("X402-Proof", JSON.stringify(proof));
|
|
1199
|
-
return fetch(input, { ...init, headers });
|
|
1200
|
-
},
|
|
1201
|
-
(proof) => ({
|
|
1202
|
-
timestamp: nowFn(),
|
|
1203
|
-
origin,
|
|
1204
|
-
satoshis: challenge.amount,
|
|
1205
|
-
txid: proof.txid
|
|
1206
|
-
})
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
|
-
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
1210
|
-
if (brc105Version) {
|
|
1211
|
-
if (!brc105ProofConstructor && !brc105Wallet) return response;
|
|
1212
|
-
let brc105Challenge;
|
|
1213
|
-
try {
|
|
1214
|
-
brc105Challenge = parseBrc105Challenge(response);
|
|
1215
|
-
} catch {
|
|
1216
|
-
return response;
|
|
1217
|
-
}
|
|
1218
|
-
return handlePaymentFlow(
|
|
1219
|
-
response,
|
|
1220
|
-
input,
|
|
1221
|
-
init,
|
|
1222
|
-
origin,
|
|
1223
|
-
brc105Challenge.satoshisRequired,
|
|
1224
|
-
"brc105",
|
|
1225
|
-
async () => {
|
|
1226
|
-
if (brc105ProofConstructor) {
|
|
1227
|
-
return brc105ProofConstructor(brc105Challenge);
|
|
1228
|
-
}
|
|
1229
|
-
return constructBrc105Proof(brc105Challenge, brc105Wallet);
|
|
1230
|
-
},
|
|
1231
|
-
(proof) => {
|
|
1232
|
-
const headers = new Headers(init?.headers);
|
|
1233
|
-
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
1234
|
-
return fetch(input, { ...init, headers });
|
|
1235
|
-
},
|
|
1236
|
-
(proof) => ({
|
|
1237
|
-
timestamp: nowFn(),
|
|
1238
|
-
origin,
|
|
1239
|
-
satoshis: brc105Challenge.satoshisRequired,
|
|
1240
|
-
txid: proof.txid,
|
|
1241
|
-
protocol: "brc105"
|
|
1242
|
-
})
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
return response;
|
|
1246
|
-
};
|
|
1247
|
-
fetchFn.resetLimits = async () => {
|
|
1248
|
-
const rl = await ensureInitialised();
|
|
1249
|
-
if (limits.require2fa.onCircuitBreakerReset) {
|
|
1250
|
-
if (!twoFactor) throw new Error("2FA required for circuit breaker reset but no twoFactorProvider configured");
|
|
1251
|
-
const verified = await twoFactor.verify({ type: "circuit-breaker-reset" });
|
|
1252
|
-
if (!verified) throw new Error("2FA verification failed for circuit breaker reset");
|
|
1253
|
-
}
|
|
1254
|
-
rl.reset();
|
|
1255
|
-
await persist(rl);
|
|
1256
|
-
};
|
|
1257
|
-
fetchFn.getState = () => {
|
|
1258
|
-
if (!limiter) return { entries: [], circuitBroken: false };
|
|
1259
|
-
const state = limiter.getState();
|
|
1260
|
-
return { entries: state.entries, circuitBroken: state.circuitBroken };
|
|
1261
|
-
};
|
|
1262
|
-
return fetchFn;
|
|
1263
|
-
}
|
|
1264
|
-
var singleton;
|
|
1265
|
-
async function x402Fetch(input, init) {
|
|
1266
|
-
if (!singleton) singleton = createX402Fetch();
|
|
1267
|
-
return singleton(input, init);
|
|
1268
|
-
}
|
|
1269
|
-
function resolveRelativeUrl(url) {
|
|
1270
|
-
const loc = globalThis.location;
|
|
1271
|
-
if (loc?.href) {
|
|
1272
|
-
return new URL(url, loc.href).origin;
|
|
1273
|
-
}
|
|
1274
|
-
return "unknown";
|
|
1275
|
-
}
|
|
1276
|
-
function extractOrigin(input) {
|
|
1277
|
-
if (input instanceof URL) return input.origin;
|
|
1278
|
-
if (typeof input === "string") {
|
|
1279
|
-
try {
|
|
1280
|
-
return new URL(input).origin;
|
|
1281
|
-
} catch {
|
|
1282
|
-
try {
|
|
1283
|
-
return resolveRelativeUrl(input);
|
|
1284
|
-
} catch {
|
|
1285
|
-
return "unknown";
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
try {
|
|
1290
|
-
return new URL(input.url).origin;
|
|
1291
|
-
} catch {
|
|
1292
|
-
try {
|
|
1293
|
-
return resolveRelativeUrl(input.url);
|
|
1294
|
-
} catch {
|
|
1295
|
-
return "unknown";
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
1139
|
// src/two-factor.ts
|
|
1301
1140
|
var WalletTwoFactorProvider = class {
|
|
1302
1141
|
async verify(action) {
|
|
@@ -1339,6 +1178,41 @@ function describeAction(action) {
|
|
|
1339
1178
|
Allow this payment of ${action.amount} sats to ${action.origin}?`;
|
|
1340
1179
|
}
|
|
1341
1180
|
}
|
|
1181
|
+
|
|
1182
|
+
// src/site-policy.ts
|
|
1183
|
+
var defaultSitePrompt = async (origin) => {
|
|
1184
|
+
if (typeof globalThis.confirm !== "function") return "global";
|
|
1185
|
+
const allow = globalThis.confirm(
|
|
1186
|
+
`First payment to ${origin}.
|
|
1187
|
+
|
|
1188
|
+
Use your global spending limits for this site?
|
|
1189
|
+
|
|
1190
|
+
OK = Use global limits
|
|
1191
|
+
Cancel = Block this site`
|
|
1192
|
+
);
|
|
1193
|
+
return allow ? "global" : "block";
|
|
1194
|
+
};
|
|
1195
|
+
async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
|
|
1196
|
+
const existing = limits.sitePolicies[origin];
|
|
1197
|
+
if (existing) return existing;
|
|
1198
|
+
if (!limits.requirePerSitePrompt) {
|
|
1199
|
+
return { origin, action: "global" };
|
|
1200
|
+
}
|
|
1201
|
+
if (limits.require2fa.onNewSiteApproval) {
|
|
1202
|
+
if (!twoFactorProvider) {
|
|
1203
|
+
return { origin, action: "block" };
|
|
1204
|
+
}
|
|
1205
|
+
const verified = await twoFactorProvider.verify({
|
|
1206
|
+
type: "new-site-approval",
|
|
1207
|
+
origin
|
|
1208
|
+
});
|
|
1209
|
+
if (!verified) {
|
|
1210
|
+
return { origin, action: "block" };
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const action = await promptFn(origin);
|
|
1214
|
+
return { origin, action };
|
|
1215
|
+
}
|
|
1342
1216
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1343
1217
|
0 && (module.exports = {
|
|
1344
1218
|
BFG_DAILY_CEILING_SATOSHIS,
|