bsv-x402 0.5.0 → 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 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
- BFG_DAILY_CEILING_SATOSHIS: () => BFG_DAILY_CEILING_SATOSHIS,
24
- BFG_PER_TX_CEILING_SATOSHIS: () => BFG_PER_TX_CEILING_SATOSHIS,
25
- LocalStorageAdapter: () => LocalStorageAdapter,
26
- RateLimiter: () => RateLimiter,
27
- TIER_PRESETS: () => TIER_PRESETS,
28
- WalletTwoFactorProvider: () => WalletTwoFactorProvider,
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
- resolveSitePolicy: () => resolveSitePolicy,
34
- resolveSpendLimits: () => resolveSpendLimits,
34
+ recordPayment: () => recordPayment,
35
35
  x402Fetch: () => x402Fetch
36
36
  });
37
37
  module.exports = __toCommonJS(index_exports);
@@ -761,471 +761,80 @@ function extractOrigin(input) {
761
761
  }
762
762
  }
763
763
 
764
- // src/limits.ts
765
- var BFG_DAILY_CEILING_SATOSHIS = 1e10;
766
- var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
767
- var WINDOW_MS = {
768
- minute: 6e4,
769
- hour: 36e5,
770
- day: 864e5,
771
- week: 6048e5
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
772
776
  };
773
- function windowToMs(window2) {
774
- return WINDOW_MS[window2];
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
- )
908
- };
909
- var NIGHTMARE = {
910
- interactive: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS),
911
- programmatic: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS)
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
912
787
  };
913
- var TIER_PRESETS = {
914
- "I'm Too Young to Die": TOO_YOUNG_TO_DIE,
915
- "Hey, Not Too Rough": HEY_NOT_TOO_ROUGH,
916
- "Hurt Me Plenty": HURT_ME_PLENTY,
917
- "Ultra-Violence": ULTRA_VIOLENCE,
918
- "Nightmare!": NIGHTMARE
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)
919
794
  };
920
- function cloneSpendLimits(preset) {
921
- return {
922
- ...preset,
923
- windows: preset.windows.map((w) => ({ ...w })),
924
- require2fa: { ...preset.require2fa },
925
- sitePolicies: { ...preset.sitePolicies }
926
- };
795
+ function isValidAmount(amount) {
796
+ return Number.isFinite(amount) && amount > 0;
927
797
  }
928
- function resolveSpendLimits(tier = "Hey, Not Too Rough", mode = "interactive", overrides) {
929
- const preset = TIER_PRESETS[tier][mode];
930
- const base = cloneSpendLimits(preset);
931
- if (!overrides) return base;
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
- };
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";
940
803
  }
941
- var RateLimiter = class {
942
- constructor(limits, state, now) {
943
- this.limits = limits;
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("");
804
+ function recordPayment(amount, state) {
805
+ if (!isValidAmount(amount)) return state;
806
+ return { balance: Math.max(0, state.balance - amount) };
1079
807
  }
1080
- function serializeForHmac(state) {
1081
- return JSON.stringify({
1082
- entries: state.entries,
1083
- circuitBroken: state.circuitBroken
1084
- });
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) };
1085
816
  }
1086
- var LocalStorageAdapter = class {
1087
- constructor(keyDeriver, storage) {
1088
- this.keyDeriver = keyDeriver;
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
- }
817
+ function clampBalanceToTier(state, config, walletBalance) {
818
+ const cap = Math.min(TIER_CAPS[config.tier], walletBalance);
819
+ return { balance: Math.min(state.balance, cap) };
1180
820
  }
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 };
821
+ function initialState(config, walletBalance) {
822
+ const cap = Math.min(TIER_CAPS[config.tier], walletBalance);
823
+ return { balance: cap };
1215
824
  }
1216
825
  // Annotate the CommonJS export names for ESM import in node:
1217
826
  0 && (module.exports = {
1218
- BFG_DAILY_CEILING_SATOSHIS,
1219
- BFG_PER_TX_CEILING_SATOSHIS,
1220
- LocalStorageAdapter,
1221
- RateLimiter,
1222
- TIER_PRESETS,
1223
- WalletTwoFactorProvider,
827
+ PICKUP_PERCENTAGES,
828
+ TIER_CAPS,
829
+ WEAPON_CAPS,
830
+ applyPickup,
831
+ checkPayment,
832
+ clampBalanceToTier,
1224
833
  constructBrc105Proof,
1225
834
  createX402Fetch,
835
+ initialState,
1226
836
  parseBrc105Challenge,
1227
837
  parseChallenge,
1228
- resolveSitePolicy,
1229
- resolveSpendLimits,
838
+ recordPayment,
1230
839
  x402Fetch
1231
840
  });