bsv-x402 0.4.1 → 0.6.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 +103 -642
- package/dist/index.d.cts +45 -159
- package/dist/index.d.ts +45 -159
- package/dist/index.js +95 -634
- package/package.json +1 -1
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);
|
|
@@ -596,416 +596,6 @@ function parseChallenge(header) {
|
|
|
596
596
|
};
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
-
// src/limits.ts
|
|
600
|
-
var BFG_DAILY_CEILING_SATOSHIS = 1e10;
|
|
601
|
-
var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
|
|
602
|
-
var WINDOW_MS = {
|
|
603
|
-
minute: 6e4,
|
|
604
|
-
hour: 36e5,
|
|
605
|
-
day: 864e5,
|
|
606
|
-
week: 6048e5
|
|
607
|
-
};
|
|
608
|
-
function windowToMs(window2) {
|
|
609
|
-
return WINDOW_MS[window2];
|
|
610
|
-
}
|
|
611
|
-
function makeLimits(windows, perTxMaxSatoshis, opts = {}) {
|
|
612
|
-
return {
|
|
613
|
-
windows,
|
|
614
|
-
perTxMaxSatoshis,
|
|
615
|
-
yellowLightThreshold: 0.8,
|
|
616
|
-
requirePerSitePrompt: false,
|
|
617
|
-
sitePolicies: {},
|
|
618
|
-
require2fa: {
|
|
619
|
-
onCircuitBreakerReset: true,
|
|
620
|
-
onTierChange: true,
|
|
621
|
-
onHighValueTx: false,
|
|
622
|
-
highValueThreshold: 0,
|
|
623
|
-
onNewSiteApproval: false
|
|
624
|
-
},
|
|
625
|
-
...opts
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
var TOO_YOUNG_TO_DIE = {
|
|
629
|
-
interactive: makeLimits(
|
|
630
|
-
[{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
|
|
631
|
-
1e8
|
|
632
|
-
),
|
|
633
|
-
programmatic: makeLimits(
|
|
634
|
-
[{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
|
|
635
|
-
1e6
|
|
636
|
-
)
|
|
637
|
-
};
|
|
638
|
-
var HEY_NOT_TOO_ROUGH = {
|
|
639
|
-
interactive: makeLimits(
|
|
640
|
-
[
|
|
641
|
-
{ window: "day", maxSatoshis: 1e8, maxTransactions: 100 },
|
|
642
|
-
{ window: "week", maxSatoshis: 5e8, maxTransactions: 500 }
|
|
643
|
-
],
|
|
644
|
-
1e7,
|
|
645
|
-
{
|
|
646
|
-
requirePerSitePrompt: true,
|
|
647
|
-
require2fa: {
|
|
648
|
-
onCircuitBreakerReset: true,
|
|
649
|
-
onTierChange: true,
|
|
650
|
-
onHighValueTx: true,
|
|
651
|
-
highValueThreshold: 5e7,
|
|
652
|
-
onNewSiteApproval: true
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
),
|
|
656
|
-
programmatic: makeLimits(
|
|
657
|
-
[
|
|
658
|
-
{ window: "day", maxSatoshis: 1e8, maxTransactions: 1e4 },
|
|
659
|
-
{ window: "week", maxSatoshis: 5e8, maxTransactions: 5e4 }
|
|
660
|
-
],
|
|
661
|
-
1e5,
|
|
662
|
-
{
|
|
663
|
-
requirePerSitePrompt: true,
|
|
664
|
-
require2fa: {
|
|
665
|
-
onCircuitBreakerReset: true,
|
|
666
|
-
onTierChange: true,
|
|
667
|
-
onHighValueTx: true,
|
|
668
|
-
highValueThreshold: 5e7,
|
|
669
|
-
onNewSiteApproval: true
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
)
|
|
673
|
-
};
|
|
674
|
-
var HURT_ME_PLENTY = {
|
|
675
|
-
interactive: makeLimits(
|
|
676
|
-
[
|
|
677
|
-
{ window: "minute", maxSatoshis: 5e6, maxTransactions: 10 },
|
|
678
|
-
{ window: "hour", maxSatoshis: 5e7, maxTransactions: 60 },
|
|
679
|
-
{ window: "day", maxSatoshis: 2e8, maxTransactions: 200 },
|
|
680
|
-
{ window: "week", maxSatoshis: 1e9, maxTransactions: 1e3 }
|
|
681
|
-
],
|
|
682
|
-
2e7,
|
|
683
|
-
{
|
|
684
|
-
requirePerSitePrompt: true,
|
|
685
|
-
require2fa: {
|
|
686
|
-
onCircuitBreakerReset: true,
|
|
687
|
-
onTierChange: true,
|
|
688
|
-
onHighValueTx: true,
|
|
689
|
-
highValueThreshold: 1e8,
|
|
690
|
-
onNewSiteApproval: true
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
),
|
|
694
|
-
programmatic: makeLimits(
|
|
695
|
-
[
|
|
696
|
-
{ window: "minute", maxSatoshis: 5e6, maxTransactions: 1e3 },
|
|
697
|
-
{ window: "hour", maxSatoshis: 5e7, maxTransactions: 6e3 },
|
|
698
|
-
{ window: "day", maxSatoshis: 2e8, maxTransactions: 2e4 },
|
|
699
|
-
{ window: "week", maxSatoshis: 1e9, maxTransactions: 1e5 }
|
|
700
|
-
],
|
|
701
|
-
2e5,
|
|
702
|
-
{
|
|
703
|
-
requirePerSitePrompt: true,
|
|
704
|
-
require2fa: {
|
|
705
|
-
onCircuitBreakerReset: true,
|
|
706
|
-
onTierChange: true,
|
|
707
|
-
onHighValueTx: true,
|
|
708
|
-
highValueThreshold: 1e8,
|
|
709
|
-
onNewSiteApproval: true
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
)
|
|
713
|
-
};
|
|
714
|
-
var ULTRA_VIOLENCE = {
|
|
715
|
-
interactive: makeLimits(
|
|
716
|
-
[...HURT_ME_PLENTY.interactive.windows],
|
|
717
|
-
HURT_ME_PLENTY.interactive.perTxMaxSatoshis,
|
|
718
|
-
{
|
|
719
|
-
requirePerSitePrompt: true,
|
|
720
|
-
require2fa: {
|
|
721
|
-
onCircuitBreakerReset: true,
|
|
722
|
-
onTierChange: true,
|
|
723
|
-
onHighValueTx: false,
|
|
724
|
-
highValueThreshold: 0,
|
|
725
|
-
onNewSiteApproval: true
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
),
|
|
729
|
-
programmatic: makeLimits(
|
|
730
|
-
[...HURT_ME_PLENTY.programmatic.windows],
|
|
731
|
-
HURT_ME_PLENTY.programmatic.perTxMaxSatoshis,
|
|
732
|
-
{
|
|
733
|
-
requirePerSitePrompt: true,
|
|
734
|
-
require2fa: {
|
|
735
|
-
onCircuitBreakerReset: true,
|
|
736
|
-
onTierChange: true,
|
|
737
|
-
onHighValueTx: false,
|
|
738
|
-
highValueThreshold: 0,
|
|
739
|
-
onNewSiteApproval: true
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
)
|
|
743
|
-
};
|
|
744
|
-
var NIGHTMARE = {
|
|
745
|
-
interactive: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS),
|
|
746
|
-
programmatic: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS)
|
|
747
|
-
};
|
|
748
|
-
var TIER_PRESETS = {
|
|
749
|
-
"I'm Too Young to Die": TOO_YOUNG_TO_DIE,
|
|
750
|
-
"Hey, Not Too Rough": HEY_NOT_TOO_ROUGH,
|
|
751
|
-
"Hurt Me Plenty": HURT_ME_PLENTY,
|
|
752
|
-
"Ultra-Violence": ULTRA_VIOLENCE,
|
|
753
|
-
"Nightmare!": NIGHTMARE
|
|
754
|
-
};
|
|
755
|
-
function cloneSpendLimits(preset) {
|
|
756
|
-
return {
|
|
757
|
-
...preset,
|
|
758
|
-
windows: preset.windows.map((w) => ({ ...w })),
|
|
759
|
-
require2fa: { ...preset.require2fa },
|
|
760
|
-
sitePolicies: { ...preset.sitePolicies }
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
function resolveSpendLimits(tier = "Hey, Not Too Rough", mode = "interactive", overrides) {
|
|
764
|
-
const preset = TIER_PRESETS[tier][mode];
|
|
765
|
-
const base = cloneSpendLimits(preset);
|
|
766
|
-
if (!overrides) return base;
|
|
767
|
-
return {
|
|
768
|
-
...base,
|
|
769
|
-
...overrides,
|
|
770
|
-
// Deep-merge require2fa if provided
|
|
771
|
-
require2fa: overrides.require2fa ? { ...base.require2fa, ...overrides.require2fa } : base.require2fa,
|
|
772
|
-
// Deep-merge sitePolicies if provided
|
|
773
|
-
sitePolicies: overrides.sitePolicies ? { ...base.sitePolicies, ...overrides.sitePolicies } : base.sitePolicies
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
var RateLimiter = class {
|
|
777
|
-
constructor(limits, state, now) {
|
|
778
|
-
this.limits = limits;
|
|
779
|
-
this.entries = state?.entries ?? [];
|
|
780
|
-
this.broken = state?.circuitBroken ?? false;
|
|
781
|
-
this.now = now ?? Date.now;
|
|
782
|
-
}
|
|
783
|
-
check(request, origin) {
|
|
784
|
-
if (this.broken) {
|
|
785
|
-
return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
|
|
786
|
-
}
|
|
787
|
-
const amount = request.amount;
|
|
788
|
-
if (!Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
|
|
789
|
-
return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
|
|
790
|
-
}
|
|
791
|
-
if (amount > BFG_PER_TX_CEILING_SATOSHIS) {
|
|
792
|
-
return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
|
|
793
|
-
}
|
|
794
|
-
const dayAgo = this.now() - WINDOW_MS.day;
|
|
795
|
-
const dailyTotal = this.sumSatoshis(dayAgo);
|
|
796
|
-
if (dailyTotal + amount > BFG_DAILY_CEILING_SATOSHIS) {
|
|
797
|
-
return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
|
|
798
|
-
}
|
|
799
|
-
if (amount > this.limits.perTxMaxSatoshis) {
|
|
800
|
-
return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
|
|
801
|
-
}
|
|
802
|
-
const isCustomSite = this.hasCustomPolicy(origin);
|
|
803
|
-
const effectiveLimits = this.effectiveWindows(origin);
|
|
804
|
-
const effectivePerTx = this.effectivePerTxMax(origin);
|
|
805
|
-
if (effectivePerTx !== void 0 && amount > effectivePerTx) {
|
|
806
|
-
return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
|
|
807
|
-
}
|
|
808
|
-
let yellowLight;
|
|
809
|
-
for (const wl of effectiveLimits) {
|
|
810
|
-
const cutoff = this.now() - windowToMs(wl.window);
|
|
811
|
-
const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
|
|
812
|
-
const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
|
|
813
|
-
const totalTx = windowEntries.length;
|
|
814
|
-
if (totalSats + amount > wl.maxSatoshis) {
|
|
815
|
-
return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
|
|
816
|
-
}
|
|
817
|
-
if (totalTx + 1 > wl.maxTransactions) {
|
|
818
|
-
return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
|
|
819
|
-
}
|
|
820
|
-
if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
|
|
821
|
-
yellowLight = {
|
|
822
|
-
origin,
|
|
823
|
-
currentSpend: totalSats,
|
|
824
|
-
limit: wl.maxSatoshis,
|
|
825
|
-
window: wl.window,
|
|
826
|
-
challenge: request
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
if (yellowLight) {
|
|
831
|
-
return { action: "yellow-light", detail: yellowLight };
|
|
832
|
-
}
|
|
833
|
-
return { action: "allow" };
|
|
834
|
-
}
|
|
835
|
-
record(entry) {
|
|
836
|
-
this.entries.push(entry);
|
|
837
|
-
this.prune();
|
|
838
|
-
}
|
|
839
|
-
trip() {
|
|
840
|
-
this.broken = true;
|
|
841
|
-
}
|
|
842
|
-
reset() {
|
|
843
|
-
this.broken = false;
|
|
844
|
-
}
|
|
845
|
-
isBroken() {
|
|
846
|
-
return this.broken;
|
|
847
|
-
}
|
|
848
|
-
getState() {
|
|
849
|
-
return {
|
|
850
|
-
entries: [...this.entries],
|
|
851
|
-
circuitBroken: this.broken,
|
|
852
|
-
hmac: ""
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
hasCustomPolicy(origin) {
|
|
856
|
-
const policy = this.limits.sitePolicies[origin];
|
|
857
|
-
return policy?.action === "custom" && !!policy.limits;
|
|
858
|
-
}
|
|
859
|
-
effectiveWindows(origin) {
|
|
860
|
-
const policy = this.limits.sitePolicies[origin];
|
|
861
|
-
if (policy?.action === "custom" && policy.limits) {
|
|
862
|
-
return policy.limits;
|
|
863
|
-
}
|
|
864
|
-
return this.limits.windows;
|
|
865
|
-
}
|
|
866
|
-
effectivePerTxMax(origin) {
|
|
867
|
-
const policy = this.limits.sitePolicies[origin];
|
|
868
|
-
if (policy?.action === "custom" && policy.perTxMaxSatoshis !== void 0) {
|
|
869
|
-
return policy.perTxMaxSatoshis;
|
|
870
|
-
}
|
|
871
|
-
return void 0;
|
|
872
|
-
}
|
|
873
|
-
entriesInWindow(cutoff, filterOrigin) {
|
|
874
|
-
return this.entries.filter(
|
|
875
|
-
(e) => e.timestamp >= cutoff && (filterOrigin === void 0 || e.origin === filterOrigin)
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
sumSatoshis(cutoff) {
|
|
879
|
-
return this.entries.filter((e) => e.timestamp >= cutoff).reduce((sum, e) => sum + e.satoshis, 0);
|
|
880
|
-
}
|
|
881
|
-
prune() {
|
|
882
|
-
let longestWindow = WINDOW_MS.day;
|
|
883
|
-
for (const wl of this.limits.windows) {
|
|
884
|
-
longestWindow = Math.max(longestWindow, windowToMs(wl.window));
|
|
885
|
-
}
|
|
886
|
-
if (this.limits.sitePolicies) {
|
|
887
|
-
for (const policy of Object.values(this.limits.sitePolicies)) {
|
|
888
|
-
if (policy?.action === "custom" && Array.isArray(policy.limits)) {
|
|
889
|
-
for (const wl of policy.limits) {
|
|
890
|
-
longestWindow = Math.max(longestWindow, windowToMs(wl.window));
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
const cutoff = this.now() - longestWindow;
|
|
896
|
-
this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
|
|
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
|
-
// src/storage.ts
|
|
936
|
-
var STATE_KEY = "x402:limit-state";
|
|
937
|
-
var POLICIES_KEY = "x402:site-policies";
|
|
938
|
-
async function computeHmac(data, key) {
|
|
939
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
940
|
-
"raw",
|
|
941
|
-
key,
|
|
942
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
943
|
-
false,
|
|
944
|
-
["sign"]
|
|
945
|
-
);
|
|
946
|
-
const encoded = new TextEncoder().encode(data);
|
|
947
|
-
const sig = await crypto.subtle.sign("HMAC", cryptoKey, encoded);
|
|
948
|
-
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
949
|
-
}
|
|
950
|
-
function serializeForHmac(state) {
|
|
951
|
-
return JSON.stringify({
|
|
952
|
-
entries: state.entries,
|
|
953
|
-
circuitBroken: state.circuitBroken
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
var LocalStorageAdapter = class {
|
|
957
|
-
constructor(keyDeriver, storage) {
|
|
958
|
-
this.keyDeriver = keyDeriver;
|
|
959
|
-
this.storage = storage ?? globalThis.localStorage;
|
|
960
|
-
}
|
|
961
|
-
async load() {
|
|
962
|
-
const raw = this.storage.getItem(STATE_KEY);
|
|
963
|
-
if (!raw) return null;
|
|
964
|
-
let state;
|
|
965
|
-
try {
|
|
966
|
-
state = JSON.parse(raw);
|
|
967
|
-
} catch {
|
|
968
|
-
console.warn("x402: limit state JSON parse failed \u2014 treating as tampered");
|
|
969
|
-
return { entries: [], circuitBroken: true, hmac: "" };
|
|
970
|
-
}
|
|
971
|
-
if (this.keyDeriver) {
|
|
972
|
-
if (!state.hmac) {
|
|
973
|
-
console.warn("x402: limit state missing HMAC \u2014 treating as tampered");
|
|
974
|
-
return { entries: [], circuitBroken: true, hmac: "" };
|
|
975
|
-
}
|
|
976
|
-
const key = await this.keyDeriver();
|
|
977
|
-
const expected = await computeHmac(serializeForHmac(state), key);
|
|
978
|
-
if (expected !== state.hmac) {
|
|
979
|
-
console.warn("x402: limit state HMAC mismatch \u2014 state may have been tampered with");
|
|
980
|
-
return { entries: [], circuitBroken: true, hmac: "" };
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
state.entries = (Array.isArray(state.entries) ? state.entries : []).filter(
|
|
984
|
-
(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
|
|
985
|
-
);
|
|
986
|
-
return state;
|
|
987
|
-
}
|
|
988
|
-
async save(state) {
|
|
989
|
-
if (this.keyDeriver) {
|
|
990
|
-
const key = await this.keyDeriver();
|
|
991
|
-
state.hmac = await computeHmac(serializeForHmac(state), key);
|
|
992
|
-
}
|
|
993
|
-
this.storage.setItem(STATE_KEY, JSON.stringify(state));
|
|
994
|
-
}
|
|
995
|
-
async loadSitePolicies() {
|
|
996
|
-
const raw = this.storage.getItem(POLICIES_KEY);
|
|
997
|
-
if (!raw) return {};
|
|
998
|
-
try {
|
|
999
|
-
return JSON.parse(raw);
|
|
1000
|
-
} catch {
|
|
1001
|
-
return {};
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
async saveSitePolicies(policies) {
|
|
1005
|
-
this.storage.setItem(POLICIES_KEY, JSON.stringify(policies));
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
599
|
// src/x402-fetch.ts
|
|
1010
600
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
1011
601
|
function base58DecodeCheck(address) {
|
|
@@ -1078,123 +668,11 @@ async function defaultConstructProof(challenge) {
|
|
|
1078
668
|
rawTx: result.rawTx
|
|
1079
669
|
};
|
|
1080
670
|
}
|
|
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
671
|
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
672
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
1101
673
|
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
1102
674
|
const brc105Wallet = config.brc105Wallet;
|
|
1103
|
-
|
|
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) {
|
|
675
|
+
return async function x402Fetch2(input, init) {
|
|
1198
676
|
const response = await fetch(input, init);
|
|
1199
677
|
if (response.status !== 402) return response;
|
|
1200
678
|
const origin = extractOrigin(input);
|
|
@@ -1206,26 +684,17 @@ function createX402Fetch(config = {}) {
|
|
|
1206
684
|
} catch {
|
|
1207
685
|
return response;
|
|
1208
686
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
return fetch(input, { ...init, headers });
|
|
1221
|
-
},
|
|
1222
|
-
(proof) => ({
|
|
1223
|
-
timestamp: nowFn(),
|
|
1224
|
-
origin,
|
|
1225
|
-
satoshis: challenge.amount,
|
|
1226
|
-
txid: proof.txid
|
|
1227
|
-
})
|
|
1228
|
-
);
|
|
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 });
|
|
1229
698
|
}
|
|
1230
699
|
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
1231
700
|
if (brc105Version) {
|
|
@@ -1236,52 +705,25 @@ function createX402Fetch(config = {}) {
|
|
|
1236
705
|
} catch {
|
|
1237
706
|
return response;
|
|
1238
707
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
);
|
|
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 });
|
|
1266
724
|
}
|
|
1267
725
|
return response;
|
|
1268
726
|
};
|
|
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
727
|
}
|
|
1286
728
|
var singleton;
|
|
1287
729
|
async function x402Fetch(input, init) {
|
|
@@ -1319,61 +761,80 @@ function extractOrigin(input) {
|
|
|
1319
761
|
}
|
|
1320
762
|
}
|
|
1321
763
|
|
|
1322
|
-
// src/
|
|
1323
|
-
var
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
});
|
|
1335
|
-
return sig !== null;
|
|
1336
|
-
} catch {
|
|
1337
|
-
return false;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
promptFallback(action) {
|
|
1341
|
-
if (typeof window === "undefined" || !window.prompt) return false;
|
|
1342
|
-
const message = describeAction(action);
|
|
1343
|
-
const result = window.prompt(`${message}
|
|
1344
|
-
|
|
1345
|
-
Type CONFIRM to proceed:`);
|
|
1346
|
-
return result === "CONFIRM";
|
|
1347
|
-
}
|
|
764
|
+
// src/autospend.ts
|
|
765
|
+
var TIER_CAPS = {
|
|
766
|
+
"I'm Too Young to Die": 1e6,
|
|
767
|
+
// 0.01 BSV
|
|
768
|
+
"Hey, Not Too Rough": 1e7,
|
|
769
|
+
// 0.1 BSV
|
|
770
|
+
"Hurt Me Plenty": 1e8,
|
|
771
|
+
// 1 BSV
|
|
772
|
+
"Ultra-Violence": 1e9,
|
|
773
|
+
// 10 BSV
|
|
774
|
+
"Nightmare!": 1e11
|
|
775
|
+
// 100 BSV
|
|
1348
776
|
};
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
777
|
+
var WEAPON_CAPS = {
|
|
778
|
+
"Fists": 1e5,
|
|
779
|
+
"Chainsaw": 25e4,
|
|
780
|
+
"Pistol": 5e5,
|
|
781
|
+
"Shotgun": 1e6,
|
|
782
|
+
"Super Shotgun": 1e7,
|
|
783
|
+
"Chaingun": 5e7,
|
|
784
|
+
"Rocket Launcher": 25e7,
|
|
785
|
+
"Plasma Rifle": 1e9,
|
|
786
|
+
"BFG9000": Infinity
|
|
787
|
+
};
|
|
788
|
+
var PICKUP_PERCENTAGES = {
|
|
789
|
+
"Medkit": 0.1,
|
|
790
|
+
"Stimpak": 0.25,
|
|
791
|
+
"Soul Sphere": 1,
|
|
792
|
+
"New Game": 1
|
|
793
|
+
// hard-set to 100% (not additive)
|
|
794
|
+
};
|
|
795
|
+
function isValidAmount(amount) {
|
|
796
|
+
return Number.isFinite(amount) && amount > 0;
|
|
797
|
+
}
|
|
798
|
+
function checkPayment(amount, state, config) {
|
|
799
|
+
if (!isValidAmount(amount)) return "confirm";
|
|
800
|
+
const perTxMax = WEAPON_CAPS[config.weapon];
|
|
801
|
+
const effectiveMax = Math.min(perTxMax, state.balance);
|
|
802
|
+
return amount <= effectiveMax ? "auto" : "confirm";
|
|
803
|
+
}
|
|
804
|
+
function recordPayment(amount, state) {
|
|
805
|
+
if (!isValidAmount(amount)) return state;
|
|
806
|
+
return { balance: Math.max(0, state.balance - amount) };
|
|
807
|
+
}
|
|
808
|
+
function applyPickup(pickup, state, config, walletBalance) {
|
|
809
|
+
const tierCap = TIER_CAPS[config.tier];
|
|
810
|
+
const cap = Math.min(tierCap, walletBalance);
|
|
811
|
+
if (pickup === "New Game") {
|
|
812
|
+
return { balance: cap };
|
|
813
|
+
}
|
|
814
|
+
const bonus = Math.floor(tierCap * PICKUP_PERCENTAGES[pickup]);
|
|
815
|
+
return { balance: Math.min(cap, state.balance + bonus) };
|
|
816
|
+
}
|
|
817
|
+
function clampBalanceToTier(state, config, walletBalance) {
|
|
818
|
+
const cap = Math.min(TIER_CAPS[config.tier], walletBalance);
|
|
819
|
+
return { balance: Math.min(state.balance, cap) };
|
|
820
|
+
}
|
|
821
|
+
function initialState(config, walletBalance) {
|
|
822
|
+
const cap = Math.min(TIER_CAPS[config.tier], walletBalance);
|
|
823
|
+
return { balance: cap };
|
|
1363
824
|
}
|
|
1364
825
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1365
826
|
0 && (module.exports = {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
827
|
+
PICKUP_PERCENTAGES,
|
|
828
|
+
TIER_CAPS,
|
|
829
|
+
WEAPON_CAPS,
|
|
830
|
+
applyPickup,
|
|
831
|
+
checkPayment,
|
|
832
|
+
clampBalanceToTier,
|
|
1372
833
|
constructBrc105Proof,
|
|
1373
834
|
createX402Fetch,
|
|
835
|
+
initialState,
|
|
1374
836
|
parseBrc105Challenge,
|
|
1375
837
|
parseChallenge,
|
|
1376
|
-
|
|
1377
|
-
resolveSpendLimits,
|
|
838
|
+
recordPayment,
|
|
1378
839
|
x402Fetch
|
|
1379
840
|
});
|