@pafi-dev/issuer 0.7.9 → 0.8.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.ts CHANGED
@@ -1,16 +1,13 @@
1
- import { PafiSdkError, SdkErrorHttpStatus, PointTokenDomainConfig, PartialUserOperation, BurnRequest, PoolKey, UserOpTypedData, decodeBatchExecuteCalls, BROKER_HASHES, BuiltSponsorAuth, ENTRY_POINT_V08 } from '@pafi-dev/core';
1
+ import { PafiSdkError, SdkErrorHttpStatus, PointTokenDomainConfig, PartialUserOperation, BurnRequest, PoolKey, RedemptionPolicy, RedemptionPolicySource, RedemptionPreview, RedemptionDecision, RedemptionDenialCode, UserOpTypedData, decodeBatchExecuteCalls, BROKER_HASHES, BuiltSponsorAuth, ENTRY_POINT_V08 } from '@pafi-dev/core';
2
2
  export { PAFI_SUBGRAPH_URL, PafiSdkError, SdkErrorHttpStatus, ValidationError } from '@pafi-dev/core';
3
3
  import { Address, Hex, PublicClient, WalletClient } from 'viem';
4
4
 
5
5
  /**
6
- * v0.7.4 — `PafiSdkError` + `SdkErrorHttpStatus` were moved to
6
+ * `PafiSdkError` + `SdkErrorHttpStatus` are exported from
7
7
  * `@pafi-dev/core/errors` so core-level errors (e.g. `OracleStaleError`)
8
- * can extend the same base. Issuer re-exports the canonical types
9
- * here for back-compat. See SDK_CORE_TRADING_AUDIT.md H3.
10
- *
11
- * Effect: `instanceof PafiSdkError` from EITHER package now catches
12
- * errors thrown from EITHER package — no more silent fall-through to
13
- * 500 for core-thrown errors.
8
+ * can extend the same base. Issuer re-exports the canonical types here
9
+ * for back-compat `instanceof PafiSdkError` from EITHER package
10
+ * catches errors thrown from EITHER package.
14
11
  */
15
12
 
16
13
  /**
@@ -19,8 +16,6 @@ import { Address, Hex, PublicClient, WalletClient } from 'viem';
19
16
  * `/pools` called but `poolsProvider` not configured). 503 because
20
17
  * the endpoint genuinely can't serve the request — caller's payload
21
18
  * is fine, the issuer's deployment is incomplete.
22
- *
23
- * v0.7.1 — added per SDK_ISSUER_AUDIT.md H1.
24
19
  */
25
20
  declare class ConfigurationError extends PafiSdkError {
26
21
  readonly httpStatus: "service_unavailable";
@@ -293,6 +288,15 @@ interface MemorySessionStoreOptions {
293
288
  nonceTtlMs?: number;
294
289
  /** Clock override for tests. */
295
290
  now?: () => number;
291
+ /**
292
+ * Bypass the production-safety guard. By default, instantiating
293
+ * `MemorySessionStore` when `process.env.NODE_ENV === "production"`
294
+ * THROWS — multi-pod K8s deploys do not share Map state, so sessions
295
+ * are not revocable across replicas, and tokens remain valid on pod
296
+ * B after `logout` on pod A. Only set this `true` for single-pod
297
+ * deployments where you've consciously accepted the trade-off.
298
+ */
299
+ dangerouslyAllowMemoryStoreInProduction?: boolean;
296
300
  }
297
301
  /**
298
302
  * In-memory `ISessionStore` — Map-backed nonces and sessions with lazy
@@ -351,6 +355,21 @@ interface AuthServiceConfig {
351
355
  domain: string;
352
356
  /** Expected chain id. */
353
357
  chainId: number;
358
+ /**
359
+ * Optional `iss` (issuer) claim baked into JWTs and validated on
360
+ * verify. Recommended for multi-issuer deployments to prevent
361
+ * cross-validation if a JWT secret is accidentally shared. Examples:
362
+ * `"gg56-issuer"`, `"issuer.gg56.com"`. When unset, no `iss` is
363
+ * written or required (back-compat).
364
+ */
365
+ issuer?: string;
366
+ /**
367
+ * Optional `aud` (audience) claim baked into JWTs and validated on
368
+ * verify. Recommended to bind tokens to a specific consumer, e.g.
369
+ * `"gg56-mobile"` or `"gg56-web"`. When unset, no `aud` is written
370
+ * or required (back-compat).
371
+ */
372
+ audience?: string;
354
373
  /** Clock override for tests. */
355
374
  now?: () => Date;
356
375
  }
@@ -384,6 +403,8 @@ declare class AuthService {
384
403
  private readonly jwtExpiresIn;
385
404
  private readonly domain;
386
405
  private readonly chainId;
406
+ private readonly issuer?;
407
+ private readonly audience?;
387
408
  private readonly nonceManager;
388
409
  private readonly now;
389
410
  constructor(config: AuthServiceConfig);
@@ -396,6 +417,7 @@ declare class AuthService {
396
417
  login(message: string, signature: Hex): Promise<LoginResult>;
397
418
  /** Revoke the session backing the given JWT (logout). */
398
419
  logout(token: string): Promise<void>;
420
+ private logSessionStoreError;
399
421
  /**
400
422
  * Verify a JWT and return the authenticated user context. Throws an
401
423
  * `AuthError` if the token is missing, malformed, expired, revoked, or
@@ -421,10 +443,9 @@ declare function authenticateRequest(authHeader: string | undefined, authService
421
443
  */
422
444
  type AuthErrorCode = "INVALID_MESSAGE" | "DOMAIN_MISMATCH" | "CHAIN_MISMATCH" | "NONCE_INVALID" | "MESSAGE_EXPIRED" | "MESSAGE_NOT_YET_VALID" | "SIGNATURE_INVALID" | "MISSING_TOKEN" | "MALFORMED_TOKEN" | "TOKEN_INVALID" | "TOKEN_EXPIRED" | "SESSION_REVOKED";
423
445
  /**
424
- * v0.7.1 — extends `PafiSdkError` so issuer controllers can route auth
425
- * failures through the same `createSdkErrorMapper` as everything else.
426
- * Previously caller had to wire `instanceof AuthError` separately. See
427
- * SDK_ISSUER_AUDIT.md H2.
446
+ * Extends `PafiSdkError` so issuer controllers can route auth failures
447
+ * through the same `createSdkErrorMapper` as everything else (no
448
+ * separate `instanceof AuthError` check needed).
428
449
  */
429
450
  declare class AuthError extends PafiSdkError {
430
451
  readonly code: AuthErrorCode;
@@ -432,6 +453,88 @@ declare class AuthError extends PafiSdkError {
432
453
  constructor(code: AuthErrorCode, message: string);
433
454
  }
434
455
 
456
+ /**
457
+ * Per-key rate limiting for unauthenticated public endpoints.
458
+ *
459
+ * `/auth/nonce` and `/auth/login` are unauthenticated and CPU-bound
460
+ * (SIWE message parsing + ECDSA `recover`), so without rate limiting
461
+ * an attacker can saturate the issuer at low cost. Worse, on
462
+ * `/auth/nonce` they can flood the session store with single-use
463
+ * nonces; with `MemorySessionStore` this is a pure DoS.
464
+ *
465
+ * Issuers wire a `IRateLimiter` impl in `IssuerApiHandlersConfig` —
466
+ * `MemoryRateLimiter` is the default reference impl (single-pod, dev).
467
+ * Production deployments use a Redis-backed impl that shares state
468
+ * across replicas (similar pattern to `ISessionStore`).
469
+ *
470
+ * The "key" is up to the caller — typically the client IP, but the
471
+ * issuer's HTTP layer chooses (could be userAddress on POST /login).
472
+ */
473
+ interface IRateLimiter {
474
+ /**
475
+ * Check + increment a counter under `key`. Returns `{ allowed: false,
476
+ * retryAfterMs }` when the caller has exceeded its quota for this
477
+ * window. Default config is action-specific (see
478
+ * `RateLimiterConfig`).
479
+ *
480
+ * Idempotent in the sense that successive calls with the same `key`
481
+ * within the same window count toward the limit; a separate window
482
+ * starts after `windowMs` elapses.
483
+ */
484
+ consume(key: string, action: RateLimitAction): Promise<{
485
+ allowed: boolean;
486
+ retryAfterMs?: number;
487
+ }>;
488
+ }
489
+ type RateLimitAction = "auth_nonce" | "auth_login";
490
+ interface RateLimiterConfig {
491
+ /**
492
+ * Per-key max requests per `windowMs`. Defaults:
493
+ * - auth_nonce: 1 per 2s (30/min)
494
+ * - auth_login: 5 per minute
495
+ *
496
+ * Issuers can tune per their threat model.
497
+ */
498
+ limits?: Partial<Record<RateLimitAction, {
499
+ max: number;
500
+ windowMs: number;
501
+ }>>;
502
+ /** Clock override for tests. */
503
+ now?: () => number;
504
+ }
505
+ /**
506
+ * In-memory `IRateLimiter` — Map of `{key, action}` → count + window
507
+ * start. Lazy expiry on read. Single-process: NOT safe for multi-pod
508
+ * deployments. Use a Redis impl in production.
509
+ */
510
+ declare class MemoryRateLimiter implements IRateLimiter {
511
+ private buckets;
512
+ private readonly limits;
513
+ private readonly now;
514
+ constructor(config?: RateLimiterConfig);
515
+ consume(key: string, action: RateLimitAction): Promise<{
516
+ allowed: boolean;
517
+ retryAfterMs?: number;
518
+ }>;
519
+ /**
520
+ * Test helper — clear all buckets. Not part of `IRateLimiter`; only
521
+ * exposed on the in-memory impl for unit tests.
522
+ */
523
+ reset(): void;
524
+ }
525
+ /**
526
+ * Convenience: a no-op `IRateLimiter` that lets every request through.
527
+ * Useful as a default in `IssuerApiHandlersConfig` when the issuer
528
+ * hasn't wired a rate limiter yet (back-compat) — emits a one-time
529
+ * warning at construction so consumers notice.
530
+ */
531
+ declare class NoopRateLimiter implements IRateLimiter {
532
+ private warned;
533
+ consume(): Promise<{
534
+ allowed: boolean;
535
+ }>;
536
+ }
537
+
435
538
  type RelayErrorCode = "ENCODE_FAILED";
436
539
  declare class RelayError extends PafiSdkError {
437
540
  readonly httpStatus: "unprocessable";
@@ -724,6 +827,16 @@ interface PointIndexerConfig {
724
827
  pollIntervalMs?: number;
725
828
  /** Clock override for tests. */
726
829
  now?: () => number;
830
+ /**
831
+ * Observability hook fired on EVERY tick error (RPC failure, ledger
832
+ * write failure, etc). Issuers MUST wire this to their alert
833
+ * pipeline (Sentry / Datadog / PagerDuty) — without it, the indexer
834
+ * silently swallows errors via `console.error`, and indexer outage
835
+ * means PENDING mint locks never resolve (user balances stuck).
836
+ *
837
+ * When omitted, falls back to `console.error` for back-compat.
838
+ */
839
+ onTickError?: (err: unknown) => void;
727
840
  }
728
841
  /**
729
842
  * Watches `PointToken.Transfer(from=0x0 → to)` events and finalizes the
@@ -755,6 +868,7 @@ declare class PointIndexer {
755
868
  private readonly confirmations;
756
869
  private readonly batchSize;
757
870
  private readonly pollIntervalMs;
871
+ private readonly onTickError?;
758
872
  private running;
759
873
  private timer;
760
874
  constructor(config: PointIndexerConfig);
@@ -768,6 +882,7 @@ declare class PointIndexer {
768
882
  * schedules the next tick. Visible for test harnesses via a public name.
769
883
  */
770
884
  tick(): Promise<void>;
885
+ private handleTickError;
771
886
  private scheduleNext;
772
887
  /**
773
888
  * Scan `[from, to]` inclusive for mint events in `batchSize` chunks.
@@ -816,6 +931,13 @@ interface BurnIndexerConfig {
816
931
  * ledger uses a lookup by lockId supplied out-of-band from the claim flow.
817
932
  */
818
933
  matchLockId: (event: BurnEvent) => Promise<string | undefined>;
934
+ /**
935
+ * Observability hook — see PointIndexerConfig.onTickError for full
936
+ * rationale. Critical for burn indexer too: silent failure means
937
+ * pending credits never resolve, user reports "I burned my PT but
938
+ * never got my off-chain credit".
939
+ */
940
+ onTickError?: (err: unknown) => void;
819
941
  }
820
942
  /**
821
943
  * Mirror of `PointIndexer` for the reverse direction — watches
@@ -846,6 +968,7 @@ declare class BurnIndexer {
846
968
  private readonly confirmations;
847
969
  private readonly batchSize;
848
970
  private readonly pollIntervalMs;
971
+ private readonly onTickError?;
849
972
  matchLockId: (event: BurnEvent) => Promise<string | undefined>;
850
973
  private running;
851
974
  private timer;
@@ -853,6 +976,7 @@ declare class BurnIndexer {
853
976
  start(): void;
854
977
  stop(): void;
855
978
  tick(): Promise<void>;
979
+ private handleTickError;
856
980
  private scheduleNext;
857
981
  /**
858
982
  * Scan `[from, to]` inclusive for burn events. Callers can drive this
@@ -975,8 +1099,161 @@ interface ApiUserResponse {
975
1099
  balance: bigint;
976
1100
  isMinter: boolean;
977
1101
  }
1102
+ interface ApiRedemptionPreviewRequest {
1103
+ pointTokenAddress?: Address;
1104
+ }
1105
+ interface ApiRedemptionPreviewResponse {
1106
+ availableAmountPt: bigint;
1107
+ dailyRemainingPt: bigint;
1108
+ cooldownUntilUnixSec: number | null;
1109
+ nextBlackoutEndsAtUnixSec: number | null;
1110
+ perTxMinPt: bigint;
1111
+ perTxMaxPt: bigint;
1112
+ policyVersion: string;
1113
+ policySource: "settlement" | "cache" | "default";
1114
+ }
1115
+ interface ApiRedemptionEvaluateRequest {
1116
+ amountPt: bigint;
1117
+ pointTokenAddress?: Address;
1118
+ }
1119
+ interface ApiRedemptionEvaluateResponse {
1120
+ allowed: boolean;
1121
+ denial?: {
1122
+ code: "AMOUNT_BELOW_MIN" | "AMOUNT_ABOVE_MAX" | "DAILY_LIMIT_EXCEEDED" | "COOLDOWN_ACTIVE" | "BLACKOUT_WINDOW";
1123
+ message: string;
1124
+ };
1125
+ preview: ApiRedemptionPreviewResponse;
1126
+ }
978
1127
  type PoolsProvider = (request: ApiPoolsRequest) => Promise<ApiPoolsResponse>;
979
1128
 
1129
+ /**
1130
+ * Per-user redemption history needed by the evaluator. The issuer can
1131
+ * implement this with their existing burn audit table (preferred) or
1132
+ * use the bundled MemoryRedemptionHistoryStore for tests.
1133
+ */
1134
+ interface IRedemptionHistoryStore {
1135
+ /**
1136
+ * Sum of PT redeemed by `user` from `sinceUnixSec` onwards. Used for
1137
+ * the rolling 24h daily-limit check. MUST count both confirmed burns
1138
+ * and currently-reserved-pending entries — the policy treats reserved
1139
+ * burns as "spent" so a flood of pending requests can't bypass the cap.
1140
+ */
1141
+ sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
1142
+ /**
1143
+ * Unix-second timestamp of the user's last redemption attempt
1144
+ * (confirmed or reserved). null if never. Used for cooldown.
1145
+ */
1146
+ getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
1147
+ /**
1148
+ * Record a new redemption attempt. Called by handleRedemptionInitiate
1149
+ * AFTER policy.evaluate() returns ALLOW and the BurnRequest is signed.
1150
+ * Implementations may persist this row in a redemption_history table or
1151
+ * derive from existing pending-credit reservations.
1152
+ */
1153
+ recordRedemption(entry: {
1154
+ user: Address;
1155
+ amountPt: bigint;
1156
+ pointTokenAddress?: Address;
1157
+ unixSec: number;
1158
+ /** Optional pointer back to the reservation lock id. */
1159
+ reservationId?: string;
1160
+ }): Promise<void>;
1161
+ }
1162
+ interface SettlementClientConfig {
1163
+ /**
1164
+ * chainId — used to derive the issuer-api base URL via
1165
+ * `getPafiServiceUrls(chainId).issuerApi`. SDK ships with the URL
1166
+ * per chainId; bump SDK version to retarget.
1167
+ */
1168
+ chainId: number;
1169
+ /** PAFI-assigned issuer id used in `X-Issuer-Id` header. */
1170
+ issuerId: string;
1171
+ /** Raw API key sent as `Authorization: Bearer <apiKey>`. */
1172
+ apiKey: string;
1173
+ /** Per-request timeout in milliseconds. Default 1000. */
1174
+ fetchTimeoutMs?: number;
1175
+ /** Override fetch (testing). */
1176
+ fetchImpl?: typeof fetch;
1177
+ }
1178
+ interface PolicyProviderConfig extends SettlementClientConfig {
1179
+ /** Cache TTL in milliseconds. Default 5 * 60 * 1000 (5min). */
1180
+ cacheTtlMs?: number;
1181
+ /**
1182
+ * Optional clock for testability. Returns unix milliseconds.
1183
+ * Defaults to () => Date.now().
1184
+ */
1185
+ now?: () => number;
1186
+ }
1187
+
1188
+ interface ResolvedPolicy {
1189
+ policy: RedemptionPolicy;
1190
+ source: RedemptionPolicySource;
1191
+ }
1192
+ /**
1193
+ * Wraps SettlementClient with a 5-minute TTL cache and a hardcoded
1194
+ * default fallback. Single-flight: concurrent getPolicy() calls during
1195
+ * a cache miss share the same in-flight request.
1196
+ *
1197
+ * Resolution order:
1198
+ * 1. fresh cache hit → return cached
1199
+ * 2. cache miss → fetch ok → cache + return (source=settlement)
1200
+ * 3. cache miss → fetch failed → DEFAULT_REDEMPTION_POLICY (source=default)
1201
+ *
1202
+ * Stale cache is NEVER returned — once the TTL expires we either fetch
1203
+ * fresh data or fall through to default. This keeps behavior predictable:
1204
+ * "default" means "we couldn't talk to settlement RIGHT NOW", not "we got
1205
+ * lucky with a stale entry".
1206
+ */
1207
+ declare class PolicyProvider {
1208
+ private readonly client;
1209
+ private readonly issuerId;
1210
+ private readonly cacheTtlMs;
1211
+ private readonly now;
1212
+ private cache;
1213
+ private inflight;
1214
+ constructor(config: PolicyProviderConfig);
1215
+ getPolicy(): Promise<ResolvedPolicy>;
1216
+ /** Drop cached policy. Next getPolicy() will refetch. */
1217
+ invalidate(): void;
1218
+ private readCache;
1219
+ private fetchAndStore;
1220
+ }
1221
+
1222
+ interface RedemptionServiceConfig {
1223
+ policyProvider: PolicyProvider | PolicyProviderConfig;
1224
+ historyStore: IRedemptionHistoryStore;
1225
+ /** Defaults to () => Math.floor(Date.now() / 1000). */
1226
+ nowUnixSec?: () => number;
1227
+ }
1228
+ /**
1229
+ * High-level facade used by HTTP handlers. Combines PolicyProvider +
1230
+ * IRedemptionHistoryStore into preview/evaluate operations.
1231
+ *
1232
+ * preview(user) → RedemptionPreview (no amount required)
1233
+ * evaluate(user, amount) → RedemptionDecision
1234
+ * recordSuccessfulInitiate(..) → call AFTER signing the BurnRequest
1235
+ *
1236
+ * Note: this service does NOT mutate anything during evaluate(). The
1237
+ * caller must call recordSuccessfulInitiate() after the BurnRequest is
1238
+ * signed and the pending credit is reserved on the ledger. Splitting
1239
+ * "decide" from "record" lets the handler atomically reserve + record
1240
+ * under one DB transaction if it wants.
1241
+ */
1242
+ declare class RedemptionService {
1243
+ private readonly policyProvider;
1244
+ private readonly historyStore;
1245
+ private readonly nowUnixSec;
1246
+ constructor(config: RedemptionServiceConfig);
1247
+ preview(user: Address, pointTokenAddress?: Address): Promise<RedemptionPreview>;
1248
+ evaluate(user: Address, amountPt: bigint, pointTokenAddress?: Address): Promise<RedemptionDecision>;
1249
+ recordSuccessfulInitiate(entry: {
1250
+ user: Address;
1251
+ amountPt: bigint;
1252
+ pointTokenAddress?: Address;
1253
+ reservationId?: string;
1254
+ }): Promise<void>;
1255
+ }
1256
+
980
1257
  interface IssuerApiHandlersConfig {
981
1258
  authService: AuthService;
982
1259
  ledger: IPointLedger;
@@ -1004,6 +1281,19 @@ interface IssuerApiHandlersConfig {
1004
1281
  feeManager?: FeeManager;
1005
1282
  /** Required by `handlePools`; omit to disable the endpoint. */
1006
1283
  poolsProvider?: PoolsProvider;
1284
+ /**
1285
+ * Required by `handleRedemptionPreview` / `handleRedemptionEvaluate`;
1286
+ * omit to disable both. Wired by createIssuerService when the top-level
1287
+ * `redemption` config is provided.
1288
+ */
1289
+ redemption?: RedemptionService;
1290
+ /**
1291
+ * Rate limiter for `/auth/nonce` and `/auth/login` (both unauthenticated +
1292
+ * CPU-bound). When omitted, defaults to a `NoopRateLimiter` that lets
1293
+ * every request through (with a one-time prod warning) — issuers MUST
1294
+ * wire a real impl in production.
1295
+ */
1296
+ rateLimiter?: IRateLimiter;
1007
1297
  }
1008
1298
  /**
1009
1299
  * Framework-agnostic HTTP handlers that match the endpoints a `PafiSDK`
@@ -1030,11 +1320,27 @@ declare class IssuerApiHandlers {
1030
1320
  private readonly pafiWebUrl?;
1031
1321
  private readonly feeManager?;
1032
1322
  private readonly poolsProvider?;
1323
+ private readonly redemption?;
1324
+ private readonly rateLimiter;
1033
1325
  constructor(config: IssuerApiHandlersConfig);
1034
- /** `GET /auth/nonce` */
1035
- handleGetNonce(): Promise<ApiNonceResponse>;
1036
- /** `POST /auth/login` */
1037
- handleLogin(body: ApiLoginRequest): Promise<ApiLoginResponse>;
1326
+ /**
1327
+ * `GET /auth/nonce`
1328
+ *
1329
+ * @param rateLimitKey Caller-side rate-limit key (typically client IP).
1330
+ * The HTTP layer (controller/middleware) extracts
1331
+ * this from the request and passes it through.
1332
+ * When omitted, no rate limit applies — production
1333
+ * callers SHOULD always pass a key.
1334
+ */
1335
+ handleGetNonce(rateLimitKey?: string): Promise<ApiNonceResponse>;
1336
+ /**
1337
+ * `POST /auth/login`
1338
+ *
1339
+ * @param body Login message + signature.
1340
+ * @param rateLimitKey Caller-side rate-limit key (typically client IP
1341
+ * or `body.userAddress` if known). See `handleGetNonce`.
1342
+ */
1343
+ handleLogin(body: ApiLoginRequest, rateLimitKey?: string): Promise<ApiLoginResponse>;
1038
1344
  /**
1039
1345
  * `GET /config?chainId=<id>`
1040
1346
  *
@@ -1061,6 +1367,26 @@ declare class IssuerApiHandlers {
1061
1367
  * balance.
1062
1368
  */
1063
1369
  handleUser(userAddress: Address, request: ApiUserRequest): Promise<ApiUserResponse>;
1370
+ /**
1371
+ * `GET /redemption/preview?pointToken=<addr>`
1372
+ *
1373
+ * Returns the headroom currently available to `userAddress` under the
1374
+ * configured RedemptionPolicy. Pure read — does not record anything.
1375
+ * Use this for UI to render "X PT redeemable now / next available at …".
1376
+ */
1377
+ handleRedemptionPreview(userAddress: Address, request: ApiRedemptionPreviewRequest): Promise<ApiRedemptionPreviewResponse>;
1378
+ /**
1379
+ * `POST /redemption/evaluate`
1380
+ *
1381
+ * Pre-flight check before the issuer signs a BurnRequest. Returns
1382
+ * { allowed, denial?, preview }. Caller (the burn-orchestrator) MUST
1383
+ * re-check on the actual initiate path — evaluate is read-only and a
1384
+ * caller could race two requests under the same headroom. The intended
1385
+ * write path is: evaluate → sign BurnRequest → reserve pending credit
1386
+ * → call `service.redemption.recordSuccessfulInitiate()`.
1387
+ */
1388
+ handleRedemptionEvaluate(userAddress: Address, request: ApiRedemptionEvaluateRequest): Promise<ApiRedemptionEvaluateResponse>;
1389
+ private requireSupportedToken;
1064
1390
  }
1065
1391
 
1066
1392
  /**
@@ -1134,6 +1460,16 @@ interface PTRedeemHandlerConfig {
1134
1460
  signatureDeadlineSeconds?: number;
1135
1461
  /** Clock injection for tests; defaults to `Date.now`. */
1136
1462
  now?: () => number;
1463
+ /**
1464
+ * Optional — when wired, the handler enforces the per-issuer
1465
+ * RedemptionPolicy (daily limit / cooldown / blackout / per-tx range)
1466
+ * BEFORE signing the BurnRequest. On denial it throws
1467
+ * PTRedeemError("REDEMPTION_POLICY_DENIED", ...) with the policy
1468
+ * denial code in `policyDenialCode`. After a successful sign+reserve,
1469
+ * the handler records the redemption against the user's history so
1470
+ * the next preview reflects the spend.
1471
+ */
1472
+ redemptionService?: RedemptionService;
1137
1473
  }
1138
1474
  interface PTRedeemRequest {
1139
1475
  /** Address extracted from the verified JWT — must match `userAddress`. */
@@ -1193,11 +1529,14 @@ interface PTRedeemResponse {
1193
1529
  /** The BurnRequest deadline (unix seconds) — FE uses this to surface a countdown. */
1194
1530
  signatureDeadline: bigint;
1195
1531
  }
1196
- type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED";
1532
+ type PTRedeemErrorCode = "UNAUTHORIZED" | "INVALID_AMOUNT" | "NONCE_READ_FAILED" | "NONCE_IN_FLIGHT" | "LEDGER_NOT_SUPPORTED" | "SIGNING_FAILED" | "REDEMPTION_POLICY_DENIED";
1197
1533
  declare class PTRedeemError extends PafiSdkError {
1198
1534
  readonly httpStatus: "unprocessable";
1199
1535
  readonly code: PTRedeemErrorCode;
1200
- constructor(code: PTRedeemErrorCode, message: string);
1536
+ readonly policyDenialCode?: RedemptionDenialCode;
1537
+ constructor(code: PTRedeemErrorCode, message: string, options?: {
1538
+ policyDenialCode?: RedemptionDenialCode;
1539
+ });
1201
1540
  }
1202
1541
  declare class PTRedeemHandler {
1203
1542
  private readonly ledger;
@@ -1212,6 +1551,7 @@ declare class PTRedeemHandler {
1212
1551
  private readonly redeemLockDurationMs;
1213
1552
  private readonly signatureDeadlineSeconds;
1214
1553
  private readonly now;
1554
+ private readonly redemptionService?;
1215
1555
  /**
1216
1556
  * Per-user in-flight nonce guard (single-process only).
1217
1557
  *
@@ -1238,7 +1578,12 @@ interface RetryConfig {
1238
1578
  maxRetryAfterMs?: number;
1239
1579
  }
1240
1580
  interface PafiBackendConfig {
1241
- url: string;
1581
+ /**
1582
+ * chainId — used to derive the sponsor-relayer base URL via
1583
+ * `getPafiServiceUrls(chainId).sponsorRelayer`. SDK ships with the
1584
+ * URL per chainId; bump SDK version to retarget.
1585
+ */
1586
+ chainId: number;
1242
1587
  issuerId: string;
1243
1588
  apiKey: string;
1244
1589
  fetchImpl?: typeof fetch;
@@ -1343,6 +1688,7 @@ interface SponsorshipResponse {
1343
1688
  }
1344
1689
  declare class PafiBackendClient {
1345
1690
  private readonly config;
1691
+ private readonly baseUrl;
1346
1692
  constructor(config: PafiBackendConfig);
1347
1693
  requestSponsorship(request: SponsorshipRequest): Promise<SponsorshipResponse>;
1348
1694
  /**
@@ -2145,6 +2491,28 @@ interface IssuerServiceConfig {
2145
2491
  */
2146
2492
  autoStart?: boolean;
2147
2493
  };
2494
+ /**
2495
+ * Redemption restriction config. When provided, the SDK fetches the
2496
+ * per-issuer policy from PAFI issuer-api (with 5min cache + default
2497
+ * fallback) and exposes preview/evaluate via `service.redemption`.
2498
+ * The handler endpoints `handleRedemptionPreview` / `handleRedemptionEvaluate`
2499
+ * are wired only when this is configured.
2500
+ *
2501
+ * `chainId` is taken from the top-level config; the issuer-api URL
2502
+ * is looked up via `getPafiServiceUrls(chainId)`. Only credentials
2503
+ * + the history store are required here.
2504
+ */
2505
+ redemption?: {
2506
+ issuerId: string;
2507
+ apiKey: string;
2508
+ historyStore: IRedemptionHistoryStore;
2509
+ /** Override fetch (testing). */
2510
+ fetchImpl?: typeof fetch;
2511
+ /** Per-fetch timeout in ms. Default 1000. */
2512
+ fetchTimeoutMs?: number;
2513
+ /** Cache TTL in ms. Default 5 * 60 * 1000. */
2514
+ cacheTtlMs?: number;
2515
+ };
2148
2516
  }
2149
2517
  interface IssuerService {
2150
2518
  /** AuthService — login, logout, nonce management. */
@@ -2166,6 +2534,12 @@ interface IssuerService {
2166
2534
  indexer: PointIndexer;
2167
2535
  /** Framework-agnostic HTTP handlers — wire into Express / Fastify / Hono. */
2168
2536
  api: IssuerApiHandlers;
2537
+ /**
2538
+ * Redemption restriction service. Undefined when `redemption` is not
2539
+ * configured — the corresponding handlers throw "not configured" at
2540
+ * request time.
2541
+ */
2542
+ redemption: RedemptionService | undefined;
2169
2543
  }
2170
2544
  /**
2171
2545
  * Wire a fully-functional issuer service from a single config object.
@@ -2866,6 +3240,79 @@ declare class IssuerStateValidator {
2866
3240
  private fetchIssuerState;
2867
3241
  }
2868
3242
 
3243
+ /**
3244
+ * SDK-side fallback used when settlement-api is unreachable, returns
3245
+ * 404, or returns 5xx. Keep it permissive enough that an outage doesn't
3246
+ * lock all users out, but tight enough that it's not an abuse vector.
3247
+ */
3248
+ declare const DEFAULT_REDEMPTION_POLICY: RedemptionPolicy;
3249
+ declare function defaultPolicyFor(issuerId: string): RedemptionPolicy;
3250
+
3251
+ /**
3252
+ * Either a successful policy fetch or a structured failure. We never
3253
+ * throw from `fetchPolicy()` — callers fall back to cache/default on
3254
+ * any failure mode, so a thrown error would just force every caller
3255
+ * to wrap in try/catch.
3256
+ */
3257
+ type FetchResult = {
3258
+ ok: true;
3259
+ policy: RedemptionPolicy;
3260
+ } | {
3261
+ ok: false;
3262
+ reason: FetchFailureReason;
3263
+ status?: number;
3264
+ };
3265
+ type FetchFailureReason = "TIMEOUT" | "NETWORK" | "NOT_FOUND" | "UNAUTHORIZED" | "SERVER_ERROR" | "INVALID_RESPONSE";
3266
+ declare class SettlementClient {
3267
+ private readonly config;
3268
+ constructor(config: SettlementClientConfig);
3269
+ fetchPolicy(): Promise<FetchResult>;
3270
+ }
3271
+
3272
+ interface UserHistory {
3273
+ /** Total PT redeemed by user in the rolling 24h window. */
3274
+ redeemedLast24hPt: bigint;
3275
+ /** Last redemption timestamp (unix seconds), or null if never. */
3276
+ lastRedeemedAtUnixSec: number | null;
3277
+ }
3278
+ interface EvaluateInput {
3279
+ policy: RedemptionPolicy;
3280
+ policySource: RedemptionPolicySource;
3281
+ history: UserHistory;
3282
+ /** Amount being requested. Pass 0n for a pure preview. */
3283
+ amountPt: bigint;
3284
+ /** Current unix time in seconds (caller-controlled for testability). */
3285
+ nowUnixSec: number;
3286
+ }
3287
+ /**
3288
+ * Pure evaluator. Given a policy + user history snapshot + requested
3289
+ * amount, returns either ALLOW + a preview of the user's remaining
3290
+ * headroom, or DENY + the first failing rule.
3291
+ *
3292
+ * Preview is always populated, even on denial — UI uses it to render
3293
+ * "X PT redeemable now" / "next available at HH:MM" regardless.
3294
+ */
3295
+ declare function evaluateRedemption(input: EvaluateInput): RedemptionDecision;
3296
+ declare const REDEMPTION_HISTORY_WINDOW_SEC: number;
3297
+
3298
+ /**
3299
+ * In-memory IRedemptionHistoryStore for tests + the bundled NestJS
3300
+ * example. Production issuers should implement this against their
3301
+ * existing burn/audit table — sumRedeemedSince is hot path on every
3302
+ * redemption preview.
3303
+ */
3304
+ declare class MemoryRedemptionHistoryStore implements IRedemptionHistoryStore {
3305
+ private readonly entries;
3306
+ sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
3307
+ getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
3308
+ recordRedemption(entry: {
3309
+ user: Address;
3310
+ amountPt: bigint;
3311
+ pointTokenAddress?: Address;
3312
+ unixSec: number;
3313
+ }): Promise<void>;
3314
+ }
3315
+
2869
3316
  declare const PAFI_ISSUER_SDK_VERSION: string;
2870
3317
 
2871
- export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BalanceAggregator, type BalanceAggregatorConfig, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type CombinedBalance, type ConfigDto, ConfigurationError, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, FeeManager, type FeeManagerConfig, type GasFeeDto, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type ISessionStore, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, type PoolsDto, type PoolsProvider, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, type RedeemDto, type RedeemPrepareDto, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, authenticateRequest, createIssuerService, createNativePtQuoter, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };
3318
+ export { AdapterMisconfiguredError, type ApiConfigResponse, type ApiGasFeeResponse, type ApiLoginRequest, type ApiLoginResponse, type ApiNonceResponse, type ApiPoolsRequest, type ApiPoolsResponse, type ApiRedemptionEvaluateRequest, type ApiRedemptionEvaluateResponse, type ApiRedemptionPreviewRequest, type ApiRedemptionPreviewResponse, type ApiUserRequest, type ApiUserResponse, type AuthContext, AuthError, type AuthErrorCode, AuthService, type AuthServiceConfig, BalanceAggregator, type BalanceAggregatorConfig, BundlerNotConfiguredError, BundlerRejectedError, type BurnEvent, BurnIndexer, type BurnIndexerConfig, type BurnStatusParams, type BurnStatusResponse, type ClaimDto, type CombinedBalance, type ConfigDto, ConfigurationError, DEFAULT_REDEMPTION_POLICY, type DecodedCallDto, DefaultPolicyEngine, type DefaultPolicyEngineOptions, type DelegatePrepareDto, type DelegateStatusDto, type EvaluateInput, FeeManager, type FeeManagerConfig, type FetchFailureReason, type FetchResult, type GasFeeDto, type HandleDelegateSubmitParams, type HandleDelegateSubmitResult, type HandleMobilePrepareParams, type HandleMobilePrepareResult, type HandleMobileSubmitParams, type IIndexerCursorStore, type IPendingUserOpStore, type IPointLedger, type IPolicyEngine, type IRateLimiter, type IRedemptionHistoryStore, type ISessionStore, InMemoryCursorStore, IssuerApiAdapter, type IssuerApiAdapterConfig, IssuerApiHandlers, type IssuerApiHandlersConfig, type IssuerRegistryRecord, type IssuerService, type IssuerServiceConfig, IssuerStateError, IssuerStateValidator, LockNotFoundError, type LockedMintRequest, type LoginResult, MemoryPendingUserOpStore, MemoryRateLimiter, MemoryRedemptionHistoryStore, MemorySessionStore, type MemorySessionStoreOptions, type MintEvent, type MintStatusParams, type MintStatusResponse, type MintingStatus, type MobilePrepareDto, type MobileSubmitDto, type NativePtQuoterConfig, NonceManager, NoopRateLimiter, PAFI_ISSUER_SDK_VERSION, PTClaimError, PTClaimHandler, type PTClaimHandlerConfig, type PTClaimRequest, type PTClaimResponse, PTRedeemError, PTRedeemHandler, type PTRedeemHandlerConfig, type PTRedeemRequest, type PTRedeemResponse, PafiBackendClient, type PafiBackendConfig, PafiBackendError, type PafiBackendErrorCode, type PendingCredit, type PendingUserOpEntry, PendingUserOpForbiddenError, PendingUserOpNotFoundError, type PerpDepositDto, PerpDepositError, PerpDepositHandler, type PerpDepositHandlerConfig, type PerpDepositRequest, type PerpDepositResponse, PointIndexer, type PointIndexerConfig, type PolicyDecision, type PolicyEvalRequest, PolicyProvider, type PolicyProviderConfig, type PoolsDto, type PoolsProvider, type PreValidateMintResult, type PrepareBurnParams, type PrepareMintParams, type PrepareMobileUserOpParams, type PrepareMobileUserOpResult, type PreparedUserOp, REDEMPTION_HISTORY_WINDOW_SEC, type RateLimitAction, type RateLimiterConfig, type RedeemDto, type RedeemPrepareDto, RedemptionService, type RedemptionServiceConfig, RelayError, type RelayErrorCode, RelayService, type RelayUserOpParams, type RelayUserOpRequest, type RelayUserOpResponse, type RequestPaymasterParams, type ResolvedPolicy, type RetryConfig, type SdkErrorBody, type SdkErrorMapperFactories, type SdkErrorStatus, type SerializedUserOpTypedData, type Session, SettlementClient, type SettlementClientConfig, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipTarget, type SponsorshipUserOp, type SubgraphNativeUsdtQuoterConfig, type SubgraphPoolsProviderConfig, type UserDto, type UserHistory, authenticateRequest, createIssuerService, createNativePtQuoter, createSdkErrorMapper, createSubgraphNativeUsdtQuoter, createSubgraphPoolsProvider, defaultPolicyFor, evaluateRedemption, handleClaimStatus, handleDelegateSubmit, handleMobilePrepare, handleMobileSubmit, handleRedeemStatus, mergePaymasterFields, prepareMobileUserOp, relayUserOp, requestPaymaster, serializeEntryToJsonRpc, serializeUserOpTypedData };