@pafi-dev/issuer 0.30.0 → 0.32.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,18 @@ 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.
1554
+ * per chainId; bump SDK version to retarget — OR pass `baseUrl`
1555
+ * to override per-deployment.
1555
1556
  */
1556
1557
  chainId: number;
1558
+ /**
1559
+ * Optional override for the issuer-api base URL. Audit PACI5-17 —
1560
+ * production issuer backends should set this from an env var
1561
+ * (e.g. `PAFI_ISSUER_API_URL`) so the deployed binary doesn't
1562
+ * depend on the SDK ship-default that may target dev infrastructure.
1563
+ * Undefined / empty → use the ship-default for `chainId`.
1564
+ */
1565
+ baseUrl?: string;
1557
1566
  /** PAFI-assigned issuer id used in `X-Issuer-Id` header. */
1558
1567
  issuerId: string;
1559
1568
  /** Raw API key sent as `Authorization: Bearer <apiKey>`. */
@@ -1566,6 +1575,34 @@ interface SettlementClientConfig {
1566
1575
  interface PolicyProviderConfig extends SettlementClientConfig {
1567
1576
  /** Cache TTL in milliseconds. Default 5 * 60 * 1000 (5min). */
1568
1577
  cacheTtlMs?: number;
1578
+ /**
1579
+ * Behavior khi settlement-api fetch fail (network blip, 5xx, timeout).
1580
+ *
1581
+ * Audit PACI5-17 — the pre-flight redeem limit is the SOLE gate
1582
+ * before the issuer signer mints a BurnRequest. Silently degrading
1583
+ * to a permissive default during settlement outages let users
1584
+ * exceed their configured limit until the outage cleared.
1585
+ *
1586
+ * - `'fail-closed'` (DEFAULT, recommended): throw
1587
+ * `PolicyProviderUnavailableError`. Caller maps to HTTP 503 so
1588
+ * the FE can retry; redemptions are blocked until policy fetch
1589
+ * succeeds.
1590
+ *
1591
+ * - `'permissive-default'`: return `defaultPolicyFor(issuerId)` and
1592
+ * fire `onWarning`. Pre-fix behavior. Operators who genuinely
1593
+ * prefer availability over enforcement (e.g. test issuers, low-
1594
+ * risk PT) MUST opt in explicitly AND wire `onWarning` to a
1595
+ * pager / Slack / Sentry alert. Silent permissive fallback is no
1596
+ * longer the default.
1597
+ */
1598
+ onFetchFailure?: "fail-closed" | "permissive-default";
1599
+ /**
1600
+ * Observability hook for non-fatal events: permissive-default
1601
+ * fallbacks (when explicitly opted in). Wire to your logger /
1602
+ * Sentry / Datadog so the `policy_provider_fallback` event reaches
1603
+ * the on-call dashboard.
1604
+ */
1605
+ onWarning?: (msg: string, ctx: Record<string, unknown>) => void;
1569
1606
  /**
1570
1607
  * Optional clock for testability. Returns unix milliseconds.
1571
1608
  * Defaults to () => Date.now().
@@ -1596,6 +1633,8 @@ declare class PolicyProvider {
1596
1633
  private readonly client;
1597
1634
  private readonly issuerId;
1598
1635
  private readonly cacheTtlMs;
1636
+ private readonly onFetchFailure;
1637
+ private readonly onWarning?;
1599
1638
  private readonly now;
1600
1639
  private cache;
1601
1640
  private inflight;
@@ -1889,17 +1928,6 @@ interface PTRedeemHandlerConfig {
1889
1928
  * Every `handle()` call validates the request's `pointTokenAddress`
1890
1929
  * against this set BEFORE any chain read, signer call, or pending
1891
1930
  * credit reservation.
1892
- *
1893
- * Audit PACI5-18 (redeem twin of the claim-side finding) — the read
1894
- * surface (`IssuerApiHandlers.handleUser` / `handleRedemptionEvaluate`)
1895
- * enforces this allowlist, but the write surface previously skipped
1896
- * it. The asymmetry meant an issuer burner signer whitelisted on
1897
- * multiple PointTokens could be coerced into signing a valid
1898
- * `BurnRequest` for an off-allowlist token where no BurnIndexer is
1899
- * running — the on-chain burn would succeed but the off-chain
1900
- * pending credit would stay PENDING forever (or worse, resolve via
1901
- * the bundler-receipt fallback against an unbacked credit balance).
1902
- *
1903
1931
  * Pass the SAME set used to construct `IssuerApiHandlers` and
1904
1932
  * `PTClaimHandler` so the read, claim, and redeem paths agree on
1905
1933
  * what this issuer is willing to sign for.
@@ -1998,7 +2026,7 @@ interface PTRedeemResponse {
1998
2026
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1999
2027
  signatureDeadline: bigint;
2000
2028
  }
2001
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "UNSUPPORTED_POINT_TOKEN";
2029
+ 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";
2002
2030
  declare class PTRedeemError extends PafiSdkError {
2003
2031
  readonly httpStatus: "unprocessable";
2004
2032
  readonly code: PTRedeemErrorCode;
@@ -2050,9 +2078,18 @@ interface PafiBackendConfig {
2050
2078
  /**
2051
2079
  * chainId — used to derive the sponsor-relayer base URL via
2052
2080
  * `getPafiServiceUrls(chainId).sponsorRelayer`. SDK ships with the
2053
- * URL per chainId; bump SDK version to retarget.
2081
+ * URL per chainId; bump SDK version to retarget — OR pass
2082
+ * `baseUrl` to override per-deployment.
2054
2083
  */
2055
2084
  chainId: number;
2085
+ /**
2086
+ * Optional override for the sponsor-relayer base URL. Audit
2087
+ * PACI5-17 — production issuer backends should set this from an env
2088
+ * var (e.g. `PAFI_SPONSOR_RELAYER_URL`) so the binary doesn't depend
2089
+ * on the SDK ship-default that may target dev infrastructure.
2090
+ * Undefined / empty → use the ship-default for `chainId`.
2091
+ */
2092
+ baseUrl?: string;
2056
2093
  issuerId: string;
2057
2094
  apiKey: string;
2058
2095
  fetchImpl?: typeof fetch;
@@ -2295,6 +2332,23 @@ interface PendingUserOpEntry {
2295
2332
  verificationGasLimit: string;
2296
2333
  preVerificationGas: string;
2297
2334
  userOpHash: Hex;
2335
+ /**
2336
+ * Lock id (PendingCredit on redeem, LockedMint on claim) reserved
2337
+ * for the FALLBACK path — distinct from the outer entry's
2338
+ * sponsored lock because the on-chain amount the user burns/mints
2339
+ * differs between variants.
2340
+ *
2341
+ * Audit PACI5-21 — pre-fix `handleMobileSubmit.bindUserOpHash`
2342
+ * always bound the submitted userOpHash to the SPONSORED lockId,
2343
+ * even when the user submitted the fallback variant. The
2344
+ * bundler-receipt fallback in `handleRedeemStatus` then resolved
2345
+ * the sponsored credit (`amount - fee`) against the on-chain
2346
+ * burn of the FULL `amount` — every fee-bearing fallback redeem
2347
+ * permanently under-credited the user by exactly the fee. Surface
2348
+ * the fallback lockId here so submit can route the bind to the
2349
+ * correct ledger row.
2350
+ */
2351
+ lockId?: string;
2298
2352
  };
2299
2353
  /**
2300
2354
  * EIP-7702 authorization tuple — present only on the `delegate`
@@ -2515,6 +2569,21 @@ interface PrepareMobileUserOpParams {
2515
2569
  maxFeePerGas?: bigint;
2516
2570
  maxPriorityFeePerGas?: bigint;
2517
2571
  };
2572
+ /**
2573
+ * Optional separate lock id for the FALLBACK path — required when
2574
+ * the upstream handler reserves a DIFFERENT ledger row for the
2575
+ * fallback variant (PTRedeemHandler reserves
2576
+ * `amount` for fallback vs `amount - fee` for sponsored).
2577
+ *
2578
+ * Audit PACI5-21 — when omitted, `handleMobileSubmit` binds the
2579
+ * fallback userOpHash to the outer sponsored `lockId`, which made
2580
+ * `handleRedeemStatus`'s bundler-receipt fallback resolve the
2581
+ * SMALLER sponsored credit against the on-chain burn of the FULL
2582
+ * amount. Every fee-bearing fallback redeem permanently
2583
+ * under-credited the user by exactly the fee. Pass the fallback
2584
+ * lockId here so submit can route the bind to the correct row.
2585
+ */
2586
+ lockIdFallback?: string;
2518
2587
  /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
2519
2588
  paymasterFields?: {
2520
2589
  paymaster: Address;
@@ -2628,6 +2697,13 @@ interface HandleMobilePrepareParams {
2628
2697
  partialUserOp: PartialUserOperation;
2629
2698
  /** Optional fee-stripped fallback variant. */
2630
2699
  partialUserOpFallback?: PartialUserOperation;
2700
+ /**
2701
+ * Optional separate lock id for the fallback path. Required when the
2702
+ * upstream handler (e.g. PTRedeemHandler) reserves a DIFFERENT
2703
+ * ledger row for the fallback variant. See audit PACI5-21 +
2704
+ * `prepareMobileUserOp` for the full rationale.
2705
+ */
2706
+ lockIdFallback?: string;
2631
2707
  /**
2632
2708
  * Scenario tag — passed to `requestSponsorship` so the relayer can
2633
2709
  * apply per-scenario L1 enforcement (`mint`, `burn`, etc.).
@@ -2781,8 +2857,6 @@ interface PTClaimHandlerConfig {
2781
2857
  * Every `handle()` call validates the request's `pointTokenAddress`
2782
2858
  * against this set BEFORE any chain read or signer call.
2783
2859
  *
2784
- * Audit PACI5-18 — the read surface (`IssuerApiHandlers.handleUser`,
2785
- * `handleRedemptionEvaluate`) enforces this allowlist, but the
2786
2860
  * write surface (claim/redeem) previously skipped it. The asymmetry
2787
2861
  * meant an issuer signer whitelisted as minter on PointTokens
2788
2862
  * outside the configured indexer set could be coerced into signing
@@ -3159,6 +3233,30 @@ interface IssuerServiceConfig {
3159
3233
  issuerId: string;
3160
3234
  apiKey: string;
3161
3235
  historyStore: IRedemptionHistoryStore;
3236
+ /**
3237
+ * Optional override for the PAFI issuer-api base URL. Audit
3238
+ * PACI5-17 — production issuer backends should set this from an
3239
+ * env var (e.g. `PAFI_ISSUER_API_URL`) so policy fetch hits the
3240
+ * canonical environment for the deploy. Undefined → SDK
3241
+ * ship-default per chainId.
3242
+ */
3243
+ baseUrl?: string;
3244
+ /**
3245
+ * Behavior khi settlement-api fetch fail. Default `'fail-closed'`
3246
+ * (audit PACI5-17 — pre-flight redeem limit is the sole gate; do
3247
+ * not silently degrade to a permissive default).
3248
+ *
3249
+ * Opt in to `'permissive-default'` ONLY when paired with an alert
3250
+ * on the `policy_provider_fallback` event surfaced via
3251
+ * `onPolicyWarning`.
3252
+ */
3253
+ onFetchFailure?: "fail-closed" | "permissive-default";
3254
+ /**
3255
+ * Observability hook for `policy_provider_fallback` events. Wire
3256
+ * to your logger / Sentry / Datadog so the on-call dashboard sees
3257
+ * settlement-api degradation.
3258
+ */
3259
+ onPolicyWarning?: (msg: string, ctx: Record<string, unknown>) => void;
3162
3260
  /** Override fetch (testing). */
3163
3261
  fetchImpl?: typeof fetch;
3164
3262
  /** Per-fetch timeout in ms. Default 1000. */
@@ -3324,6 +3422,12 @@ interface MobilePrepareDto {
3324
3422
  interface RedeemPrepareDto extends MobilePrepareDto {
3325
3423
  netCreditAmount: string;
3326
3424
  netCreditAmountFallback?: string;
3425
+ /**
3426
+ * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3427
+ * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3428
+ * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3429
+ */
3430
+ lockIdFallback?: string;
3327
3431
  }
3328
3432
  interface MobileSubmitDto {
3329
3433
  userOpHash: Hex;
package/dist/index.d.ts CHANGED
@@ -1551,9 +1551,18 @@ 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.
1554
+ * per chainId; bump SDK version to retarget — OR pass `baseUrl`
1555
+ * to override per-deployment.
1555
1556
  */
1556
1557
  chainId: number;
1558
+ /**
1559
+ * Optional override for the issuer-api base URL. Audit PACI5-17 —
1560
+ * production issuer backends should set this from an env var
1561
+ * (e.g. `PAFI_ISSUER_API_URL`) so the deployed binary doesn't
1562
+ * depend on the SDK ship-default that may target dev infrastructure.
1563
+ * Undefined / empty → use the ship-default for `chainId`.
1564
+ */
1565
+ baseUrl?: string;
1557
1566
  /** PAFI-assigned issuer id used in `X-Issuer-Id` header. */
1558
1567
  issuerId: string;
1559
1568
  /** Raw API key sent as `Authorization: Bearer <apiKey>`. */
@@ -1566,6 +1575,34 @@ interface SettlementClientConfig {
1566
1575
  interface PolicyProviderConfig extends SettlementClientConfig {
1567
1576
  /** Cache TTL in milliseconds. Default 5 * 60 * 1000 (5min). */
1568
1577
  cacheTtlMs?: number;
1578
+ /**
1579
+ * Behavior khi settlement-api fetch fail (network blip, 5xx, timeout).
1580
+ *
1581
+ * Audit PACI5-17 — the pre-flight redeem limit is the SOLE gate
1582
+ * before the issuer signer mints a BurnRequest. Silently degrading
1583
+ * to a permissive default during settlement outages let users
1584
+ * exceed their configured limit until the outage cleared.
1585
+ *
1586
+ * - `'fail-closed'` (DEFAULT, recommended): throw
1587
+ * `PolicyProviderUnavailableError`. Caller maps to HTTP 503 so
1588
+ * the FE can retry; redemptions are blocked until policy fetch
1589
+ * succeeds.
1590
+ *
1591
+ * - `'permissive-default'`: return `defaultPolicyFor(issuerId)` and
1592
+ * fire `onWarning`. Pre-fix behavior. Operators who genuinely
1593
+ * prefer availability over enforcement (e.g. test issuers, low-
1594
+ * risk PT) MUST opt in explicitly AND wire `onWarning` to a
1595
+ * pager / Slack / Sentry alert. Silent permissive fallback is no
1596
+ * longer the default.
1597
+ */
1598
+ onFetchFailure?: "fail-closed" | "permissive-default";
1599
+ /**
1600
+ * Observability hook for non-fatal events: permissive-default
1601
+ * fallbacks (when explicitly opted in). Wire to your logger /
1602
+ * Sentry / Datadog so the `policy_provider_fallback` event reaches
1603
+ * the on-call dashboard.
1604
+ */
1605
+ onWarning?: (msg: string, ctx: Record<string, unknown>) => void;
1569
1606
  /**
1570
1607
  * Optional clock for testability. Returns unix milliseconds.
1571
1608
  * Defaults to () => Date.now().
@@ -1596,6 +1633,8 @@ declare class PolicyProvider {
1596
1633
  private readonly client;
1597
1634
  private readonly issuerId;
1598
1635
  private readonly cacheTtlMs;
1636
+ private readonly onFetchFailure;
1637
+ private readonly onWarning?;
1599
1638
  private readonly now;
1600
1639
  private cache;
1601
1640
  private inflight;
@@ -1889,17 +1928,6 @@ interface PTRedeemHandlerConfig {
1889
1928
  * Every `handle()` call validates the request's `pointTokenAddress`
1890
1929
  * against this set BEFORE any chain read, signer call, or pending
1891
1930
  * credit reservation.
1892
- *
1893
- * Audit PACI5-18 (redeem twin of the claim-side finding) — the read
1894
- * surface (`IssuerApiHandlers.handleUser` / `handleRedemptionEvaluate`)
1895
- * enforces this allowlist, but the write surface previously skipped
1896
- * it. The asymmetry meant an issuer burner signer whitelisted on
1897
- * multiple PointTokens could be coerced into signing a valid
1898
- * `BurnRequest` for an off-allowlist token where no BurnIndexer is
1899
- * running — the on-chain burn would succeed but the off-chain
1900
- * pending credit would stay PENDING forever (or worse, resolve via
1901
- * the bundler-receipt fallback against an unbacked credit balance).
1902
- *
1903
1931
  * Pass the SAME set used to construct `IssuerApiHandlers` and
1904
1932
  * `PTClaimHandler` so the read, claim, and redeem paths agree on
1905
1933
  * what this issuer is willing to sign for.
@@ -1998,7 +2026,7 @@ interface PTRedeemResponse {
1998
2026
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1999
2027
  signatureDeadline: bigint;
2000
2028
  }
2001
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED" | "UNSUPPORTED_POINT_TOKEN";
2029
+ 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";
2002
2030
  declare class PTRedeemError extends PafiSdkError {
2003
2031
  readonly httpStatus: "unprocessable";
2004
2032
  readonly code: PTRedeemErrorCode;
@@ -2050,9 +2078,18 @@ interface PafiBackendConfig {
2050
2078
  /**
2051
2079
  * chainId — used to derive the sponsor-relayer base URL via
2052
2080
  * `getPafiServiceUrls(chainId).sponsorRelayer`. SDK ships with the
2053
- * URL per chainId; bump SDK version to retarget.
2081
+ * URL per chainId; bump SDK version to retarget — OR pass
2082
+ * `baseUrl` to override per-deployment.
2054
2083
  */
2055
2084
  chainId: number;
2085
+ /**
2086
+ * Optional override for the sponsor-relayer base URL. Audit
2087
+ * PACI5-17 — production issuer backends should set this from an env
2088
+ * var (e.g. `PAFI_SPONSOR_RELAYER_URL`) so the binary doesn't depend
2089
+ * on the SDK ship-default that may target dev infrastructure.
2090
+ * Undefined / empty → use the ship-default for `chainId`.
2091
+ */
2092
+ baseUrl?: string;
2056
2093
  issuerId: string;
2057
2094
  apiKey: string;
2058
2095
  fetchImpl?: typeof fetch;
@@ -2295,6 +2332,23 @@ interface PendingUserOpEntry {
2295
2332
  verificationGasLimit: string;
2296
2333
  preVerificationGas: string;
2297
2334
  userOpHash: Hex;
2335
+ /**
2336
+ * Lock id (PendingCredit on redeem, LockedMint on claim) reserved
2337
+ * for the FALLBACK path — distinct from the outer entry's
2338
+ * sponsored lock because the on-chain amount the user burns/mints
2339
+ * differs between variants.
2340
+ *
2341
+ * Audit PACI5-21 — pre-fix `handleMobileSubmit.bindUserOpHash`
2342
+ * always bound the submitted userOpHash to the SPONSORED lockId,
2343
+ * even when the user submitted the fallback variant. The
2344
+ * bundler-receipt fallback in `handleRedeemStatus` then resolved
2345
+ * the sponsored credit (`amount - fee`) against the on-chain
2346
+ * burn of the FULL `amount` — every fee-bearing fallback redeem
2347
+ * permanently under-credited the user by exactly the fee. Surface
2348
+ * the fallback lockId here so submit can route the bind to the
2349
+ * correct ledger row.
2350
+ */
2351
+ lockId?: string;
2298
2352
  };
2299
2353
  /**
2300
2354
  * EIP-7702 authorization tuple — present only on the `delegate`
@@ -2515,6 +2569,21 @@ interface PrepareMobileUserOpParams {
2515
2569
  maxFeePerGas?: bigint;
2516
2570
  maxPriorityFeePerGas?: bigint;
2517
2571
  };
2572
+ /**
2573
+ * Optional separate lock id for the FALLBACK path — required when
2574
+ * the upstream handler reserves a DIFFERENT ledger row for the
2575
+ * fallback variant (PTRedeemHandler reserves
2576
+ * `amount` for fallback vs `amount - fee` for sponsored).
2577
+ *
2578
+ * Audit PACI5-21 — when omitted, `handleMobileSubmit` binds the
2579
+ * fallback userOpHash to the outer sponsored `lockId`, which made
2580
+ * `handleRedeemStatus`'s bundler-receipt fallback resolve the
2581
+ * SMALLER sponsored credit against the on-chain burn of the FULL
2582
+ * amount. Every fee-bearing fallback redeem permanently
2583
+ * under-credited the user by exactly the fee. Pass the fallback
2584
+ * lockId here so submit can route the bind to the correct row.
2585
+ */
2586
+ lockIdFallback?: string;
2518
2587
  /** Paymaster sponsorship response, or `undefined` if PAFI declined. */
2519
2588
  paymasterFields?: {
2520
2589
  paymaster: Address;
@@ -2628,6 +2697,13 @@ interface HandleMobilePrepareParams {
2628
2697
  partialUserOp: PartialUserOperation;
2629
2698
  /** Optional fee-stripped fallback variant. */
2630
2699
  partialUserOpFallback?: PartialUserOperation;
2700
+ /**
2701
+ * Optional separate lock id for the fallback path. Required when the
2702
+ * upstream handler (e.g. PTRedeemHandler) reserves a DIFFERENT
2703
+ * ledger row for the fallback variant. See audit PACI5-21 +
2704
+ * `prepareMobileUserOp` for the full rationale.
2705
+ */
2706
+ lockIdFallback?: string;
2631
2707
  /**
2632
2708
  * Scenario tag — passed to `requestSponsorship` so the relayer can
2633
2709
  * apply per-scenario L1 enforcement (`mint`, `burn`, etc.).
@@ -2781,8 +2857,6 @@ interface PTClaimHandlerConfig {
2781
2857
  * Every `handle()` call validates the request's `pointTokenAddress`
2782
2858
  * against this set BEFORE any chain read or signer call.
2783
2859
  *
2784
- * Audit PACI5-18 — the read surface (`IssuerApiHandlers.handleUser`,
2785
- * `handleRedemptionEvaluate`) enforces this allowlist, but the
2786
2860
  * write surface (claim/redeem) previously skipped it. The asymmetry
2787
2861
  * meant an issuer signer whitelisted as minter on PointTokens
2788
2862
  * outside the configured indexer set could be coerced into signing
@@ -3159,6 +3233,30 @@ interface IssuerServiceConfig {
3159
3233
  issuerId: string;
3160
3234
  apiKey: string;
3161
3235
  historyStore: IRedemptionHistoryStore;
3236
+ /**
3237
+ * Optional override for the PAFI issuer-api base URL. Audit
3238
+ * PACI5-17 — production issuer backends should set this from an
3239
+ * env var (e.g. `PAFI_ISSUER_API_URL`) so policy fetch hits the
3240
+ * canonical environment for the deploy. Undefined → SDK
3241
+ * ship-default per chainId.
3242
+ */
3243
+ baseUrl?: string;
3244
+ /**
3245
+ * Behavior khi settlement-api fetch fail. Default `'fail-closed'`
3246
+ * (audit PACI5-17 — pre-flight redeem limit is the sole gate; do
3247
+ * not silently degrade to a permissive default).
3248
+ *
3249
+ * Opt in to `'permissive-default'` ONLY when paired with an alert
3250
+ * on the `policy_provider_fallback` event surfaced via
3251
+ * `onPolicyWarning`.
3252
+ */
3253
+ onFetchFailure?: "fail-closed" | "permissive-default";
3254
+ /**
3255
+ * Observability hook for `policy_provider_fallback` events. Wire
3256
+ * to your logger / Sentry / Datadog so the on-call dashboard sees
3257
+ * settlement-api degradation.
3258
+ */
3259
+ onPolicyWarning?: (msg: string, ctx: Record<string, unknown>) => void;
3162
3260
  /** Override fetch (testing). */
3163
3261
  fetchImpl?: typeof fetch;
3164
3262
  /** Per-fetch timeout in ms. Default 1000. */
@@ -3324,6 +3422,12 @@ interface MobilePrepareDto {
3324
3422
  interface RedeemPrepareDto extends MobilePrepareDto {
3325
3423
  netCreditAmount: string;
3326
3424
  netCreditAmountFallback?: string;
3425
+ /**
3426
+ * Lock id reserved for the FALLBACK redeem path (= full `amount`).
3427
+ * Mobile FE polls `/redeem/status/:lockIdFallback` when it submits
3428
+ * the fallback variant (`variant: 'fallback'` on `/redeem/submit`).
3429
+ */
3430
+ lockIdFallback?: string;
3327
3431
  }
3328
3432
  interface MobileSubmitDto {
3329
3433
  userOpHash: Hex;
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(
@@ -2685,7 +2697,10 @@ async function prepareMobileUserOp(params) {
2685
2697
  callGasLimit: fallback.userOp.callGasLimit.toString(),
2686
2698
  verificationGasLimit: fallback.userOp.verificationGasLimit.toString(),
2687
2699
  preVerificationGas: fallback.userOp.preVerificationGas.toString(),
2688
- userOpHash: fallback.userOpHash
2700
+ userOpHash: fallback.userOpHash,
2701
+ // Audit PACI5-21 — carry the fallback-specific lockId so submit
2702
+ // can bind the fallback userOpHash to the correct ledger row.
2703
+ lockId: params.lockIdFallback
2689
2704
  };
2690
2705
  }
2691
2706
  const entry = {
@@ -2878,6 +2893,7 @@ async function handleMobilePrepare(params) {
2878
2893
  lockId: params.lockId,
2879
2894
  partialUserOp: sponsoredOp,
2880
2895
  partialUserOpFallback: params.partialUserOpFallback,
2896
+ lockIdFallback: params.lockIdFallback,
2881
2897
  paymasterFields,
2882
2898
  chainId: params.chainId,
2883
2899
  store: params.store,
@@ -2906,7 +2922,8 @@ async function handleMobileSubmit(params) {
2906
2922
  entryPoint: params.entryPoint ?? ENTRY_POINT_V08,
2907
2923
  eip7702Auth: entry.eip7702Auth
2908
2924
  });
2909
- await params.bindUserOpHash(params.lockId, result.userOpHash);
2925
+ const targetLockId = variant === "fallback" && entry.fallback?.lockId ? entry.fallback.lockId : params.lockId;
2926
+ await params.bindUserOpHash(targetLockId, result.userOpHash);
2910
2927
  await params.store.delete(params.lockId);
2911
2928
  return { userOpHash: result.userOpHash };
2912
2929
  }
@@ -3676,10 +3693,12 @@ var IssuerApiAdapter = class {
3676
3693
  "burn",
3677
3694
  pointTokenAddress,
3678
3695
  redeemResponse.expiresInSeconds,
3679
- input.eip7702Auth
3696
+ input.eip7702Auth,
3697
+ redeemResponse.fallback?.lockId
3680
3698
  );
3681
3699
  return {
3682
3700
  lockId: redeemResponse.lockId,
3701
+ lockIdFallback: redeemResponse.fallback?.lockId,
3683
3702
  userOpHash: prepared.sponsored.userOpHash,
3684
3703
  typedData: prepared.sponsored.typedData,
3685
3704
  userOpHashFallback: prepared.fallback?.userOpHash,
@@ -3810,13 +3829,14 @@ var IssuerApiAdapter = class {
3810
3829
  issuerSignerWallet: this.cfg.issuerSignerWallet
3811
3830
  });
3812
3831
  }
3813
- async runMobilePrepare(authenticatedAddress, chainId, lockId, partialUserOp, partialUserOpFallback, scenario, pointTokenAddress, ttlSeconds, eip7702Auth) {
3832
+ async runMobilePrepare(authenticatedAddress, chainId, lockId, partialUserOp, partialUserOpFallback, scenario, pointTokenAddress, ttlSeconds, eip7702Auth, lockIdFallback) {
3814
3833
  return await handleMobilePrepare({
3815
3834
  userAddress: authenticatedAddress,
3816
3835
  chainId,
3817
3836
  lockId,
3818
3837
  partialUserOp,
3819
3838
  partialUserOpFallback,
3839
+ lockIdFallback,
3820
3840
  scenario,
3821
3841
  pointTokenAddress,
3822
3842
  ttlSeconds,
@@ -4252,7 +4272,9 @@ var PafiBackendClient = class {
4252
4272
  if (!config.issuerId) throw new Error("PafiBackendClient: issuerId is required");
4253
4273
  if (!config.apiKey) throw new Error("PafiBackendClient: apiKey is required");
4254
4274
  this.config = config;
4255
- this.baseUrl = getPafiServiceUrls(config.chainId).sponsorRelayer;
4275
+ this.baseUrl = getPafiServiceUrls(config.chainId, {
4276
+ sponsorRelayer: config.baseUrl
4277
+ }).sponsorRelayer;
4256
4278
  }
4257
4279
  async requestSponsorship(request) {
4258
4280
  const maxAttempts = this.config.retry?.maxAttempts ?? 1;
@@ -4517,6 +4539,9 @@ function nextBlackoutEndAfter(windows, nowUnixSec) {
4517
4539
  }
4518
4540
  var REDEMPTION_HISTORY_WINDOW_SEC = SECONDS_PER_DAY;
4519
4541
 
4542
+ // src/redemption/policyProvider.ts
4543
+ import { PafiSdkError as PafiSdkError2 } from "@pafi-dev/core";
4544
+
4520
4545
  // src/redemption/settlementClient.ts
4521
4546
  import {
4522
4547
  getPafiServiceUrls as getPafiServiceUrls2
@@ -4529,7 +4554,12 @@ var SettlementClient = class {
4529
4554
  if (!config.issuerId) throw new Error("SettlementClient: issuerId is required");
4530
4555
  if (!config.apiKey) throw new Error("SettlementClient: apiKey is required");
4531
4556
  this.config = {
4532
- baseUrl: getPafiServiceUrls2(config.chainId).issuerApi.replace(/\/+$/, ""),
4557
+ // Audit PACI5-17 — honor optional baseUrl override wired from
4558
+ // an env var (e.g. PAFI_ISSUER_API_URL). Empty / undefined →
4559
+ // ship-default for chainId.
4560
+ baseUrl: getPafiServiceUrls2(config.chainId, {
4561
+ issuerApi: config.baseUrl
4562
+ }).issuerApi.replace(/\/+$/, ""),
4533
4563
  issuerId: config.issuerId,
4534
4564
  apiKey: config.apiKey,
4535
4565
  fetchTimeoutMs: config.fetchTimeoutMs ?? DEFAULT_TIMEOUT_MS,
@@ -4633,10 +4663,23 @@ function defaultPolicyFor(issuerId) {
4633
4663
 
4634
4664
  // src/redemption/policyProvider.ts
4635
4665
  var DEFAULT_CACHE_TTL_MS3 = 5 * 60 * 1e3;
4666
+ var PolicyProviderUnavailableError = class extends PafiSdkError2 {
4667
+ code = "POLICY_PROVIDER_UNAVAILABLE";
4668
+ httpStatus = "service_unavailable";
4669
+ details;
4670
+ constructor(issuerId, reason) {
4671
+ super(
4672
+ `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.`
4673
+ );
4674
+ this.details = { issuerId, reason };
4675
+ }
4676
+ };
4636
4677
  var PolicyProvider = class {
4637
4678
  client;
4638
4679
  issuerId;
4639
4680
  cacheTtlMs;
4681
+ onFetchFailure;
4682
+ onWarning;
4640
4683
  now;
4641
4684
  cache = null;
4642
4685
  inflight = null;
@@ -4644,6 +4687,8 @@ var PolicyProvider = class {
4644
4687
  this.client = new SettlementClient(config);
4645
4688
  this.issuerId = config.issuerId;
4646
4689
  this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS3;
4690
+ this.onFetchFailure = config.onFetchFailure ?? "fail-closed";
4691
+ this.onWarning = config.onWarning;
4647
4692
  this.now = config.now ?? (() => Date.now());
4648
4693
  }
4649
4694
  async getPolicy() {
@@ -4676,7 +4721,19 @@ var PolicyProvider = class {
4676
4721
  };
4677
4722
  return { policy: result.policy, source: "settlement" };
4678
4723
  }
4679
- return { policy: defaultPolicyFor(this.issuerId), source: "default" };
4724
+ const reason = "reason" in result && typeof result.reason === "string" ? result.reason : "unknown";
4725
+ if (this.onFetchFailure === "permissive-default") {
4726
+ this.onWarning?.(
4727
+ "PolicyProvider: settlement-api unreachable, falling back to permissive default. Pre-flight redeem limit is DEGRADED until settlement-api recovers.",
4728
+ {
4729
+ event: "policy_provider_fallback",
4730
+ issuerId: this.issuerId,
4731
+ reason
4732
+ }
4733
+ );
4734
+ return { policy: defaultPolicyFor(this.issuerId), source: "default" };
4735
+ }
4736
+ throw new PolicyProviderUnavailableError(this.issuerId, reason);
4680
4737
  }
4681
4738
  };
4682
4739
 
@@ -4823,6 +4880,15 @@ async function createIssuerService(config) {
4823
4880
  issuerId: config.redemption.issuerId,
4824
4881
  apiKey: config.redemption.apiKey
4825
4882
  };
4883
+ if (config.redemption.baseUrl) {
4884
+ policyConfig.baseUrl = config.redemption.baseUrl;
4885
+ }
4886
+ if (config.redemption.onFetchFailure) {
4887
+ policyConfig.onFetchFailure = config.redemption.onFetchFailure;
4888
+ }
4889
+ if (config.redemption.onPolicyWarning) {
4890
+ policyConfig.onWarning = config.redemption.onPolicyWarning;
4891
+ }
4826
4892
  if (config.redemption.fetchImpl) policyConfig.fetchImpl = config.redemption.fetchImpl;
4827
4893
  if (config.redemption.fetchTimeoutMs !== void 0) {
4828
4894
  policyConfig.fetchTimeoutMs = config.redemption.fetchTimeoutMs;
@@ -5087,7 +5153,7 @@ var MemoryRedemptionHistoryStore = class {
5087
5153
  };
5088
5154
 
5089
5155
  // src/index.ts
5090
- var PAFI_ISSUER_SDK_VERSION = true ? "0.30.0" : "dev";
5156
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.32.0" : "dev";
5091
5157
  export {
5092
5158
  AdapterMisconfiguredError,
5093
5159
  AuthError,