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