@pafi-dev/issuer 0.3.0-beta.10 → 0.3.0-beta.11

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,
@@ -36,7 +35,6 @@ __export(index_exports, {
36
35
  PTRedeemHandler: () => PTRedeemHandler,
37
36
  PafiBackendError: () => PafiBackendError,
38
37
  PointIndexer: () => PointIndexer,
39
- PrivateKeySigner: () => PrivateKeySigner,
40
38
  RelayError: () => RelayError,
41
39
  RelayService: () => RelayService,
42
40
  TopUpRedemptionError: () => TopUpRedemptionError,
@@ -48,204 +46,6 @@ __export(index_exports, {
48
46
  });
49
47
  module.exports = __toCommonJS(index_exports);
50
48
 
51
- // src/ledger/memoryLedger.ts
52
- var import_viem = require("viem");
53
- var MemoryPointLedger = class {
54
- balances = /* @__PURE__ */ new Map();
55
- locks = /* @__PURE__ */ new Map();
56
- nextLockId = 1;
57
- now;
58
- constructor(opts = {}) {
59
- this.now = opts.now ?? (() => Date.now());
60
- }
61
- // -------------------------------------------------------------------------
62
- // Read
63
- // -------------------------------------------------------------------------
64
- async getBalance(userAddress, tokenAddress) {
65
- const user = (0, import_viem.getAddress)(userAddress);
66
- const token = normalizeToken(tokenAddress);
67
- this.purgeExpired();
68
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
69
- const locked = this.lockedTotalFor(user, token);
70
- return total - locked;
71
- }
72
- async getLockedRequests(userAddress, tokenAddress) {
73
- const user = (0, import_viem.getAddress)(userAddress);
74
- const token = normalizeToken(tokenAddress);
75
- this.purgeExpired();
76
- const out = [];
77
- for (const lock of this.locks.values()) {
78
- if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
79
- out.push({ ...lock });
80
- }
81
- }
82
- return out;
83
- }
84
- // -------------------------------------------------------------------------
85
- // Write
86
- // -------------------------------------------------------------------------
87
- async creditBalance(userAddress, amount, _reason, tokenAddress) {
88
- if (amount <= 0n) {
89
- throw new Error("MemoryPointLedger: credit amount must be positive");
90
- }
91
- const user = (0, import_viem.getAddress)(userAddress);
92
- const token = normalizeToken(tokenAddress);
93
- const key = balanceKey(user, token);
94
- const current = this.balances.get(key) ?? 0n;
95
- this.balances.set(key, current + amount);
96
- }
97
- async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
98
- if (amount <= 0n) {
99
- throw new Error("MemoryPointLedger: lock amount must be positive");
100
- }
101
- if (lockDurationMs <= 0) {
102
- throw new Error("MemoryPointLedger: lockDurationMs must be positive");
103
- }
104
- const user = (0, import_viem.getAddress)(userAddress);
105
- const token = normalizeToken(tokenAddress);
106
- this.purgeExpired();
107
- const total = this.balances.get(balanceKey(user, token)) ?? 0n;
108
- const alreadyLocked = this.lockedTotalFor(user, token);
109
- const available = total - alreadyLocked;
110
- if (available < amount) {
111
- throw new Error(
112
- `MemoryPointLedger: insufficient balance \u2014 available=${available}, requested=${amount}`
113
- );
114
- }
115
- const lockId = `lock-${this.nextLockId++}`;
116
- const now = this.now();
117
- const lock = {
118
- lockId,
119
- userAddress: user,
120
- amount,
121
- status: "PENDING",
122
- createdAt: now,
123
- expiresAt: now + lockDurationMs
124
- };
125
- if (tokenAddress !== void 0) {
126
- lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
127
- }
128
- this.locks.set(lockId, lock);
129
- return lockId;
130
- }
131
- async releaseLock(lockId) {
132
- const lock = this.locks.get(lockId);
133
- if (!lock) return;
134
- if (lock.status === "PENDING") {
135
- this.locks.delete(lockId);
136
- }
137
- }
138
- async deductBalance(userAddress, amount, txHash, tokenAddress) {
139
- if (amount <= 0n) {
140
- throw new Error("MemoryPointLedger: deduct amount must be positive");
141
- }
142
- const user = (0, import_viem.getAddress)(userAddress);
143
- const token = normalizeToken(tokenAddress);
144
- const key = balanceKey(user, token);
145
- const current = this.balances.get(key) ?? 0n;
146
- if (current < amount) {
147
- throw new Error(
148
- `MemoryPointLedger: cannot deduct ${amount} from balance ${current}`
149
- );
150
- }
151
- this.balances.set(key, current - amount);
152
- for (const lock of this.locks.values()) {
153
- if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
154
- lock.status = "MINTED";
155
- lock.txHash = txHash;
156
- return;
157
- }
158
- }
159
- }
160
- async updateMintStatus(lockId, status, txHash) {
161
- const lock = this.locks.get(lockId);
162
- if (!lock) {
163
- throw new Error(`MemoryPointLedger: unknown lockId ${lockId}`);
164
- }
165
- lock.status = status;
166
- if (txHash) lock.txHash = txHash;
167
- }
168
- // -------------------------------------------------------------------------
169
- // v1.4 — Reverse flow (PT burn → off-chain credit)
170
- // -------------------------------------------------------------------------
171
- pendingCredits = /* @__PURE__ */ new Map();
172
- nextCreditId = 1;
173
- async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
174
- if (amount <= 0n) {
175
- throw new Error(
176
- "MemoryPointLedger: pending credit amount must be positive"
177
- );
178
- }
179
- if (durationMs <= 0) {
180
- throw new Error("MemoryPointLedger: durationMs must be positive");
181
- }
182
- const user = (0, import_viem.getAddress)(userAddress);
183
- const lockId = `credit-${this.nextCreditId++}`;
184
- const now = this.now();
185
- this.pendingCredits.set(lockId, {
186
- lockId,
187
- userAddress: user,
188
- amount,
189
- tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
190
- createdAt: now,
191
- expiresAt: now + durationMs,
192
- status: "PENDING"
193
- });
194
- return lockId;
195
- }
196
- async resolveCreditByBurnTx(lockId, txHash) {
197
- const credit = this.pendingCredits.get(lockId);
198
- if (!credit) {
199
- throw new Error(
200
- `MemoryPointLedger: unknown pending credit lockId ${lockId}`
201
- );
202
- }
203
- if (credit.status === "RESOLVED") {
204
- if (credit.txHash === txHash) return;
205
- throw new Error(
206
- `MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
207
- );
208
- }
209
- const token = normalizeToken(credit.tokenAddress);
210
- const key = balanceKey(credit.userAddress, token);
211
- const current = this.balances.get(key) ?? 0n;
212
- this.balances.set(key, current + credit.amount);
213
- credit.status = "RESOLVED";
214
- credit.txHash = txHash;
215
- }
216
- // -------------------------------------------------------------------------
217
- // Internal helpers
218
- // -------------------------------------------------------------------------
219
- /**
220
- * Auto-expire any PENDING lock past its expiry. Called lazily on every
221
- * read/write so the in-memory state stays self-cleaning without a timer.
222
- */
223
- purgeExpired() {
224
- const now = this.now();
225
- for (const lock of this.locks.values()) {
226
- if (lock.status === "PENDING" && lock.expiresAt <= now) {
227
- lock.status = "EXPIRED";
228
- }
229
- }
230
- }
231
- lockedTotalFor(userAddress, tokenKey) {
232
- let total = 0n;
233
- for (const lock of this.locks.values()) {
234
- if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
235
- total += lock.amount;
236
- }
237
- }
238
- return total;
239
- }
240
- };
241
- var DEFAULT_TOKEN_KEY = "default";
242
- function normalizeToken(tokenAddress) {
243
- return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
244
- }
245
- function balanceKey(user, tokenKey) {
246
- return `${user}|${tokenKey}`;
247
- }
248
-
249
49
  // src/policy/defaultPolicy.ts
250
50
  var DefaultPolicyEngine = class {
251
51
  ledger;
@@ -261,6 +61,13 @@ var DefaultPolicyEngine = class {
261
61
  }
262
62
  if (opts.verifyMintCap) this.verifyMintCap = opts.verifyMintCap;
263
63
  if (opts.resolveIssuer) this.resolveIssuer = opts.resolveIssuer;
64
+ if (!opts.mintingOracleAddress || !opts.provider || !opts.verifyMintCap || !opts.resolveIssuer) {
65
+ if (process.env.NODE_ENV === "production") {
66
+ throw new Error(
67
+ "[PAFI] DefaultPolicyEngine: on-chain MintingOracle cap check is required in production. Configure mintingOracleAddress, provider, verifyMintCap, and resolveIssuer."
68
+ );
69
+ }
70
+ }
264
71
  }
265
72
  async evaluate(request) {
266
73
  if (request.amount <= 0n) {
@@ -297,32 +104,9 @@ var DefaultPolicyEngine = class {
297
104
  }
298
105
  };
299
106
 
300
- // src/signer/privateKeySigner.ts
301
- var import_viem2 = require("viem");
302
- var import_accounts = require("viem/accounts");
303
- var import_core = require("@pafi-dev/core");
304
- var PrivateKeySigner = class {
305
- account;
306
- walletClient;
307
- constructor(opts) {
308
- this.account = (0, import_accounts.privateKeyToAccount)(opts.privateKey);
309
- this.walletClient = (0, import_viem2.createWalletClient)({
310
- account: this.account,
311
- chain: opts.chain,
312
- transport: (0, import_viem2.http)(opts.rpcUrl)
313
- });
314
- }
315
- async signMintRequest(domain, message) {
316
- return (0, import_core.signMintRequest)(this.walletClient, domain, message);
317
- }
318
- async getAddress() {
319
- return this.account.address;
320
- }
321
- };
322
-
323
107
  // src/auth/memorySessionStore.ts
324
108
  var import_node_crypto = require("crypto");
325
- var import_viem3 = require("viem");
109
+ var import_viem = require("viem");
326
110
  var DEFAULT_NONCE_TTL_MS = 5 * 60 * 1e3;
327
111
  var MemorySessionStore = class {
328
112
  nonces = /* @__PURE__ */ new Map();
@@ -332,6 +116,11 @@ var MemorySessionStore = class {
332
116
  nonceTtlMs;
333
117
  now;
334
118
  constructor(opts = {}) {
119
+ if (process.env.NODE_ENV === "production") {
120
+ console.error(
121
+ "[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."
122
+ );
123
+ }
335
124
  this.nonceTtlMs = opts.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS;
336
125
  this.now = opts.now ?? (() => Date.now());
337
126
  }
@@ -358,7 +147,7 @@ var MemorySessionStore = class {
358
147
  this.purgeExpiredSessions();
359
148
  const normalized = {
360
149
  ...session,
361
- userAddress: (0, import_viem3.getAddress)(session.userAddress)
150
+ userAddress: (0, import_viem.getAddress)(session.userAddress)
362
151
  };
363
152
  this.sessions.set(session.tokenId, normalized);
364
153
  }
@@ -376,7 +165,7 @@ var MemorySessionStore = class {
376
165
  this.sessions.delete(tokenId);
377
166
  }
378
167
  async revokeAllSessions(userAddress) {
379
- const key = (0, import_viem3.getAddress)(userAddress);
168
+ const key = (0, import_viem.getAddress)(userAddress);
380
169
  for (const [tokenId, session] of this.sessions.entries()) {
381
170
  if (session.userAddress === key) {
382
171
  this.sessions.delete(tokenId);
@@ -422,8 +211,8 @@ var NonceManager = class {
422
211
  // src/auth/loginVerifier.ts
423
212
  var import_node_crypto2 = require("crypto");
424
213
  var import_jose = require("jose");
425
- var import_viem4 = require("viem");
426
- var import_core2 = require("@pafi-dev/core");
214
+ var import_viem2 = require("viem");
215
+ var import_core = require("@pafi-dev/core");
427
216
 
428
217
  // src/auth/errors.ts
429
218
  var AuthError = class extends Error {
@@ -446,8 +235,8 @@ var AuthService = class {
446
235
  nonceManager;
447
236
  now;
448
237
  constructor(config) {
449
- if (!config.jwtSecret || config.jwtSecret.length < 16) {
450
- throw new Error("AuthService: jwtSecret must be at least 16 chars");
238
+ if (!config.jwtSecret || config.jwtSecret.length < 32) {
239
+ throw new Error("AuthService: jwtSecret must be at least 32 characters for HS256 security");
451
240
  }
452
241
  this.sessionStore = config.sessionStore;
453
242
  this.jwtSecret = new TextEncoder().encode(config.jwtSecret);
@@ -471,11 +260,17 @@ var AuthService = class {
471
260
  async login(message, signature) {
472
261
  let parsed;
473
262
  try {
474
- parsed = (0, import_core2.parseLoginMessage)(message);
263
+ parsed = (0, import_core.parseLoginMessage)(message);
475
264
  } catch (err) {
476
265
  const msg = err instanceof Error ? err.message : String(err);
477
266
  throw new AuthError("INVALID_MESSAGE", `Could not parse login message: ${msg}`);
478
267
  }
268
+ if (parsed.expirationTime == null) {
269
+ throw new AuthError(
270
+ "INVALID_MESSAGE",
271
+ "login message must include expirationTime"
272
+ );
273
+ }
479
274
  if (parsed.domain !== this.domain) {
480
275
  throw new AuthError(
481
276
  "DOMAIN_MISMATCH",
@@ -498,7 +293,7 @@ var AuthService = class {
498
293
  if (parsed.expirationTime && parsed.expirationTime.getTime() <= now.getTime()) {
499
294
  throw new AuthError("MESSAGE_EXPIRED", "Login message has expired");
500
295
  }
501
- const verifyResult = await (0, import_core2.verifyLoginMessage)(message, signature);
296
+ const verifyResult = await (0, import_core.verifyLoginMessage)(message, signature);
502
297
  if (!verifyResult.valid) {
503
298
  throw new AuthError(
504
299
  "SIGNATURE_INVALID",
@@ -512,7 +307,7 @@ var AuthService = class {
512
307
  "Nonce is unknown, expired, or already used"
513
308
  );
514
309
  }
515
- const userAddress = (0, import_viem4.getAddress)(verifyResult.address);
310
+ const userAddress = (0, import_viem2.getAddress)(verifyResult.address);
516
311
  const tokenId = (0, import_node_crypto2.randomBytes)(16).toString("hex");
517
312
  const issuedAt = now;
518
313
  const expiresAt = parseExpiry(issuedAt, this.jwtExpiresIn);
@@ -540,7 +335,11 @@ var AuthService = class {
540
335
  if (payload.jti) {
541
336
  await this.sessionStore.revokeSession(payload.jti);
542
337
  }
543
- } catch {
338
+ } catch (err) {
339
+ const msg = err instanceof Error ? err.message : String(err);
340
+ if (!msg.includes("not found") && !msg.includes("expired")) {
341
+ console.error("[PAFI] AuthService logout: session store error", err);
342
+ }
544
343
  }
545
344
  }
546
345
  /**
@@ -579,7 +378,7 @@ var AuthService = class {
579
378
  throw new AuthError("TOKEN_INVALID", "JWT payload is malformed");
580
379
  }
581
380
  return {
582
- userAddress: (0, import_viem4.getAddress)(userAddress),
381
+ userAddress: (0, import_viem2.getAddress)(userAddress),
583
382
  chainId,
584
383
  tokenId
585
384
  };
@@ -630,8 +429,8 @@ var RelayError = class extends Error {
630
429
  };
631
430
 
632
431
  // src/relay/relayService.ts
633
- var import_viem5 = require("viem");
634
- var import_core3 = require("@pafi-dev/core");
432
+ var import_viem3 = require("viem");
433
+ var import_core2 = require("@pafi-dev/core");
635
434
  var RelayService = class {
636
435
  /**
637
436
  * Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
@@ -665,9 +464,20 @@ var RelayService = class {
665
464
  if (params.deadline <= 0n) {
666
465
  throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
667
466
  }
467
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
468
+ if (params.deadline <= nowSecs) {
469
+ throw new RelayError("ENCODE_FAILED", "prepareMint: deadline is in the past");
470
+ }
471
+ const MAX_DEADLINE_WINDOW = 3600n;
472
+ if (params.deadline > nowSecs + MAX_DEADLINE_WINDOW) {
473
+ throw new RelayError(
474
+ "ENCODE_FAILED",
475
+ "prepareMint: deadline exceeds maximum allowed window (1 hour)"
476
+ );
477
+ }
668
478
  let minterSig;
669
479
  try {
670
- const sig = await (0, import_core3.signMintRequest)(
480
+ const sig = await (0, import_core2.signMintRequest)(
671
481
  params.issuerSignerWallet,
672
482
  params.domain,
673
483
  {
@@ -687,8 +497,8 @@ var RelayService = class {
687
497
  }
688
498
  let mintCallData;
689
499
  try {
690
- mintCallData = (0, import_viem5.encodeFunctionData)({
691
- abi: import_core3.POINT_TOKEN_V2_ABI,
500
+ mintCallData = (0, import_viem3.encodeFunctionData)({
501
+ abi: import_core2.POINT_TOKEN_V2_ABI,
692
502
  functionName: "mint",
693
503
  args: [params.userAddress, params.amount, params.deadline, minterSig]
694
504
  });
@@ -713,17 +523,23 @@ var RelayService = class {
713
523
  "prepareMint: feeRecipient required when feeAmount > 0"
714
524
  );
715
525
  }
526
+ if (params.feeRecipient === "0x0000000000000000000000000000000000000000") {
527
+ throw new RelayError(
528
+ "ENCODE_FAILED",
529
+ "prepareMint: feeRecipient must not be zero address"
530
+ );
531
+ }
716
532
  operations.push({
717
533
  target: params.pointTokenAddress,
718
534
  value: 0n,
719
- data: (0, import_viem5.encodeFunctionData)({
720
- abi: import_viem5.erc20Abi,
535
+ data: (0, import_viem3.encodeFunctionData)({
536
+ abi: import_viem3.erc20Abi,
721
537
  functionName: "transfer",
722
538
  args: [params.feeRecipient, params.feeAmount]
723
539
  })
724
540
  });
725
541
  }
726
- return (0, import_core3.buildPartialUserOperation)({
542
+ return (0, import_core2.buildPartialUserOperation)({
727
543
  sender: params.userAddress,
728
544
  nonce: params.aaNonce,
729
545
  operations,
@@ -761,8 +577,8 @@ var RelayService = class {
761
577
  if (!params.burnRequest || !params.burnerSignature) {
762
578
  throw new Error("burnWithSig requires burnRequest + burnerSignature");
763
579
  }
764
- burnCallData = (0, import_viem5.encodeFunctionData)({
765
- abi: import_core3.POINT_TOKEN_V2_ABI,
580
+ burnCallData = (0, import_viem3.encodeFunctionData)({
581
+ abi: import_core2.POINT_TOKEN_V2_ABI,
766
582
  functionName: "burn",
767
583
  args: [
768
584
  params.burnRequest.from,
@@ -772,8 +588,8 @@ var RelayService = class {
772
588
  ]
773
589
  });
774
590
  } else {
775
- burnCallData = (0, import_viem5.encodeFunctionData)({
776
- abi: import_core3.POINT_TOKEN_V2_ABI,
591
+ burnCallData = (0, import_viem3.encodeFunctionData)({
592
+ abi: import_core2.POINT_TOKEN_V2_ABI,
777
593
  functionName: "burn",
778
594
  args: [params.userAddress, params.amount]
779
595
  });
@@ -792,7 +608,7 @@ var RelayService = class {
792
608
  data: burnCallData
793
609
  }
794
610
  ];
795
- return (0, import_core3.buildPartialUserOperation)({
611
+ return (0, import_core2.buildPartialUserOperation)({
796
612
  sender: params.userAddress,
797
613
  nonce: params.aaNonce,
798
614
  operations,
@@ -811,11 +627,14 @@ function errorMessage(err) {
811
627
  // src/relay/feeManager.ts
812
628
  var DEFAULT_GAS_UNITS = 500000n;
813
629
  var DEFAULT_PREMIUM_BPS = 12e3;
814
- var FeeManager = class {
630
+ var FeeManager = class _FeeManager {
815
631
  provider;
816
632
  gasUnits;
817
633
  gasPremiumBps;
818
634
  quoteNativeToFee;
635
+ cachedFee = null;
636
+ cacheExpiresAt = 0;
637
+ static CACHE_TTL_MS = 1e4;
819
638
  constructor(config) {
820
639
  if (!config.provider) throw new Error("FeeManager: provider required");
821
640
  if (!config.quoteNativeToFee)
@@ -838,10 +657,17 @@ var FeeManager = class {
838
657
  * currency depends on how the caller wired `quoteNativeToFee`.
839
658
  */
840
659
  async estimateGasFee() {
660
+ const now = Date.now();
661
+ if (this.cachedFee !== null && now < this.cacheExpiresAt) {
662
+ return this.cachedFee;
663
+ }
841
664
  const gasPrice = await this.provider.getGasPrice();
842
665
  const nativeCost = gasPrice * this.gasUnits;
843
666
  const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
844
- return this.quoteNativeToFee(withPremium);
667
+ const fee = await this.quoteNativeToFee(withPremium);
668
+ this.cachedFee = fee;
669
+ this.cacheExpiresAt = now + _FeeManager.CACHE_TTL_MS;
670
+ return fee;
845
671
  }
846
672
  };
847
673
 
@@ -857,8 +683,8 @@ var InMemoryCursorStore = class {
857
683
  };
858
684
 
859
685
  // src/indexer/pointIndexer.ts
860
- var import_viem6 = require("viem");
861
- var TRANSFER_EVENT = (0, import_viem6.parseAbiItem)(
686
+ var import_viem4 = require("viem");
687
+ var TRANSFER_EVENT = (0, import_viem4.parseAbiItem)(
862
688
  "event Transfer(address indexed from, address indexed to, uint256 value)"
863
689
  );
864
690
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
@@ -928,7 +754,8 @@ var PointIndexer = class {
928
754
  return;
929
755
  }
930
756
  await this.processBlockRange(from, safeHead);
931
- } catch {
757
+ } catch (err) {
758
+ console.error("[PAFI] PointIndexer tick error:", err);
932
759
  }
933
760
  this.scheduleNext();
934
761
  }
@@ -978,10 +805,10 @@ var PointIndexer = class {
978
805
  for (const log of logs) {
979
806
  const args = log.args;
980
807
  if (!args.from || !args.to || args.value === void 0) continue;
981
- if ((0, import_viem6.getAddress)(args.from) !== ZERO_ADDRESS) continue;
808
+ if ((0, import_viem4.getAddress)(args.from) !== ZERO_ADDRESS) continue;
982
809
  if (log.blockNumber === null || log.transactionHash === null) continue;
983
810
  out.push({
984
- to: (0, import_viem6.getAddress)(args.to),
811
+ to: (0, import_viem4.getAddress)(args.to),
985
812
  amount: args.value,
986
813
  blockNumber: log.blockNumber,
987
814
  txHash: log.transactionHash,
@@ -1037,8 +864,8 @@ function pickMatchingLock(locks, amount) {
1037
864
  }
1038
865
 
1039
866
  // src/indexer/burnIndexer.ts
1040
- var import_viem7 = require("viem");
1041
- var TRANSFER_EVENT2 = (0, import_viem7.parseAbiItem)(
867
+ var import_viem5 = require("viem");
868
+ var TRANSFER_EVENT2 = (0, import_viem5.parseAbiItem)(
1042
869
  "event Transfer(address indexed from, address indexed to, uint256 value)"
1043
870
  );
1044
871
  var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
@@ -1054,18 +881,7 @@ var BurnIndexer = class {
1054
881
  confirmations;
1055
882
  batchSize;
1056
883
  pollIntervalMs;
1057
- /**
1058
- * Caller-supplied matcher. Return the lockId to resolve for a given
1059
- * burn event, or `undefined` to skip. Runs synchronously via the
1060
- * ledger's query path.
1061
- *
1062
- * Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
1063
- * lock id `burn-${from}-${amount}` — the in-memory ledger assigns
1064
- * incrementing IDs so callers with the memory ledger must provide a
1065
- * custom matcher. Real DB-backed ledgers override this to JOIN on
1066
- * their `pending_credits` table.
1067
- */
1068
- matchLockId = async () => void 0;
884
+ matchLockId;
1069
885
  running = false;
1070
886
  timer;
1071
887
  constructor(config) {
@@ -1083,6 +899,12 @@ var BurnIndexer = class {
1083
899
  );
1084
900
  this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
1085
901
  this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
902
+ if (!config.matchLockId) {
903
+ throw new Error(
904
+ "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."
905
+ );
906
+ }
907
+ this.matchLockId = config.matchLockId;
1086
908
  }
1087
909
  start() {
1088
910
  if (this.running) return;
@@ -1112,7 +934,8 @@ var BurnIndexer = class {
1112
934
  return;
1113
935
  }
1114
936
  await this.processBlockRange(from, safeHead);
1115
- } catch {
937
+ } catch (err) {
938
+ console.error("[PAFI] BurnIndexer tick error:", err);
1116
939
  }
1117
940
  this.scheduleNext();
1118
941
  }
@@ -1157,10 +980,10 @@ var BurnIndexer = class {
1157
980
  for (const log of logs) {
1158
981
  const args = log.args;
1159
982
  if (!args.from || !args.to || args.value === void 0) continue;
1160
- if ((0, import_viem7.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
983
+ if ((0, import_viem5.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
1161
984
  if (log.blockNumber === null || log.transactionHash === null) continue;
1162
985
  out.push({
1163
- from: (0, import_viem7.getAddress)(args.from),
986
+ from: (0, import_viem5.getAddress)(args.from),
1164
987
  amount: args.value,
1165
988
  blockNumber: log.blockNumber,
1166
989
  txHash: log.transactionHash,
@@ -1175,21 +998,28 @@ var BurnIndexer = class {
1175
998
  * log + skip.
1176
999
  */
1177
1000
  async finalize(evt) {
1001
+ const txHash = evt.txHash;
1178
1002
  const lockId = await this.matchLockId(evt);
1179
- if (!lockId) return;
1003
+ if (lockId === void 0) {
1004
+ console.warn(
1005
+ "[PAFI] BurnIndexer: matchLockId returned undefined for burn tx " + txHash + ". This burn will NOT be credited. Implement matchLockId to map burn events to lock IDs."
1006
+ );
1007
+ return;
1008
+ }
1180
1009
  if (!this.ledger.resolveCreditByBurnTx) {
1181
1010
  return;
1182
1011
  }
1183
1012
  try {
1184
1013
  await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
1185
- } catch {
1014
+ } catch (err) {
1015
+ console.error("[PAFI] BurnIndexer finalize error \u2014 credit may be lost:", err);
1186
1016
  }
1187
1017
  }
1188
1018
  };
1189
1019
 
1190
1020
  // src/api/handlers.ts
1191
- var import_viem8 = require("viem");
1192
- var import_core4 = require("@pafi-dev/core");
1021
+ var import_viem6 = require("viem");
1022
+ var import_core3 = require("@pafi-dev/core");
1193
1023
  var IssuerApiHandlers = class {
1194
1024
  authService;
1195
1025
  ledger;
@@ -1199,15 +1029,12 @@ var IssuerApiHandlers = class {
1199
1029
  * validate the request's `pointTokenAddress` against this set.
1200
1030
  */
1201
1031
  supportedTokens;
1202
- /** First supported token — used as default when a handler doesn't
1203
- * receive a `pointTokenAddress` in the request (shouldn't happen in
1204
- * practice, but keeps type-narrowing happy). */
1205
- defaultToken;
1206
1032
  chainId;
1207
1033
  contracts;
1208
1034
  pafiWebUrl;
1209
1035
  feeManager;
1210
1036
  poolsProvider;
1037
+ claim;
1211
1038
  constructor(config) {
1212
1039
  this.authService = config.authService;
1213
1040
  this.ledger = config.ledger;
@@ -1218,14 +1045,14 @@ var IssuerApiHandlers = class {
1218
1045
  "IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
1219
1046
  );
1220
1047
  }
1221
- const normalized = raw.map((a) => (0, import_viem8.getAddress)(a));
1048
+ const normalized = raw.map((a) => (0, import_viem6.getAddress)(a));
1222
1049
  this.supportedTokens = new Set(normalized);
1223
- this.defaultToken = normalized[0];
1224
1050
  this.chainId = config.chainId;
1225
1051
  this.contracts = config.contracts;
1226
1052
  if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
1227
1053
  if (config.feeManager) this.feeManager = config.feeManager;
1228
1054
  if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
1055
+ if (config.claim) this.claim = config.claim;
1229
1056
  }
1230
1057
  // =========================================================================
1231
1058
  // Public handlers (no auth required)
@@ -1240,6 +1067,12 @@ var IssuerApiHandlers = class {
1240
1067
  if (!body || typeof body.message !== "string" || body.message.length === 0 || typeof body.signature !== "string" || body.signature.length <= 2) {
1241
1068
  throw new Error("handleLogin: message and signature are required");
1242
1069
  }
1070
+ if (body.message.length > 4096) {
1071
+ throw new Error("message too long");
1072
+ }
1073
+ if (body.signature.length > 260) {
1074
+ throw new Error("signature too long");
1075
+ }
1243
1076
  const result = await this.authService.login(body.message, body.signature);
1244
1077
  return {
1245
1078
  token: result.token,
@@ -1254,9 +1087,12 @@ var IssuerApiHandlers = class {
1254
1087
  * needs to build EIP-712 messages and interact with on-chain.
1255
1088
  */
1256
1089
  async handleConfig(chainId) {
1090
+ if (!Number.isInteger(chainId) || chainId <= 0) {
1091
+ throw new Error("invalid chainId");
1092
+ }
1257
1093
  if (chainId !== this.chainId) {
1258
1094
  throw new Error(
1259
- `handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
1095
+ `handleConfig: unsupported chainId ${chainId}`
1260
1096
  );
1261
1097
  }
1262
1098
  const contracts = {
@@ -1319,25 +1155,25 @@ var IssuerApiHandlers = class {
1319
1155
  `handleUser: unsupported chainId ${request.chainId}`
1320
1156
  );
1321
1157
  }
1322
- const normalizedAuthed = (0, import_viem8.getAddress)(userAddress);
1323
- const normalizedRequest = (0, import_viem8.getAddress)(request.userAddress);
1158
+ const normalizedAuthed = (0, import_viem6.getAddress)(userAddress);
1159
+ const normalizedRequest = (0, import_viem6.getAddress)(request.userAddress);
1324
1160
  if (normalizedAuthed !== normalizedRequest) {
1325
1161
  throw new Error(
1326
1162
  "handleUser: request userAddress must match authenticated user"
1327
1163
  );
1328
1164
  }
1329
- const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1165
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1330
1166
  if (!this.supportedTokens.has(pointToken)) {
1331
1167
  throw new Error(
1332
1168
  `handleUser: unsupported pointToken ${pointToken}`
1333
1169
  );
1334
1170
  }
1335
1171
  const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
1336
- (0, import_core4.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1337
- (0, import_core4.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1172
+ (0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
1173
+ (0, import_core3.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
1338
1174
  this.ledger.getBalance(normalizedAuthed, pointToken),
1339
- (0, import_core4.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
1340
- (0, import_core4.isMinter)(this.provider, pointToken, normalizedAuthed)
1175
+ (0, import_core3.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
1176
+ (0, import_core3.isMinter)(this.provider, pointToken, normalizedAuthed)
1341
1177
  ]);
1342
1178
  return {
1343
1179
  mintRequestNonce,
@@ -1369,19 +1205,32 @@ var IssuerApiHandlers = class {
1369
1205
  `handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
1370
1206
  );
1371
1207
  }
1372
- const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
1208
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1373
1209
  if (!this.supportedTokens.has(pointToken)) {
1374
1210
  throw new Error(
1375
1211
  `handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
1376
1212
  );
1377
1213
  }
1378
- const name = await (0, import_core4.getTokenName)(this.provider, pointToken);
1214
+ const consent = request.receiverConsent;
1215
+ if ((0, import_viem6.getAddress)(consent.originalReceiver) !== (0, import_viem6.getAddress)(userAddress)) {
1216
+ throw new Error(
1217
+ "handleBuildConsentTypedData: receiverConsent.originalReceiver must match authenticated user"
1218
+ );
1219
+ }
1220
+ if (consent.amount <= 0n) {
1221
+ throw new Error("handleBuildConsentTypedData: amount must be positive");
1222
+ }
1223
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1224
+ if (consent.deadline <= nowSecs) {
1225
+ throw new Error("handleBuildConsentTypedData: deadline is in the past");
1226
+ }
1227
+ const name = await (0, import_core3.getTokenName)(this.provider, pointToken);
1379
1228
  const domain = {
1380
1229
  name,
1381
1230
  verifyingContract: pointToken,
1382
1231
  chainId: this.chainId
1383
1232
  };
1384
- const typedData = (0, import_core4.buildReceiverConsentTypedData)(domain, request.receiverConsent);
1233
+ const typedData = (0, import_core3.buildReceiverConsentTypedData)(domain, consent);
1385
1234
  return {
1386
1235
  typedData: {
1387
1236
  domain: typedData.domain,
@@ -1391,11 +1240,96 @@ var IssuerApiHandlers = class {
1391
1240
  }
1392
1241
  };
1393
1242
  }
1243
+ /**
1244
+ * `POST /claim`
1245
+ *
1246
+ * Policy gate + ledger lock + MintRequest signing in a single atomic
1247
+ * step. Returns an unsigned UserOp the frontend attaches paymaster data
1248
+ * to and submits via EIP-7702 + Bundler.
1249
+ *
1250
+ * Order of operations:
1251
+ * 1. Validate request fields.
1252
+ * 2. policy.evaluate() — throws if denied; cannot be bypassed.
1253
+ * 3. ledger.lockForMinting() — reserves the balance.
1254
+ * 4. Read on-chain mintRequestNonce + token name in parallel.
1255
+ * 5. relayService.prepareMint() — sign MintRequest + encode UserOp.
1256
+ * 6. On any error after step 3, release the lock before re-throwing.
1257
+ */
1258
+ async handleClaim(userAddress, request) {
1259
+ if (!this.claim) {
1260
+ throw new Error("handleClaim: claim is not configured on this issuer");
1261
+ }
1262
+ if (request.chainId !== this.chainId) {
1263
+ throw new Error(`handleClaim: unsupported chainId ${request.chainId}`);
1264
+ }
1265
+ const pointToken = (0, import_viem6.getAddress)(request.pointTokenAddress);
1266
+ if (!this.supportedTokens.has(pointToken)) {
1267
+ throw new Error(`handleClaim: unsupported pointToken ${pointToken}`);
1268
+ }
1269
+ if (request.amount <= 0n) {
1270
+ throw new Error("handleClaim: amount must be positive");
1271
+ }
1272
+ const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
1273
+ if (request.deadline <= nowSecs) {
1274
+ throw new Error("handleClaim: deadline is in the past");
1275
+ }
1276
+ const { policy, relayService, issuerSignerWallet, batchExecutorAddress } = this.claim;
1277
+ const lockDurationMs = this.claim.lockDurationMs ?? 15 * 60 * 1e3;
1278
+ const normalizedUser = (0, import_viem6.getAddress)(userAddress);
1279
+ const decision = await policy.evaluate({
1280
+ userAddress: normalizedUser,
1281
+ amount: request.amount,
1282
+ pointTokenAddress: pointToken,
1283
+ chainId: this.chainId
1284
+ });
1285
+ if (!decision.approved) {
1286
+ throw new Error(`handleClaim: policy denied \u2014 ${decision.reason ?? "no reason given"}`);
1287
+ }
1288
+ const lockId = await this.ledger.lockForMinting(
1289
+ normalizedUser,
1290
+ request.amount,
1291
+ lockDurationMs,
1292
+ pointToken
1293
+ );
1294
+ try {
1295
+ const [mintRequestNonce, tokenName] = await Promise.all([
1296
+ (0, import_core3.getMintRequestNonce)(this.provider, pointToken, normalizedUser),
1297
+ (0, import_core3.getTokenName)(this.provider, pointToken)
1298
+ ]);
1299
+ const domain = {
1300
+ name: tokenName,
1301
+ verifyingContract: pointToken,
1302
+ chainId: this.chainId
1303
+ };
1304
+ const userOp = await relayService.prepareMint({
1305
+ userAddress: normalizedUser,
1306
+ aaNonce: request.aaNonce,
1307
+ batchExecutorAddress,
1308
+ pointTokenAddress: pointToken,
1309
+ amount: request.amount,
1310
+ issuerSignerWallet,
1311
+ domain,
1312
+ mintRequestNonce,
1313
+ deadline: request.deadline,
1314
+ feeAmount: request.feeAmount,
1315
+ feeRecipient: request.feeRecipient
1316
+ });
1317
+ return {
1318
+ lockId,
1319
+ userOp,
1320
+ expiresInSeconds: Math.floor(lockDurationMs / 1e3)
1321
+ };
1322
+ } catch (err) {
1323
+ await this.ledger.releaseLock(lockId).catch(() => {
1324
+ });
1325
+ throw err;
1326
+ }
1327
+ }
1394
1328
  };
1395
1329
 
1396
1330
  // src/api/handlers/ptRedeemHandler.ts
1397
- var import_viem9 = require("viem");
1398
- var import_core5 = require("@pafi-dev/core");
1331
+ var import_viem7 = require("viem");
1332
+ var import_core4 = require("@pafi-dev/core");
1399
1333
  var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
1400
1334
  var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
1401
1335
  var PTRedeemError = class extends Error {
@@ -1434,16 +1368,25 @@ var PTRedeemHandler = class {
1434
1368
  this.ledger = config.ledger;
1435
1369
  this.relayService = config.relayService;
1436
1370
  this.provider = config.provider;
1437
- this.pointTokenAddress = (0, import_viem9.getAddress)(config.pointTokenAddress);
1438
- this.batchExecutorAddress = (0, import_viem9.getAddress)(config.batchExecutorAddress);
1371
+ this.pointTokenAddress = (0, import_viem7.getAddress)(config.pointTokenAddress);
1372
+ this.batchExecutorAddress = (0, import_viem7.getAddress)(config.batchExecutorAddress);
1439
1373
  this.chainId = config.chainId;
1440
1374
  this.domain = config.domain;
1441
1375
  this.burnerSignerWallet = config.burnerSignerWallet;
1376
+ if (this.burnerSignerWallet?.account?.type === "local") {
1377
+ console.warn("[PAFI] PTRedeemHandler: burnerSignerWallet uses a local (private key) account. Use a KMS-backed signer in production.");
1378
+ }
1442
1379
  this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
1443
1380
  this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
1444
1381
  this.now = config.now ?? (() => Date.now());
1445
1382
  }
1446
1383
  async handle(request) {
1384
+ if ((0, import_viem7.getAddress)(request.authenticatedAddress) !== (0, import_viem7.getAddress)(request.userAddress)) {
1385
+ throw new PTRedeemError(
1386
+ "UNAUTHORIZED",
1387
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1388
+ );
1389
+ }
1447
1390
  if (request.amount <= 0n) {
1448
1391
  throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
1449
1392
  }
@@ -1451,7 +1394,7 @@ var PTRedeemHandler = class {
1451
1394
  try {
1452
1395
  burnNonce = await this.provider.readContract({
1453
1396
  address: this.pointTokenAddress,
1454
- abi: import_core5.POINT_TOKEN_V2_ABI,
1397
+ abi: import_core4.POINT_TOKEN_V2_ABI,
1455
1398
  functionName: "burnRequestNonces",
1456
1399
  args: [request.userAddress]
1457
1400
  });
@@ -1461,6 +1404,17 @@ var PTRedeemHandler = class {
1461
1404
  `failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
1462
1405
  );
1463
1406
  }
1407
+ const onChainBalance = await (0, import_core4.getPointTokenBalance)(
1408
+ this.provider,
1409
+ this.pointTokenAddress,
1410
+ request.userAddress
1411
+ );
1412
+ if (onChainBalance < request.amount) {
1413
+ throw new PTRedeemError(
1414
+ "INVALID_AMOUNT",
1415
+ `insufficient on-chain PT balance: have ${onChainBalance}, need ${request.amount}`
1416
+ );
1417
+ }
1464
1418
  const deadline = BigInt(
1465
1419
  Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
1466
1420
  );
@@ -1477,7 +1431,7 @@ var PTRedeemHandler = class {
1477
1431
  };
1478
1432
  let burnerSignature;
1479
1433
  try {
1480
- const sig = await (0, import_core5.signBurnRequest)(
1434
+ const sig = await (0, import_core4.signBurnRequest)(
1481
1435
  this.burnerSignerWallet,
1482
1436
  domain,
1483
1437
  burnRequest
@@ -1514,8 +1468,8 @@ var PTRedeemHandler = class {
1514
1468
  };
1515
1469
 
1516
1470
  // src/api/handlers/topUpRedemptionHandler.ts
1517
- var import_viem10 = require("viem");
1518
- var import_core6 = require("@pafi-dev/core");
1471
+ var import_viem8 = require("viem");
1472
+ var import_core5 = require("@pafi-dev/core");
1519
1473
  var TopUpRedemptionError = class extends Error {
1520
1474
  constructor(code, message) {
1521
1475
  super(message);
@@ -1533,9 +1487,15 @@ var TopUpRedemptionHandler = class {
1533
1487
  this.ledger = config.ledger;
1534
1488
  this.ptRedeemHandler = config.ptRedeemHandler;
1535
1489
  this.provider = config.provider;
1536
- this.pointTokenAddress = (0, import_viem10.getAddress)(config.pointTokenAddress);
1490
+ this.pointTokenAddress = (0, import_viem8.getAddress)(config.pointTokenAddress);
1537
1491
  }
1538
1492
  async handle(request) {
1493
+ if ((0, import_viem8.getAddress)(request.authenticatedAddress) !== (0, import_viem8.getAddress)(request.userAddress)) {
1494
+ throw new TopUpRedemptionError(
1495
+ "UNAUTHORIZED",
1496
+ `userAddress (${request.userAddress}) does not match authenticated session (${request.authenticatedAddress})`
1497
+ );
1498
+ }
1539
1499
  const offChainBalance = await this.ledger.getBalance(
1540
1500
  request.userAddress,
1541
1501
  this.pointTokenAddress
@@ -1544,7 +1504,7 @@ var TopUpRedemptionHandler = class {
1544
1504
  return { action: "NO_TOP_UP_NEEDED", offChainBalance };
1545
1505
  }
1546
1506
  const shortfall = request.requiredAmount - offChainBalance;
1547
- const onChainBalance = await (0, import_core6.getPointTokenBalance)(
1507
+ const onChainBalance = await (0, import_core5.getPointTokenBalance)(
1548
1508
  this.provider,
1549
1509
  this.pointTokenAddress,
1550
1510
  request.userAddress
@@ -1558,6 +1518,7 @@ var TopUpRedemptionHandler = class {
1558
1518
  };
1559
1519
  }
1560
1520
  const redeem = await this.ptRedeemHandler.handle({
1521
+ authenticatedAddress: request.authenticatedAddress,
1561
1522
  userAddress: request.userAddress,
1562
1523
  amount: shortfall,
1563
1524
  aaNonce: request.aaNonce
@@ -1571,6 +1532,7 @@ var TopUpRedemptionHandler = class {
1571
1532
  };
1572
1533
 
1573
1534
  // src/pools/subgraphPoolsProvider.ts
1535
+ var import_viem9 = require("viem");
1574
1536
  var DEFAULT_CACHE_TTL_MS = 3e4;
1575
1537
  var POOL_QUERY = `
1576
1538
  query GetPoolForPointToken($id: ID!) {
@@ -1593,6 +1555,19 @@ function createSubgraphPoolsProvider(config) {
1593
1555
  "createSubgraphPoolsProvider: subgraphUrl is required"
1594
1556
  );
1595
1557
  }
1558
+ try {
1559
+ const parsed = new URL(config.subgraphUrl);
1560
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1561
+ throw new Error("subgraphUrl must use HTTPS in production");
1562
+ }
1563
+ } catch (err) {
1564
+ if (err instanceof TypeError) {
1565
+ throw new Error(
1566
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1567
+ );
1568
+ }
1569
+ throw err;
1570
+ }
1596
1571
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1597
1572
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1598
1573
  const now = config.now ?? (() => Date.now());
@@ -1661,6 +1636,26 @@ async function fetchPoolsFromSubgraph(fetchImpl, subgraphUrl, pointTokenAddress)
1661
1636
  return [];
1662
1637
  }
1663
1638
  const { pool } = token;
1639
+ if (!(0, import_viem9.isAddress)(pool.hooks)) {
1640
+ console.error(
1641
+ "[PAFI] SubgraphPoolsProvider: invalid hooks address in response:",
1642
+ pool.hooks,
1643
+ "\u2014 skipping pool"
1644
+ );
1645
+ return [];
1646
+ }
1647
+ if (!(0, import_viem9.isAddress)(pool.token0.id) || !(0, import_viem9.isAddress)(pool.token1.id)) {
1648
+ console.error(
1649
+ "[PAFI] SubgraphPoolsProvider: invalid token address in response \u2014 skipping pool"
1650
+ );
1651
+ return [];
1652
+ }
1653
+ if (!Number.isFinite(Number(pool.feeTier)) || !Number.isFinite(Number(pool.tickSpacing))) {
1654
+ console.error(
1655
+ "[PAFI] SubgraphPoolsProvider: invalid feeTier/tickSpacing \u2014 skipping pool"
1656
+ );
1657
+ return [];
1658
+ }
1664
1659
  const [currency0, currency1] = sortCurrencies(
1665
1660
  pool.token0.id,
1666
1661
  pool.token1.id
@@ -1696,6 +1691,19 @@ function createSubgraphNativeUsdtQuoter(config) {
1696
1691
  "createSubgraphNativeUsdtQuoter: subgraphUrl is required"
1697
1692
  );
1698
1693
  }
1694
+ try {
1695
+ const parsed = new URL(config.subgraphUrl);
1696
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
1697
+ throw new Error("subgraphUrl must use HTTPS in production");
1698
+ }
1699
+ } catch (err) {
1700
+ if (err instanceof TypeError) {
1701
+ throw new Error(
1702
+ `subgraphPoolsProvider: invalid subgraphUrl: ${config.subgraphUrl}`
1703
+ );
1704
+ }
1705
+ throw err;
1706
+ }
1699
1707
  const usdtDecimals = config.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
1700
1708
  const nativeDecimals = config.nativeDecimals ?? DEFAULT_NATIVE_DECIMALS;
1701
1709
  const cacheTtl = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
@@ -1772,6 +1780,14 @@ async function fetchEthPriceFromSubgraph(fetchImpl, subgraphUrl) {
1772
1780
  );
1773
1781
  return null;
1774
1782
  }
1783
+ const MIN_REASONABLE_ETH_PRICE = 100;
1784
+ const MAX_REASONABLE_ETH_PRICE = 1e5;
1785
+ if (parsed < MIN_REASONABLE_ETH_PRICE || parsed > MAX_REASONABLE_ETH_PRICE) {
1786
+ console.warn(
1787
+ `[PAFI] SubgraphNativeUsdtQuoter: ETH/USD price ${parsed} is outside reasonable range. Using fallback.`
1788
+ );
1789
+ return null;
1790
+ }
1775
1791
  return parsed;
1776
1792
  }
1777
1793
  function toUsdtPerNative(priceFloat, usdtDecimals) {
@@ -1782,7 +1798,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
1782
1798
  }
1783
1799
 
1784
1800
  // src/balance/balanceAggregator.ts
1785
- var import_core7 = require("@pafi-dev/core");
1801
+ var import_core6 = require("@pafi-dev/core");
1786
1802
  var BalanceAggregator = class {
1787
1803
  provider;
1788
1804
  ledger;
@@ -1803,7 +1819,7 @@ var BalanceAggregator = class {
1803
1819
  async getCombinedBalance(user, pointToken) {
1804
1820
  const [offChain, onChain] = await Promise.all([
1805
1821
  this.ledger.getBalance(user, pointToken),
1806
- (0, import_core7.getPointTokenBalance)(this.provider, pointToken, user)
1822
+ (0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
1807
1823
  ]);
1808
1824
  return {
1809
1825
  offChain,
@@ -1863,11 +1879,14 @@ var PafiBackendError = class extends Error {
1863
1879
  };
1864
1880
 
1865
1881
  // src/config.ts
1866
- var import_viem11 = require("viem");
1882
+ var import_viem10 = require("viem");
1867
1883
  function createIssuerService(config) {
1868
1884
  if (!config.provider) {
1869
1885
  throw new Error("createIssuerService: provider is required");
1870
1886
  }
1887
+ if (!config.ledger) {
1888
+ throw new Error("createIssuerService: ledger is required");
1889
+ }
1871
1890
  if (!config.auth?.jwtSecret) {
1872
1891
  throw new Error("createIssuerService: auth.jwtSecret is required");
1873
1892
  }
@@ -1880,8 +1899,8 @@ function createIssuerService(config) {
1880
1899
  "createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
1881
1900
  );
1882
1901
  }
1883
- const tokenAddresses = rawAddresses.map((a) => (0, import_viem11.getAddress)(a));
1884
- const ledger = config.ledger ?? new MemoryPointLedger();
1902
+ const tokenAddresses = rawAddresses.map((a) => (0, import_viem10.getAddress)(a));
1903
+ const ledger = config.ledger;
1885
1904
  const sessionStore = config.sessionStore ?? new MemorySessionStore();
1886
1905
  const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
1887
1906
  const authServiceConfig = {
@@ -1937,6 +1956,15 @@ function createIssuerService(config) {
1937
1956
  };
1938
1957
  if (feeManager) handlersConfig.feeManager = feeManager;
1939
1958
  if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
1959
+ if (config.claim) {
1960
+ handlersConfig.claim = {
1961
+ policy,
1962
+ relayService,
1963
+ issuerSignerWallet: config.claim.issuerSignerWallet,
1964
+ batchExecutorAddress: config.claim.batchExecutorAddress,
1965
+ lockDurationMs: config.claim.lockDurationMs
1966
+ };
1967
+ }
1940
1968
  const handlers = new IssuerApiHandlers(handlersConfig);
1941
1969
  if (config.indexer?.autoStart) {
1942
1970
  for (const idx of indexers.values()) {
@@ -1968,7 +1996,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
1968
1996
  FeeManager,
1969
1997
  InMemoryCursorStore,
1970
1998
  IssuerApiHandlers,
1971
- MemoryPointLedger,
1972
1999
  MemorySessionStore,
1973
2000
  NonceManager,
1974
2001
  PAFI_ISSUER_SDK_VERSION,
@@ -1976,7 +2003,6 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
1976
2003
  PTRedeemHandler,
1977
2004
  PafiBackendError,
1978
2005
  PointIndexer,
1979
- PrivateKeySigner,
1980
2006
  RelayError,
1981
2007
  RelayService,
1982
2008
  TopUpRedemptionError,