bsv-x402 0.5.0 → 0.7.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 +125 -466
- package/dist/index.d.cts +56 -145
- package/dist/index.d.ts +56 -145
- package/dist/index.js +117 -458
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -20,18 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
PICKUP_PERCENTAGES: () => PICKUP_PERCENTAGES,
|
|
24
|
+
TIER_CAPS: () => TIER_CAPS,
|
|
25
|
+
WEAPON_CAPS: () => WEAPON_CAPS,
|
|
26
|
+
applyPickup: () => applyPickup,
|
|
27
|
+
checkPayment: () => checkPayment,
|
|
28
|
+
clampBalanceToTier: () => clampBalanceToTier,
|
|
29
29
|
constructBrc105Proof: () => constructBrc105Proof,
|
|
30
30
|
createX402Fetch: () => createX402Fetch,
|
|
31
|
+
initialState: () => initialState,
|
|
31
32
|
parseBrc105Challenge: () => parseBrc105Challenge,
|
|
32
33
|
parseChallenge: () => parseChallenge,
|
|
33
|
-
|
|
34
|
-
resolveSpendLimits: () => resolveSpendLimits,
|
|
34
|
+
recordPayment: () => recordPayment,
|
|
35
35
|
x402Fetch: () => x402Fetch
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -544,6 +544,8 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
544
544
|
})
|
|
545
545
|
}],
|
|
546
546
|
options: {
|
|
547
|
+
returnTXIDOnly: false,
|
|
548
|
+
noSend: true,
|
|
547
549
|
randomizeOutputs: false
|
|
548
550
|
}
|
|
549
551
|
});
|
|
@@ -555,13 +557,21 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
555
557
|
} else {
|
|
556
558
|
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
557
559
|
}
|
|
558
|
-
|
|
560
|
+
const proof = {
|
|
559
561
|
derivationPrefix: challenge.derivationPrefix,
|
|
560
562
|
derivationSuffix,
|
|
561
563
|
transaction: transactionBase64,
|
|
562
564
|
clientIdentityKey,
|
|
563
565
|
txid: result.txid
|
|
564
566
|
};
|
|
567
|
+
const abort = wallet.abortAction ? async () => {
|
|
568
|
+
try {
|
|
569
|
+
await wallet.abortAction({ reference: result.txid });
|
|
570
|
+
} catch (err) {
|
|
571
|
+
console.warn("[x402] abortAction failed:", err);
|
|
572
|
+
}
|
|
573
|
+
} : void 0;
|
|
574
|
+
return { proof, abort };
|
|
565
575
|
}
|
|
566
576
|
|
|
567
577
|
// src/challenge.ts
|
|
@@ -597,6 +607,22 @@ function parseChallenge(header) {
|
|
|
597
607
|
}
|
|
598
608
|
|
|
599
609
|
// src/x402-fetch.ts
|
|
610
|
+
function bytesToBase642(bytes) {
|
|
611
|
+
let binary = "";
|
|
612
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
613
|
+
return btoa(binary);
|
|
614
|
+
}
|
|
615
|
+
function numberArrayToBase642(arr) {
|
|
616
|
+
return bytesToBase642(new Uint8Array(arr));
|
|
617
|
+
}
|
|
618
|
+
function hexToBytes2(hex) {
|
|
619
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
620
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
621
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
622
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
623
|
+
}
|
|
624
|
+
return bytes;
|
|
625
|
+
}
|
|
600
626
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
601
627
|
function base58DecodeCheck(address) {
|
|
602
628
|
let leadingZeros = 0;
|
|
@@ -660,12 +686,17 @@ async function defaultConstructProof(challenge) {
|
|
|
660
686
|
if (!result || !result.txid) {
|
|
661
687
|
throw new Error("Wallet declined payment or returned invalid result");
|
|
662
688
|
}
|
|
663
|
-
|
|
664
|
-
|
|
689
|
+
let beef;
|
|
690
|
+
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
691
|
+
beef = numberArrayToBase642(result.tx);
|
|
692
|
+
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
693
|
+
beef = bytesToBase642(hexToBytes2(result.rawTx));
|
|
694
|
+
} else {
|
|
695
|
+
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
665
696
|
}
|
|
666
697
|
return {
|
|
667
698
|
txid: result.txid,
|
|
668
|
-
|
|
699
|
+
beef
|
|
669
700
|
};
|
|
670
701
|
}
|
|
671
702
|
function createX402Fetch(config = {}) {
|
|
@@ -706,11 +737,16 @@ function createX402Fetch(config = {}) {
|
|
|
706
737
|
return response;
|
|
707
738
|
}
|
|
708
739
|
let proof;
|
|
740
|
+
let abort;
|
|
709
741
|
try {
|
|
710
742
|
if (brc105ProofConstructor) {
|
|
711
|
-
|
|
743
|
+
const result = await brc105ProofConstructor(brc105Challenge);
|
|
744
|
+
proof = result.proof;
|
|
745
|
+
abort = result.abort;
|
|
712
746
|
} else {
|
|
713
|
-
|
|
747
|
+
const result = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
748
|
+
proof = result.proof;
|
|
749
|
+
abort = result.abort;
|
|
714
750
|
}
|
|
715
751
|
} catch (err) {
|
|
716
752
|
console.error("[x402] Proof construction failed (brc105):", err);
|
|
@@ -720,7 +756,21 @@ function createX402Fetch(config = {}) {
|
|
|
720
756
|
const headers = new Headers(init?.headers);
|
|
721
757
|
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
722
758
|
headers.set("x-bsv-auth-identity-key", proof.clientIdentityKey);
|
|
723
|
-
|
|
759
|
+
let retryResponse;
|
|
760
|
+
try {
|
|
761
|
+
retryResponse = await fetch(input, { ...init, headers });
|
|
762
|
+
} catch (err) {
|
|
763
|
+
if (abort) {
|
|
764
|
+
await abort();
|
|
765
|
+
console.warn("[x402] BRC-105 retry fetch failed, UTXOs released via abortAction");
|
|
766
|
+
}
|
|
767
|
+
throw err;
|
|
768
|
+
}
|
|
769
|
+
if (!retryResponse.ok && abort) {
|
|
770
|
+
await abort();
|
|
771
|
+
console.warn("[x402] Server rejected BRC-105 payment, UTXOs released via abortAction");
|
|
772
|
+
}
|
|
773
|
+
return retryResponse;
|
|
724
774
|
}
|
|
725
775
|
return response;
|
|
726
776
|
};
|
|
@@ -761,471 +811,80 @@ function extractOrigin(input) {
|
|
|
761
811
|
}
|
|
762
812
|
}
|
|
763
813
|
|
|
764
|
-
// src/
|
|
765
|
-
var
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
function makeLimits(windows, perTxMaxSatoshis, opts = {}) {
|
|
777
|
-
return {
|
|
778
|
-
windows,
|
|
779
|
-
perTxMaxSatoshis,
|
|
780
|
-
yellowLightThreshold: 0.8,
|
|
781
|
-
requirePerSitePrompt: false,
|
|
782
|
-
sitePolicies: {},
|
|
783
|
-
require2fa: {
|
|
784
|
-
onCircuitBreakerReset: true,
|
|
785
|
-
onTierChange: true,
|
|
786
|
-
onHighValueTx: false,
|
|
787
|
-
highValueThreshold: 0,
|
|
788
|
-
onNewSiteApproval: false
|
|
789
|
-
},
|
|
790
|
-
...opts
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
var TOO_YOUNG_TO_DIE = {
|
|
794
|
-
interactive: makeLimits(
|
|
795
|
-
[{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
|
|
796
|
-
1e8
|
|
797
|
-
),
|
|
798
|
-
programmatic: makeLimits(
|
|
799
|
-
[{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
|
|
800
|
-
1e6
|
|
801
|
-
)
|
|
802
|
-
};
|
|
803
|
-
var HEY_NOT_TOO_ROUGH = {
|
|
804
|
-
interactive: makeLimits(
|
|
805
|
-
[
|
|
806
|
-
{ window: "day", maxSatoshis: 1e8, maxTransactions: 100 },
|
|
807
|
-
{ window: "week", maxSatoshis: 5e8, maxTransactions: 500 }
|
|
808
|
-
],
|
|
809
|
-
1e7,
|
|
810
|
-
{
|
|
811
|
-
requirePerSitePrompt: true,
|
|
812
|
-
require2fa: {
|
|
813
|
-
onCircuitBreakerReset: true,
|
|
814
|
-
onTierChange: true,
|
|
815
|
-
onHighValueTx: true,
|
|
816
|
-
highValueThreshold: 5e7,
|
|
817
|
-
onNewSiteApproval: true
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
),
|
|
821
|
-
programmatic: makeLimits(
|
|
822
|
-
[
|
|
823
|
-
{ window: "day", maxSatoshis: 1e8, maxTransactions: 1e4 },
|
|
824
|
-
{ window: "week", maxSatoshis: 5e8, maxTransactions: 5e4 }
|
|
825
|
-
],
|
|
826
|
-
1e5,
|
|
827
|
-
{
|
|
828
|
-
requirePerSitePrompt: true,
|
|
829
|
-
require2fa: {
|
|
830
|
-
onCircuitBreakerReset: true,
|
|
831
|
-
onTierChange: true,
|
|
832
|
-
onHighValueTx: true,
|
|
833
|
-
highValueThreshold: 5e7,
|
|
834
|
-
onNewSiteApproval: true
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
)
|
|
838
|
-
};
|
|
839
|
-
var HURT_ME_PLENTY = {
|
|
840
|
-
interactive: makeLimits(
|
|
841
|
-
[
|
|
842
|
-
{ window: "minute", maxSatoshis: 5e6, maxTransactions: 10 },
|
|
843
|
-
{ window: "hour", maxSatoshis: 5e7, maxTransactions: 60 },
|
|
844
|
-
{ window: "day", maxSatoshis: 2e8, maxTransactions: 200 },
|
|
845
|
-
{ window: "week", maxSatoshis: 1e9, maxTransactions: 1e3 }
|
|
846
|
-
],
|
|
847
|
-
2e7,
|
|
848
|
-
{
|
|
849
|
-
requirePerSitePrompt: true,
|
|
850
|
-
require2fa: {
|
|
851
|
-
onCircuitBreakerReset: true,
|
|
852
|
-
onTierChange: true,
|
|
853
|
-
onHighValueTx: true,
|
|
854
|
-
highValueThreshold: 1e8,
|
|
855
|
-
onNewSiteApproval: true
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
),
|
|
859
|
-
programmatic: makeLimits(
|
|
860
|
-
[
|
|
861
|
-
{ window: "minute", maxSatoshis: 5e6, maxTransactions: 1e3 },
|
|
862
|
-
{ window: "hour", maxSatoshis: 5e7, maxTransactions: 6e3 },
|
|
863
|
-
{ window: "day", maxSatoshis: 2e8, maxTransactions: 2e4 },
|
|
864
|
-
{ window: "week", maxSatoshis: 1e9, maxTransactions: 1e5 }
|
|
865
|
-
],
|
|
866
|
-
2e5,
|
|
867
|
-
{
|
|
868
|
-
requirePerSitePrompt: true,
|
|
869
|
-
require2fa: {
|
|
870
|
-
onCircuitBreakerReset: true,
|
|
871
|
-
onTierChange: true,
|
|
872
|
-
onHighValueTx: true,
|
|
873
|
-
highValueThreshold: 1e8,
|
|
874
|
-
onNewSiteApproval: true
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
)
|
|
878
|
-
};
|
|
879
|
-
var ULTRA_VIOLENCE = {
|
|
880
|
-
interactive: makeLimits(
|
|
881
|
-
[...HURT_ME_PLENTY.interactive.windows],
|
|
882
|
-
HURT_ME_PLENTY.interactive.perTxMaxSatoshis,
|
|
883
|
-
{
|
|
884
|
-
requirePerSitePrompt: true,
|
|
885
|
-
require2fa: {
|
|
886
|
-
onCircuitBreakerReset: true,
|
|
887
|
-
onTierChange: true,
|
|
888
|
-
onHighValueTx: false,
|
|
889
|
-
highValueThreshold: 0,
|
|
890
|
-
onNewSiteApproval: true
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
),
|
|
894
|
-
programmatic: makeLimits(
|
|
895
|
-
[...HURT_ME_PLENTY.programmatic.windows],
|
|
896
|
-
HURT_ME_PLENTY.programmatic.perTxMaxSatoshis,
|
|
897
|
-
{
|
|
898
|
-
requirePerSitePrompt: true,
|
|
899
|
-
require2fa: {
|
|
900
|
-
onCircuitBreakerReset: true,
|
|
901
|
-
onTierChange: true,
|
|
902
|
-
onHighValueTx: false,
|
|
903
|
-
highValueThreshold: 0,
|
|
904
|
-
onNewSiteApproval: true
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
)
|
|
814
|
+
// src/autospend.ts
|
|
815
|
+
var TIER_CAPS = {
|
|
816
|
+
"I'm Too Young to Die": 1e6,
|
|
817
|
+
// 0.01 BSV
|
|
818
|
+
"Hey, Not Too Rough": 1e7,
|
|
819
|
+
// 0.1 BSV
|
|
820
|
+
"Hurt Me Plenty": 1e8,
|
|
821
|
+
// 1 BSV
|
|
822
|
+
"Ultra-Violence": 1e9,
|
|
823
|
+
// 10 BSV
|
|
824
|
+
"Nightmare!": 1e11
|
|
825
|
+
// 100 BSV
|
|
908
826
|
};
|
|
909
|
-
var
|
|
910
|
-
|
|
911
|
-
|
|
827
|
+
var WEAPON_CAPS = {
|
|
828
|
+
"Fists": 1e5,
|
|
829
|
+
"Chainsaw": 25e4,
|
|
830
|
+
"Pistol": 5e5,
|
|
831
|
+
"Shotgun": 1e6,
|
|
832
|
+
"Super Shotgun": 1e7,
|
|
833
|
+
"Chaingun": 5e7,
|
|
834
|
+
"Rocket Launcher": 25e7,
|
|
835
|
+
"Plasma Rifle": 1e9,
|
|
836
|
+
"BFG9000": Infinity
|
|
912
837
|
};
|
|
913
|
-
var
|
|
914
|
-
"
|
|
915
|
-
"
|
|
916
|
-
"
|
|
917
|
-
"
|
|
918
|
-
|
|
838
|
+
var PICKUP_PERCENTAGES = {
|
|
839
|
+
"Medkit": 0.1,
|
|
840
|
+
"Stimpak": 0.25,
|
|
841
|
+
"Soul Sphere": 1,
|
|
842
|
+
"New Game": 1
|
|
843
|
+
// hard-set to 100% (not additive)
|
|
919
844
|
};
|
|
920
|
-
function
|
|
921
|
-
return
|
|
922
|
-
...preset,
|
|
923
|
-
windows: preset.windows.map((w) => ({ ...w })),
|
|
924
|
-
require2fa: { ...preset.require2fa },
|
|
925
|
-
sitePolicies: { ...preset.sitePolicies }
|
|
926
|
-
};
|
|
845
|
+
function isValidAmount(amount) {
|
|
846
|
+
return Number.isFinite(amount) && amount > 0;
|
|
927
847
|
}
|
|
928
|
-
function
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
return
|
|
933
|
-
...base,
|
|
934
|
-
...overrides,
|
|
935
|
-
// Deep-merge require2fa if provided
|
|
936
|
-
require2fa: overrides.require2fa ? { ...base.require2fa, ...overrides.require2fa } : base.require2fa,
|
|
937
|
-
// Deep-merge sitePolicies if provided
|
|
938
|
-
sitePolicies: overrides.sitePolicies ? { ...base.sitePolicies, ...overrides.sitePolicies } : base.sitePolicies
|
|
939
|
-
};
|
|
848
|
+
function checkPayment(amount, state, config) {
|
|
849
|
+
if (!isValidAmount(amount)) return "confirm";
|
|
850
|
+
const perTxMax = WEAPON_CAPS[config.weapon];
|
|
851
|
+
const effectiveMax = Math.min(perTxMax, state.balance);
|
|
852
|
+
return amount <= effectiveMax ? "auto" : "confirm";
|
|
940
853
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
this.entries = state?.entries ?? [];
|
|
945
|
-
this.broken = state?.circuitBroken ?? false;
|
|
946
|
-
this.now = now ?? Date.now;
|
|
947
|
-
}
|
|
948
|
-
check(request, origin) {
|
|
949
|
-
if (this.broken) {
|
|
950
|
-
return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
|
|
951
|
-
}
|
|
952
|
-
const amount = request.amount;
|
|
953
|
-
if (!Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
|
|
954
|
-
return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
|
|
955
|
-
}
|
|
956
|
-
if (amount > BFG_PER_TX_CEILING_SATOSHIS) {
|
|
957
|
-
return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
|
|
958
|
-
}
|
|
959
|
-
const dayAgo = this.now() - WINDOW_MS.day;
|
|
960
|
-
const dailyTotal = this.sumSatoshis(dayAgo);
|
|
961
|
-
if (dailyTotal + amount > BFG_DAILY_CEILING_SATOSHIS) {
|
|
962
|
-
return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
|
|
963
|
-
}
|
|
964
|
-
if (amount > this.limits.perTxMaxSatoshis) {
|
|
965
|
-
return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
|
|
966
|
-
}
|
|
967
|
-
const isCustomSite = this.hasCustomPolicy(origin);
|
|
968
|
-
const effectiveLimits = this.effectiveWindows(origin);
|
|
969
|
-
const effectivePerTx = this.effectivePerTxMax(origin);
|
|
970
|
-
if (effectivePerTx !== void 0 && amount > effectivePerTx) {
|
|
971
|
-
return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
|
|
972
|
-
}
|
|
973
|
-
let yellowLight;
|
|
974
|
-
for (const wl of effectiveLimits) {
|
|
975
|
-
const cutoff = this.now() - windowToMs(wl.window);
|
|
976
|
-
const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
|
|
977
|
-
const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
|
|
978
|
-
const totalTx = windowEntries.length;
|
|
979
|
-
if (totalSats + amount > wl.maxSatoshis) {
|
|
980
|
-
return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
|
|
981
|
-
}
|
|
982
|
-
if (totalTx + 1 > wl.maxTransactions) {
|
|
983
|
-
return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
|
|
984
|
-
}
|
|
985
|
-
if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
|
|
986
|
-
yellowLight = {
|
|
987
|
-
origin,
|
|
988
|
-
currentSpend: totalSats,
|
|
989
|
-
limit: wl.maxSatoshis,
|
|
990
|
-
window: wl.window,
|
|
991
|
-
challenge: request
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
if (yellowLight) {
|
|
996
|
-
return { action: "yellow-light", detail: yellowLight };
|
|
997
|
-
}
|
|
998
|
-
return { action: "allow" };
|
|
999
|
-
}
|
|
1000
|
-
record(entry) {
|
|
1001
|
-
this.entries.push(entry);
|
|
1002
|
-
this.prune();
|
|
1003
|
-
}
|
|
1004
|
-
trip() {
|
|
1005
|
-
this.broken = true;
|
|
1006
|
-
}
|
|
1007
|
-
reset() {
|
|
1008
|
-
this.broken = false;
|
|
1009
|
-
}
|
|
1010
|
-
isBroken() {
|
|
1011
|
-
return this.broken;
|
|
1012
|
-
}
|
|
1013
|
-
getState() {
|
|
1014
|
-
return {
|
|
1015
|
-
entries: [...this.entries],
|
|
1016
|
-
circuitBroken: this.broken,
|
|
1017
|
-
hmac: ""
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
hasCustomPolicy(origin) {
|
|
1021
|
-
const policy = this.limits.sitePolicies[origin];
|
|
1022
|
-
return policy?.action === "custom" && !!policy.limits;
|
|
1023
|
-
}
|
|
1024
|
-
effectiveWindows(origin) {
|
|
1025
|
-
const policy = this.limits.sitePolicies[origin];
|
|
1026
|
-
if (policy?.action === "custom" && policy.limits) {
|
|
1027
|
-
return policy.limits;
|
|
1028
|
-
}
|
|
1029
|
-
return this.limits.windows;
|
|
1030
|
-
}
|
|
1031
|
-
effectivePerTxMax(origin) {
|
|
1032
|
-
const policy = this.limits.sitePolicies[origin];
|
|
1033
|
-
if (policy?.action === "custom" && policy.perTxMaxSatoshis !== void 0) {
|
|
1034
|
-
return policy.perTxMaxSatoshis;
|
|
1035
|
-
}
|
|
1036
|
-
return void 0;
|
|
1037
|
-
}
|
|
1038
|
-
entriesInWindow(cutoff, filterOrigin) {
|
|
1039
|
-
return this.entries.filter(
|
|
1040
|
-
(e) => e.timestamp >= cutoff && (filterOrigin === void 0 || e.origin === filterOrigin)
|
|
1041
|
-
);
|
|
1042
|
-
}
|
|
1043
|
-
sumSatoshis(cutoff) {
|
|
1044
|
-
return this.entries.filter((e) => e.timestamp >= cutoff).reduce((sum, e) => sum + e.satoshis, 0);
|
|
1045
|
-
}
|
|
1046
|
-
prune() {
|
|
1047
|
-
let longestWindow = WINDOW_MS.day;
|
|
1048
|
-
for (const wl of this.limits.windows) {
|
|
1049
|
-
longestWindow = Math.max(longestWindow, windowToMs(wl.window));
|
|
1050
|
-
}
|
|
1051
|
-
if (this.limits.sitePolicies) {
|
|
1052
|
-
for (const policy of Object.values(this.limits.sitePolicies)) {
|
|
1053
|
-
if (policy?.action === "custom" && Array.isArray(policy.limits)) {
|
|
1054
|
-
for (const wl of policy.limits) {
|
|
1055
|
-
longestWindow = Math.max(longestWindow, windowToMs(wl.window));
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
const cutoff = this.now() - longestWindow;
|
|
1061
|
-
this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
|
|
1065
|
-
// src/storage.ts
|
|
1066
|
-
var STATE_KEY = "x402:limit-state";
|
|
1067
|
-
var POLICIES_KEY = "x402:site-policies";
|
|
1068
|
-
async function computeHmac(data, key) {
|
|
1069
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
1070
|
-
"raw",
|
|
1071
|
-
key,
|
|
1072
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
1073
|
-
false,
|
|
1074
|
-
["sign"]
|
|
1075
|
-
);
|
|
1076
|
-
const encoded = new TextEncoder().encode(data);
|
|
1077
|
-
const sig = await crypto.subtle.sign("HMAC", cryptoKey, encoded);
|
|
1078
|
-
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
854
|
+
function recordPayment(amount, state) {
|
|
855
|
+
if (!isValidAmount(amount)) return state;
|
|
856
|
+
return { balance: Math.max(0, state.balance - amount) };
|
|
1079
857
|
}
|
|
1080
|
-
function
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
858
|
+
function applyPickup(pickup, state, config, walletBalance) {
|
|
859
|
+
const tierCap = TIER_CAPS[config.tier];
|
|
860
|
+
const cap = Math.min(tierCap, walletBalance);
|
|
861
|
+
if (pickup === "New Game") {
|
|
862
|
+
return { balance: cap };
|
|
863
|
+
}
|
|
864
|
+
const bonus = Math.floor(tierCap * PICKUP_PERCENTAGES[pickup]);
|
|
865
|
+
return { balance: Math.min(cap, state.balance + bonus) };
|
|
1085
866
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
this.storage = storage ?? globalThis.localStorage;
|
|
1090
|
-
}
|
|
1091
|
-
async load() {
|
|
1092
|
-
const raw = this.storage.getItem(STATE_KEY);
|
|
1093
|
-
if (!raw) return null;
|
|
1094
|
-
let state;
|
|
1095
|
-
try {
|
|
1096
|
-
state = JSON.parse(raw);
|
|
1097
|
-
} catch {
|
|
1098
|
-
console.warn("x402: limit state JSON parse failed \u2014 treating as tampered");
|
|
1099
|
-
return { entries: [], circuitBroken: true, hmac: "" };
|
|
1100
|
-
}
|
|
1101
|
-
if (this.keyDeriver) {
|
|
1102
|
-
if (!state.hmac) {
|
|
1103
|
-
console.warn("x402: limit state missing HMAC \u2014 treating as tampered");
|
|
1104
|
-
return { entries: [], circuitBroken: true, hmac: "" };
|
|
1105
|
-
}
|
|
1106
|
-
const key = await this.keyDeriver();
|
|
1107
|
-
const expected = await computeHmac(serializeForHmac(state), key);
|
|
1108
|
-
if (expected !== state.hmac) {
|
|
1109
|
-
console.warn("x402: limit state HMAC mismatch \u2014 state may have been tampered with");
|
|
1110
|
-
return { entries: [], circuitBroken: true, hmac: "" };
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
state.entries = (Array.isArray(state.entries) ? state.entries : []).filter(
|
|
1114
|
-
(e) => e != null && typeof e.origin === "string" && typeof e.txid === "string" && typeof e.satoshis === "number" && Number.isFinite(e.satoshis) && e.satoshis >= 0 && typeof e.timestamp === "number" && Number.isFinite(e.timestamp) && e.timestamp > 0
|
|
1115
|
-
);
|
|
1116
|
-
return state;
|
|
1117
|
-
}
|
|
1118
|
-
async save(state) {
|
|
1119
|
-
if (this.keyDeriver) {
|
|
1120
|
-
const key = await this.keyDeriver();
|
|
1121
|
-
state.hmac = await computeHmac(serializeForHmac(state), key);
|
|
1122
|
-
}
|
|
1123
|
-
this.storage.setItem(STATE_KEY, JSON.stringify(state));
|
|
1124
|
-
}
|
|
1125
|
-
async loadSitePolicies() {
|
|
1126
|
-
const raw = this.storage.getItem(POLICIES_KEY);
|
|
1127
|
-
if (!raw) return {};
|
|
1128
|
-
try {
|
|
1129
|
-
return JSON.parse(raw);
|
|
1130
|
-
} catch {
|
|
1131
|
-
return {};
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
async saveSitePolicies(policies) {
|
|
1135
|
-
this.storage.setItem(POLICIES_KEY, JSON.stringify(policies));
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
|
|
1139
|
-
// src/two-factor.ts
|
|
1140
|
-
var WalletTwoFactorProvider = class {
|
|
1141
|
-
async verify(action) {
|
|
1142
|
-
if (typeof window === "undefined" || !window.CWI) {
|
|
1143
|
-
return this.promptFallback(action);
|
|
1144
|
-
}
|
|
1145
|
-
const challengeData = `x402-2fa:${JSON.stringify(action)}:${Date.now()}`;
|
|
1146
|
-
try {
|
|
1147
|
-
const sig = await window.CWI.createSignature({
|
|
1148
|
-
data: new TextEncoder().encode(challengeData),
|
|
1149
|
-
protocolID: [1, "x402-2fa"],
|
|
1150
|
-
keyID: "spending-limits"
|
|
1151
|
-
});
|
|
1152
|
-
return sig !== null;
|
|
1153
|
-
} catch {
|
|
1154
|
-
return false;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
promptFallback(action) {
|
|
1158
|
-
if (typeof window === "undefined" || !window.prompt) return false;
|
|
1159
|
-
const message = describeAction(action);
|
|
1160
|
-
const result = window.prompt(`${message}
|
|
1161
|
-
|
|
1162
|
-
Type CONFIRM to proceed:`);
|
|
1163
|
-
return result === "CONFIRM";
|
|
1164
|
-
}
|
|
1165
|
-
};
|
|
1166
|
-
function describeAction(action) {
|
|
1167
|
-
switch (action.type) {
|
|
1168
|
-
case "circuit-breaker-reset":
|
|
1169
|
-
return "Reset spending circuit breaker? This re-enables automated payments.";
|
|
1170
|
-
case "tier-change":
|
|
1171
|
-
return `Change spending tier from "${action.from}" to "${action.to}"?`;
|
|
1172
|
-
case "high-value-tx":
|
|
1173
|
-
return `Approve high-value payment of ${action.amount} sats to ${action.origin}?`;
|
|
1174
|
-
case "new-site-approval":
|
|
1175
|
-
return `Allow automated payments to ${action.origin}?`;
|
|
1176
|
-
case "limit-override":
|
|
1177
|
-
return `Spending limit reached: ${action.reason}
|
|
1178
|
-
Allow this payment of ${action.amount} sats to ${action.origin}?`;
|
|
1179
|
-
}
|
|
867
|
+
function clampBalanceToTier(state, config, walletBalance) {
|
|
868
|
+
const cap = Math.min(TIER_CAPS[config.tier], walletBalance);
|
|
869
|
+
return { balance: Math.min(state.balance, cap) };
|
|
1180
870
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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 };
|
|
871
|
+
function initialState(config, walletBalance) {
|
|
872
|
+
const cap = Math.min(TIER_CAPS[config.tier], walletBalance);
|
|
873
|
+
return { balance: cap };
|
|
1215
874
|
}
|
|
1216
875
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1217
876
|
0 && (module.exports = {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
877
|
+
PICKUP_PERCENTAGES,
|
|
878
|
+
TIER_CAPS,
|
|
879
|
+
WEAPON_CAPS,
|
|
880
|
+
applyPickup,
|
|
881
|
+
checkPayment,
|
|
882
|
+
clampBalanceToTier,
|
|
1224
883
|
constructBrc105Proof,
|
|
1225
884
|
createX402Fetch,
|
|
885
|
+
initialState,
|
|
1226
886
|
parseBrc105Challenge,
|
|
1227
887
|
parseChallenge,
|
|
1228
|
-
|
|
1229
|
-
resolveSpendLimits,
|
|
888
|
+
recordPayment,
|
|
1230
889
|
x402Fetch
|
|
1231
890
|
});
|