bsv-x402 0.4.1 → 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 +200 -348
- package/dist/index.d.cts +1 -17
- package/dist/index.d.ts +1 -17
- package/dist/index.js +200 -348
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -596,6 +596,171 @@ function parseChallenge(header) {
|
|
|
596
596
|
};
|
|
597
597
|
}
|
|
598
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
|
+
|
|
599
764
|
// src/limits.ts
|
|
600
765
|
var BFG_DAILY_CEILING_SATOSHIS = 1e10;
|
|
601
766
|
var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
|
|
@@ -897,41 +1062,6 @@ var RateLimiter = class {
|
|
|
897
1062
|
}
|
|
898
1063
|
};
|
|
899
1064
|
|
|
900
|
-
// src/site-policy.ts
|
|
901
|
-
var defaultSitePrompt = async (origin) => {
|
|
902
|
-
if (typeof globalThis.confirm !== "function") return "global";
|
|
903
|
-
const allow = globalThis.confirm(
|
|
904
|
-
`First payment to ${origin}.
|
|
905
|
-
|
|
906
|
-
Use your global spending limits for this site?
|
|
907
|
-
|
|
908
|
-
OK = Use global limits
|
|
909
|
-
Cancel = Block this site`
|
|
910
|
-
);
|
|
911
|
-
return allow ? "global" : "block";
|
|
912
|
-
};
|
|
913
|
-
async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
|
|
914
|
-
const existing = limits.sitePolicies[origin];
|
|
915
|
-
if (existing) return existing;
|
|
916
|
-
if (!limits.requirePerSitePrompt) {
|
|
917
|
-
return { origin, action: "global" };
|
|
918
|
-
}
|
|
919
|
-
if (limits.require2fa.onNewSiteApproval) {
|
|
920
|
-
if (!twoFactorProvider) {
|
|
921
|
-
return { origin, action: "block" };
|
|
922
|
-
}
|
|
923
|
-
const verified = await twoFactorProvider.verify({
|
|
924
|
-
type: "new-site-approval",
|
|
925
|
-
origin
|
|
926
|
-
});
|
|
927
|
-
if (!verified) {
|
|
928
|
-
return { origin, action: "block" };
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
const action = await promptFn(origin);
|
|
932
|
-
return { origin, action };
|
|
933
|
-
}
|
|
934
|
-
|
|
935
1065
|
// src/storage.ts
|
|
936
1066
|
var STATE_KEY = "x402:limit-state";
|
|
937
1067
|
var POLICIES_KEY = "x402:site-policies";
|
|
@@ -1006,319 +1136,6 @@ var LocalStorageAdapter = class {
|
|
|
1006
1136
|
}
|
|
1007
1137
|
};
|
|
1008
1138
|
|
|
1009
|
-
// src/x402-fetch.ts
|
|
1010
|
-
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
1011
|
-
function base58DecodeCheck(address) {
|
|
1012
|
-
let leadingZeros = 0;
|
|
1013
|
-
for (const c of address) {
|
|
1014
|
-
if (c === "1") leadingZeros++;
|
|
1015
|
-
else break;
|
|
1016
|
-
}
|
|
1017
|
-
let n = BigInt(0);
|
|
1018
|
-
for (const c of address) {
|
|
1019
|
-
const i = BASE58_ALPHABET.indexOf(c);
|
|
1020
|
-
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
1021
|
-
n = n * 58n + BigInt(i);
|
|
1022
|
-
}
|
|
1023
|
-
const hexFromBigint = n === 0n ? "" : n.toString(16);
|
|
1024
|
-
const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
|
|
1025
|
-
const bigintBytes = [];
|
|
1026
|
-
for (let i = 0; i < paddedHex.length; i += 2) {
|
|
1027
|
-
bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
|
|
1028
|
-
}
|
|
1029
|
-
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
1030
|
-
allBytes.set(bigintBytes, leadingZeros);
|
|
1031
|
-
if (allBytes.length !== 25) {
|
|
1032
|
-
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
1033
|
-
}
|
|
1034
|
-
const body = allBytes.slice(0, 21);
|
|
1035
|
-
const checksum = allBytes.slice(21);
|
|
1036
|
-
const version = allBytes[0];
|
|
1037
|
-
if (version !== 0 && version !== 111) {
|
|
1038
|
-
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
1039
|
-
}
|
|
1040
|
-
return { version, payload: body.slice(1) };
|
|
1041
|
-
}
|
|
1042
|
-
function payeeAddressToLockingScript(address) {
|
|
1043
|
-
const { payload } = base58DecodeCheck(address);
|
|
1044
|
-
if (payload.length !== 20) {
|
|
1045
|
-
throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
|
|
1046
|
-
}
|
|
1047
|
-
const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1048
|
-
return `76a914${pubkeyHash}88ac`;
|
|
1049
|
-
}
|
|
1050
|
-
async function defaultConstructProof(challenge) {
|
|
1051
|
-
const cwi = globalThis.CWI;
|
|
1052
|
-
if (!cwi || typeof cwi.createAction !== "function") {
|
|
1053
|
-
throw new Error(
|
|
1054
|
-
"No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
const result = await cwi.createAction({
|
|
1058
|
-
description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
|
|
1059
|
-
outputs: [{
|
|
1060
|
-
satoshis: challenge.amount,
|
|
1061
|
-
lockingScript: payeeAddressToLockingScript(challenge.payee),
|
|
1062
|
-
description: `Payment to ${challenge.payee}`
|
|
1063
|
-
}],
|
|
1064
|
-
labels: ["x402-payment"],
|
|
1065
|
-
options: {
|
|
1066
|
-
returnTXIDOnly: false,
|
|
1067
|
-
noSend: false
|
|
1068
|
-
}
|
|
1069
|
-
});
|
|
1070
|
-
if (!result || !result.txid) {
|
|
1071
|
-
throw new Error("Wallet declined payment or returned invalid result");
|
|
1072
|
-
}
|
|
1073
|
-
if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
|
|
1074
|
-
throw new Error("Wallet did not return raw transaction");
|
|
1075
|
-
}
|
|
1076
|
-
return {
|
|
1077
|
-
txid: result.txid,
|
|
1078
|
-
rawTx: result.rawTx
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
function createMutex() {
|
|
1082
|
-
let chain = Promise.resolve();
|
|
1083
|
-
return (fn) => {
|
|
1084
|
-
const result = chain.then(fn, fn);
|
|
1085
|
-
chain = result.then(() => {
|
|
1086
|
-
}, () => {
|
|
1087
|
-
});
|
|
1088
|
-
return result;
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
function createX402Fetch(config = {}) {
|
|
1092
|
-
const tier = config.tier ?? "Hey, Not Too Rough";
|
|
1093
|
-
const mode = config.mode ?? "interactive";
|
|
1094
|
-
if (tier === "Nightmare!" && config.nightmareConfirmation !== "NIGHTMARE") {
|
|
1095
|
-
throw new Error('Nightmare! tier requires nightmareConfirmation: "NIGHTMARE"');
|
|
1096
|
-
}
|
|
1097
|
-
const limits = resolveSpendLimits(tier, mode, config.limits);
|
|
1098
|
-
const storage = config.storage ?? new LocalStorageAdapter();
|
|
1099
|
-
const twoFactor = config.twoFactorProvider;
|
|
1100
|
-
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
1101
|
-
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
1102
|
-
const brc105Wallet = config.brc105Wallet;
|
|
1103
|
-
const nowFn = config.now ?? Date.now;
|
|
1104
|
-
const mutex = createMutex();
|
|
1105
|
-
const needs2fa = limits.require2fa;
|
|
1106
|
-
if (!twoFactor && (needs2fa.onCircuitBreakerReset || needs2fa.onHighValueTx || needs2fa.onNewSiteApproval || needs2fa.onTierChange)) {
|
|
1107
|
-
console.warn("x402: tier requires 2FA but no twoFactorProvider configured \u2014 2FA-gated actions will be blocked");
|
|
1108
|
-
}
|
|
1109
|
-
let limiter;
|
|
1110
|
-
let initialised = false;
|
|
1111
|
-
async function ensureInitialised() {
|
|
1112
|
-
if (limiter && initialised) return limiter;
|
|
1113
|
-
const state = await storage.load();
|
|
1114
|
-
limiter = new RateLimiter(limits, state ?? void 0, nowFn);
|
|
1115
|
-
const policies = await storage.loadSitePolicies();
|
|
1116
|
-
Object.assign(limits.sitePolicies, policies);
|
|
1117
|
-
initialised = true;
|
|
1118
|
-
return limiter;
|
|
1119
|
-
}
|
|
1120
|
-
async function persist(rl) {
|
|
1121
|
-
await storage.save(rl.getState());
|
|
1122
|
-
await storage.saveSitePolicies(limits.sitePolicies);
|
|
1123
|
-
}
|
|
1124
|
-
async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
|
|
1125
|
-
return mutex(async () => {
|
|
1126
|
-
const rl = await ensureInitialised();
|
|
1127
|
-
const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
|
|
1128
|
-
if (sitePolicy.action === "block") return originalResponse;
|
|
1129
|
-
if (!limits.sitePolicies[origin]) {
|
|
1130
|
-
limits.sitePolicies[origin] = sitePolicy;
|
|
1131
|
-
await storage.saveSitePolicies(limits.sitePolicies);
|
|
1132
|
-
}
|
|
1133
|
-
const spendCheckable = { amount, origin, protocol };
|
|
1134
|
-
const result = rl.check(spendCheckable, origin);
|
|
1135
|
-
if (result.action === "block") {
|
|
1136
|
-
if (result.severity === "trip") {
|
|
1137
|
-
rl.trip();
|
|
1138
|
-
await persist(rl);
|
|
1139
|
-
config.onLimitReached?.(result.reason);
|
|
1140
|
-
return originalResponse;
|
|
1141
|
-
}
|
|
1142
|
-
if (result.severity === "window" && twoFactor) {
|
|
1143
|
-
config.onLimitReached?.(result.reason);
|
|
1144
|
-
const override = await twoFactor.verify({
|
|
1145
|
-
type: "limit-override",
|
|
1146
|
-
amount,
|
|
1147
|
-
origin,
|
|
1148
|
-
reason: result.reason
|
|
1149
|
-
});
|
|
1150
|
-
if (override) {
|
|
1151
|
-
} else {
|
|
1152
|
-
return originalResponse;
|
|
1153
|
-
}
|
|
1154
|
-
} else {
|
|
1155
|
-
config.onLimitReached?.(result.reason);
|
|
1156
|
-
return originalResponse;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
if (result.action === "yellow-light") {
|
|
1160
|
-
const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
|
|
1161
|
-
if (!proceed) return originalResponse;
|
|
1162
|
-
}
|
|
1163
|
-
if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
|
|
1164
|
-
if (!twoFactor) return originalResponse;
|
|
1165
|
-
const verified = await twoFactor.verify({
|
|
1166
|
-
type: "high-value-tx",
|
|
1167
|
-
amount,
|
|
1168
|
-
origin
|
|
1169
|
-
});
|
|
1170
|
-
if (!verified) return originalResponse;
|
|
1171
|
-
}
|
|
1172
|
-
let proof;
|
|
1173
|
-
try {
|
|
1174
|
-
proof = await buildProof();
|
|
1175
|
-
} catch (err) {
|
|
1176
|
-
console.error(`[x402] Proof construction failed (${protocol}):`, err);
|
|
1177
|
-
config.onProofError?.(err, protocol);
|
|
1178
|
-
return originalResponse;
|
|
1179
|
-
}
|
|
1180
|
-
rl.record(makeLedgerEntry(proof));
|
|
1181
|
-
await persist(rl);
|
|
1182
|
-
const maxAttempts = 3;
|
|
1183
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1184
|
-
try {
|
|
1185
|
-
return await retryWithProof(proof);
|
|
1186
|
-
} catch (err) {
|
|
1187
|
-
if (attempt >= maxAttempts) {
|
|
1188
|
-
console.error(`[x402] Paid request failed after ${maxAttempts} attempts (${protocol}):`, err);
|
|
1189
|
-
return originalResponse;
|
|
1190
|
-
}
|
|
1191
|
-
await new Promise((r) => setTimeout(r, 250 * Math.pow(2, attempt)));
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
return originalResponse;
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
const fetchFn = async function x402Fetch2(input, init) {
|
|
1198
|
-
const response = await fetch(input, init);
|
|
1199
|
-
if (response.status !== 402) return response;
|
|
1200
|
-
const origin = extractOrigin(input);
|
|
1201
|
-
const challengeHeader = response.headers.get("X402-Challenge");
|
|
1202
|
-
if (challengeHeader) {
|
|
1203
|
-
let challenge;
|
|
1204
|
-
try {
|
|
1205
|
-
challenge = parseChallenge(challengeHeader);
|
|
1206
|
-
} catch {
|
|
1207
|
-
return response;
|
|
1208
|
-
}
|
|
1209
|
-
return handlePaymentFlow(
|
|
1210
|
-
response,
|
|
1211
|
-
input,
|
|
1212
|
-
init,
|
|
1213
|
-
origin,
|
|
1214
|
-
challenge.amount,
|
|
1215
|
-
"x402",
|
|
1216
|
-
async () => constructProof(challenge),
|
|
1217
|
-
(proof) => {
|
|
1218
|
-
const headers = new Headers(init?.headers);
|
|
1219
|
-
headers.set("X402-Proof", JSON.stringify(proof));
|
|
1220
|
-
return fetch(input, { ...init, headers });
|
|
1221
|
-
},
|
|
1222
|
-
(proof) => ({
|
|
1223
|
-
timestamp: nowFn(),
|
|
1224
|
-
origin,
|
|
1225
|
-
satoshis: challenge.amount,
|
|
1226
|
-
txid: proof.txid
|
|
1227
|
-
})
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
1231
|
-
if (brc105Version) {
|
|
1232
|
-
if (!brc105ProofConstructor && !brc105Wallet) return response;
|
|
1233
|
-
let brc105Challenge;
|
|
1234
|
-
try {
|
|
1235
|
-
brc105Challenge = parseBrc105Challenge(response);
|
|
1236
|
-
} catch {
|
|
1237
|
-
return response;
|
|
1238
|
-
}
|
|
1239
|
-
return handlePaymentFlow(
|
|
1240
|
-
response,
|
|
1241
|
-
input,
|
|
1242
|
-
init,
|
|
1243
|
-
origin,
|
|
1244
|
-
brc105Challenge.satoshisRequired,
|
|
1245
|
-
"brc105",
|
|
1246
|
-
async () => {
|
|
1247
|
-
if (brc105ProofConstructor) {
|
|
1248
|
-
return brc105ProofConstructor(brc105Challenge);
|
|
1249
|
-
}
|
|
1250
|
-
return constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
1251
|
-
},
|
|
1252
|
-
(proof) => {
|
|
1253
|
-
const headers = new Headers(init?.headers);
|
|
1254
|
-
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
1255
|
-
headers.set("x-bsv-auth-identity-key", proof.clientIdentityKey);
|
|
1256
|
-
return fetch(input, { ...init, headers });
|
|
1257
|
-
},
|
|
1258
|
-
(proof) => ({
|
|
1259
|
-
timestamp: nowFn(),
|
|
1260
|
-
origin,
|
|
1261
|
-
satoshis: brc105Challenge.satoshisRequired,
|
|
1262
|
-
txid: proof.txid,
|
|
1263
|
-
protocol: "brc105"
|
|
1264
|
-
})
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
return response;
|
|
1268
|
-
};
|
|
1269
|
-
fetchFn.resetLimits = async () => {
|
|
1270
|
-
const rl = await ensureInitialised();
|
|
1271
|
-
if (limits.require2fa.onCircuitBreakerReset) {
|
|
1272
|
-
if (!twoFactor) throw new Error("2FA required for circuit breaker reset but no twoFactorProvider configured");
|
|
1273
|
-
const verified = await twoFactor.verify({ type: "circuit-breaker-reset" });
|
|
1274
|
-
if (!verified) throw new Error("2FA verification failed for circuit breaker reset");
|
|
1275
|
-
}
|
|
1276
|
-
rl.reset();
|
|
1277
|
-
await persist(rl);
|
|
1278
|
-
};
|
|
1279
|
-
fetchFn.getState = () => {
|
|
1280
|
-
if (!limiter) return { entries: [], circuitBroken: false };
|
|
1281
|
-
const state = limiter.getState();
|
|
1282
|
-
return { entries: state.entries, circuitBroken: state.circuitBroken };
|
|
1283
|
-
};
|
|
1284
|
-
return fetchFn;
|
|
1285
|
-
}
|
|
1286
|
-
var singleton;
|
|
1287
|
-
async function x402Fetch(input, init) {
|
|
1288
|
-
if (!singleton) singleton = createX402Fetch();
|
|
1289
|
-
return singleton(input, init);
|
|
1290
|
-
}
|
|
1291
|
-
function resolveRelativeUrl(url) {
|
|
1292
|
-
const loc = globalThis.location;
|
|
1293
|
-
if (loc?.href) {
|
|
1294
|
-
return new URL(url, loc.href).origin;
|
|
1295
|
-
}
|
|
1296
|
-
return "unknown";
|
|
1297
|
-
}
|
|
1298
|
-
function extractOrigin(input) {
|
|
1299
|
-
if (input instanceof URL) return input.origin;
|
|
1300
|
-
if (typeof input === "string") {
|
|
1301
|
-
try {
|
|
1302
|
-
return new URL(input).origin;
|
|
1303
|
-
} catch {
|
|
1304
|
-
try {
|
|
1305
|
-
return resolveRelativeUrl(input);
|
|
1306
|
-
} catch {
|
|
1307
|
-
return "unknown";
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
try {
|
|
1312
|
-
return new URL(input.url).origin;
|
|
1313
|
-
} catch {
|
|
1314
|
-
try {
|
|
1315
|
-
return resolveRelativeUrl(input.url);
|
|
1316
|
-
} catch {
|
|
1317
|
-
return "unknown";
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
1139
|
// src/two-factor.ts
|
|
1323
1140
|
var WalletTwoFactorProvider = class {
|
|
1324
1141
|
async verify(action) {
|
|
@@ -1361,6 +1178,41 @@ function describeAction(action) {
|
|
|
1361
1178
|
Allow this payment of ${action.amount} sats to ${action.origin}?`;
|
|
1362
1179
|
}
|
|
1363
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
|
+
}
|
|
1364
1216
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1365
1217
|
0 && (module.exports = {
|
|
1366
1218
|
BFG_DAILY_CEILING_SATOSHIS,
|
package/dist/index.d.cts
CHANGED
|
@@ -87,19 +87,10 @@ interface TierPreset {
|
|
|
87
87
|
programmatic: SpendLimits;
|
|
88
88
|
}
|
|
89
89
|
interface X402Config {
|
|
90
|
-
tier?: TierName;
|
|
91
|
-
mode?: SpendMode;
|
|
92
|
-
limits?: Partial<SpendLimits>;
|
|
93
|
-
storage?: StorageAdapter;
|
|
94
|
-
twoFactorProvider?: TwoFactorProvider;
|
|
95
90
|
proofConstructor?: (challenge: Challenge) => Promise<Proof>;
|
|
96
91
|
brc105ProofConstructor?: Brc105ProofConstructor;
|
|
97
92
|
brc105Wallet?: Brc105Wallet;
|
|
98
|
-
nightmareConfirmation?: string;
|
|
99
|
-
onLimitReached?: (reason: string) => void;
|
|
100
|
-
onYellowLight?: (detail: YellowLightEvent) => Promise<boolean>;
|
|
101
93
|
onProofError?: (error: unknown, protocol: PaymentProtocol) => void;
|
|
102
|
-
now?: () => number;
|
|
103
94
|
}
|
|
104
95
|
interface YellowLightEvent {
|
|
105
96
|
origin: string;
|
|
@@ -182,14 +173,7 @@ interface TwoFactorProvider {
|
|
|
182
173
|
verify(action: TwoFactorAction): Promise<boolean>;
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
|
|
186
|
-
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
187
|
-
resetLimits(): Promise<void>;
|
|
188
|
-
getState(): {
|
|
189
|
-
entries: unknown[];
|
|
190
|
-
circuitBroken: boolean;
|
|
191
|
-
};
|
|
192
|
-
}
|
|
176
|
+
type X402FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
193
177
|
declare function createX402Fetch(config?: X402Config): X402FetchFn;
|
|
194
178
|
declare function x402Fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
195
179
|
|
package/dist/index.d.ts
CHANGED
|
@@ -87,19 +87,10 @@ interface TierPreset {
|
|
|
87
87
|
programmatic: SpendLimits;
|
|
88
88
|
}
|
|
89
89
|
interface X402Config {
|
|
90
|
-
tier?: TierName;
|
|
91
|
-
mode?: SpendMode;
|
|
92
|
-
limits?: Partial<SpendLimits>;
|
|
93
|
-
storage?: StorageAdapter;
|
|
94
|
-
twoFactorProvider?: TwoFactorProvider;
|
|
95
90
|
proofConstructor?: (challenge: Challenge) => Promise<Proof>;
|
|
96
91
|
brc105ProofConstructor?: Brc105ProofConstructor;
|
|
97
92
|
brc105Wallet?: Brc105Wallet;
|
|
98
|
-
nightmareConfirmation?: string;
|
|
99
|
-
onLimitReached?: (reason: string) => void;
|
|
100
|
-
onYellowLight?: (detail: YellowLightEvent) => Promise<boolean>;
|
|
101
93
|
onProofError?: (error: unknown, protocol: PaymentProtocol) => void;
|
|
102
|
-
now?: () => number;
|
|
103
94
|
}
|
|
104
95
|
interface YellowLightEvent {
|
|
105
96
|
origin: string;
|
|
@@ -182,14 +173,7 @@ interface TwoFactorProvider {
|
|
|
182
173
|
verify(action: TwoFactorAction): Promise<boolean>;
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
|
|
186
|
-
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
187
|
-
resetLimits(): Promise<void>;
|
|
188
|
-
getState(): {
|
|
189
|
-
entries: unknown[];
|
|
190
|
-
circuitBroken: boolean;
|
|
191
|
-
};
|
|
192
|
-
}
|
|
176
|
+
type X402FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
193
177
|
declare function createX402Fetch(config?: X402Config): X402FetchFn;
|
|
194
178
|
declare function x402Fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
195
179
|
|
package/dist/index.js
CHANGED
|
@@ -558,6 +558,171 @@ function parseChallenge(header) {
|
|
|
558
558
|
};
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
// src/x402-fetch.ts
|
|
562
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
563
|
+
function base58DecodeCheck(address) {
|
|
564
|
+
let leadingZeros = 0;
|
|
565
|
+
for (const c of address) {
|
|
566
|
+
if (c === "1") leadingZeros++;
|
|
567
|
+
else break;
|
|
568
|
+
}
|
|
569
|
+
let n = BigInt(0);
|
|
570
|
+
for (const c of address) {
|
|
571
|
+
const i = BASE58_ALPHABET.indexOf(c);
|
|
572
|
+
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
573
|
+
n = n * 58n + BigInt(i);
|
|
574
|
+
}
|
|
575
|
+
const hexFromBigint = n === 0n ? "" : n.toString(16);
|
|
576
|
+
const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
|
|
577
|
+
const bigintBytes = [];
|
|
578
|
+
for (let i = 0; i < paddedHex.length; i += 2) {
|
|
579
|
+
bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
|
|
580
|
+
}
|
|
581
|
+
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
582
|
+
allBytes.set(bigintBytes, leadingZeros);
|
|
583
|
+
if (allBytes.length !== 25) {
|
|
584
|
+
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
585
|
+
}
|
|
586
|
+
const body = allBytes.slice(0, 21);
|
|
587
|
+
const checksum = allBytes.slice(21);
|
|
588
|
+
const version = allBytes[0];
|
|
589
|
+
if (version !== 0 && version !== 111) {
|
|
590
|
+
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
591
|
+
}
|
|
592
|
+
return { version, payload: body.slice(1) };
|
|
593
|
+
}
|
|
594
|
+
function payeeAddressToLockingScript(address) {
|
|
595
|
+
const { payload } = base58DecodeCheck(address);
|
|
596
|
+
if (payload.length !== 20) {
|
|
597
|
+
throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
|
|
598
|
+
}
|
|
599
|
+
const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
600
|
+
return `76a914${pubkeyHash}88ac`;
|
|
601
|
+
}
|
|
602
|
+
async function defaultConstructProof(challenge) {
|
|
603
|
+
const cwi = globalThis.CWI;
|
|
604
|
+
if (!cwi || typeof cwi.createAction !== "function") {
|
|
605
|
+
throw new Error(
|
|
606
|
+
"No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const result = await cwi.createAction({
|
|
610
|
+
description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
|
|
611
|
+
outputs: [{
|
|
612
|
+
satoshis: challenge.amount,
|
|
613
|
+
lockingScript: payeeAddressToLockingScript(challenge.payee),
|
|
614
|
+
description: `Payment to ${challenge.payee}`
|
|
615
|
+
}],
|
|
616
|
+
labels: ["x402-payment"],
|
|
617
|
+
options: {
|
|
618
|
+
returnTXIDOnly: false,
|
|
619
|
+
noSend: false
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
if (!result || !result.txid) {
|
|
623
|
+
throw new Error("Wallet declined payment or returned invalid result");
|
|
624
|
+
}
|
|
625
|
+
if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
|
|
626
|
+
throw new Error("Wallet did not return raw transaction");
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
txid: result.txid,
|
|
630
|
+
rawTx: result.rawTx
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function createX402Fetch(config = {}) {
|
|
634
|
+
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
635
|
+
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
636
|
+
const brc105Wallet = config.brc105Wallet;
|
|
637
|
+
return async function x402Fetch2(input, init) {
|
|
638
|
+
const response = await fetch(input, init);
|
|
639
|
+
if (response.status !== 402) return response;
|
|
640
|
+
const origin = extractOrigin(input);
|
|
641
|
+
const challengeHeader = response.headers.get("X402-Challenge");
|
|
642
|
+
if (challengeHeader) {
|
|
643
|
+
let challenge;
|
|
644
|
+
try {
|
|
645
|
+
challenge = parseChallenge(challengeHeader);
|
|
646
|
+
} catch {
|
|
647
|
+
return response;
|
|
648
|
+
}
|
|
649
|
+
let proof;
|
|
650
|
+
try {
|
|
651
|
+
proof = await constructProof(challenge);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
console.error("[x402] Proof construction failed (x402):", err);
|
|
654
|
+
config.onProofError?.(err, "x402");
|
|
655
|
+
return response;
|
|
656
|
+
}
|
|
657
|
+
const headers = new Headers(init?.headers);
|
|
658
|
+
headers.set("X402-Proof", JSON.stringify(proof));
|
|
659
|
+
return fetch(input, { ...init, headers });
|
|
660
|
+
}
|
|
661
|
+
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
662
|
+
if (brc105Version) {
|
|
663
|
+
if (!brc105ProofConstructor && !brc105Wallet) return response;
|
|
664
|
+
let brc105Challenge;
|
|
665
|
+
try {
|
|
666
|
+
brc105Challenge = parseBrc105Challenge(response);
|
|
667
|
+
} catch {
|
|
668
|
+
return response;
|
|
669
|
+
}
|
|
670
|
+
let proof;
|
|
671
|
+
try {
|
|
672
|
+
if (brc105ProofConstructor) {
|
|
673
|
+
proof = await brc105ProofConstructor(brc105Challenge);
|
|
674
|
+
} else {
|
|
675
|
+
proof = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
676
|
+
}
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.error("[x402] Proof construction failed (brc105):", err);
|
|
679
|
+
config.onProofError?.(err, "brc105");
|
|
680
|
+
return response;
|
|
681
|
+
}
|
|
682
|
+
const headers = new Headers(init?.headers);
|
|
683
|
+
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
684
|
+
headers.set("x-bsv-auth-identity-key", proof.clientIdentityKey);
|
|
685
|
+
return fetch(input, { ...init, headers });
|
|
686
|
+
}
|
|
687
|
+
return response;
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
var singleton;
|
|
691
|
+
async function x402Fetch(input, init) {
|
|
692
|
+
if (!singleton) singleton = createX402Fetch();
|
|
693
|
+
return singleton(input, init);
|
|
694
|
+
}
|
|
695
|
+
function resolveRelativeUrl(url) {
|
|
696
|
+
const loc = globalThis.location;
|
|
697
|
+
if (loc?.href) {
|
|
698
|
+
return new URL(url, loc.href).origin;
|
|
699
|
+
}
|
|
700
|
+
return "unknown";
|
|
701
|
+
}
|
|
702
|
+
function extractOrigin(input) {
|
|
703
|
+
if (input instanceof URL) return input.origin;
|
|
704
|
+
if (typeof input === "string") {
|
|
705
|
+
try {
|
|
706
|
+
return new URL(input).origin;
|
|
707
|
+
} catch {
|
|
708
|
+
try {
|
|
709
|
+
return resolveRelativeUrl(input);
|
|
710
|
+
} catch {
|
|
711
|
+
return "unknown";
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
return new URL(input.url).origin;
|
|
717
|
+
} catch {
|
|
718
|
+
try {
|
|
719
|
+
return resolveRelativeUrl(input.url);
|
|
720
|
+
} catch {
|
|
721
|
+
return "unknown";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
561
726
|
// src/limits.ts
|
|
562
727
|
var BFG_DAILY_CEILING_SATOSHIS = 1e10;
|
|
563
728
|
var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
|
|
@@ -859,41 +1024,6 @@ var RateLimiter = class {
|
|
|
859
1024
|
}
|
|
860
1025
|
};
|
|
861
1026
|
|
|
862
|
-
// src/site-policy.ts
|
|
863
|
-
var defaultSitePrompt = async (origin) => {
|
|
864
|
-
if (typeof globalThis.confirm !== "function") return "global";
|
|
865
|
-
const allow = globalThis.confirm(
|
|
866
|
-
`First payment to ${origin}.
|
|
867
|
-
|
|
868
|
-
Use your global spending limits for this site?
|
|
869
|
-
|
|
870
|
-
OK = Use global limits
|
|
871
|
-
Cancel = Block this site`
|
|
872
|
-
);
|
|
873
|
-
return allow ? "global" : "block";
|
|
874
|
-
};
|
|
875
|
-
async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
|
|
876
|
-
const existing = limits.sitePolicies[origin];
|
|
877
|
-
if (existing) return existing;
|
|
878
|
-
if (!limits.requirePerSitePrompt) {
|
|
879
|
-
return { origin, action: "global" };
|
|
880
|
-
}
|
|
881
|
-
if (limits.require2fa.onNewSiteApproval) {
|
|
882
|
-
if (!twoFactorProvider) {
|
|
883
|
-
return { origin, action: "block" };
|
|
884
|
-
}
|
|
885
|
-
const verified = await twoFactorProvider.verify({
|
|
886
|
-
type: "new-site-approval",
|
|
887
|
-
origin
|
|
888
|
-
});
|
|
889
|
-
if (!verified) {
|
|
890
|
-
return { origin, action: "block" };
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
const action = await promptFn(origin);
|
|
894
|
-
return { origin, action };
|
|
895
|
-
}
|
|
896
|
-
|
|
897
1027
|
// src/storage.ts
|
|
898
1028
|
var STATE_KEY = "x402:limit-state";
|
|
899
1029
|
var POLICIES_KEY = "x402:site-policies";
|
|
@@ -968,319 +1098,6 @@ var LocalStorageAdapter = class {
|
|
|
968
1098
|
}
|
|
969
1099
|
};
|
|
970
1100
|
|
|
971
|
-
// src/x402-fetch.ts
|
|
972
|
-
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
973
|
-
function base58DecodeCheck(address) {
|
|
974
|
-
let leadingZeros = 0;
|
|
975
|
-
for (const c of address) {
|
|
976
|
-
if (c === "1") leadingZeros++;
|
|
977
|
-
else break;
|
|
978
|
-
}
|
|
979
|
-
let n = BigInt(0);
|
|
980
|
-
for (const c of address) {
|
|
981
|
-
const i = BASE58_ALPHABET.indexOf(c);
|
|
982
|
-
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
983
|
-
n = n * 58n + BigInt(i);
|
|
984
|
-
}
|
|
985
|
-
const hexFromBigint = n === 0n ? "" : n.toString(16);
|
|
986
|
-
const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
|
|
987
|
-
const bigintBytes = [];
|
|
988
|
-
for (let i = 0; i < paddedHex.length; i += 2) {
|
|
989
|
-
bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
|
|
990
|
-
}
|
|
991
|
-
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
992
|
-
allBytes.set(bigintBytes, leadingZeros);
|
|
993
|
-
if (allBytes.length !== 25) {
|
|
994
|
-
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
995
|
-
}
|
|
996
|
-
const body = allBytes.slice(0, 21);
|
|
997
|
-
const checksum = allBytes.slice(21);
|
|
998
|
-
const version = allBytes[0];
|
|
999
|
-
if (version !== 0 && version !== 111) {
|
|
1000
|
-
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
1001
|
-
}
|
|
1002
|
-
return { version, payload: body.slice(1) };
|
|
1003
|
-
}
|
|
1004
|
-
function payeeAddressToLockingScript(address) {
|
|
1005
|
-
const { payload } = base58DecodeCheck(address);
|
|
1006
|
-
if (payload.length !== 20) {
|
|
1007
|
-
throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
|
|
1008
|
-
}
|
|
1009
|
-
const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1010
|
-
return `76a914${pubkeyHash}88ac`;
|
|
1011
|
-
}
|
|
1012
|
-
async function defaultConstructProof(challenge) {
|
|
1013
|
-
const cwi = globalThis.CWI;
|
|
1014
|
-
if (!cwi || typeof cwi.createAction !== "function") {
|
|
1015
|
-
throw new Error(
|
|
1016
|
-
"No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
const result = await cwi.createAction({
|
|
1020
|
-
description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
|
|
1021
|
-
outputs: [{
|
|
1022
|
-
satoshis: challenge.amount,
|
|
1023
|
-
lockingScript: payeeAddressToLockingScript(challenge.payee),
|
|
1024
|
-
description: `Payment to ${challenge.payee}`
|
|
1025
|
-
}],
|
|
1026
|
-
labels: ["x402-payment"],
|
|
1027
|
-
options: {
|
|
1028
|
-
returnTXIDOnly: false,
|
|
1029
|
-
noSend: false
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1032
|
-
if (!result || !result.txid) {
|
|
1033
|
-
throw new Error("Wallet declined payment or returned invalid result");
|
|
1034
|
-
}
|
|
1035
|
-
if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
|
|
1036
|
-
throw new Error("Wallet did not return raw transaction");
|
|
1037
|
-
}
|
|
1038
|
-
return {
|
|
1039
|
-
txid: result.txid,
|
|
1040
|
-
rawTx: result.rawTx
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
function createMutex() {
|
|
1044
|
-
let chain = Promise.resolve();
|
|
1045
|
-
return (fn) => {
|
|
1046
|
-
const result = chain.then(fn, fn);
|
|
1047
|
-
chain = result.then(() => {
|
|
1048
|
-
}, () => {
|
|
1049
|
-
});
|
|
1050
|
-
return result;
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
function createX402Fetch(config = {}) {
|
|
1054
|
-
const tier = config.tier ?? "Hey, Not Too Rough";
|
|
1055
|
-
const mode = config.mode ?? "interactive";
|
|
1056
|
-
if (tier === "Nightmare!" && config.nightmareConfirmation !== "NIGHTMARE") {
|
|
1057
|
-
throw new Error('Nightmare! tier requires nightmareConfirmation: "NIGHTMARE"');
|
|
1058
|
-
}
|
|
1059
|
-
const limits = resolveSpendLimits(tier, mode, config.limits);
|
|
1060
|
-
const storage = config.storage ?? new LocalStorageAdapter();
|
|
1061
|
-
const twoFactor = config.twoFactorProvider;
|
|
1062
|
-
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
1063
|
-
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
1064
|
-
const brc105Wallet = config.brc105Wallet;
|
|
1065
|
-
const nowFn = config.now ?? Date.now;
|
|
1066
|
-
const mutex = createMutex();
|
|
1067
|
-
const needs2fa = limits.require2fa;
|
|
1068
|
-
if (!twoFactor && (needs2fa.onCircuitBreakerReset || needs2fa.onHighValueTx || needs2fa.onNewSiteApproval || needs2fa.onTierChange)) {
|
|
1069
|
-
console.warn("x402: tier requires 2FA but no twoFactorProvider configured \u2014 2FA-gated actions will be blocked");
|
|
1070
|
-
}
|
|
1071
|
-
let limiter;
|
|
1072
|
-
let initialised = false;
|
|
1073
|
-
async function ensureInitialised() {
|
|
1074
|
-
if (limiter && initialised) return limiter;
|
|
1075
|
-
const state = await storage.load();
|
|
1076
|
-
limiter = new RateLimiter(limits, state ?? void 0, nowFn);
|
|
1077
|
-
const policies = await storage.loadSitePolicies();
|
|
1078
|
-
Object.assign(limits.sitePolicies, policies);
|
|
1079
|
-
initialised = true;
|
|
1080
|
-
return limiter;
|
|
1081
|
-
}
|
|
1082
|
-
async function persist(rl) {
|
|
1083
|
-
await storage.save(rl.getState());
|
|
1084
|
-
await storage.saveSitePolicies(limits.sitePolicies);
|
|
1085
|
-
}
|
|
1086
|
-
async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
|
|
1087
|
-
return mutex(async () => {
|
|
1088
|
-
const rl = await ensureInitialised();
|
|
1089
|
-
const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
|
|
1090
|
-
if (sitePolicy.action === "block") return originalResponse;
|
|
1091
|
-
if (!limits.sitePolicies[origin]) {
|
|
1092
|
-
limits.sitePolicies[origin] = sitePolicy;
|
|
1093
|
-
await storage.saveSitePolicies(limits.sitePolicies);
|
|
1094
|
-
}
|
|
1095
|
-
const spendCheckable = { amount, origin, protocol };
|
|
1096
|
-
const result = rl.check(spendCheckable, origin);
|
|
1097
|
-
if (result.action === "block") {
|
|
1098
|
-
if (result.severity === "trip") {
|
|
1099
|
-
rl.trip();
|
|
1100
|
-
await persist(rl);
|
|
1101
|
-
config.onLimitReached?.(result.reason);
|
|
1102
|
-
return originalResponse;
|
|
1103
|
-
}
|
|
1104
|
-
if (result.severity === "window" && twoFactor) {
|
|
1105
|
-
config.onLimitReached?.(result.reason);
|
|
1106
|
-
const override = await twoFactor.verify({
|
|
1107
|
-
type: "limit-override",
|
|
1108
|
-
amount,
|
|
1109
|
-
origin,
|
|
1110
|
-
reason: result.reason
|
|
1111
|
-
});
|
|
1112
|
-
if (override) {
|
|
1113
|
-
} else {
|
|
1114
|
-
return originalResponse;
|
|
1115
|
-
}
|
|
1116
|
-
} else {
|
|
1117
|
-
config.onLimitReached?.(result.reason);
|
|
1118
|
-
return originalResponse;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (result.action === "yellow-light") {
|
|
1122
|
-
const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
|
|
1123
|
-
if (!proceed) return originalResponse;
|
|
1124
|
-
}
|
|
1125
|
-
if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
|
|
1126
|
-
if (!twoFactor) return originalResponse;
|
|
1127
|
-
const verified = await twoFactor.verify({
|
|
1128
|
-
type: "high-value-tx",
|
|
1129
|
-
amount,
|
|
1130
|
-
origin
|
|
1131
|
-
});
|
|
1132
|
-
if (!verified) return originalResponse;
|
|
1133
|
-
}
|
|
1134
|
-
let proof;
|
|
1135
|
-
try {
|
|
1136
|
-
proof = await buildProof();
|
|
1137
|
-
} catch (err) {
|
|
1138
|
-
console.error(`[x402] Proof construction failed (${protocol}):`, err);
|
|
1139
|
-
config.onProofError?.(err, protocol);
|
|
1140
|
-
return originalResponse;
|
|
1141
|
-
}
|
|
1142
|
-
rl.record(makeLedgerEntry(proof));
|
|
1143
|
-
await persist(rl);
|
|
1144
|
-
const maxAttempts = 3;
|
|
1145
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1146
|
-
try {
|
|
1147
|
-
return await retryWithProof(proof);
|
|
1148
|
-
} catch (err) {
|
|
1149
|
-
if (attempt >= maxAttempts) {
|
|
1150
|
-
console.error(`[x402] Paid request failed after ${maxAttempts} attempts (${protocol}):`, err);
|
|
1151
|
-
return originalResponse;
|
|
1152
|
-
}
|
|
1153
|
-
await new Promise((r) => setTimeout(r, 250 * Math.pow(2, attempt)));
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
return originalResponse;
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
const fetchFn = async function x402Fetch2(input, init) {
|
|
1160
|
-
const response = await fetch(input, init);
|
|
1161
|
-
if (response.status !== 402) return response;
|
|
1162
|
-
const origin = extractOrigin(input);
|
|
1163
|
-
const challengeHeader = response.headers.get("X402-Challenge");
|
|
1164
|
-
if (challengeHeader) {
|
|
1165
|
-
let challenge;
|
|
1166
|
-
try {
|
|
1167
|
-
challenge = parseChallenge(challengeHeader);
|
|
1168
|
-
} catch {
|
|
1169
|
-
return response;
|
|
1170
|
-
}
|
|
1171
|
-
return handlePaymentFlow(
|
|
1172
|
-
response,
|
|
1173
|
-
input,
|
|
1174
|
-
init,
|
|
1175
|
-
origin,
|
|
1176
|
-
challenge.amount,
|
|
1177
|
-
"x402",
|
|
1178
|
-
async () => constructProof(challenge),
|
|
1179
|
-
(proof) => {
|
|
1180
|
-
const headers = new Headers(init?.headers);
|
|
1181
|
-
headers.set("X402-Proof", JSON.stringify(proof));
|
|
1182
|
-
return fetch(input, { ...init, headers });
|
|
1183
|
-
},
|
|
1184
|
-
(proof) => ({
|
|
1185
|
-
timestamp: nowFn(),
|
|
1186
|
-
origin,
|
|
1187
|
-
satoshis: challenge.amount,
|
|
1188
|
-
txid: proof.txid
|
|
1189
|
-
})
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
1193
|
-
if (brc105Version) {
|
|
1194
|
-
if (!brc105ProofConstructor && !brc105Wallet) return response;
|
|
1195
|
-
let brc105Challenge;
|
|
1196
|
-
try {
|
|
1197
|
-
brc105Challenge = parseBrc105Challenge(response);
|
|
1198
|
-
} catch {
|
|
1199
|
-
return response;
|
|
1200
|
-
}
|
|
1201
|
-
return handlePaymentFlow(
|
|
1202
|
-
response,
|
|
1203
|
-
input,
|
|
1204
|
-
init,
|
|
1205
|
-
origin,
|
|
1206
|
-
brc105Challenge.satoshisRequired,
|
|
1207
|
-
"brc105",
|
|
1208
|
-
async () => {
|
|
1209
|
-
if (brc105ProofConstructor) {
|
|
1210
|
-
return brc105ProofConstructor(brc105Challenge);
|
|
1211
|
-
}
|
|
1212
|
-
return constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
1213
|
-
},
|
|
1214
|
-
(proof) => {
|
|
1215
|
-
const headers = new Headers(init?.headers);
|
|
1216
|
-
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
1217
|
-
headers.set("x-bsv-auth-identity-key", proof.clientIdentityKey);
|
|
1218
|
-
return fetch(input, { ...init, headers });
|
|
1219
|
-
},
|
|
1220
|
-
(proof) => ({
|
|
1221
|
-
timestamp: nowFn(),
|
|
1222
|
-
origin,
|
|
1223
|
-
satoshis: brc105Challenge.satoshisRequired,
|
|
1224
|
-
txid: proof.txid,
|
|
1225
|
-
protocol: "brc105"
|
|
1226
|
-
})
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
return response;
|
|
1230
|
-
};
|
|
1231
|
-
fetchFn.resetLimits = async () => {
|
|
1232
|
-
const rl = await ensureInitialised();
|
|
1233
|
-
if (limits.require2fa.onCircuitBreakerReset) {
|
|
1234
|
-
if (!twoFactor) throw new Error("2FA required for circuit breaker reset but no twoFactorProvider configured");
|
|
1235
|
-
const verified = await twoFactor.verify({ type: "circuit-breaker-reset" });
|
|
1236
|
-
if (!verified) throw new Error("2FA verification failed for circuit breaker reset");
|
|
1237
|
-
}
|
|
1238
|
-
rl.reset();
|
|
1239
|
-
await persist(rl);
|
|
1240
|
-
};
|
|
1241
|
-
fetchFn.getState = () => {
|
|
1242
|
-
if (!limiter) return { entries: [], circuitBroken: false };
|
|
1243
|
-
const state = limiter.getState();
|
|
1244
|
-
return { entries: state.entries, circuitBroken: state.circuitBroken };
|
|
1245
|
-
};
|
|
1246
|
-
return fetchFn;
|
|
1247
|
-
}
|
|
1248
|
-
var singleton;
|
|
1249
|
-
async function x402Fetch(input, init) {
|
|
1250
|
-
if (!singleton) singleton = createX402Fetch();
|
|
1251
|
-
return singleton(input, init);
|
|
1252
|
-
}
|
|
1253
|
-
function resolveRelativeUrl(url) {
|
|
1254
|
-
const loc = globalThis.location;
|
|
1255
|
-
if (loc?.href) {
|
|
1256
|
-
return new URL(url, loc.href).origin;
|
|
1257
|
-
}
|
|
1258
|
-
return "unknown";
|
|
1259
|
-
}
|
|
1260
|
-
function extractOrigin(input) {
|
|
1261
|
-
if (input instanceof URL) return input.origin;
|
|
1262
|
-
if (typeof input === "string") {
|
|
1263
|
-
try {
|
|
1264
|
-
return new URL(input).origin;
|
|
1265
|
-
} catch {
|
|
1266
|
-
try {
|
|
1267
|
-
return resolveRelativeUrl(input);
|
|
1268
|
-
} catch {
|
|
1269
|
-
return "unknown";
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
try {
|
|
1274
|
-
return new URL(input.url).origin;
|
|
1275
|
-
} catch {
|
|
1276
|
-
try {
|
|
1277
|
-
return resolveRelativeUrl(input.url);
|
|
1278
|
-
} catch {
|
|
1279
|
-
return "unknown";
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
1101
|
// src/two-factor.ts
|
|
1285
1102
|
var WalletTwoFactorProvider = class {
|
|
1286
1103
|
async verify(action) {
|
|
@@ -1323,6 +1140,41 @@ function describeAction(action) {
|
|
|
1323
1140
|
Allow this payment of ${action.amount} sats to ${action.origin}?`;
|
|
1324
1141
|
}
|
|
1325
1142
|
}
|
|
1143
|
+
|
|
1144
|
+
// src/site-policy.ts
|
|
1145
|
+
var defaultSitePrompt = async (origin) => {
|
|
1146
|
+
if (typeof globalThis.confirm !== "function") return "global";
|
|
1147
|
+
const allow = globalThis.confirm(
|
|
1148
|
+
`First payment to ${origin}.
|
|
1149
|
+
|
|
1150
|
+
Use your global spending limits for this site?
|
|
1151
|
+
|
|
1152
|
+
OK = Use global limits
|
|
1153
|
+
Cancel = Block this site`
|
|
1154
|
+
);
|
|
1155
|
+
return allow ? "global" : "block";
|
|
1156
|
+
};
|
|
1157
|
+
async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
|
|
1158
|
+
const existing = limits.sitePolicies[origin];
|
|
1159
|
+
if (existing) return existing;
|
|
1160
|
+
if (!limits.requirePerSitePrompt) {
|
|
1161
|
+
return { origin, action: "global" };
|
|
1162
|
+
}
|
|
1163
|
+
if (limits.require2fa.onNewSiteApproval) {
|
|
1164
|
+
if (!twoFactorProvider) {
|
|
1165
|
+
return { origin, action: "block" };
|
|
1166
|
+
}
|
|
1167
|
+
const verified = await twoFactorProvider.verify({
|
|
1168
|
+
type: "new-site-approval",
|
|
1169
|
+
origin
|
|
1170
|
+
});
|
|
1171
|
+
if (!verified) {
|
|
1172
|
+
return { origin, action: "block" };
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const action = await promptFn(origin);
|
|
1176
|
+
return { origin, action };
|
|
1177
|
+
}
|
|
1326
1178
|
export {
|
|
1327
1179
|
BFG_DAILY_CEILING_SATOSHIS,
|
|
1328
1180
|
BFG_PER_TX_CEILING_SATOSHIS,
|