@pafi-dev/issuer 0.31.0 → 0.33.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.d.cts CHANGED
@@ -1551,9 +1551,9 @@ interface SettlementClientConfig {
1551
1551
  /**
1552
1552
  * chainId — used to derive the issuer-api base URL via
1553
1553
  * `getPafiServiceUrls(chainId).issuerApi`. SDK ships with the URL
1554
- * per chainId; bump SDK version to retarget.
1555
1554
  */
1556
1555
  chainId: number;
1556
+ baseUrl?: string;
1557
1557
  /** PAFI-assigned issuer id used in `X-Issuer-Id` header. */
1558
1558
  issuerId: string;
1559
1559
  /** Raw API key sent as `Authorization: Bearer <apiKey>`. */
@@ -1566,6 +1566,8 @@ interface SettlementClientConfig {
1566
1566
  interface PolicyProviderConfig extends SettlementClientConfig {
1567
1567
  /** Cache TTL in milliseconds. Default 5 * 60 * 1000 (5min). */
1568
1568
  cacheTtlMs?: number;
1569
+ onFetchFailure?: "fail-closed" | "permissive-default";
1570
+ onWarning?: (msg: string, ctx: Record<string, unknown>) => void;
1569
1571
  /**
1570
1572
  * Optional clock for testability. Returns unix milliseconds.
1571
1573
  * Defaults to () => Date.now().
@@ -1596,6 +1598,8 @@ declare class PolicyProvider {
1596
1598
  private readonly client;
1597
1599
  private readonly issuerId;
1598
1600
  private readonly cacheTtlMs;
1601
+ private readonly onFetchFailure;
1602
+ private readonly onWarning?;
1599
1603
  private readonly now;
1600
1604
  private cache;
1601
1605
  private inflight;
@@ -1987,7 +1991,7 @@ interface PTRedeemResponse {
1987
1991
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1988
1992
  signatureDeadline: bigint;
1989
1993
  }
1990
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "UNSUPPORTED_POINT_TOKEN";
1994
+ type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "REDEMPTION_POLICY_UNAVAILABLE" | "UNSUPPORTED_POINT_TOKEN";
1991
1995
  declare class PTRedeemError extends PafiSdkError {
1992
1996
  readonly httpStatus: "unprocessable";
1993
1997
  readonly code: PTRedeemErrorCode;
@@ -2039,9 +2043,9 @@ interface PafiBackendConfig {
2039
2043
  /**
2040
2044
  * chainId — used to derive the sponsor-relayer base URL via
2041
2045
  * `getPafiServiceUrls(chainId).sponsorRelayer`. SDK ships with the
2042
- * URL per chainId; bump SDK version to retarget.
2043
2046
  */
2044
2047
  chainId: number;
2048
+ baseUrl?: string;
2045
2049
  issuerId: string;
2046
2050
  apiKey: string;
2047
2051
  fetchImpl?: typeof fetch;
@@ -2198,12 +2202,32 @@ interface MintStatusParams {
2198
2202
  * - lock.status === "PENDING"
2199
2203
  * - lock.userOpHash is bound (set by `/claim/submit`)
2200
2204
  *
2201
- * If the bundler reports the UserOp confirmed, the handler updates
2205
+ * If the bundler reports the UserOp confirmed AND the receipt block
2206
+ * is past the configured `confirmations` depth, the handler updates
2202
2207
  * the ledger lock + returns `MINTED` immediately, bypassing
2203
2208
  * `PointIndexer`'s amount-based race (multiple PENDING locks with
2204
2209
  * the same amount can be matched to the wrong tx_hash).
2205
2210
  */
2206
2211
  pafiBackendClient?: PafiBackendClient | null;
2212
+ /**
2213
+ * Audit PACI5-13 — required for the confirmation-depth check on the
2214
+ * bundler-receipt fallback. The bundler returns success at zero
2215
+ * confs, but `PointIndexer` enforces a 3-block reorg window;
2216
+ * crediting / debiting off-chain at 0 confs while the indexer waits
2217
+ * for finality leaves an unbacked durable mutation if the tx is
2218
+ * reorged out. Pass the SAME PublicClient that the indexer uses so
2219
+ * both paths see consistent chain head. Optional only for legacy
2220
+ * callers that don't supply `pafiBackendClient`; required whenever
2221
+ * the receipt-fallback path can run.
2222
+ */
2223
+ provider?: PublicClient;
2224
+ /**
2225
+ * Audit PACI5-13 — confirmation depth required before the receipt
2226
+ * fallback applies the credit / debit. MUST match
2227
+ * `PointIndexer.confirmations` (default 3). Operators who reduce
2228
+ * the indexer depth must set this to the same value.
2229
+ */
2230
+ confirmations?: number;
2207
2231
  /** Optional logger for "ledger update failed" warnings. */
2208
2232
  onWarning?: (msg: string) => void;
2209
2233
  }
@@ -2212,6 +2236,20 @@ interface BurnStatusParams {
2212
2236
  userAddress: Address;
2213
2237
  ledger: IPointLedger;
2214
2238
  pafiBackendClient?: PafiBackendClient | null;
2239
+ /**
2240
+ * Audit PACI5-13 — see `MintStatusParams.provider` for full
2241
+ * rationale. The receipt-fallback path applies a spendable
2242
+ * off-chain credit; without a confirmation-depth gate a reorg
2243
+ * before `BurnIndexer.confirmations` leaves durable unbacked
2244
+ * credit with no reversal mechanism.
2245
+ */
2246
+ provider?: PublicClient;
2247
+ /**
2248
+ * Audit PACI5-13 — confirmation depth required before the receipt
2249
+ * fallback applies the credit. MUST match
2250
+ * `BurnIndexer.confirmations` (default 3).
2251
+ */
2252
+ confirmations?: number;
2215
2253
  onWarning?: (msg: string) => void;
2216
2254
  }
2217
2255
  declare class LockNotFoundError extends PafiSdkError {
@@ -3185,6 +3223,26 @@ interface IssuerServiceConfig {
3185
3223
  issuerId: string;
3186
3224
  apiKey: string;
3187
3225
  historyStore: IRedemptionHistoryStore;
3226
+ /**
3227
+ * Optional override for the PAFI issuer-api base URL. Audit
3228
+ * env var (e.g. `PAFI_ISSUER_API_URL`) so policy fetch hits the
3229
+ * canonical environment for the deploy. Undefined → SDK
3230
+ * ship-default per chainId.
3231
+ */
3232
+ baseUrl?: string;
3233
+ /**
3234
+ * Behavior khi settlement-api fetch fail. Default `'fail-closed'`
3235
+ * Opt in to `'permissive-default'` ONLY when paired with an alert
3236
+ * on the `policy_provider_fallback` event surfaced via
3237
+ * `onPolicyWarning`.
3238
+ */
3239
+ onFetchFailure?: "fail-closed" | "permissive-default";
3240
+ /**
3241
+ * Observability hook for `policy_provider_fallback` events. Wire
3242
+ * to your logger / Sentry / Datadog so the on-call dashboard sees
3243
+ * settlement-api degradation.
3244
+ */
3245
+ onPolicyWarning?: (msg: string, ctx: Record<string, unknown>) => void;
3188
3246
  /** Override fetch (testing). */
3189
3247
  fetchImpl?: typeof fetch;
3190
3248
  /** Per-fetch timeout in ms. Default 1000. */
@@ -3354,12 +3412,6 @@ interface RedeemPrepareDto extends MobilePrepareDto {
3354
3412
  * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3355
3413
  * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3356
3414
  * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3357
- *
3358
- * Audit PACI5-21 — pre-fix the adapter exposed only the sponsored
3359
- * `lockId` (= `amount - fee`), so the bundler-receipt fallback in
3360
- * `handleRedeemStatus` resolved the smaller credit against the
3361
- * on-chain burn of the full amount → user under-credited by exactly
3362
- * the fee on every fallback redeem.
3363
3415
  */
3364
3416
  lockIdFallback?: string;
3365
3417
  }
package/dist/index.d.ts CHANGED
@@ -1551,9 +1551,9 @@ interface SettlementClientConfig {
1551
1551
  /**
1552
1552
  * chainId — used to derive the issuer-api base URL via
1553
1553
  * `getPafiServiceUrls(chainId).issuerApi`. SDK ships with the URL
1554
- * per chainId; bump SDK version to retarget.
1555
1554
  */
1556
1555
  chainId: number;
1556
+ baseUrl?: string;
1557
1557
  /** PAFI-assigned issuer id used in `X-Issuer-Id` header. */
1558
1558
  issuerId: string;
1559
1559
  /** Raw API key sent as `Authorization: Bearer <apiKey>`. */
@@ -1566,6 +1566,8 @@ interface SettlementClientConfig {
1566
1566
  interface PolicyProviderConfig extends SettlementClientConfig {
1567
1567
  /** Cache TTL in milliseconds. Default 5 * 60 * 1000 (5min). */
1568
1568
  cacheTtlMs?: number;
1569
+ onFetchFailure?: "fail-closed" | "permissive-default";
1570
+ onWarning?: (msg: string, ctx: Record<string, unknown>) => void;
1569
1571
  /**
1570
1572
  * Optional clock for testability. Returns unix milliseconds.
1571
1573
  * Defaults to () => Date.now().
@@ -1596,6 +1598,8 @@ declare class PolicyProvider {
1596
1598
  private readonly client;
1597
1599
  private readonly issuerId;
1598
1600
  private readonly cacheTtlMs;
1601
+ private readonly onFetchFailure;
1602
+ private readonly onWarning?;
1599
1603
  private readonly now;
1600
1604
  private cache;
1601
1605
  private inflight;
@@ -1987,7 +1991,7 @@ interface PTRedeemResponse {
1987
1991
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1988
1992
  signatureDeadline: bigint;
1989
1993
  }
1990
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "UNSUPPORTED_POINT_TOKEN";
1994
+ type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "REDEMPTION_POLICY_UNAVAILABLE" | "UNSUPPORTED_POINT_TOKEN";
1991
1995
  declare class PTRedeemError extends PafiSdkError {
1992
1996
  readonly httpStatus: "unprocessable";
1993
1997
  readonly code: PTRedeemErrorCode;
@@ -2039,9 +2043,9 @@ interface PafiBackendConfig {
2039
2043
  /**
2040
2044
  * chainId — used to derive the sponsor-relayer base URL via
2041
2045
  * `getPafiServiceUrls(chainId).sponsorRelayer`. SDK ships with the
2042
- * URL per chainId; bump SDK version to retarget.
2043
2046
  */
2044
2047
  chainId: number;
2048
+ baseUrl?: string;
2045
2049
  issuerId: string;
2046
2050
  apiKey: string;
2047
2051
  fetchImpl?: typeof fetch;
@@ -2198,12 +2202,32 @@ interface MintStatusParams {
2198
2202
  * - lock.status === "PENDING"
2199
2203
  * - lock.userOpHash is bound (set by `/claim/submit`)
2200
2204
  *
2201
- * If the bundler reports the UserOp confirmed, the handler updates
2205
+ * If the bundler reports the UserOp confirmed AND the receipt block
2206
+ * is past the configured `confirmations` depth, the handler updates
2202
2207
  * the ledger lock + returns `MINTED` immediately, bypassing
2203
2208
  * `PointIndexer`'s amount-based race (multiple PENDING locks with
2204
2209
  * the same amount can be matched to the wrong tx_hash).
2205
2210
  */
2206
2211
  pafiBackendClient?: PafiBackendClient | null;
2212
+ /**
2213
+ * Audit PACI5-13 — required for the confirmation-depth check on the
2214
+ * bundler-receipt fallback. The bundler returns success at zero
2215
+ * confs, but `PointIndexer` enforces a 3-block reorg window;
2216
+ * crediting / debiting off-chain at 0 confs while the indexer waits
2217
+ * for finality leaves an unbacked durable mutation if the tx is
2218
+ * reorged out. Pass the SAME PublicClient that the indexer uses so
2219
+ * both paths see consistent chain head. Optional only for legacy
2220
+ * callers that don't supply `pafiBackendClient`; required whenever
2221
+ * the receipt-fallback path can run.
2222
+ */
2223
+ provider?: PublicClient;
2224
+ /**
2225
+ * Audit PACI5-13 — confirmation depth required before the receipt
2226
+ * fallback applies the credit / debit. MUST match
2227
+ * `PointIndexer.confirmations` (default 3). Operators who reduce
2228
+ * the indexer depth must set this to the same value.
2229
+ */
2230
+ confirmations?: number;
2207
2231
  /** Optional logger for "ledger update failed" warnings. */
2208
2232
  onWarning?: (msg: string) => void;
2209
2233
  }
@@ -2212,6 +2236,20 @@ interface BurnStatusParams {
2212
2236
  userAddress: Address;
2213
2237
  ledger: IPointLedger;
2214
2238
  pafiBackendClient?: PafiBackendClient | null;
2239
+ /**
2240
+ * Audit PACI5-13 — see `MintStatusParams.provider` for full
2241
+ * rationale. The receipt-fallback path applies a spendable
2242
+ * off-chain credit; without a confirmation-depth gate a reorg
2243
+ * before `BurnIndexer.confirmations` leaves durable unbacked
2244
+ * credit with no reversal mechanism.
2245
+ */
2246
+ provider?: PublicClient;
2247
+ /**
2248
+ * Audit PACI5-13 — confirmation depth required before the receipt
2249
+ * fallback applies the credit. MUST match
2250
+ * `BurnIndexer.confirmations` (default 3).
2251
+ */
2252
+ confirmations?: number;
2215
2253
  onWarning?: (msg: string) => void;
2216
2254
  }
2217
2255
  declare class LockNotFoundError extends PafiSdkError {
@@ -3185,6 +3223,26 @@ interface IssuerServiceConfig {
3185
3223
  issuerId: string;
3186
3224
  apiKey: string;
3187
3225
  historyStore: IRedemptionHistoryStore;
3226
+ /**
3227
+ * Optional override for the PAFI issuer-api base URL. Audit
3228
+ * env var (e.g. `PAFI_ISSUER_API_URL`) so policy fetch hits the
3229
+ * canonical environment for the deploy. Undefined → SDK
3230
+ * ship-default per chainId.
3231
+ */
3232
+ baseUrl?: string;
3233
+ /**
3234
+ * Behavior khi settlement-api fetch fail. Default `'fail-closed'`
3235
+ * Opt in to `'permissive-default'` ONLY when paired with an alert
3236
+ * on the `policy_provider_fallback` event surfaced via
3237
+ * `onPolicyWarning`.
3238
+ */
3239
+ onFetchFailure?: "fail-closed" | "permissive-default";
3240
+ /**
3241
+ * Observability hook for `policy_provider_fallback` events. Wire
3242
+ * to your logger / Sentry / Datadog so the on-call dashboard sees
3243
+ * settlement-api degradation.
3244
+ */
3245
+ onPolicyWarning?: (msg: string, ctx: Record<string, unknown>) => void;
3188
3246
  /** Override fetch (testing). */
3189
3247
  fetchImpl?: typeof fetch;
3190
3248
  /** Per-fetch timeout in ms. Default 1000. */
@@ -3354,12 +3412,6 @@ interface RedeemPrepareDto extends MobilePrepareDto {
3354
3412
  * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3355
3413
  * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3356
3414
  * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3357
- *
3358
- * Audit PACI5-21 — pre-fix the adapter exposed only the sponsored
3359
- * `lockId` (= `amount - fee`), so the bundler-receipt fallback in
3360
- * `handleRedeemStatus` resolved the smaller credit against the
3361
- * on-chain burn of the full amount → user under-credited by exactly
3362
- * the fee on every fallback redeem.
3363
3415
  */
3364
3416
  lockIdFallback?: string;
3365
3417
  }
package/dist/index.js CHANGED
@@ -2190,11 +2190,23 @@ var PTRedeemHandler = class {
2190
2190
  );
2191
2191
  }
2192
2192
  if (this.redemptionService) {
2193
- const decision = await this.redemptionService.evaluate(
2194
- request.userAddress,
2195
- request.amount,
2196
- pointTokenAddress
2197
- );
2193
+ let decision;
2194
+ try {
2195
+ decision = await this.redemptionService.evaluate(
2196
+ request.userAddress,
2197
+ request.amount,
2198
+ pointTokenAddress
2199
+ );
2200
+ } catch (err) {
2201
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
2202
+ if (code === "POLICY_PROVIDER_UNAVAILABLE") {
2203
+ throw new PTRedeemError(
2204
+ "REDEMPTION_POLICY_UNAVAILABLE",
2205
+ "Redemption policy temporarily unavailable \u2014 please try again shortly."
2206
+ );
2207
+ }
2208
+ throw err;
2209
+ }
2198
2210
  if (!decision.allowed) {
2199
2211
  const denial = decision.denial;
2200
2212
  throw new PTRedeemError(
@@ -2410,6 +2422,45 @@ var PTRedeemHandler = class {
2410
2422
  };
2411
2423
 
2412
2424
  // src/api/statusHandlers.ts
2425
+ var DEFAULT_STATUS_CONFIRMATIONS = 3;
2426
+ async function isReceiptPastConfirmations(receipt, provider, confirmations, onWarning, handlerName) {
2427
+ if (!provider) {
2428
+ onWarning?.(
2429
+ `${handlerName}: provider missing \u2014 cannot enforce confirmation depth; deferring receipt fallback to on-chain indexer (audit PACI5-13).`
2430
+ );
2431
+ return false;
2432
+ }
2433
+ if (!receipt.blockNumber) {
2434
+ onWarning?.(
2435
+ `${handlerName}: receipt has no blockNumber \u2014 cannot enforce confirmation depth; deferring to indexer (audit PACI5-13).`
2436
+ );
2437
+ return false;
2438
+ }
2439
+ const requiredConfs = BigInt(confirmations ?? DEFAULT_STATUS_CONFIRMATIONS);
2440
+ let receiptBlock;
2441
+ try {
2442
+ receiptBlock = BigInt(receipt.blockNumber);
2443
+ } catch {
2444
+ onWarning?.(
2445
+ `${handlerName}: malformed receipt blockNumber (${receipt.blockNumber}) \u2014 deferring to indexer (audit PACI5-13).`
2446
+ );
2447
+ return false;
2448
+ }
2449
+ let head;
2450
+ try {
2451
+ head = await provider.getBlockNumber();
2452
+ } catch (err) {
2453
+ onWarning?.(
2454
+ `${handlerName}: getBlockNumber failed (${err instanceof Error ? err.message : String(err)}) \u2014 deferring to indexer (audit PACI5-13).`
2455
+ );
2456
+ return false;
2457
+ }
2458
+ const depth = head - receiptBlock;
2459
+ if (depth < requiredConfs) {
2460
+ return false;
2461
+ }
2462
+ return true;
2463
+ }
2413
2464
  var LockNotFoundError = class extends PafiSdkError {
2414
2465
  code = "LOCK_NOT_FOUND";
2415
2466
  httpStatus = "not_found";
@@ -2438,6 +2489,23 @@ async function handleClaimStatus(params) {
2438
2489
  lock.userOpHash
2439
2490
  );
2440
2491
  if (receipt) {
2492
+ const passesConfirmationDepth = await isReceiptPastConfirmations(
2493
+ receipt,
2494
+ params.provider,
2495
+ params.confirmations,
2496
+ params.onWarning,
2497
+ "handleClaimStatus"
2498
+ );
2499
+ if (!passesConfirmationDepth) {
2500
+ return {
2501
+ lockId: lock.lockId,
2502
+ status: "PENDING",
2503
+ txHash: lock.txHash ?? null,
2504
+ amount: lock.amount.toString(),
2505
+ createdAt: new Date(lock.createdAt).toISOString(),
2506
+ expiresAt: new Date(lock.expiresAt).toISOString()
2507
+ };
2508
+ }
2441
2509
  if (receipt.success && receipt.txHash) {
2442
2510
  if (!lock.tokenAddress) {
2443
2511
  params.onWarning?.(
@@ -2512,14 +2580,23 @@ async function handleRedeemStatus(params) {
2512
2580
  credit.userOpHash
2513
2581
  );
2514
2582
  if (receipt && receipt.success) {
2515
- status = "RESOLVED";
2516
- txHash = receipt.txHash;
2517
- if (params.ledger.resolveCreditByBurnTx) {
2518
- await params.ledger.resolveCreditByBurnTx(credit.lockId, receipt.txHash).catch((err) => {
2519
- params.onWarning?.(
2520
- `handleRedeemStatus: resolveCreditByBurnTx failed for lock ${credit.lockId}: ${err}`
2521
- );
2522
- });
2583
+ const passesConfirmationDepth = await isReceiptPastConfirmations(
2584
+ receipt,
2585
+ params.provider,
2586
+ params.confirmations,
2587
+ params.onWarning,
2588
+ "handleRedeemStatus"
2589
+ );
2590
+ if (passesConfirmationDepth) {
2591
+ status = "RESOLVED";
2592
+ txHash = receipt.txHash;
2593
+ if (params.ledger.resolveCreditByBurnTx) {
2594
+ await params.ledger.resolveCreditByBurnTx(credit.lockId, receipt.txHash).catch((err) => {
2595
+ params.onWarning?.(
2596
+ `handleRedeemStatus: resolveCreditByBurnTx failed for lock ${credit.lockId}: ${err}`
2597
+ );
2598
+ });
2599
+ }
2523
2600
  }
2524
2601
  }
2525
2602
  } catch (err) {
@@ -3682,10 +3759,6 @@ var IssuerApiAdapter = class {
3682
3759
  pointTokenAddress,
3683
3760
  redeemResponse.expiresInSeconds,
3684
3761
  input.eip7702Auth,
3685
- // Audit PACI5-21 — fallback path reserves a separate
3686
- // PendingCredit row for the full `amount`. Surface its lockId so
3687
- // mobile FE can poll the correct row + `handleMobileSubmit`
3688
- // routes the userOpHash bind to it on fallback submit.
3689
3762
  redeemResponse.fallback?.lockId
3690
3763
  );
3691
3764
  return {
@@ -3721,6 +3794,9 @@ var IssuerApiAdapter = class {
3721
3794
  userAddress: authenticatedAddress,
3722
3795
  ledger: this.cfg.ledger,
3723
3796
  pafiBackendClient: this.cfg.pafiBackendClient,
3797
+ // Audit PACI5-13 — pass the same provider the indexers use so
3798
+ // the receipt fallback gates on the same reorg depth.
3799
+ provider: this.cfg.provider,
3724
3800
  onWarning: this.cfg.onWarning
3725
3801
  });
3726
3802
  }
@@ -3730,6 +3806,8 @@ var IssuerApiAdapter = class {
3730
3806
  userAddress: authenticatedAddress,
3731
3807
  ledger: this.cfg.ledger,
3732
3808
  pafiBackendClient: this.cfg.pafiBackendClient,
3809
+ // Audit PACI5-13 — see claimStatus comment.
3810
+ provider: this.cfg.provider,
3733
3811
  onWarning: this.cfg.onWarning
3734
3812
  });
3735
3813
  }
@@ -4264,7 +4342,9 @@ var PafiBackendClient = class {
4264
4342
  if (!config.issuerId) throw new Error("PafiBackendClient: issuerId is required");
4265
4343
  if (!config.apiKey) throw new Error("PafiBackendClient: apiKey is required");
4266
4344
  this.config = config;
4267
- this.baseUrl = getPafiServiceUrls(config.chainId).sponsorRelayer;
4345
+ this.baseUrl = getPafiServiceUrls(config.chainId, {
4346
+ sponsorRelayer: config.baseUrl
4347
+ }).sponsorRelayer;
4268
4348
  }
4269
4349
  async requestSponsorship(request) {
4270
4350
  const maxAttempts = this.config.retry?.maxAttempts ?? 1;
@@ -4529,6 +4609,9 @@ function nextBlackoutEndAfter(windows, nowUnixSec) {
4529
4609
  }
4530
4610
  var REDEMPTION_HISTORY_WINDOW_SEC = SECONDS_PER_DAY;
4531
4611
 
4612
+ // src/redemption/policyProvider.ts
4613
+ import { PafiSdkError as PafiSdkError2 } from "@pafi-dev/core";
4614
+
4532
4615
  // src/redemption/settlementClient.ts
4533
4616
  import {
4534
4617
  getPafiServiceUrls as getPafiServiceUrls2
@@ -4541,7 +4624,9 @@ var SettlementClient = class {
4541
4624
  if (!config.issuerId) throw new Error("SettlementClient: issuerId is required");
4542
4625
  if (!config.apiKey) throw new Error("SettlementClient: apiKey is required");
4543
4626
  this.config = {
4544
- baseUrl: getPafiServiceUrls2(config.chainId).issuerApi.replace(/\/+$/, ""),
4627
+ baseUrl: getPafiServiceUrls2(config.chainId, {
4628
+ issuerApi: config.baseUrl
4629
+ }).issuerApi.replace(/\/+$/, ""),
4545
4630
  issuerId: config.issuerId,
4546
4631
  apiKey: config.apiKey,
4547
4632
  fetchTimeoutMs: config.fetchTimeoutMs ?? DEFAULT_TIMEOUT_MS,
@@ -4645,10 +4730,23 @@ function defaultPolicyFor(issuerId) {
4645
4730
 
4646
4731
  // src/redemption/policyProvider.ts
4647
4732
  var DEFAULT_CACHE_TTL_MS3 = 5 * 60 * 1e3;
4733
+ var PolicyProviderUnavailableError = class extends PafiSdkError2 {
4734
+ code = "POLICY_PROVIDER_UNAVAILABLE";
4735
+ httpStatus = "service_unavailable";
4736
+ details;
4737
+ constructor(issuerId, reason) {
4738
+ super(
4739
+ `Redemption policy provider unavailable for issuer ${issuerId}: ${reason}. Pre-flight redeem limit cannot be enforced \u2014 refusing to sign BurnRequest. Mobile FE: surface "try again shortly" and retry with backoff.`
4740
+ );
4741
+ this.details = { issuerId, reason };
4742
+ }
4743
+ };
4648
4744
  var PolicyProvider = class {
4649
4745
  client;
4650
4746
  issuerId;
4651
4747
  cacheTtlMs;
4748
+ onFetchFailure;
4749
+ onWarning;
4652
4750
  now;
4653
4751
  cache = null;
4654
4752
  inflight = null;
@@ -4656,6 +4754,8 @@ var PolicyProvider = class {
4656
4754
  this.client = new SettlementClient(config);
4657
4755
  this.issuerId = config.issuerId;
4658
4756
  this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS3;
4757
+ this.onFetchFailure = config.onFetchFailure ?? "fail-closed";
4758
+ this.onWarning = config.onWarning;
4659
4759
  this.now = config.now ?? (() => Date.now());
4660
4760
  }
4661
4761
  async getPolicy() {
@@ -4688,7 +4788,19 @@ var PolicyProvider = class {
4688
4788
  };
4689
4789
  return { policy: result.policy, source: "settlement" };
4690
4790
  }
4691
- return { policy: defaultPolicyFor(this.issuerId), source: "default" };
4791
+ const reason = "reason" in result && typeof result.reason === "string" ? result.reason : "unknown";
4792
+ if (this.onFetchFailure === "permissive-default") {
4793
+ this.onWarning?.(
4794
+ "PolicyProvider: settlement-api unreachable, falling back to permissive default. Pre-flight redeem limit is DEGRADED until settlement-api recovers.",
4795
+ {
4796
+ event: "policy_provider_fallback",
4797
+ issuerId: this.issuerId,
4798
+ reason
4799
+ }
4800
+ );
4801
+ return { policy: defaultPolicyFor(this.issuerId), source: "default" };
4802
+ }
4803
+ throw new PolicyProviderUnavailableError(this.issuerId, reason);
4692
4804
  }
4693
4805
  };
4694
4806
 
@@ -4835,6 +4947,15 @@ async function createIssuerService(config) {
4835
4947
  issuerId: config.redemption.issuerId,
4836
4948
  apiKey: config.redemption.apiKey
4837
4949
  };
4950
+ if (config.redemption.baseUrl) {
4951
+ policyConfig.baseUrl = config.redemption.baseUrl;
4952
+ }
4953
+ if (config.redemption.onFetchFailure) {
4954
+ policyConfig.onFetchFailure = config.redemption.onFetchFailure;
4955
+ }
4956
+ if (config.redemption.onPolicyWarning) {
4957
+ policyConfig.onWarning = config.redemption.onPolicyWarning;
4958
+ }
4838
4959
  if (config.redemption.fetchImpl) policyConfig.fetchImpl = config.redemption.fetchImpl;
4839
4960
  if (config.redemption.fetchTimeoutMs !== void 0) {
4840
4961
  policyConfig.fetchTimeoutMs = config.redemption.fetchTimeoutMs;
@@ -5099,7 +5220,7 @@ var MemoryRedemptionHistoryStore = class {
5099
5220
  };
5100
5221
 
5101
5222
  // src/index.ts
5102
- var PAFI_ISSUER_SDK_VERSION = true ? "0.31.0" : "dev";
5223
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.33.0" : "dev";
5103
5224
  export {
5104
5225
  AdapterMisconfiguredError,
5105
5226
  AuthError,