@pafi-dev/issuer 0.3.0-beta.8 → 0.4.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
@@ -28,7 +28,6 @@ __export(index_exports, {
28
28
  FeeManager: () => FeeManager,
29
29
  InMemoryCursorStore: () => InMemoryCursorStore,
30
30
  IssuerApiHandlers: () => IssuerApiHandlers,
31
- MemoryPointLedger: () => MemoryPointLedger,
32
31
  MemorySessionStore: () => MemorySessionStore,
33
32
  NonceManager: () => NonceManager,
34
33
  PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
@@ -37,7 +36,6 @@ __export(index_exports, {
37
36
  PafiBackendClient: () => PafiBackendClient,
38
37
  PafiBackendError: () => PafiBackendError,
39
38
  PointIndexer: () => PointIndexer,
40
- PrivateKeySigner: () => PrivateKeySigner,
41
39
  RelayError: () => RelayError,
42
40
  RelayService: () => RelayService,
43
41
  TopUpRedemptionError: () => TopUpRedemptionError,
@@ -49,204 +47,6 @@ __export(index_exports, {
49
47
  });
50
48
  module.exports = __toCommonJS(index_exports);
51
49
 
52
- // src/ledger/memoryLedger.ts
53
- var import_viem = require("viem");
54
- var MemoryPointLedger = class {
55
- balances = /* @__PURE__ */ new Map();
56
- locks = /* @__PURE__ */ new Map();
57
- nextLockId = 1;
58
- now;
59
- constructor(opts = {}) {
60
- this.now = opts.now ?? (() => Date.now());
61
- }
62
- // -------------------------------------------------------------------------
63
- // Read
64
- // -------------------------------------------------------------------------
65
- async getBalance(userAddress, tokenAddress) {
66
- const user = (0, import_viem.getAddress)(userAddress);
67
- const token = normalizeToken(tokenAddress);
68
- this.purgeExpired();
69
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
70
- const locked = this.lockedTotalFor(user, token);
71
- return total - locked;
72
- }
73
- async getLockedRequests(userAddress, tokenAddress) {
74
- const user = (0, import_viem.getAddress)(userAddress);
75
- const token = normalizeToken(tokenAddress);
76
- this.purgeExpired();
77
- const out = [];
78
- for (const lock of this.locks.values()) {
79
- if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
80
- out.push({ ...lock });
81
- }
82
- }
83
- return out;
84
- }
85
- // -------------------------------------------------------------------------
86
- // Write
87
- // -------------------------------------------------------------------------
88
- async creditBalance(userAddress, amount, _reason, tokenAddress) {
89
- if (amount <= 0n) {
90
- throw new Error("MemoryPointLedger: credit amount must be positive");
91
- }
92
- const user = (0, import_viem.getAddress)(userAddress);
93
- const token = normalizeToken(tokenAddress);
94
- const key = balanceKey(user, token);
95
- const current = this.balances.get(key) ?? 0n;
96
- this.balances.set(key, current + amount);
97
- }
98
- async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
99
- if (amount <= 0n) {
100
- throw new Error("MemoryPointLedger: lock amount must be positive");
101
- }
102
- if (lockDurationMs <= 0) {
103
- throw new Error("MemoryPointLedger: lockDurationMs must be positive");
104
- }
105
- const user = (0, import_viem.getAddress)(userAddress);
106
- const token = normalizeToken(tokenAddress);
107
- this.purgeExpired();
108
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
109
- const alreadyLocked = this.lockedTotalFor(user, token);
110
- const available = total - alreadyLocked;
111
- if (available < amount) {
112
- throw new Error(
113
- `MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
114
- );
115
- }
116
- const lockId = `lock-${this.nextLockId++}`;
117
- const now = this.now();
118
- const lock = {
119
- lockId,
120
- userAddress: user,
121
- amount,
122
- status: "PENDING",
123
- createdAt: now,
124
- expiresAt: now + lockDurationMs
125
- };
126
- if (tokenAddress !== void 0) {
127
- lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
128
- }
129
- this.locks.set(lockId, lock);
130
- return lockId;
131
- }
132
- async releaseLock(lockId) {
133
- const lock = this.locks.get(lockId);
134
- if (!lock) return;
135
- if (lock.status === "PENDING") {
136
- this.locks.delete(lockId);
137
- }
138
- }
139
- async deductBalance(userAddress, amount, txHash, tokenAddress) {
140
- if (amount <= 0n) {
141
- throw new Error("MemoryPointLedger: deduct amount must be positive");
142
- }
143
- const user = (0, import_viem.getAddress)(userAddress);
144
- const token = normalizeToken(tokenAddress);
145
- const key = balanceKey(user, token);
146
- const current = this.balances.get(key) ?? 0n;
147
- if (current < amount) {
148
- throw new Error(
149
- `MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
150
- );
151
- }
152
- this.balances.set(key, current - amount);
153
- for (const lock of this.locks.values()) {
154
- if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
155
- lock.status = "MINTED";
156
- lock.txHash = txHash;
157
- return;
158
- }
159
- }
160
- }
161
- async updateMintStatus(lockId, status, txHash) {
162
- const lock = this.locks.get(lockId);
163
- if (!lock) {
164
- throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
165
- }
166
- lock.status = status;
167
- if (txHash) lock.txHash = txHash;
168
- }
169
- // -------------------------------------------------------------------------
170
- // v1.4 — Reverse flow (PT burn → off-chain credit)
171
- // -------------------------------------------------------------------------
172
- pendingCredits = /* @__PURE__ */ new Map();
173
- nextCreditId = 1;
174
- async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
175
- if (amount <= 0n) {
176
- throw new Error(
177
- "MemoryPointLedger: pending credit amount must be positive"
178
- );
179
- }
180
- if (durationMs <= 0) {
181
- throw new Error("MemoryPointLedger: durationMs must be positive");
182
- }
183
- const user = (0, import_viem.getAddress)(userAddress);
184
- const lockId = `credit-${this.nextCreditId++}`;
185
- const now = this.now();
186
- this.pendingCredits.set(lockId, {
187
- lockId,
188
- userAddress: user,
189
- amount,
190
- tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
191
- createdAt: now,
192
- expiresAt: now + durationMs,
193
- status: "PENDING"
194
- });
195
- return lockId;
196
- }
197
- async resolveCreditByBurnTx(lockId, txHash) {
198
- const credit = this.pendingCredits.get(lockId);
199
- if (!credit) {
200
- throw new Error(
201
- `MemoryPointLedger: unknown pending credit lockId ${lockId}`
202
- );
203
- }
204
- if (credit.status === "RESOLVED") {
205
- if (credit.txHash === txHash) return;
206
- throw new Error(
207
- `MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
208
- );
209
- }
210
- const token = normalizeToken(credit.tokenAddress);
211
- const key = balanceKey(credit.userAddress, token);
212
- const current = this.balances.get(key) ?? 0n;
213
- this.balances.set(key, current + credit.amount);
214
- credit.status = "RESOLVED";
215
- credit.txHash = txHash;
216
- }
217
- // -------------------------------------------------------------------------
218
- // Internal helpers
219
- // -------------------------------------------------------------------------
220
- /**
221
- * Auto-expire any PENDING lock past its expiry. Called lazily on every
222
- * read/write so the in-memory state stays self-cleaning without a timer.
223
- */
224
- purgeExpired() {
225
- const now = this.now();
226
- for (const lock of this.locks.values()) {
227
- if (lock.status === "PENDING" && lock.expiresAt <= now) {
228
- lock.status = "EXPIRED";
229
- }
230
- }
231
- }
232
- lockedTotalFor(userAddress, tokenKey) {
233
- let total = 0n;
234
- for (const lock of this.locks.values()) {
235
- if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
236
- total += lock.amount;
237
- }
238
- }
239
- return total;
240
- }
241
- };
242
- var DEFAULT_TOKEN_KEY = "default";
243
- function normalizeToken(tokenAddress) {
244
- return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
245
- }
246
- function balanceKey(user, tokenKey) {
247
- return `${user}|${tokenKey}`;
248
- }
249
-
250
50
  // src/policy/defaultPolicy.ts
251
51
  var DefaultPolicyEngine = class {
252
52
  ledger;
@@ -262,6 +62,13 @@ var DefaultPolicyEngine = class {
262
62
  }
263
63
  if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
264
64
  if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
65
+ if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
66
+ if (process.env.NODE_ENV === "production") {
67
+ throw new Error(
68
+ "[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
69
+ );
70
+ }
71
+ }
265
72
  }
266
73
  async evaluate(request) {
267
74
  if (request.amount <= 0n) {
@@ -298,32 +105,9 @@ var DefaultPolicyEngine = class {
298
105
  }
299
106
  };
300
107
 
301
- // src/signer/privateKeySigner.ts
302
- var import_viem2 = require("viem");
303
- var import_accounts = require("viem/accounts");
304
- var import_core = require("@pafi-dev/core");
305
- var PrivateKeySigner = class {
306
- account;
307
- walletClient;
308
- constructor(opts) {
309
- this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
310
- this.walletClient = (0, import_viem2.createWalletClient)({
311
- account: this.account,
312
- chain: opts.chain,
313
- transport: (0, import_viem2.http)(opts.rpcUrl)
314
- });
315
- }
316
- async signMintRequest(domain, message) {
317
- return (0, import_core.signMintRequest)(this.walletClient, domain, message);
318
- }
319
- async getAddress() {
320
- return this.account.address;
321
- }
322
- };
323
-
324
108
  // src/auth/memorySessionStore.ts
325
109
  var import_node_crypto = require("crypto");
326
- var import_viem3 = require("viem");
110
+ var import_viem = require("viem");
327
111
  var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
328
112
  var MemorySessionStore = class {
329
113
  nonces = /* @__PURE__ */ new Map();
@@ -333,6 +117,11 @@ var MemorySessionStore = class {
333
117
  nonceTtlMs;
334
118
  now;
335
119
  constructor(opts = {}) {
120
+ if (process.env.NODE_ENV === "production") {
121
+ console.error(
122
+ "[PAFI] MemorySessionStore is not safe for multi-process/K8s deployments. Session revocations are NOT propagated across pods. Use a Redis-backed session store in production."
123
+ );
124
+ }
336
125
  this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
337
126
  this.now = opts.now ?? (() => Date.now());
338
127
  }
@@ -359,7 +148,7 @@ var MemorySessionStore = class {
359
148
  this.purgeExpiredSessions();
360
149
  const normalized = {
361
150
  ...session,
362
- userAddress: (0, import_viem3.getAddress)(session.userAddress)
151
+ userAddress: (0, import_viem.getAddress)(session.userAddress)
363
152
  };
364
153
  this.sessions.set(session.tokenId, normalized);
365
154
  }
@@ -377,7 +166,7 @@ var MemorySessionStore = class {
377
166
  this.sessions.delete(tokenId);
378
167
  }
379
168
  async revokeAllSessions(userAddress) {
380
- const key = (0, import_viem3.getAddress)(userAddress);
169
+ const key = (0, import_viem.getAddress)(userAddress);
381
170
  for (const [tokenId, session] of this.sessions.entries()) {
382
171
  if (session.userAddress === key) {
383
172
  this.sessions.delete(tokenId);
@@ -423,8 +212,8 @@ var NonceManager = class {
423
212
  // src/auth/loginVerifier.ts
424
213
  var import_node_crypto2 = require("crypto");
425
214
  var import_jose = require("jose");
426
- var import_viem4 = require("viem");
427
- var import_core2 = require("@pafi-dev/core");
215
+ var import_viem2 = require("viem");
216
+ var import_core = require("@pafi-dev/core");
428
217
 
429
218
  // src/auth/errors.ts
430
219
  var AuthError = class extends Error {
@@ -447,8 +236,8 @@ var AuthService = class {
447
236
  nonceManager;
448
237
  now;
449
238
  constructor(config) {
450
- if (!config.jwtSecret || config.jwtSecret.length < 16) {
451
- throw new Error("AuthService: jwtSecret must be at least 16 chars");
239
+ if (!config.jwtSecret || config.jwtSecret.length < 32) {
240
+ throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
452
241
  }
453
242
  this.sessionStore = config.sessionStore;
454
243
  this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
@@ -472,11 +261,17 @@ var AuthService = class {
472
261
  async login(message, signature) {
473
262
  let parsed;
474
263
  try {
475
- parsed = (0, import_core2.parseLoginMessage)(message);
264
+ parsed = (0, import_core.parseLoginMessage)(message);
476
265
  } catch (err) {
477
266
  const msg = err instanceof Error ? err.message : String(err);
478
267
  throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
479
268
  }
269
+ if (parsed.expirationTime == null) {
270
+ throw new AuthError(
271
+ "INVALID_MESSAGE",
272
+ "login message must include expirationTime"
273
+ );
274
+ }
480
275
  if (parsed.domain !== this.domain) {
481
276
  throw new AuthError(
482
277
  "DOMAIN_MISMATCH",
@@ -499,7 +294,7 @@ var AuthService = class {
499
294
  if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
500
295
  throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
501
296
  }
502
- const verifyResult = await (0, import_core2.verifyLoginMessage)(message, signature);
297
+ const verifyResult = await (0, import_core.verifyLoginMessage)(message, signature);
503
298
  if (!verifyResult.valid) {
504
299
  throw new AuthError(
505
300
  "SIGNATURE_INVALID",
@@ -513,7 +308,7 @@ var AuthService = class {
513
308
  "Nonce is unknown, expired, or already used"
514
309
  );
515
310
  }
516
- const userAddress = (0, import_viem4.getAddress)(verifyResult.address);
311
+ const userAddress = (0, import_viem2.getAddress)(verifyResult.address);
517
312
  const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
518
313
  const issuedAt = now;
519
314
  const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
@@ -541,7 +336,11 @@ var AuthService = class {
541
336
  if (payload.jti) {
542
337
  await this.sessionStore.revokeSession(payload.jti);
543
338
  }
544
- } catch {
339
+ } catch (err) {
340
+ const msg = err instanceof Error ? err.message : String(err);
341
+ if (!msg.includes("not found") && !msg.includes("expired")) {
342
+ console.error("[PAFI] AuthService logout: session store error", err);
343
+ }
545
344
  }
546
345
  }
547
346
  /**
@@ -580,7 +379,7 @@ var AuthService = class {
580
379
  throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
581
380
  }
582
381
  return {
583
- userAddress: (0, import_viem4.getAddress)(userAddress),
382
+ userAddress: (0, import_viem2.getAddress)(userAddress),
584
383
  chainId,
585
384
  tokenId
586
385
  };
@@ -631,8 +430,8 @@ var RelayError = class extends Error {
631
430
  };
632
431
 
633
432
  // src/relay/relayService.ts
634
- var import_viem5 = require("viem");
635
- var import_core3 = require("@pafi-dev/core");
433
+ var import_viem3 = require("viem");
434
+ var import_core2 = require("@pafi-dev/core");
636
435
  var RelayService = class {
637
436
  /**
638
437
  * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
@@ -666,9 +465,20 @@ var RelayService = class {
666
465
  if (params.deadline <= 0n) {
667
466
  throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
668
467
  }
468
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
469
+ if (params.deadline <= nowSecs) {
470
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
471
+ }
472
+ const MAX_DEADLINE_WINDOW = 3600n;
473
+ if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
474
+ throw new RelayError(
475
+ "ENCODE_FAILED",
476
+ "prepareMint: deadline exceeds maximum allowed window (1 hour)"
477
+ );
478
+ }
669
479
  let minterSig;
670
480
  try {
671
- const sig = await (0, import_core3.signMintRequest)(
481
+ const sig = await (0, import_core2.signMintRequest)(
672
482
  params.issuerSignerWallet,
673
483
  params.domain,
674
484
  {
@@ -688,8 +498,8 @@ var RelayService = class {
688
498
  }
689
499
  let mintCallData;
690
500
  try {
691
- mintCallData = (0, import_viem5.encodeFunctionData)({
692
- abi: import_core3.POINT_TOKEN_V2_ABI,
501
+ mintCallData = (0, import_viem3.encodeFunctionData)({
502
+ abi: import_core2.POINT_TOKEN_V2_ABI,
693
503
  functionName: "mint",
694
504
  args: [params.userAddress, params.amount, params.deadline, minterSig]
695
505
  });
@@ -714,17 +524,23 @@ var RelayService = class {
714
524
  "prepareMint: feeRecipient required when feeAmount > 0"
715
525
  );
716
526
  }
527
+ if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
528
+ throw new RelayError(
529
+ "ENCODE_FAILED",
530
+ "prepareMint: feeRecipient must not be zero address"
531
+ );
532
+ }
717
533
  operations.push({
718
534
  target: params.pointTokenAddress,
719
535
  value: 0n,
720
- data: (0, import_viem5.encodeFunctionData)({
721
- abi: import_viem5.erc20Abi,
536
+ data: (0, import_viem3.encodeFunctionData)({
537
+ abi: import_viem3.erc20Abi,
722
538
  functionName: "transfer",
723
539
  args: [params.feeRecipient, params.feeAmount]
724
540
  })
725
541
  });
726
542
  }
727
- return (0, import_core3.buildPartialUserOperation)({
543
+ return (0, import_core2.buildPartialUserOperation)({
728
544
  sender: params.userAddress,
729
545
  nonce: params.aaNonce,
730
546
  operations,
@@ -762,8 +578,8 @@ var RelayService = class {
762
578
  if (!params.burnRequest || !params.burnerSignature) {
763
579
  throw new Error("burnWithSig requires burnRequest + burnerSignature");
764
580
  }
765
- burnCallData = (0, import_viem5.encodeFunctionData)({
766
- abi: import_core3.POINT_TOKEN_V2_ABI,
581
+ burnCallData = (0, import_viem3.encodeFunctionData)({
582
+ abi: import_core2.POINT_TOKEN_V2_ABI,
767
583
  functionName: "burn",
768
584
  args: [
769
585
  params.burnRequest.from,
@@ -773,8 +589,8 @@ var RelayService = class {
773
589
  ]
774
590
  });
775
591
  } else {
776
- burnCallData = (0, import_viem5.encodeFunctionData)({
777
- abi: import_core3.POINT_TOKEN_V2_ABI,
592
+ burnCallData = (0, import_viem3.encodeFunctionData)({
593
+ abi: import_core2.POINT_TOKEN_V2_ABI,
778
594
  functionName: "burn",
779
595
  args: [params.userAddress, params.amount]
780
596
  });
@@ -793,7 +609,7 @@ var RelayService = class {
793
609
  data: burnCallData
794
610
  }
795
611
  ];
796
- return (0, import_core3.buildPartialUserOperation)({
612
+ return (0, import_core2.buildPartialUserOperation)({
797
613
  sender: params.userAddress,
798
614
  nonce: params.aaNonce,
799
615
  operations,
@@ -812,11 +628,14 @@ function errorMessage(err) {
812
628
  // src/relay/feeManager.ts
813
629
  var DEFAULT_GAS_UNITS = 500000n;
814
630
  var DEFAULT_PREMIUM_BPS = 12e3;
815
- var FeeManager = class {
631
+ var FeeManager = class _FeeManager {
816
632
  provider;
817
633
  gasUnits;
818
634
  gasPremiumBps;
819
635
  quoteNativeToFee;
636
+ cachedFee = null;
637
+ cacheExpiresAt = 0;
638
+ static CACHE_TTL_MS = 1e4;
820
639
  constructor(config) {
821
640
  if (!config.provider) throw new Error("FeeManager: provider required");
822
641
  if (!config.quoteNativeToFee)
@@ -839,10 +658,17 @@ var FeeManager = class {
839
658
  * currency depends on how the caller wired `quoteNativeToFee`.
840
659
  */
841
660
  async estimateGasFee() {
661
+ const now = Date.now();
662
+ if (this.cachedFee !== null && now < this.cacheExpiresAt) {
663
+ return this.cachedFee;
664
+ }
842
665
  const gasPrice = await this.provider.getGasPrice();
843
666
  const nativeCost = gasPrice * this.gasUnits;
844
667
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
845
- return this.quoteNativeToFee(withPremium);
668
+ const fee = await this.quoteNativeToFee(withPremium);
669
+ this.cachedFee = fee;
670
+ this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
671
+ return fee;
846
672
  }
847
673
  };
848
674
 
@@ -858,8 +684,8 @@ var InMemoryCursorStore = class {
858
684
  };
859
685
 
860
686
  // src/indexer/pointIndexer.ts
861
- var import_viem6 = require("viem");
862
- var TRANSFER_EVENT = (0, import_viem6.parseAbiItem)(
687
+ var import_viem4 = require("viem");
688
+ var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
863
689
  "event Transfer(address indexed from, address indexed to, uint256 value)"
864
690
  );
865
691
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
@@ -929,7 +755,8 @@ var PointIndexer = class {
929
755
  return;
930
756
  }
931
757
  await this.processBlockRange(from, safeHead);
932
- } catch {
758
+ } catch (err) {
759
+ console.error("[PAFI] PointIndexer tick error:", err);
933
760
  }
934
761
  this.scheduleNext();
935
762
  }
@@ -979,10 +806,10 @@ var PointIndexer = class {
979
806
  for (const log of logs) {
980
807
  const args = log.args;
981
808
  if (!args.from || !args.to || args.value === void 0) continue;
982
- if ((0, import_viem6.getAddress)(args.from) !== ZERO_ADDRESS) continue;
809
+ if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
983
810
  if (log.blockNumber === null || log.transactionHash === null) continue;
984
811
  out.push({
985
- to: (0, import_viem6.getAddress)(args.to),
812
+ to: (0, import_viem4.getAddress)(args.to),
986
813
  amount: args.value,
987
814
  blockNumber: log.blockNumber,
988
815
  txHash: log.transactionHash,
@@ -1038,8 +865,8 @@ function pickMatchingLock(locks, amount) {
1038
865
  }
1039
866
 
1040
867
  // src/indexer/burnIndexer.ts
1041
- var import_viem7 = require("viem");
1042
- var TRANSFER_EVENT2 = (0, import_viem7.parseAbiItem)(
868
+ var import_viem5 = require("viem");
869
+ var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
1043
870
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1044
871
  );
1045
872
  var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
@@ -1055,18 +882,7 @@ var BurnIndexer = class {
1055
882
  confirmations;
1056
883
  batchSize;
1057
884
  pollIntervalMs;
1058
- /**
1059
- * Caller-supplied matcher. Return the lockId to resolve for a given
1060
- * burn event, or `undefined` to skip. Runs synchronously via the
1061
- * ledger's query path.
1062
- *
1063
- * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
1064
- * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
1065
- * incrementing IDs so callers with the memory ledger must provide a
1066
- * custom matcher. Real DB-backed ledgers override this to JOIN on
1067
- * their `pending_credits` table.
1068
- */
1069
- matchLockId = async () => void 0;
885
+ matchLockId;
1070
886
  running = false;
1071
887
  timer;
1072
888
  constructor(config) {
@@ -1084,6 +900,12 @@ var BurnIndexer = class {
1084
900
  );
1085
901
  this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
1086
902
  this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
903
+ if (!config.matchLockId) {
904
+ throw new Error(
905
+ "BurnIndexer: matchLockId is required. Provide a function that maps a burn event to its pending credit lockId. Without it, no on-chain burns will ever grant off-chain credits."
906
+ );
907
+ }
908
+ this.matchLockId = config.matchLockId;
1087
909
  }
1088
910
  start() {
1089
911
  if (this.running) return;
@@ -1113,7 +935,8 @@ var BurnIndexer = class {
1113
935
  return;
1114
936
  }
1115
937
  await this.processBlockRange(from, safeHead);
1116
- } catch {
938
+ } catch (err) {
939
+ console.error("[PAFI] BurnIndexer tick error:", err);
1117
940
  }
1118
941
  this.scheduleNext();
1119
942
  }
@@ -1158,10 +981,10 @@ var BurnIndexer = class {
1158
981
  for (const log of logs) {
1159
982
  const args = log.args;
1160
983
  if (!args.from || !args.to || args.value === void 0) continue;
1161
- if ((0, import_viem7.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
984
+ if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
1162
985
  if (log.blockNumber === null || log.transactionHash === null) continue;
1163
986
  out.push({
1164
- from: (0, import_viem7.getAddress)(args.from),
987
+ from: (0, import_viem5.getAddress)(args.from),
1165
988
  amount: args.value,
1166
989
  blockNumber: log.blockNumber,
1167
990
  txHash: log.transactionHash,
@@ -1176,21 +999,28 @@ var BurnIndexer = class {
1176
999
  * log + skip.
1177
1000
  */
1178
1001
  async finalize(evt) {
1002
+ const txHash = evt.txHash;
1179
1003
  const lockId = await this.matchLockId(evt);
1180
- if (!lockId) return;
1004
+ if (lockId === void 0) {
1005
+ console.warn(
1006
+ "[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
1007
+ );
1008
+ return;
1009
+ }
1181
1010
  if (!this.ledger.resolveCreditByBurnTx) {
1182
1011
  return;
1183
1012
  }
1184
1013
  try {
1185
1014
  await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
1186
- } catch {
1015
+ } catch (err) {
1016
+ console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
1187
1017
  }
1188
1018
  }
1189
1019
  };
1190
1020
 
1191
1021
  // src/api/handlers.ts
1192
- var import_viem8 = require("viem");
1193
- var import_core4 = require("@pafi-dev/core");
1022
+ var import_viem6 = require("viem");
1023
+ var import_core3 = require("@pafi-dev/core");
1194
1024
  var IssuerApiHandlers = class {
1195
1025
  authService;
1196
1026
  ledger;
@@ -1200,15 +1030,12 @@ var IssuerApiHandlers = class {
1200
1030
  * validate the request's `pointTokenAddress` against this set.
1201
1031
  */
1202
1032
  supportedTokens;
1203
- /** First supported token — used as default when a handler doesn't
1204
- * receive a `pointTokenAddress` in the request (shouldn't happen in
1205
- * practice, but keeps type-narrowing happy). */
1206
- defaultToken;
1207
1033
  chainId;
1208
1034
  contracts;
1209
1035
  pafiWebUrl;
1210
1036
  feeManager;
1211
1037
  poolsProvider;
1038
+ claim;
1212
1039
  constructor(config) {
1213
1040
  this.authService = config.authService;
1214
1041
  this.ledger = config.ledger;
@@ -1219,14 +1046,14 @@ var IssuerApiHandlers = class {
1219
1046
  "IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
1220
1047
  );
1221
1048
  }
1222
- const normalized = raw.map((a) => (0, import_viem8.getAddress)(a));
1049
+ const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
1223
1050
  this.supportedTokens = new Set(normalized);
1224
- this.defaultToken = normalized[0];
1225
1051
  this.chainId = config.chainId;
1226
1052
  this.contracts = config.contracts;
1227
1053
  if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
1228
1054
  if (config.feeManager) this.feeManager = config.feeManager;
1229
1055
  if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1056
+ if (config.claim) this.claim = config.claim;
1230
1057
  }
1231
1058
  // =========================================================================
1232
1059
  // Public handlers (no auth required)
@@ -1241,6 +1068,12 @@ var IssuerApiHandlers = class {
1241
1068
  if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
1242
1069
  throw new Error("handleLogin: message and signature are required");
1243
1070
  }
1071
+ if (body.message.length > 4096) {
1072
+ throw new Error("message too long");
1073
+ }
1074
+ if (body.signature.length > 260) {
1075
+ throw new Error("signature too long");
1076
+ }
1244
1077
  const result = await this.authService.login(body.message, body.signature);
1245
1078
  return {
1246
1079
  token: result.token,
@@ -1255,9 +1088,12 @@ var IssuerApiHandlers = class {
1255
1088
  * needs to build EIP-712 messages and interact with on-chain.
1256
1089
  */
1257
1090
  async handleConfig(chainId) {
1091
+ if (!Number.isInteger(chainId) || chainId <= 0) {
1092
+ throw new Error("invalid chainId");
1093
+ }
1258
1094
  if (chainId !== this.chainId) {
1259
1095
  throw new Error(
1260
- `handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
1096
+ `handleConfig: unsupported chainId ${chainId}`
1261
1097
  );
1262
1098
  }
1263
1099
  const contracts = {
@@ -1320,25 +1156,25 @@ var IssuerApiHandlers = class {
1320
1156
  `handleUser: unsupported chainId ${request.chainId}`
1321
1157
  );
1322
1158
  }
1323
- const normalizedAuthed = (0, import_viem8.getAddress)(userAddress);
1324
- const normalizedRequest = (0, import_viem8.getAddress)(request.userAddress);
1159
+ const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
1160
+ const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
1325
1161
  if (normalizedAuthed !== normalizedRequest) {
1326
1162
  throw new Error(
1327
1163
  "handleUser: request userAddress must match authenticated user"
1328
1164
  );
1329
1165
  }
1330
- const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1166
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1331
1167
  if (!this.supportedTokens.has(pointToken)) {
1332
1168
  throw new Error(
1333
1169
  `handleUser: unsupported pointToken ${pointToken}`
1334
1170
  );
1335
1171
  }
1336
1172
  const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
1337
- (0, import_core4.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1338
- (0, import_core4.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1173
+ (0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1174
+ (0, import_core3.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1339
1175
  this.ledger.getBalance(normalizedAuthed, pointToken),
1340
- (0, import_core4.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
1341
- (0, import_core4.isMinter)(this.provider, pointToken, normalizedAuthed)
1176
+ (0, import_core3.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
1177
+ (0, import_core3.isMinter)(this.provider, pointToken, normalizedAuthed)
1342
1178
  ]);
1343
1179
  return {
1344
1180
  mintRequestNonce,
@@ -1370,19 +1206,32 @@ var IssuerApiHandlers = class {
1370
1206
  `handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
1371
1207
  );
1372
1208
  }
1373
- const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1209
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1374
1210
  if (!this.supportedTokens.has(pointToken)) {
1375
1211
  throw new Error(
1376
1212
  `handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
1377
1213
  );
1378
1214
  }
1379
- const name = await (0, import_core4.getTokenName)(this.provider, pointToken);
1215
+ const consent = request.receiverConsent;
1216
+ if ((0, import_viem6.getAddress)(consent.originalReceiver) !== (0, import_viem6.getAddress)(userAddress)) {
1217
+ throw new Error(
1218
+ "handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
1219
+ );
1220
+ }
1221
+ if (consent.amount <= 0n) {
1222
+ throw new Error("handleBuildConsentTypedData: amount must be positive");
1223
+ }
1224
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1225
+ if (consent.deadline <= nowSecs) {
1226
+ throw new Error("handleBuildConsentTypedData: deadline is in the past");
1227
+ }
1228
+ const name = await (0, import_core3.getTokenName)(this.provider, pointToken);
1380
1229
  const domain = {
1381
1230
  name,
1382
1231
  verifyingContract: pointToken,
1383
1232
  chainId: this.chainId
1384
1233
  };
1385
- const typedData = (0, import_core4.buildReceiverConsentTypedData)(domain, request.receiverConsent);
1234
+ const typedData = (0, import_core3.buildReceiverConsentTypedData)(domain, consent);
1386
1235
  return {
1387
1236
  typedData: {
1388
1237
  domain: typedData.domain,
@@ -1392,11 +1241,96 @@ var IssuerApiHandlers = class {
1392
1241
  }
1393
1242
  };
1394
1243
  }
1244
+ /**
1245
+ * `POST /claim`
1246
+ *
1247
+ * Policy gate + ledger lock + MintRequest signing in a single atomic
1248
+ * step. Returns an unsigned UserOp the frontend attaches paymaster data
1249
+ * to and submits via EIP-7702 + Bundler.
1250
+ *
1251
+ * Order of operations:
1252
+ * 1. Validate request fields.
1253
+ * 2. policy.evaluate() — throws if denied; cannot be bypassed.
1254
+ * 3. ledger.lockForMinting() — reserves the balance.
1255
+ * 4. Read on-chain mintRequestNonce + token name in parallel.
1256
+ * 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
1257
+ * 6. On any error after step 3, release the lock before re-throwing.
1258
+ */
1259
+ async handleClaim(userAddress, request) {
1260
+ if (!this.claim) {
1261
+ throw new Error("handleClaim: claim is not configured on this issuer");
1262
+ }
1263
+ if (request.chainId !== this.chainId) {
1264
+ throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
1265
+ }
1266
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1267
+ if (!this.supportedTokens.has(pointToken)) {
1268
+ throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
1269
+ }
1270
+ if (request.amount <= 0n) {
1271
+ throw new Error("handleClaim: amount must be positive");
1272
+ }
1273
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1274
+ if (request.deadline <= nowSecs) {
1275
+ throw new Error("handleClaim: deadline is in the past");
1276
+ }
1277
+ const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
1278
+ const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
1279
+ const normalizedUser = (0, import_viem6.getAddress)(userAddress);
1280
+ const decision = await policy.evaluate({
1281
+ userAddress: normalizedUser,
1282
+ amount: request.amount,
1283
+ pointTokenAddress: pointToken,
1284
+ chainId: this.chainId
1285
+ });
1286
+ if (!decision.approved) {
1287
+ throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
1288
+ }
1289
+ const lockId = await this.ledger.lockForMinting(
1290
+ normalizedUser,
1291
+ request.amount,
1292
+ lockDurationMs,
1293
+ pointToken
1294
+ );
1295
+ try {
1296
+ const [mintRequestNonce, tokenName] = await Promise.all([
1297
+ (0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedUser),
1298
+ (0, import_core3.getTokenName)(this.provider, pointToken)
1299
+ ]);
1300
+ const domain = {
1301
+ name: tokenName,
1302
+ verifyingContract: pointToken,
1303
+ chainId: this.chainId
1304
+ };
1305
+ const userOp = await relayService.prepareMint({
1306
+ userAddress: normalizedUser,
1307
+ aaNonce: request.aaNonce,
1308
+ batchExecutorAddress,
1309
+ pointTokenAddress: pointToken,
1310
+ amount: request.amount,
1311
+ issuerSignerWallet,
1312
+ domain,
1313
+ mintRequestNonce,
1314
+ deadline: request.deadline,
1315
+ feeAmount: request.feeAmount,
1316
+ feeRecipient: request.feeRecipient
1317
+ });
1318
+ return {
1319
+ lockId,
1320
+ userOp,
1321
+ expiresInSeconds: Math.floor(lockDurationMs / 1e3)
1322
+ };
1323
+ } catch (err) {
1324
+ await this.ledger.releaseLock(lockId).catch(() => {
1325
+ });
1326
+ throw err;
1327
+ }
1328
+ }
1395
1329
  };
1396
1330
 
1397
1331
  // src/api/handlers/ptRedeemHandler.ts
1398
- var import_viem9 = require("viem");
1399
- var import_core5 = require("@pafi-dev/core");
1332
+ var import_viem7 = require("viem");
1333
+ var import_core4 = require("@pafi-dev/core");
1400
1334
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1401
1335
  var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1402
1336
  var PTRedeemError = class extends Error {
@@ -1435,16 +1369,25 @@ var PTRedeemHandler = class {
1435
1369
  this.ledger = config.ledger;
1436
1370
  this.relayService = config.relayService;
1437
1371
  this.provider = config.provider;
1438
- this.pointTokenAddress = (0, import_viem9.getAddress)(config.pointTokenAddress);
1439
- this.batchExecutorAddress = (0, import_viem9.getAddress)(config.batchExecutorAddress);
1372
+ this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
1373
+ this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
1440
1374
  this.chainId = config.chainId;
1441
1375
  this.domain = config.domain;
1442
1376
  this.burnerSignerWallet = config.burnerSignerWallet;
1377
+ if (this.burnerSignerWallet?.account?.type === "local") {
1378
+ console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
1379
+ }
1443
1380
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1444
1381
  this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1445
1382
  this.now = config.now ?? (() => Date.now());
1446
1383
  }
1447
1384
  async handle(request) {
1385
+ if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
1386
+ throw new PTRedeemError(
1387
+ "UNAUTHORIZED",
1388
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1389
+ );
1390
+ }
1448
1391
  if (request.amount <= 0n) {
1449
1392
  throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1450
1393
  }
@@ -1452,7 +1395,7 @@ var PTRedeemHandler = class {
1452
1395
  try {
1453
1396
  burnNonce = await this.provider.readContract({
1454
1397
  address: this.pointTokenAddress,
1455
- abi: import_core5.POINT_TOKEN_V2_ABI,
1398
+ abi: import_core4.POINT_TOKEN_V2_ABI,
1456
1399
  functionName: "burnRequestNonces",
1457
1400
  args: [request.userAddress]
1458
1401
  });
@@ -1462,6 +1405,17 @@ var PTRedeemHandler = class {
1462
1405
  `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1463
1406
  );
1464
1407
  }
1408
+ const onChainBalance = await (0, import_core4.getPointTokenBalance)(
1409
+ this.provider,
1410
+ this.pointTokenAddress,
1411
+ request.userAddress
1412
+ );
1413
+ if (onChainBalance < request.amount) {
1414
+ throw new PTRedeemError(
1415
+ "INVALID_AMOUNT",
1416
+ `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
1417
+ );
1418
+ }
1465
1419
  const deadline = BigInt(
1466
1420
  Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1467
1421
  );
@@ -1478,7 +1432,7 @@ var PTRedeemHandler = class {
1478
1432
  };
1479
1433
  let burnerSignature;
1480
1434
  try {
1481
- const sig = await (0, import_core5.signBurnRequest)(
1435
+ const sig = await (0, import_core4.signBurnRequest)(
1482
1436
  this.burnerSignerWallet,
1483
1437
  domain,
1484
1438
  burnRequest
@@ -1515,8 +1469,8 @@ var PTRedeemHandler = class {
1515
1469
  };
1516
1470
 
1517
1471
  // src/api/handlers/topUpRedemptionHandler.ts
1518
- var import_viem10 = require("viem");
1519
- var import_core6 = require("@pafi-dev/core");
1472
+ var import_viem8 = require("viem");
1473
+ var import_core5 = require("@pafi-dev/core");
1520
1474
  var TopUpRedemptionError = class extends Error {
1521
1475
  constructor(code, message) {
1522
1476
  super(message);
@@ -1534,9 +1488,15 @@ var TopUpRedemptionHandler = class {
1534
1488
  this.ledger = config.ledger;
1535
1489
  this.ptRedeemHandler = config.ptRedeemHandler;
1536
1490
  this.provider = config.provider;
1537
- this.pointTokenAddress = (0, import_viem10.getAddress)(config.pointTokenAddress);
1491
+ this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
1538
1492
  }
1539
1493
  async handle(request) {
1494
+ if ((0, import_viem8.getAddress)(request.authenticatedAddress) !== (0, import_viem8.getAddress)(request.userAddress)) {
1495
+ throw new TopUpRedemptionError(
1496
+ "UNAUTHORIZED",
1497
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1498
+ );
1499
+ }
1540
1500
  const offChainBalance = await this.ledger.getBalance(
1541
1501
  request.userAddress,
1542
1502
  this.pointTokenAddress
@@ -1545,7 +1505,7 @@ var TopUpRedemptionHandler = class {
1545
1505
  return { action: "NO_TOP_UP_NEEDED", offChainBalance };
1546
1506
  }
1547
1507
  const shortfall = request.requiredAmount - offChainBalance;
1548
- const onChainBalance = await (0, import_core6.getPointTokenBalance)(
1508
+ const onChainBalance = await (0, import_core5.getPointTokenBalance)(
1549
1509
  this.provider,
1550
1510
  this.pointTokenAddress,
1551
1511
  request.userAddress
@@ -1559,6 +1519,7 @@ var TopUpRedemptionHandler = class {
1559
1519
  };
1560
1520
  }
1561
1521
  const redeem = await this.ptRedeemHandler.handle({
1522
+ authenticatedAddress: request.authenticatedAddress,
1562
1523
  userAddress: request.userAddress,
1563
1524
  amount: shortfall,
1564
1525
  aaNonce: request.aaNonce
@@ -1572,6 +1533,7 @@ var TopUpRedemptionHandler = class {
1572
1533
  };
1573
1534
 
1574
1535
  // src/pools/subgraphPoolsProvider.ts
1536
+ var import_viem9 = require("viem");
1575
1537
  var DEFAULT_CACHE_TTL_MS = 3e4;
1576
1538
  var POOL_QUERY = `
1577
1539
  query GetPoolForPointToken($id: ID!) {
@@ -1594,6 +1556,19 @@ function createSubgraphPoolsProvider(config) {
1594
1556
  "createSubgraphPoolsProvider: subgraphUrl is required"
1595
1557
  );
1596
1558
  }
1559
+ try {
1560
+ const parsed = new URL(config.subgraphUrl);
1561
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1562
+ throw new Error("subgraphUrl must use HTTPS in production");
1563
+ }
1564
+ } catch (err) {
1565
+ if (err instanceof TypeError) {
1566
+ throw new Error(
1567
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1568
+ );
1569
+ }
1570
+ throw err;
1571
+ }
1597
1572
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1598
1573
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1599
1574
  const now = config.now ?? (() => Date.now());
@@ -1662,6 +1637,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
1662
1637
  return [];
1663
1638
  }
1664
1639
  const { pool } = token;
1640
+ if (!(0, import_viem9.isAddress)(pool.hooks)) {
1641
+ console.error(
1642
+ "[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
1643
+ pool.hooks,
1644
+ "\u2014 skipping pool"
1645
+ );
1646
+ return [];
1647
+ }
1648
+ if (!(0, import_viem9.isAddress)(pool.token0.id) || !(0, import_viem9.isAddress)(pool.token1.id)) {
1649
+ console.error(
1650
+ "[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
1651
+ );
1652
+ return [];
1653
+ }
1654
+ if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
1655
+ console.error(
1656
+ "[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
1657
+ );
1658
+ return [];
1659
+ }
1665
1660
  const [currency0, currency1] = sortCurrencies(
1666
1661
  pool.token0.id,
1667
1662
  pool.token1.id
@@ -1697,6 +1692,19 @@ function createSubgraphNativeUsdtQuoter(config) {
1697
1692
  "createSubgraphNativeUsdtQuoter: subgraphUrl is required"
1698
1693
  );
1699
1694
  }
1695
+ try {
1696
+ const parsed = new URL(config.subgraphUrl);
1697
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1698
+ throw new Error("subgraphUrl must use HTTPS in production");
1699
+ }
1700
+ } catch (err) {
1701
+ if (err instanceof TypeError) {
1702
+ throw new Error(
1703
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1704
+ );
1705
+ }
1706
+ throw err;
1707
+ }
1700
1708
  const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
1701
1709
  const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
1702
1710
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
@@ -1773,6 +1781,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
1773
1781
  );
1774
1782
  return null;
1775
1783
  }
1784
+ const MIN_REASONABLE_ETH_PRICE = 100;
1785
+ const MAX_REASONABLE_ETH_PRICE = 1e5;
1786
+ if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
1787
+ console.warn(
1788
+ `[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
1789
+ );
1790
+ return null;
1791
+ }
1776
1792
  return parsed;
1777
1793
  }
1778
1794
  function toUsdtPerNative(priceFloat, usdtDecimals) {
@@ -1783,7 +1799,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
1783
1799
  }
1784
1800
 
1785
1801
  // src/balance/balanceAggregator.ts
1786
- var import_core7 = require("@pafi-dev/core");
1802
+ var import_core6 = require("@pafi-dev/core");
1787
1803
  var BalanceAggregator = class {
1788
1804
  provider;
1789
1805
  ledger;
@@ -1804,7 +1820,7 @@ var BalanceAggregator = class {
1804
1820
  async getCombinedBalance(user, pointToken) {
1805
1821
  const [offChain, onChain] = await Promise.all([
1806
1822
  this.ledger.getBalance(user, pointToken),
1807
- (0, import_core7.getPointTokenBalance)(this.provider, pointToken, user)
1823
+ (0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
1808
1824
  ]);
1809
1825
  return {
1810
1826
  offChain,
@@ -1842,37 +1858,16 @@ var PafiBackendError = class extends Error {
1842
1858
  code;
1843
1859
  httpStatus;
1844
1860
  details;
1845
- /**
1846
- * Seconds to wait before retry. Populated from the server body
1847
- * (e.g. rate limit returns the number of seconds until UTC midnight).
1848
- */
1849
1861
  retryAfter;
1850
- /**
1851
- * `safeToRetry` as reported by the server body. Prefer this over the
1852
- * code-based heuristic when available — the server knows more about
1853
- * whether the same request will succeed on retry.
1854
- */
1855
1862
  serverSafeToRetry;
1856
- /**
1857
- * Whether the caller can safely retry the same request.
1858
- *
1859
- * If the server provided `safeToRetry` in the body, trust that.
1860
- * Otherwise fall back to a code-based heuristic.
1861
- */
1862
1863
  get safeToRetry() {
1863
1864
  if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
1864
1865
  switch (this.code) {
1865
- // Transient infra
1866
- case "PAYMASTER_UNAVAILABLE":
1867
- case "PAYMASTER_TIMEOUT":
1868
1866
  case "RATE_LIMITER_UNAVAILABLE":
1869
- case "KMS_UNAVAILABLE":
1870
- case "SPONSOR_AUTH_SIGNING_FAILED":
1871
1867
  case "INTERNAL_ERROR":
1872
1868
  case "TIMEOUT":
1873
1869
  case "NETWORK_ERROR":
1874
1870
  return true;
1875
- // Rate-limited — safe to retry after retryAfter window
1876
1871
  case "RATE_LIMIT_EXCEEDED":
1877
1872
  case "RATE_LIMIT_EXCEEDED_DAILY":
1878
1873
  case "RATE_LIMIT_EXCEEDED_PER_USER":
@@ -1884,201 +1879,104 @@ var PafiBackendError = class extends Error {
1884
1879
  }
1885
1880
  };
1886
1881
 
1887
- // src/pafi-backend/pafiBackendClient.ts
1888
- var DEFAULT_TIMEOUT_MS = 1e4;
1889
- var RETRY_DEFAULTS = {
1890
- maxAttempts: 1,
1891
- initialDelayMs: 500,
1892
- maxDelayMs: 1e4,
1893
- maxRetryAfterMs: 3e4
1894
- };
1882
+ // src/pafi-backend/client.ts
1883
+ function serializeBigInt(_key, value) {
1884
+ return typeof value === "bigint" ? value.toString(10) : value;
1885
+ }
1886
+ function sleep(ms) {
1887
+ return new Promise((resolve) => setTimeout(resolve, ms));
1888
+ }
1895
1889
  var PafiBackendClient = class {
1896
- url;
1897
- issuerId;
1898
- apiKey;
1899
- fetchImpl;
1900
- timeoutMs;
1901
- retry;
1890
+ config;
1902
1891
  constructor(config) {
1903
- if (!config.url) {
1904
- throw new Error("PafiBackendClient: url is required");
1905
- }
1906
- if (!config.issuerId) {
1907
- throw new Error("PafiBackendClient: issuerId is required");
1908
- }
1909
- if (!config.apiKey) {
1910
- throw new Error("PafiBackendClient: apiKey is required");
1911
- }
1912
- this.url = config.url.replace(/\/+$/, "");
1913
- this.issuerId = config.issuerId;
1914
- this.apiKey = config.apiKey;
1915
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
1916
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1917
- this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
1918
- if (!this.fetchImpl) {
1919
- throw new Error(
1920
- "PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
1921
- );
1922
- }
1923
- if (this.retry.maxAttempts < 1) {
1924
- throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
1925
- }
1926
- }
1927
- /**
1928
- * Request a SponsorAuth signature from PAFI sponsor-relayer (beta.8+).
1929
- *
1930
- * The relayer:
1931
- * 1. Authenticates user (JWT) + issuer (API key)
1932
- * 2. Per-(user, scenario) rate limit + per-issuer daily budget
1933
- * 3. Scenario-specific intent validation (mint cap, KYC, etc.)
1934
- * 4. Allocates nonce + signs SponsorAuth payload via KMS PAFI key
1935
- * 5. Returns `{ sponsorAuth, payload }` for the FE to forward to
1936
- * Privy's `signUserOperation({ sponsorAuth, payload })`.
1937
- *
1938
- * Retries on transient failures (5xx, timeouts, KMS unavailable,
1939
- * rate-limit-with-retryAfter). 4xx that are not `safeToRetry` fail fast.
1940
- *
1941
- * See `pafi-backend/docs/SPONSOR_AUTH_DESIGN.md` for the full spec.
1942
- *
1943
- * @throws PafiBackendError on final failure after exhausting retries
1944
- */
1945
- async requestSponsorAuth(req) {
1946
- return this.postWithRetry(
1947
- "/sponsor-auth",
1948
- req
1949
- );
1950
- }
1951
- /**
1952
- * @deprecated Coinbase paymaster path — replaced by `requestSponsorAuth`
1953
- * in beta.8. Will be removed in 1.0. Migrate by:
1954
- * 1. Switch to `requestSponsorAuth` returning `{ sponsorAuth, payload }`
1955
- * 2. Pass both to Privy `signUserOperation` instead of merging
1956
- * paymasterData into the UserOp callData
1957
- */
1958
- async requestSponsorship(req) {
1959
- return this.postWithRetry(
1960
- "/paymaster/sponsor",
1961
- req
1962
- );
1963
- }
1964
- // -------------------------------------------------------------------------
1965
- // Internals
1966
- // -------------------------------------------------------------------------
1967
- async postWithRetry(path, body) {
1892
+ if (!config.url) throw new Error("PafiBackendClient: url is required");
1893
+ if (!config.issuerId) throw new Error("PafiBackendClient: issuerId is required");
1894
+ this.config = config;
1895
+ }
1896
+ async requestSponsorship(request) {
1897
+ const maxAttempts = this.config.retry?.maxAttempts ?? 1;
1898
+ const initialDelayMs = this.config.retry?.initialDelayMs ?? 100;
1899
+ const maxDelayMs = this.config.retry?.maxDelayMs ?? 1e4;
1900
+ const maxRetryAfterMs = this.config.retry?.maxRetryAfterMs;
1968
1901
  let lastError;
1969
- for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
1902
+ let delay = initialDelayMs;
1903
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1970
1904
  try {
1971
- return await this.post(path, body);
1905
+ return await this._doRequest(request);
1972
1906
  } catch (err) {
1973
1907
  if (!(err instanceof PafiBackendError)) throw err;
1974
1908
  lastError = err;
1975
- const isLastAttempt = attempt >= this.retry.maxAttempts;
1976
- if (isLastAttempt || !err.safeToRetry) throw err;
1977
- const delay = this.computeBackoff(attempt, err.retryAfter);
1978
- if (delay === null) throw err;
1979
- await this.sleep(delay);
1909
+ if (attempt >= maxAttempts) break;
1910
+ if (!err.safeToRetry) break;
1911
+ const retryAfterMs = err.retryAfter !== void 0 ? err.retryAfter * 1e3 : void 0;
1912
+ if (maxRetryAfterMs !== void 0 && retryAfterMs !== void 0 && retryAfterMs > maxRetryAfterMs) {
1913
+ break;
1914
+ }
1915
+ await sleep(retryAfterMs ?? delay);
1916
+ delay = Math.min(delay * 2, maxDelayMs);
1980
1917
  }
1981
1918
  }
1982
1919
  throw lastError;
1983
1920
  }
1984
- /**
1985
- * Pick the delay before the next retry.
1986
- * - If the server sent `retryAfter` (seconds), honor it (capped by
1987
- * `maxRetryAfterMs`) returns null if the server wait exceeds the
1988
- * cap, signalling the caller should give up.
1989
- * - Otherwise: exponential backoff with ±20% jitter, capped at
1990
- * `maxDelayMs`.
1991
- */
1992
- computeBackoff(attempt, retryAfter) {
1993
- if (retryAfter !== void 0) {
1994
- const serverMs = retryAfter * 1e3;
1995
- if (serverMs > this.retry.maxRetryAfterMs) return null;
1996
- return serverMs;
1997
- }
1998
- const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
1999
- const capped = Math.min(exp, this.retry.maxDelayMs);
2000
- const jitter = capped * (0.8 + Math.random() * 0.4);
2001
- return Math.round(jitter);
2002
- }
2003
- sleep(ms) {
2004
- return new Promise((resolve) => setTimeout(resolve, ms));
2005
- }
2006
- async post(path, body) {
2007
- const controller = new AbortController();
2008
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
1921
+ async _doRequest(request) {
1922
+ const fetchFn = this.config.fetchImpl ?? fetch;
1923
+ const url = `${this.config.url}/paymaster/sponsor`;
1924
+ const body = JSON.stringify(request, serializeBigInt);
2009
1925
  let response;
2010
1926
  try {
2011
- response = await this.fetchImpl(`${this.url}${path}`, {
1927
+ response = await fetchFn(url, {
2012
1928
  method: "POST",
2013
1929
  headers: {
2014
1930
  "Content-Type": "application/json",
2015
- "Authorization": `Bearer ${this.apiKey}`,
2016
- "X-Issuer-Id": this.issuerId
1931
+ Authorization: `Bearer ${this.config.apiKey}`,
1932
+ "X-Issuer-Id": this.config.issuerId
2017
1933
  },
2018
- body: JSON.stringify(body, this.bigintReplacer),
2019
- signal: controller.signal
1934
+ body
2020
1935
  });
2021
1936
  } catch (err) {
2022
- if (err.name === "AbortError") {
2023
- throw new PafiBackendError(
2024
- "TIMEOUT",
2025
- `PAFI Backend request timed out after ${this.timeoutMs}ms`,
2026
- 0
2027
- );
2028
- }
2029
1937
  throw new PafiBackendError(
2030
1938
  "NETWORK_ERROR",
2031
- `PAFI Backend unreachable: ${err.message}`,
1939
+ `Network error: ${err instanceof Error ? err.message : String(err)}`,
2032
1940
  0
2033
1941
  );
2034
- } finally {
2035
- clearTimeout(timeoutId);
2036
1942
  }
2037
1943
  const text = await response.text();
1944
+ let json = {};
1945
+ try {
1946
+ json = JSON.parse(text);
1947
+ } catch {
1948
+ }
2038
1949
  if (!response.ok) {
2039
- let code = "INTERNAL_ERROR";
2040
- let message = text || response.statusText;
2041
- let details;
2042
- let retryAfter;
2043
- let serverSafeToRetry;
2044
- try {
2045
- const parsed = JSON.parse(text);
2046
- code = parsed.code ?? code;
2047
- message = parsed.message ?? message;
2048
- details = parsed.details;
2049
- if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
2050
- if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
2051
- } catch {
2052
- }
2053
- throw new PafiBackendError(code, message, response.status, details, {
2054
- ...retryAfter !== void 0 ? { retryAfter } : {},
2055
- ...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
1950
+ const code = json.code ?? "INTERNAL_ERROR";
1951
+ const message = json.message ?? `HTTP ${response.status}`;
1952
+ const retryAfter = typeof json.retryAfter === "number" ? json.retryAfter : void 0;
1953
+ const safeToRetry = typeof json.safeToRetry === "boolean" ? json.safeToRetry : void 0;
1954
+ throw new PafiBackendError(code, message, response.status, json, {
1955
+ retryAfter,
1956
+ safeToRetry
2056
1957
  });
2057
1958
  }
2058
- return JSON.parse(text, this.bigintReviver);
1959
+ return {
1960
+ paymaster: json.paymaster,
1961
+ paymasterData: json.paymasterData,
1962
+ paymasterVerificationGasLimit: BigInt(
1963
+ json.paymasterVerificationGasLimit
1964
+ ),
1965
+ paymasterPostOpGasLimit: BigInt(json.paymasterPostOpGasLimit),
1966
+ expiresAt: json.expiresAt
1967
+ };
2059
1968
  }
2060
- /** JSON replacer that stringifies bigints. Paired with bigintReviver. */
2061
- bigintReplacer = (_key, value) => {
2062
- return typeof value === "bigint" ? value.toString() : value;
2063
- };
2064
- /**
2065
- * JSON reviver that coerces specific numeric-string fields back to
2066
- * bigint. The server must send these fields as decimal strings.
2067
- */
2068
- bigintReviver = (key, value) => {
2069
- if (typeof value === "string" && (key.endsWith("GasLimit") || key === "nonce" || key === "callGasLimit" || key === "verificationGasLimit" || key === "preVerificationGas" || key === "maxFeePerGas" || key === "maxPriorityFeePerGas" || key === "paymasterVerificationGasLimit" || key === "paymasterPostOpGasLimit") && /^\d+$/.test(value)) {
2070
- return BigInt(value);
2071
- }
2072
- return value;
2073
- };
2074
1969
  };
2075
1970
 
2076
1971
  // src/config.ts
2077
- var import_viem11 = require("viem");
1972
+ var import_viem10 = require("viem");
2078
1973
  function createIssuerService(config) {
2079
1974
  if (!config.provider) {
2080
1975
  throw new Error("createIssuerService: provider is required");
2081
1976
  }
1977
+ if (!config.ledger) {
1978
+ throw new Error("createIssuerService: ledger is required");
1979
+ }
2082
1980
  if (!config.auth?.jwtSecret) {
2083
1981
  throw new Error("createIssuerService: auth.jwtSecret is required");
2084
1982
  }
@@ -2091,8 +1989,8 @@ function createIssuerService(config) {
2091
1989
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
2092
1990
  );
2093
1991
  }
2094
- const tokenAddresses = rawAddresses.map((a) => (0, import_viem11.getAddress)(a));
2095
- const ledger = config.ledger ?? new MemoryPointLedger();
1992
+ const tokenAddresses = rawAddresses.map((a) => (0, import_viem10.getAddress)(a));
1993
+ const ledger = config.ledger;
2096
1994
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
2097
1995
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
2098
1996
  const authServiceConfig = {
@@ -2148,6 +2046,15 @@ function createIssuerService(config) {
2148
2046
  };
2149
2047
  if (feeManager) handlersConfig.feeManager = feeManager;
2150
2048
  if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
2049
+ if (config.claim) {
2050
+ handlersConfig.claim = {
2051
+ policy,
2052
+ relayService,
2053
+ issuerSignerWallet: config.claim.issuerSignerWallet,
2054
+ batchExecutorAddress: config.claim.batchExecutorAddress,
2055
+ lockDurationMs: config.claim.lockDurationMs
2056
+ };
2057
+ }
2151
2058
  const handlers = new IssuerApiHandlers(handlersConfig);
2152
2059
  if (config.indexer?.autoStart) {
2153
2060
  for (const idx of indexers.values()) {
@@ -2179,7 +2086,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
2179
2086
  FeeManager,
2180
2087
  InMemoryCursorStore,
2181
2088
  IssuerApiHandlers,
2182
- MemoryPointLedger,
2183
2089
  MemorySessionStore,
2184
2090
  NonceManager,
2185
2091
  PAFI_ISSUER_SDK_VERSION,
@@ -2188,7 +2094,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
2188
2094
  PafiBackendClient,
2189
2095
  PafiBackendError,
2190
2096
  PointIndexer,
2191
- PrivateKeySigner,
2192
2097
  RelayError,
2193
2098
  RelayService,
2194
2099
  TopUpRedemptionError,