@pafi-dev/issuer 0.3.0-beta.9 → 0.4.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/README.md CHANGED
@@ -1,352 +1,224 @@
1
1
  # @pafi-dev/issuer
2
2
 
3
- Issuer backend boilerplate for the PAFI point token system. Wraps
4
- `@pafi-dev/core` with everything a partner needs to run the off-chain side
5
- of the mint-and-cash-out flow: authentication, point ledger, policy
6
- engine, EIP-712 issuer signing, relay submission, mint event indexing,
7
- and framework-agnostic HTTP handlers.
8
-
9
- ## What's new in 0.3.0-alpha.0 — sponsored UserOp flow (v1.4 preview)
10
-
11
- Preview release that wires the SDK into the new **sponsored path**:
12
- user EOAs delegate to a BatchExecutor via EIP-7702, UserOps are
13
- signed by the user and submitted to a Bundler, and gas is paid by
14
- Coinbase Paymaster — proxied through the new PAFI-hosted
15
- `paymaster-proxy` service.
16
-
17
- **What ships in this alpha:**
18
-
19
- - `PafiBackendClient` — HTTP client for `paymaster-proxy` with retry +
20
- backoff on `safeToRetry` errors (opt-in via `retry.maxAttempts`)
21
- - `BalanceAggregator` — merge off-chain + on-chain balance into one number
22
- - `BurnIndexer` — watches `Transfer(user → 0x0)` events, finalizes
23
- pending off-chain credits for the reverse flow
24
- - `IPointLedger.reservePendingCredit()` / `resolveCreditByBurnTx()` —
25
- additive ledger methods for burn→credit flow
26
- - `ApiConfigResponse.pafiWebUrl` + `contracts.pointTokens[]` — new
27
- fields surfaced by `handleConfig()`
28
- - `FeeManager` slimmed: no more `operatorWallet` /
29
- `usdtAddress` / `nativeWrappedAddress`; rebalancing gone (operator
30
- doesn't hold ETH anymore); `quoteNativeToFee` replaces
31
- `quoteNativeToUsdt` so fee currency is caller-chosen (PT for
32
- mint/burn, USDT for swap/perp_deposit)
33
-
34
- **What's still blocked (comes in 0.3.0-beta / stable):**
35
-
36
- - `processMint()` / `processBurn()` — blocked on SC Relayer v2 ABI
37
- (blocker B1). Current `processMintAndCashOut()` is `@deprecated`
38
- but still works
39
- - `PTRedeemHandler` + `TopUpRedemptionHandler` — blocked on B1 +
40
- `paymaster-proxy` staging env being live
41
- - Deletion of legacy `claimAndSwap` flow — kept for v0.2.x consumers
42
- until 2.0
43
-
44
- **Install the alpha:**
3
+ Issuer backend SDK for the PAFI point token system. Wraps `@pafi-dev/core`
4
+ with everything a partner needs to run the off-chain side of the cashout
5
+ flow: SIWE authentication, point ledger, policy engine, EIP-712 issuer
6
+ signing, relay submission, and mint/burn event indexing.
7
+
8
+ **Version:** `0.3.0-beta.10`
9
+
10
+ > This package runs on the **issuer backend server only** (Node.js). Do not
11
+ > import into browser bundles it pulls in `jose`, `node:crypto`, and
12
+ > server-side dependencies.
13
+
14
+ ---
15
+
16
+ ## Installation
45
17
 
46
18
  ```bash
47
- pnpm add @pafi-dev/issuer@0.3.0-alpha.0 @pafi-dev/core@0.3.0-alpha.0
19
+ pnpm add @pafi-dev/issuer @pafi-dev/core viem
48
20
  ```
49
21
 
50
- Pin the exact version, or use the `@alpha` dist-tag.
22
+ ---
51
23
 
52
- ## What's new in 0.2.0 — multi-token support
24
+ ## What this package provides
53
25
 
54
- An issuer backend can now support more than one PointToken in a single
55
- service instance. Pass `pointTokenAddresses: Address[]` in
56
- `IssuerServiceConfig`; the factory creates one `PointIndexer` per token
57
- and handlers validate the requested `pointTokenAddress` against the
58
- configured set.
26
+ | Module | What it does |
27
+ |--------|-------------|
28
+ | `relay/` | `RelayService.prepareMint()` / `prepareBurn()` build unsigned UserOps |
29
+ | `indexer/` | `PointIndexer` (Mint events) + `BurnIndexer` (Transfer→0x0 events) |
30
+ | `auth/` | `AuthService` (SIWE verify + JWT issue) + `authenticateRequest` middleware |
31
+ | `ledger/` | `IPointLedger` interface — implement against your own database |
32
+ | `policy/` | `DefaultPolicyEngine` + `IPolicyEngine` interface |
33
+ | `pools/` | `PoolsProvider` — fetch V4 pool list from PAFI subgraph |
34
+ | `balance/` | `BalanceAggregator` — merge off-chain ledger + on-chain `balanceOf` |
35
+ | `api/` | `IssuerApiHandlers`, `PTRedeemHandler`, `TopUpRedemptionHandler` |
59
36
 
60
- `IPointLedger` methods take an optional `tokenAddress?: Address` param.
61
- Single-token issuers can ignore it — legacy 0.1.x code keeps working.
62
- Multi-token issuers must persist balances keyed by `(user, token)`.
37
+ ---
63
38
 
64
- Legacy `pointTokenAddress: Address` still works as a shorthand for a
65
- one-element list. No code change is required if you only have one token.
39
+ ## What you must bring
66
40
 
67
- ## Install
41
+ The SDK deliberately does not ship production implementations for these
42
+ three concerns — every issuer's infrastructure is different:
68
43
 
69
- ```bash
70
- pnpm add @pafi-dev/issuer @pafi-dev/core viem
71
- ```
44
+ | What | Why you own it |
45
+ |------|---------------|
46
+ | **`IPointLedger`** | Your database, your schema, your locking strategy |
47
+ | **`ISessionStore`** | Must be shared across pods — Redis, Postgres, etc. |
48
+ | **Signing wallet** | Private key must never touch server memory — use KMS |
72
49
 
73
- Peer deps: `viem ^2.0`, `@pafi-dev/core` (workspace), `jose ^5` (transitive).
50
+ See [`examples/adapters/`](./examples/adapters/) for reference implementations
51
+ of all three (Postgres ledger, Redis session store, AWS KMS wallet).
74
52
 
75
- ## At a glance
53
+ ---
54
+
55
+ ## Quick start
76
56
 
77
57
  ```ts
78
- import { createIssuerService, PrivateKeySigner } from "@pafi-dev/issuer";
58
+ import { createIssuerService } from "@pafi-dev/issuer";
79
59
  import { createPublicClient, createWalletClient, http } from "viem";
80
- import { privateKeyToAccount } from "viem/accounts";
81
60
  import { base } from "viem/chains";
82
61
 
83
- const provider = createPublicClient({ chain: base, transport: http(RPC_URL) });
84
- const operatorWallet = createWalletClient({
85
- account: privateKeyToAccount(OPERATOR_PK),
62
+ // --- Production: replace with KMS-backed wallet (see examples/adapters/kms-signer.ts)
63
+ // --- Dev only: raw private key
64
+ import { privateKeyToAccount } from "viem/accounts";
65
+ const issuerSignerWallet = createWalletClient({
66
+ account: privateKeyToAccount(process.env.MINTER_PRIVATE_KEY as `0x${string}`),
86
67
  chain: base,
87
- transport: http(RPC_URL),
68
+ transport: http(process.env.RPC_URL),
88
69
  });
89
70
 
90
71
  const service = createIssuerService({
91
72
  chainId: 8453,
92
- pointTokenAddress: "0x5F...",
93
- relayAddress: "0x92...",
94
- contracts: { pointToken, relay, issuerRegistry, usdt },
95
- provider,
96
- operatorWallet,
73
+ pointTokenAddresses: ["0x<PointToken>"],
74
+ contracts: {
75
+ relay: "0x<Relay>",
76
+ issuerRegistry: "0x<IssuerRegistry>",
77
+ usdt: "0x<USDT>",
78
+ batchExecutor: "0x<BatchExecutor>",
79
+ },
80
+ provider: createPublicClient({ chain: base, transport: http(process.env.RPC_URL) }),
97
81
  auth: {
98
82
  jwtSecret: process.env.JWT_SECRET!,
99
83
  domain: "app.example.com",
100
84
  },
101
- // ⚠️ NOT for production — replace with a KMS-backed IIssuerSigner
102
- signer: new PrivateKeySigner({ privateKey: ISSUER_PK, chain: base }),
85
+ ledger: new YourPostgresLedger(), // implements IPointLedger
86
+ sessionStore: new YourRedisSessionStore(), // implements ISessionStore
87
+ claim: {
88
+ issuerSignerWallet,
89
+ batchExecutorAddress: "0x<BatchExecutor>",
90
+ },
103
91
  });
104
92
 
105
- // Wire `service.handlers` into Express / Fastify / Hono — see
106
- // examples/express-issuer/ for a full runnable server.
93
+ // Mount handlers into your framework (Express, Fastify, NestJS, Hono...)
94
+ app.get("/auth/nonce", () => service.handlers.handleGetNonce());
95
+ app.post("/auth/login", (req) => service.handlers.handleLogin(req.body));
96
+ app.get("/user", (req) => service.handlers.handleUser(req.user, req.query));
97
+ app.post("/claim", (req) => service.handlers.handleClaim(req.user, req.body));
107
98
  ```
108
99
 
109
- ## What you get
110
-
111
- | Component | Default | Replace for production? |
112
- | ----------------------- | --------------------- | ------------------------------------- |
113
- | `IPointLedger` | `MemoryPointLedger` | ✅ Postgres / Prisma |
114
- | `ISessionStore` | `MemorySessionStore` | ✅ Redis |
115
- | `IPolicyEngine` | `DefaultPolicyEngine` | ➕ extend for KYC / caps / claim budget |
116
- | `IIssuerSigner` | `PrivateKeySigner` | 🚨 **MUST replace** — KMS / HSM / MPC |
117
- | `AuthService` | ✅ built-in | — |
118
- | `RelayService` | ✅ built-in | — |
119
- | `FeeManager` | ✅ built-in (opt-in) | injection-based — no DEX lock-in |
120
- | `MintingGateway` | ✅ built-in | — |
121
- | `PointIndexer` | ✅ built-in (polling) | — |
122
- | `IssuerApiHandlers` | ✅ built-in | — |
123
-
124
- See [`examples/adapters/`](./examples/adapters) for reference
125
- implementations of the three interfaces you must replace.
100
+ ---
126
101
 
127
- ## The minting flow
102
+ ## Implementing IPointLedger
128
103
 
129
- The SDK's central orchestrator is `MintingGateway.processMintAndCashOut()`,
130
- which runs the full 11-step flow documented in
131
- [`docs/PAFI_ISSUER_SDK_SPEC.md`](../../../pafi-backend/docs/PAFI_ISSUER_SDK_SPEC.md):
132
-
133
- ```
134
- 1. Validate request fields (cheap rejects)
135
- 2. Verify ReceiverConsent signature via @pafi-dev/core
136
- 3. Check off-chain balance via ledger
137
- 4. Check locked requests via ledger
138
- 5. Run policy engine (balance + cap + issuer rules)
139
- 6. Lock the requested amount in the ledger
140
- 7. Sign MintRequest as the issuer (via IIssuerSigner)
141
- 8. Build Relay calldata (MintParams + SwapParams)
142
- 9. Submit via RelayService (simulate write receipt)
143
- 10. Return { txHash, lockId, block/gas }
144
- 11. PointIndexer finalizes the ledger on the Mint event (out of band)
104
+ ```ts
105
+ import type { IPointLedger } from "@pafi-dev/issuer";
106
+ import type { Address, Hex } from "viem";
107
+
108
+ export class PostgresPointLedger implements IPointLedger {
109
+ async getBalance(user: Address, tokenAddress?: Address): Promise<bigint> { ... }
110
+ async lockForMinting(user: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string> { ... }
111
+ async releaseLock(lockId: string): Promise<void> { ... }
112
+ async deductBalance(user: Address, amount: bigint, txHash: Hex, tokenAddress?: Address): Promise<void> { ... }
113
+ async creditBalance(user: Address, amount: bigint, reason: string, tokenAddress?: Address): Promise<void> { ... }
114
+ async getLockedRequests(user: Address, tokenAddress?: Address): Promise<LockedMintRequest[]> { ... }
115
+ async updateMintStatus(lockId: string, status: MintingStatus, txHash?: Hex): Promise<void> { ... }
116
+ // v1.4 reverse flow (burn off-chain credit):
117
+ async reservePendingCredit(user: Address, amount: bigint, durationMs: number, tokenAddress?: Address): Promise<string> { ... }
118
+ async resolveCreditByBurnTx(lockId: string, txHash: Hex): Promise<void> { ... }
119
+ }
145
120
  ```
146
121
 
147
- **The gateway never deducts the balance directly.** Deduction is
148
- driven by the `PointIndexer` when it observes the on-chain Mint event.
149
- That makes the system crash-safe: if the gateway dies between
150
- broadcast and receipt, the indexer still finalizes the ledger the next
151
- time it polls.
122
+ Critical: `lockForMinting` must use a **row-level lock** (e.g. `SELECT ... FOR UPDATE`)
123
+ so concurrent requests from multiple pods cannot read the same available balance and
124
+ double-spend.
152
125
 
153
- ### Lock release semantics
126
+ See `examples/adapters/postgres-ledger.ts` for a full Prisma implementation.
154
127
 
155
- Every `MintingGatewayError` carries a `safeToRetry: boolean` flag that
156
- tells the API layer whether the underlying ledger lock was released:
128
+ ---
157
129
 
158
- | Error code | Lock released? | `safeToRetry` |
159
- | --------------------------- | -------------- | ------------- |
160
- | `INVALID_REQUEST` | never locked | `true` |
161
- | `INVALID_CONSENT_SIGNATURE` | never locked | `true` |
162
- | `CONSENT_EXPIRED` | never locked | `true` |
163
- | `POLICY_REJECTED` | never locked | `true` |
164
- | `INSUFFICIENT_BALANCE` | never locked | `true` |
165
- | `SIGNER_FAILED` | ✅ released | `true` |
166
- | `RELAY_SIMULATION_FAILED` | ✅ released | `true` |
167
- | `RELAY_SUBMIT_FAILED` | ✅ released | `true` |
168
- | `RELAY_REVERTED` | 🔒 kept | `false` |
169
- | `RELAY_TIMEOUT` | 🔒 kept | `false` |
130
+ ## Production signing KMS
170
131
 
171
- `RELAY_REVERTED` and `RELAY_TIMEOUT` keep the lock because the tx may
172
- still be in the mempool or already mined releasing would enable a
173
- double-spend on retry. Your API should surface `409 Conflict` in these
174
- cases and require manual reconciliation.
132
+ The `claim.issuerSignerWallet` must be backed by a hardware key in production.
133
+ The SDK accepts any viem `WalletClient` wire it to AWS KMS by giving viem
134
+ a custom account:
175
135
 
176
- ## Authentication
136
+ ```ts
137
+ import { createKmsWalletClient } from "./adapters/kms-signer";
138
+ import { KMSClient } from "@aws-sdk/client-kms";
177
139
 
178
- Wallet-based login uses EIP-4361 (Sign-In with Ethereum):
140
+ const kms = new KMSClient({ region: "us-east-1" });
179
141
 
142
+ const issuerSignerWallet = createKmsWalletClient({
143
+ kms,
144
+ keyId: process.env.KMS_MINTER_KEY_ID!,
145
+ address: process.env.KMS_MINTER_ADDRESS! as `0x${string}`,
146
+ chain: base,
147
+ });
180
148
  ```
181
- Frontend Issuer backend
182
- ──────── ──────────────
183
- 1. GET /auth/nonce → authService.getNonce()
184
- ← { nonce }
185
- 2. Build EIP-4361 message with
186
- nonce (via PafiSDK or
187
- @pafi-dev/core createLoginMessage)
188
- 3. Sign with wallet
189
- 4. POST /auth/login → authService.login(message, sig)
190
- { message, signature } → parse → verify → consume nonce
191
- → create session → issue JWT
192
- ← { token, userAddress, expiresAt }
193
- 5. Authorization: Bearer <jwt> → authenticateRequest()
194
- → verify JWT + session
195
- → pass { userAddress, chainId } to handlers
196
- ```
197
-
198
- Key property: a failed signature does **not** burn the nonce. Users can
199
- retry with the correct wallet on the same nonce without hitting the
200
- backend again.
201
-
202
- ## HTTP endpoints
203
149
 
204
- The handlers are framework-agnostic `async` functions that take plain
205
- inputs and return plain outputs. Wrap them in Express / Fastify / Hono /
206
- whatever you use. See `examples/express-issuer/server.ts` for the full
207
- wiring.
150
+ See `examples/adapters/kms-signer.ts` for the full implementation including
151
+ DER→(r,s) conversion and v-byte recovery.
208
152
 
209
- | Method | Path | Auth | Handler |
210
- | ------ | ---------------- | ---- | -------------------- |
211
- | GET | /auth/nonce | no | `handleGetNonce` |
212
- | POST | /auth/login | no | `handleLogin` |
213
- | GET | /config | no | `handleConfig` |
214
- | GET | /gas-fee | no | `handleGasFee` |
215
- | POST | /auth/logout | yes | `handleLogout` |
216
- | GET | /pools | yes | `handlePools` |
217
- | GET | /user | yes | `handleUser` |
218
- | POST | /claim-and-swap | yes | `handleClaimAndSwap` |
153
+ ---
219
154
 
220
- ## Security notes
155
+ ## Indexers
221
156
 
222
- - 🚨 **Never deploy `PrivateKeySigner` to production.** The private key lives in process memory and is trivially extractable from a compromised host. Use `examples/adapters/kms-signer.ts` as a starting point for a KMS/HSM integration.
223
- - 🔑 **`jwtSecret` must be at least 16 characters (32+ recommended) and rotated periodically.** Sessions survive a rotation because the JWT lookup cross-checks the session store — but existing tokens signed with the old secret will fail `verifyToken`, forcing users to re-login.
224
- - 🕐 **The default `MemorySessionStore` does not survive restarts.** Nonces and sessions are both lost. Use `examples/adapters/redis-session-store.ts` for anything past local development.
225
- - 🔒 **The `MintingGatewayError.safeToRetry` flag is load-bearing.** Do not ignore it in your API layer surfacing `RELAY_REVERTED` as a retryable 400 will cause double-spend the moment a tx lands after the first response.
226
-
227
- ## Tests
228
-
229
- The package ships with 142 unit tests covering every collaborator + the
230
- full gateway flow through mocked providers. Run them with:
231
-
232
- ```bash
233
- pnpm --filter @pafi-dev/issuer test
157
+ ```ts
158
+ import { PointIndexer, BurnIndexer } from "@pafi-dev/issuer";
159
+
160
+ // Watches Transfer(0x0 user)deducts off-chain balance when mint confirmed.
161
+ const pointIndexer = new PointIndexer({
162
+ provider: publicClient,
163
+ pointTokenAddress: "0x<PointToken>",
164
+ ledger,
165
+ cursorStore, // PostgresCursorStore recommended
166
+ fromBlock: 28_000_000n,
167
+ confirmations: 2,
168
+ pollIntervalMs: 3_000,
169
+ });
170
+ pointIndexer.start();
171
+
172
+ // Watches Transfer(user → 0x0) — resolves pending credit when burn confirmed.
173
+ const burnIndexer = new BurnIndexer({
174
+ provider: publicClient,
175
+ pointTokenAddress: "0x<PointToken>",
176
+ ledger,
177
+ cursorStore,
178
+ matchLockId: async (evt) => {
179
+ // Return the lockId from ledger.reservePendingCredit() that matches
180
+ // this burn event. Typically a DB lookup by (from, amount, status=PENDING).
181
+ return yourDb.findPendingCreditLockId(evt.from, evt.amount);
182
+ },
183
+ });
184
+ burnIndexer.start();
234
185
  ```
235
186
 
236
- No network or on-chain state is required all tests are hermetic.
187
+ Indexers must be **singletons** run exactly one instance per token per deployment.
188
+ Use a Kubernetes Deployment with `replicas: 1`, not a Deployment behind a load balancer.
189
+
190
+ ---
237
191
 
238
- ## Exports
192
+ ## Error codes
239
193
 
240
194
  ```ts
241
- import {
242
- // Factory (recommended entry point)
243
- createIssuerService,
244
- type IssuerService,
245
- type IssuerServiceConfig,
246
-
247
- // Ledger
248
- MemoryPointLedger,
249
- type IPointLedger,
250
- type LockedMintRequest,
251
- type MintingStatus,
252
-
253
- // Policy
254
- DefaultPolicyEngine,
255
- type IPolicyEngine,
256
- type PolicyDecision,
257
- type PolicyEvalRequest,
258
-
259
- // Signer
260
- PrivateKeySigner, // DEV ONLY
261
- type IIssuerSigner,
262
-
263
- // Auth
264
- AuthService,
265
- AuthError,
266
- type AuthErrorCode,
267
- type AuthContext,
268
- MemorySessionStore,
269
- type ISessionStore,
270
- type Session,
271
- NonceManager,
272
- authenticateRequest,
273
-
274
- // Relay + fees
275
- RelayService,
276
- RelayError,
277
- type RelayErrorCode,
278
- type SubmitMintAndSwapParams,
279
- type RelayResult,
280
- type OperatorWalletLike,
281
- FeeManager,
282
- type FeeManagerConfig,
283
-
284
- // Gateway
285
- MintingGateway,
286
- MintingGatewayError,
287
- type MintingGatewayErrorCode,
288
- type MintAndCashOutRequest,
289
- type MintAndCashOutResponse,
290
- encodeExtData,
291
-
292
- // Indexer
293
- PointIndexer,
294
- type PointIndexerConfig,
295
- type MintEvent,
296
- type IIndexerCursorStore,
297
- InMemoryCursorStore,
298
-
299
- // API handlers + HTTP contract (source of truth — FE imports these type-only)
300
- IssuerApiHandlers,
301
- type IssuerApiHandlersConfig,
302
- type ApiConfigResponse,
303
- type ApiNonceResponse,
304
- type ApiLoginRequest,
305
- type ApiLoginResponse,
306
- type ApiGasFeeResponse,
307
- type ApiPoolsRequest,
308
- type ApiPoolsResponse,
309
- type ApiUserRequest,
310
- type ApiUserResponse,
311
- type ApiClaimAndSwapRequest,
312
- type ApiClaimAndSwapResponse,
313
- type PoolsProvider,
314
- } from "@pafi-dev/issuer";
195
+ import { PafiBackendError, type PafiBackendErrorCode } from "@pafi-dev/issuer";
196
+
197
+ type PafiBackendErrorCode =
198
+ | "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_EXCEEDED_DAILY" | "RATE_LIMIT_EXCEEDED_PER_USER"
199
+ | "RATE_LIMITER_UNAVAILABLE"
200
+ | "INTENT_REJECTED" | "MINT_CAP_EXCEEDED" | "ISSUER_INACTIVE"
201
+ | "ISSUER_UNAUTHORIZED" | "USER_UNAUTHORIZED"
202
+ | "BAD_REQUEST" | "INTERNAL_ERROR" | "TIMEOUT" | "NETWORK_ERROR";
315
203
  ```
316
204
 
317
- ### HTTP contract ownership
205
+ ---
318
206
 
319
- `@pafi-dev/issuer` is the **single source of truth** for the HTTP API contract
320
- between the issuer backend and any frontend / mobile / SDK consumer. The
321
- request/response types above are defined once here, then imported
322
- **type-only** by frontend code so browser bundles never pull in server
323
- code (`jose`, `node:crypto`, indexer polling loops, etc):
207
+ ## Changelog
324
208
 
325
- ```ts
326
- // Frontendtype-only import, erased at build time
327
- import type {
328
- ApiLoginRequest,
329
- ApiClaimAndSwapRequest,
330
- ApiUserResponse,
331
- } from "@pafi-dev/issuer";
332
-
333
- // Build your own fetch call with full type safety
334
- const res = await fetch(`${API_URL}/claim-and-swap`, {
335
- method: "POST",
336
- headers: {
337
- "Content-Type": "application/json",
338
- Authorization: `Bearer ${token}`,
339
- },
340
- body: JSON.stringify(request satisfies ApiClaimAndSwapRequest),
341
- });
342
- const body = (await res.json()) as ApiClaimAndSwapResponse;
343
- ```
209
+ ### 0.3.0-beta.10
210
+ - `MemoryPointLedger` removed from public exports each issuer implements `IPointLedger` against their own DB
211
+ - `PrivateKeySigner` / `IIssuerSigner` removed — SDK accepts viem `WalletClient` directly; see `examples/adapters/kms-signer.ts`
212
+ - `ledger` is now a required field in `IssuerServiceConfig` (was optional with in-memory default)
213
+ - `handleClaim` added to `IssuerApiHandlers` — enforces policy evaluation atomically before signing
344
214
 
345
- **`@pafi-dev/core` deliberately does NOT ship an HTTP client** — it handles
346
- only cryptography (EIP-712 / EIP-4361), contract reads, and V4 quoting.
347
- That's what keeps the frontend bundle small and keeps the protocol
348
- contract in exactly one place.
215
+ ### 0.3.0-beta.9
216
+ - `handleBuildConsentTypedData` validates `receiverConsent.originalReceiver` against authenticated user
217
+ - `PTRedeemHandler` enforces on-chain balance check before signing BurnRequest
349
218
 
350
- ## License
219
+ ### 0.3.0-beta.8
220
+ - `BalanceAggregator`, `BurnIndexer`, `PTRedeemHandler`, `TopUpRedemptionHandler`
221
+ - `BurnIndexer.matchLockId` is now required (throws if not provided)
351
222
 
352
- Apache-2.0
223
+ ### 0.3.0-alpha.0
224
+ - Initial `RelayService`, `PointIndexer`, `AuthService`, `IssuerApiHandlers`