kawasekit 0.1.0-beta.3 → 0.1.0-beta.5

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.
Files changed (108) hide show
  1. package/README.md +78 -26
  2. package/dist/asset-domain-CpJuDkI2.d.cts +102 -0
  3. package/dist/asset-domain-CpJuDkI2.d.ts +102 -0
  4. package/dist/chunk-DYTONQW2.js +76 -0
  5. package/dist/chunk-DYTONQW2.js.map +1 -0
  6. package/dist/{chunk-2V3W4B64.js → chunk-E2EG72U2.js} +112 -180
  7. package/dist/chunk-E2EG72U2.js.map +1 -0
  8. package/dist/chunk-E47SIVFY.js +44 -0
  9. package/dist/chunk-E47SIVFY.js.map +1 -0
  10. package/dist/chunk-KT7XDT2T.js +209 -0
  11. package/dist/chunk-KT7XDT2T.js.map +1 -0
  12. package/dist/chunk-MTMJNYOD.js +125 -0
  13. package/dist/chunk-MTMJNYOD.js.map +1 -0
  14. package/dist/{chunk-KWCPYGFE.js → chunk-N3CVLISJ.js} +3 -3
  15. package/dist/{chunk-KWCPYGFE.js.map → chunk-N3CVLISJ.js.map} +1 -1
  16. package/dist/{chunk-CD6SQBZN.js → chunk-PVUKX6IF.js} +7 -229
  17. package/dist/chunk-PVUKX6IF.js.map +1 -0
  18. package/dist/chunk-QHUCU5YX.js +179 -0
  19. package/dist/chunk-QHUCU5YX.js.map +1 -0
  20. package/dist/chunk-SOTYGX67.js +95 -0
  21. package/dist/chunk-SOTYGX67.js.map +1 -0
  22. package/dist/chunk-TTX3RBIZ.js +159 -0
  23. package/dist/chunk-TTX3RBIZ.js.map +1 -0
  24. package/dist/chunk-UQ7WJY6O.js +43 -0
  25. package/dist/chunk-UQ7WJY6O.js.map +1 -0
  26. package/dist/chunk-VPRR3TNA.js +204 -0
  27. package/dist/chunk-VPRR3TNA.js.map +1 -0
  28. package/dist/chunk-WMVJNPX2.js +41 -0
  29. package/dist/chunk-WMVJNPX2.js.map +1 -0
  30. package/dist/cli/index.cjs +66 -9
  31. package/dist/cli/index.cjs.map +1 -1
  32. package/dist/cli/index.js +14 -7
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/idempotency/index.cjs +52 -3
  35. package/dist/idempotency/index.cjs.map +1 -1
  36. package/dist/idempotency/index.d.cts +3 -3
  37. package/dist/idempotency/index.d.ts +3 -3
  38. package/dist/idempotency/index.js +3 -2
  39. package/dist/{chunk-WMFCI6KC.js → idempotency/redis/index.cjs} +136 -130
  40. package/dist/idempotency/redis/index.cjs.map +1 -0
  41. package/dist/idempotency/redis/index.d.cts +83 -0
  42. package/dist/idempotency/redis/index.d.ts +83 -0
  43. package/dist/idempotency/redis/index.js +105 -0
  44. package/dist/idempotency/redis/index.js.map +1 -0
  45. package/dist/index-2fEOL83n.d.cts +472 -0
  46. package/dist/index-2fEOL83n.d.ts +472 -0
  47. package/dist/{index-kqH78Yms.d.ts → index-BaAOB0xd.d.ts} +158 -72
  48. package/dist/{index-CykLOgYD.d.ts → index-CSpNGigO.d.ts} +1 -1
  49. package/dist/{index-DzveM0RN.d.cts → index-DFChv_fT.d.cts} +1 -1
  50. package/dist/{index-NdNKNnZP.d.cts → index-Z6AL1MR_.d.cts} +158 -72
  51. package/dist/index.cjs +497 -96
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +366 -101
  54. package/dist/index.d.ts +366 -101
  55. package/dist/index.js +13 -6
  56. package/dist/observability/index.d.cts +2 -2
  57. package/dist/observability/index.d.ts +2 -2
  58. package/dist/observability/otlp/index.d.cts +2 -2
  59. package/dist/observability/otlp/index.d.ts +2 -2
  60. package/dist/observability/prometheus/index.d.cts +2 -2
  61. package/dist/observability/prometheus/index.d.ts +2 -2
  62. package/dist/policy/index.cjs +419 -0
  63. package/dist/policy/index.cjs.map +1 -0
  64. package/dist/policy/index.d.cts +66 -0
  65. package/dist/policy/index.d.ts +66 -0
  66. package/dist/policy/index.js +7 -0
  67. package/dist/policy/index.js.map +1 -0
  68. package/dist/{server-CSpATNiH.d.ts → server-D_rZc-cW.d.ts} +2 -2
  69. package/dist/{server-BMeg_hNB.d.cts → server-ov8YstNS.d.cts} +2 -2
  70. package/dist/session/index.cjs +52 -3
  71. package/dist/session/index.cjs.map +1 -1
  72. package/dist/session/index.d.cts +2 -2
  73. package/dist/session/index.d.ts +2 -2
  74. package/dist/session/index.js +2 -2
  75. package/dist/signer/index.cjs +360 -0
  76. package/dist/signer/index.cjs.map +1 -0
  77. package/dist/signer/index.d.cts +131 -0
  78. package/dist/signer/index.d.ts +131 -0
  79. package/dist/signer/index.js +9 -0
  80. package/dist/signer/index.js.map +1 -0
  81. package/dist/spending-policy-BnNp_y5d.d.cts +129 -0
  82. package/dist/spending-policy-DiZBTBeN.d.ts +129 -0
  83. package/dist/{store-BY16tCbe.d.cts → store-DPOLS6yp.d.cts} +1 -1
  84. package/dist/{store-Bd-91QL0.d.ts → store-DmdoV3Ii.d.ts} +1 -1
  85. package/dist/{types-DwFfT4E7.d.ts → types-BR9UcvJO.d.ts} +1 -1
  86. package/dist/types-IEl-iOIx.d.cts +148 -0
  87. package/dist/types-IEl-iOIx.d.ts +148 -0
  88. package/dist/{types-A_WwFpcv.d.cts → types-TpS_8ztt.d.cts} +1 -1
  89. package/dist/x402/hono/index.cjs.map +1 -1
  90. package/dist/x402/hono/index.d.cts +4 -4
  91. package/dist/x402/hono/index.d.ts +4 -4
  92. package/dist/x402/hono/index.js +6 -3
  93. package/dist/x402/hono/index.js.map +1 -1
  94. package/dist/x402/index.cjs +274 -80
  95. package/dist/x402/index.cjs.map +1 -1
  96. package/dist/x402/index.d.cts +7 -5
  97. package/dist/x402/index.d.ts +7 -5
  98. package/dist/x402/index.js +8 -4
  99. package/package.json +32 -1
  100. package/dist/chunk-2V3W4B64.js.map +0 -1
  101. package/dist/chunk-6G345P2I.js +0 -81
  102. package/dist/chunk-6G345P2I.js.map +0 -1
  103. package/dist/chunk-CD6SQBZN.js.map +0 -1
  104. package/dist/chunk-SA7LMQFG.js +0 -44
  105. package/dist/chunk-SA7LMQFG.js.map +0 -1
  106. package/dist/chunk-WMFCI6KC.js.map +0 -1
  107. package/dist/index-jF4BGOIb.d.cts +0 -173
  108. package/dist/index-jF4BGOIb.d.ts +0 -173
package/README.md CHANGED
@@ -5,10 +5,15 @@
5
5
  [![npm version](https://img.shields.io/npm/v/kawasekit.svg)](https://www.npmjs.com/package/kawasekit)
6
6
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
 
8
- 🚧 **Status**: M4 complete — `kawasekit@0.1.0-beta.2` is published on npm (SLSA provenance, `beta` dist-tag) and mainnet-capable, with payment flows verified on Polygon mainnet. **Not yet GA.** Production use is currently constrained to **small per-call values**: the reasoning-step idempotency gap (see [`docs/THREAT_MODEL.md` §6.1](./docs/THREAT_MODEL.md#61-reasoning-step-idempotency-gap)) is not yet closed, so duplicate-payment scenarios are the integrator's responsibility. GA (`0.1.0` on the `latest` tag) is gated on closing the fund-correctness gaps the §6.1 idempotency layer (or GA explicitly scoped to small per-call values) plus a `maxAmountPerSign` ceiling — and a clean beta soak. Review happens **continuously in the open**: issues and counter-examples are welcome on GitHub and via [SECURITY.md](./SECURITY.md) (the §6.1 gap itself came from public feedback). A formal third-party audit is a goal on the road to `1.0`, not a `0.1.0` GA blocker. Built in public.
8
+ 🚧 **Status**: M5 backbone complete — `kawasekit@0.1.0-beta.3` is published on npm (SLSA provenance, `beta` dist-tag) and mainnet-capable, with payment flows verified on Polygon mainnet. **Not yet GA**, but **both `0.1.0` fund-correctness gates are now closed**:
9
+
10
+ - **Reasoning-step idempotency** ([`docs/THREAT_MODEL.md` §6.1](./docs/THREAT_MODEL.md), now closed) — a default-on server dedup layer prevents duplicate payments for identical re-sends, and an opt-in client-derived EIP-3009 nonce makes the token contract reject re-signed same-intent duplicates on-chain. See the [idempotency note](#x402-paywall-m3-1).
11
+ - **`maxAmountPerSign`** (threat 1.14) — the signer can now pin a per-signature value ceiling, the way it already pins the asset.
12
+
13
+ This removes the earlier blanket "small per-call values only" caveat: preventing duplicate payment is now a matter of correct integration (wiring a reasoning-step key for the regenerate / fan-out case), not a missing SDK capability. GA (`0.1.0` on the `latest` tag) now waits on a **clean one-week beta soak**. Review happens **continuously in the open**: issues and counter-examples are welcome on GitHub and via [SECURITY.md](./SECURITY.md) (the §6.1 gap itself came from public feedback). A formal third-party audit is a goal on the road to `1.0`, not a `0.1.0` GA blocker. Built in public.
9
14
 
10
15
  ```bash
11
- pnpm add kawasekit@beta # 0.1.0-beta.2 — pre-GA, mainnet-capable
16
+ pnpm add kawasekit@beta # 0.1.0-beta.3 — pre-GA, mainnet-capable
12
17
  ```
13
18
 
14
19
  ## Vision
@@ -25,7 +30,7 @@ Built around modern account abstraction (ERC-4337 / Kernel v3.1) and Japan's fir
25
30
  - [x] **M2**: JPYC transfer via UserOp + EIP-3009 signing helpers + Daily Limit spending policy
26
31
  - [x] **M3**: x402 v2 server/client/facilitator + session-key lifecycle + Mastra/Hono integration example
27
32
  - [x] **M4**: Polygon mainnet support + observability (Prometheus / OTLP) + CLI + docs site + npm `0.1.0-alpha`/`0.1.0-beta` release
28
- - [ ] **M5**: Reasoning-step idempotency layer (§6.1) + `0.1.0` GA promote + Kaia support — the technical prerequisites for first real integrations
33
+ - [ ] **M5** *(backbone done)*: reasoning-step idempotency layer (§6.1) + `maxAmountPerSign` ceiling (✅) → **both `0.1.0` fund-correctness gates closed**. Remaining: `0.1.0` GA promote (after a clean beta soak) and Kaia support (fast-follow). The README roadmap's framing "Community building + first real integrations" — is the *outcome*; closing these gaps is the technical *prerequisite* for it.
29
34
  - [ ] **M6**: Managed service alpha + Rust policy engine
30
35
 
31
36
  ## Quick Start
@@ -173,8 +178,10 @@ const app = new Hono();
173
178
  app.use(
174
179
  "/weather/*",
175
180
  x402Middleware({
176
- // `network` is required (M4-1): it is cross-checked against
177
- // walletClient.chain.isTestnet and throws on mismatch.
181
+ // `network` is required (M4-1): cross-checked against walletClient.chain.isTestnet,
182
+ // throws on mismatch. walletClient.account MUST be built with viem's `nonceManager`
183
+ // — createSelfFacilitator throws at construction otherwise (threat 2.2). On mainnet
184
+ // it waits for 4 confirmations by default (1 on testnet); override with `confirmations`.
178
185
  facilitator: createSelfFacilitator({ network: "testnet", walletClient, publicClient }),
179
186
  requirementsFor: () => [
180
187
  buildPaymentRequirements({
@@ -189,10 +196,17 @@ app.use(
189
196
  app.get("/weather/:city", (c) => c.json({ city: c.req.param("city"), weather: "sunny" }));
190
197
  ```
191
198
 
199
+ The server **deduplicates identical re-sent paid requests by default** (M5-1): an
200
+ in-memory store replays the cached response instead of settling twice, and closes
201
+ the verify→settle race. It is single-process — for multi-replica deployments pass a
202
+ shared store (or rely on the client-derived nonce below); disable with
203
+ `idempotency: { store: "none" }`. See the [idempotency note](#x402-paywall-m3-1) below.
204
+
192
205
  Client (any `fetch` becomes x402-aware):
193
206
 
194
207
  ```typescript
195
208
  import { createX402PaymentSigner, JPYC_DECIMALS, wrapFetch } from "kawasekit";
209
+ import { createIdempotencyKeyBuilder } from "kawasekit/idempotency";
196
210
  import { parseUnits } from "viem";
197
211
  import { privateKeyToAccount } from "viem/accounts";
198
212
 
@@ -202,8 +216,16 @@ const signer = createX402PaymentSigner({
202
216
  // Pin to the JPYC v2 EIP-712 domain at construction. The wire-format
203
217
  // extra.name / extra.version are ignored — Threat 1.4 mitigation.
204
218
  asset: { kind: "known", id: "jpyc-v2" },
219
+ // Per-signature value ceiling (M5-2, threat 1.14): refuse to sign any
220
+ // requirement above this. Pins the *amount* the way `asset` pins the token.
221
+ maxAmountPerSign: parseUnits("1", JPYC_DECIMALS), // ≤ 1 JPYC per call
205
222
  });
206
223
 
224
+ // One key builder per agent run; call .next(intent) at each tool-execution
225
+ // boundary and reuse the returned key for retries of that step.
226
+ const keys = createIdempotencyKeyBuilder({ conversationId: "conv-42" });
227
+ let stepKey = keys.next("fetch_weather:Tokyo");
228
+
207
229
  // onPayment is *required* at the type level — kawasekit refuses to default
208
230
  // to "always pay" silently. The callback is your budget guard.
209
231
  let spent = 0n;
@@ -216,20 +238,36 @@ const fetch402 = wrapFetch({
216
238
  spent = next;
217
239
  return true;
218
240
  },
241
+ // Reasoning-step idempotency (M5-1): the key is sent as an `Idempotency-Key`
242
+ // header AND derives a deterministic EIP-3009 nonce, so a regenerate /
243
+ // multi-agent re-sign of the SAME intent reuses the nonce and the token
244
+ // contract rejects the duplicate on-chain. Omit for today's random-nonce path.
245
+ idempotencyKeyFor: () => stepKey,
219
246
  });
220
247
 
221
248
  const res = await fetch402("https://api.example.com/weather/Tokyo");
222
249
  // → 402 → onPayment guard → signed retry → 200 with JPYC settled on-chain
223
250
  ```
224
251
 
225
- > **⚠️ Call-level idempotency only.** kawasekit guarantees that a single
226
- > `fetch402(...)` call settles **at most once** (EIP-3009 nonce + viem
227
- > `nonceManager`). It does **not** prevent your agent from invoking
228
- > `fetch402(...)` twice for the same reasoning step retries, regeneration,
229
- > pause-resume, and multi-agent fan-out can each cause duplicate charges.
230
- > **Step-level idempotency is your responsibility**: track an
231
- > `Idempotency-Key` per reasoning step at the agent framework layer.
232
- > See [`docs/THREAT_MODEL.md` §6.1](./docs/THREAT_MODEL.md#61-reasoning-step-idempotency-gap) for the threat boundary.
252
+ > **Reasoning-step idempotency (M5-1).** kawasekit now prevents one agent
253
+ > reasoning step from paying twice, across two layers:
254
+ >
255
+ > - **Identical re-send handled by default.** The server dedup layer (shown in
256
+ > the server example above) replays the cached response for a re-sent /
257
+ > network-duplicate request and closes the verify→settle race — no
258
+ > configuration needed (threat 1.8c, ✅). It is single-process; use a shared
259
+ > store for multiple replicas, or rely on the derived nonce below.
260
+ > - **Re-signed same intent — wire the key.** A "Regenerate" click or multi-agent
261
+ > fan-out signs a *fresh* authorization for the same intent. Pass a
262
+ > reasoning-step key (`idempotencyKeyFor` + `createIdempotencyKeyBuilder`, shown
263
+ > in the client example) so the EIP-3009 nonce is derived deterministically and
264
+ > the JPYC contract rejects the duplicate **on-chain**, across uncoordinated
265
+ > replicas. The SDK can't do this for you — it never sees the LLM intent, only
266
+ > your harness does — so this half is **operator responsibility** (threat 1.8b,
267
+ > parallel to the asset pin). Omit the key to fall back to random-nonce behaviour.
268
+ >
269
+ > See [`docs/THREAT_MODEL.md` §6.1](./docs/THREAT_MODEL.md) (now closed) and the
270
+ > [design RFC](./docs/rfc/m5-1-reasoning-step-idempotency.md) for the full model.
233
271
 
234
272
  ### Session-key lifecycle (M3-2)
235
273
 
@@ -263,22 +301,36 @@ const restored = await restoreSessionAccount({
263
301
 
264
302
  ## Supported Chains
265
303
 
266
- JPYC availability and kawasekit support are **two separate axes** JPYC being
267
- live on a chain does **not** mean kawasekit has a config or has been tested
268
- there. Today kawasekit ships a chain config only for **Polygon + Polygon Amoy**
269
- (`src/chains/`); `getJpycAddress` / `SupportedChainId` accept only those two.
270
-
271
- | Chain | JPYC availability | kawasekit support |
304
+ JPYC availability and kawasekit support are **two separate axes**. As of M5-3,
305
+ kawasekit ships chain configs for **Polygon, Kaia, Avalanche, and Ethereum**
306
+ (+ their testnets) in `src/chains/`, each carrying a per-chain finality default
307
+ (`defaultConfirmations`). JPYC is live at the same address on **all eight
308
+ chains** (Kaia / Kairos / Avalanche / Fuji / Sepolia confirmed by a read-only
309
+ on-chain `name()`/`symbol()` check; Polygon / Amoy / Ethereum established). Two
310
+ honest caveats: the **x402 EOA-payer path** works on every chain, but the
311
+ **smart-account path** (session keys, sponsored UserOps) is verified only on
312
+ Polygon — Kaia's runs via Pimlico in a later phase. Real x402 settlement is
313
+ **verified on Kaia Kairos** — a JPYC `transferWithAuthorization` settled through
314
+ the self-facilitator ([tx `0xe0a0…79c0`](https://kairos.kaiascan.io/tx/0xe0a0bfc75a447ff86c3502d49ff4e45cdf0396a1edd7eb5ed132dcb0130379c0),
315
+ `pnpm m5:kairos-x402-self-settle`); the other new chains are liveness-verified
316
+ but settlement is not yet exercised there.
317
+
318
+ | Chain (id) | JPYC (`0xE7C3…c29`) | kawasekit support |
272
319
  |---|---|---|
273
- | Polygon (mainnet) | ✅ Live (`0xE7C3…c29`) | ✅ M4 config shipped, verified with live mainnet txs |
274
- | Polygon Amoy (testnet) | ✅ Live (`0xE7C3…c29`) | ✅ primary testnet target |
275
- | Kaia | ✅ Live (`0xE7C3…c29`, same address)¹ | 🚧 planned M5 (x402 EOA-payer path first) |
276
- | Avalanche | ✅ Live (`0xE7C3…c29`) | not yet no chain config |
277
- | Ethereum | ✅ Live (`0xE7C3…c29`) | not yetno chain config |
320
+ | Polygon (137) | ✅ Live | ✅ config + x402 + smart-account; verified with live mainnet txs |
321
+ | Polygon Amoy (80002) | ✅ Live | ✅ primary testnet target |
322
+ | Kaia (8217) | ✅ Live, same address¹ | M5-3 config — x402 EOA path; smart-account via Pimlico (later) |
323
+ | Kaia Kairos (1001) | ✅ Live (on-chain verified) | M5-3x402 EOA path, **settlement verified on-chain** (tx `0xe0a0…79c0`) |
324
+ | Avalanche (43114) | ✅ Live | M5-3 configx402 EOA path; smart-account untested |
325
+ | Avalanche Fuji (43113) | ✅ Live (on-chain verified) | ✅ M5-3 config — x402 EOA path |
326
+ | Ethereum (1) | ✅ Live | ✅ M5-3 config — x402 EOA path; smart-account untested; deep confirmations (32) |
327
+ | Sepolia (11155111) | ✅ Live (on-chain verified) | ✅ M5-3 config — x402 EOA path |
278
328
 
279
329
  ¹ JPYC officially launched on Kaia in 2026-05 (Kaia DLT Foundation; Unifi began
280
- JPYC support 2026-05-22), same contract address as the other chains. kawasekit
281
- has no Kaia chain config yet support is scheduled for M5.
330
+ JPYC support 2026-05-22), at the same contract address as the other chains. Kaia
331
+ runs IBFT consensus with immediate finality, so its `defaultConfirmations` is `1`
332
+ (not Polygon's `4`). See the
333
+ [finality-tuning recipe](./docs/recipes/facilitator-finality-tuning.md).
282
334
 
283
335
  ## Why Japan-first
284
336
 
@@ -0,0 +1,102 @@
1
+ import { Address } from 'viem';
2
+
3
+ /**
4
+ * Known-asset registry for {@link createX402PaymentSigner}'s
5
+ * `asset: { kind: "known", id }` discriminated-union branch.
6
+ *
7
+ * kawasekit only ships pinned EIP-712 domain definitions for assets it has
8
+ * verified empirically against the deployed contracts. Adding a new entry
9
+ * here requires citing the source-file + line reference for the contract
10
+ * that owns the `name` / `version` (so the next reviewer can spot-check the
11
+ * claim, the same discipline `docs/THREAT_MODEL.md` §0 demands of any ✅
12
+ * verdict that delegates to an out-of-scope component).
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ /** Known asset identifiers. New entries must update this union AND the table. */
18
+ type KnownAssetId = "jpyc-v2";
19
+ /** Fully-pinned EIP-712 domain for a known asset. */
20
+ interface KnownAssetDomain {
21
+ readonly id: KnownAssetId;
22
+ readonly name: string;
23
+ readonly version: string;
24
+ readonly verifyingContract: Address;
25
+ }
26
+ /**
27
+ * Look up a known asset's pinned EIP-712 domain by id.
28
+ *
29
+ * @returns The domain, or `undefined` if the id is not in the registry.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { getKnownAssetDomain } from "kawasekit";
34
+ *
35
+ * const jpyc = getKnownAssetDomain("jpyc-v2");
36
+ * if (jpyc === undefined) throw new Error("unreachable");
37
+ * console.log(jpyc.verifyingContract); // 0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29
38
+ * ```
39
+ */
40
+ declare function getKnownAssetDomain(id: KnownAssetId): KnownAssetDomain | undefined;
41
+ /** List every known asset id (for diagnostics / error messages). */
42
+ declare function listKnownAssetIds(): readonly KnownAssetId[];
43
+
44
+ /**
45
+ * EIP-712 asset-domain resolution for x402 / EIP-3009 signing.
46
+ *
47
+ * Construction-time pinning of the EIP-712 domain (`name` / `version` /
48
+ * `verifyingContract`) a signer will use. The integrator declares an
49
+ * {@link X402AssetParam} — either a kawasekit-maintained `known` asset or a
50
+ * loud `unsafeOverride` — and {@link resolveAssetParam} resolves it to a pinned
51
+ * {@link ResolvedAsset}. The signer then trusts only this pinned domain and
52
+ * refuses to sign for a mismatched advertised asset (Threat 1.4: misadvertised
53
+ * EIP-712 domain).
54
+ *
55
+ * Token-domain concern, reused by both the x402 signer (`src/x402/client.ts`)
56
+ * and the M6 PolicyGatedSigner (`src/signer/`).
57
+ *
58
+ * @packageDocumentation
59
+ */
60
+
61
+ /** EIP-712 token domain `name` / `version` pair. */
62
+ interface X402TokenDomain {
63
+ readonly name: string;
64
+ readonly version: string;
65
+ }
66
+ /**
67
+ * Asset binding for {@link createX402PaymentSigner} and the M6 PolicyGatedSigner.
68
+ * Required, discriminated.
69
+ *
70
+ * **Default-on whitelist**: integrators MUST declare which asset they intend
71
+ * to sign for. The `known` branch references a kawasekit-maintained
72
+ * whitelist (see `src/tokens/known-assets.ts`); the `unsafeOverride` branch
73
+ * is the deliberate escape hatch for any other asset and is named loudly so
74
+ * it survives a code review. Either way, the signer pins the EIP-712 domain
75
+ * at construction time and refuses to sign if `paymentRequirements.asset`
76
+ * disagrees with the pinned `verifyingContract`.
77
+ *
78
+ * Closes Threat 1.4 (misadvertised EIP-712 domain): the server's advertised
79
+ * `extra.name` / `extra.version` and `asset` are all ignored for signing
80
+ * purposes — the signer trusts only what the integrator declared here.
81
+ */
82
+ type X402AssetParam = {
83
+ /** Use a kawasekit-maintained pinned EIP-712 domain. */
84
+ readonly kind: "known";
85
+ /** The asset id to pin. See {@link KnownAssetId} for the registry. */
86
+ readonly id: KnownAssetId;
87
+ } | {
88
+ /**
89
+ * Use a caller-supplied EIP-712 domain for an asset NOT on the
90
+ * kawasekit whitelist. The name is deliberately loud — pick this
91
+ * branch only when you have separately audited the contract and its
92
+ * `eip712Domain()` output.
93
+ */
94
+ readonly kind: "unsafeOverride";
95
+ readonly domain: {
96
+ readonly name: string;
97
+ readonly version: string;
98
+ readonly verifyingContract: Address;
99
+ };
100
+ };
101
+
102
+ export { type KnownAssetDomain as K, type X402AssetParam as X, type KnownAssetId as a, type X402TokenDomain as b, getKnownAssetDomain as g, listKnownAssetIds as l };
@@ -0,0 +1,102 @@
1
+ import { Address } from 'viem';
2
+
3
+ /**
4
+ * Known-asset registry for {@link createX402PaymentSigner}'s
5
+ * `asset: { kind: "known", id }` discriminated-union branch.
6
+ *
7
+ * kawasekit only ships pinned EIP-712 domain definitions for assets it has
8
+ * verified empirically against the deployed contracts. Adding a new entry
9
+ * here requires citing the source-file + line reference for the contract
10
+ * that owns the `name` / `version` (so the next reviewer can spot-check the
11
+ * claim, the same discipline `docs/THREAT_MODEL.md` §0 demands of any ✅
12
+ * verdict that delegates to an out-of-scope component).
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ /** Known asset identifiers. New entries must update this union AND the table. */
18
+ type KnownAssetId = "jpyc-v2";
19
+ /** Fully-pinned EIP-712 domain for a known asset. */
20
+ interface KnownAssetDomain {
21
+ readonly id: KnownAssetId;
22
+ readonly name: string;
23
+ readonly version: string;
24
+ readonly verifyingContract: Address;
25
+ }
26
+ /**
27
+ * Look up a known asset's pinned EIP-712 domain by id.
28
+ *
29
+ * @returns The domain, or `undefined` if the id is not in the registry.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { getKnownAssetDomain } from "kawasekit";
34
+ *
35
+ * const jpyc = getKnownAssetDomain("jpyc-v2");
36
+ * if (jpyc === undefined) throw new Error("unreachable");
37
+ * console.log(jpyc.verifyingContract); // 0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29
38
+ * ```
39
+ */
40
+ declare function getKnownAssetDomain(id: KnownAssetId): KnownAssetDomain | undefined;
41
+ /** List every known asset id (for diagnostics / error messages). */
42
+ declare function listKnownAssetIds(): readonly KnownAssetId[];
43
+
44
+ /**
45
+ * EIP-712 asset-domain resolution for x402 / EIP-3009 signing.
46
+ *
47
+ * Construction-time pinning of the EIP-712 domain (`name` / `version` /
48
+ * `verifyingContract`) a signer will use. The integrator declares an
49
+ * {@link X402AssetParam} — either a kawasekit-maintained `known` asset or a
50
+ * loud `unsafeOverride` — and {@link resolveAssetParam} resolves it to a pinned
51
+ * {@link ResolvedAsset}. The signer then trusts only this pinned domain and
52
+ * refuses to sign for a mismatched advertised asset (Threat 1.4: misadvertised
53
+ * EIP-712 domain).
54
+ *
55
+ * Token-domain concern, reused by both the x402 signer (`src/x402/client.ts`)
56
+ * and the M6 PolicyGatedSigner (`src/signer/`).
57
+ *
58
+ * @packageDocumentation
59
+ */
60
+
61
+ /** EIP-712 token domain `name` / `version` pair. */
62
+ interface X402TokenDomain {
63
+ readonly name: string;
64
+ readonly version: string;
65
+ }
66
+ /**
67
+ * Asset binding for {@link createX402PaymentSigner} and the M6 PolicyGatedSigner.
68
+ * Required, discriminated.
69
+ *
70
+ * **Default-on whitelist**: integrators MUST declare which asset they intend
71
+ * to sign for. The `known` branch references a kawasekit-maintained
72
+ * whitelist (see `src/tokens/known-assets.ts`); the `unsafeOverride` branch
73
+ * is the deliberate escape hatch for any other asset and is named loudly so
74
+ * it survives a code review. Either way, the signer pins the EIP-712 domain
75
+ * at construction time and refuses to sign if `paymentRequirements.asset`
76
+ * disagrees with the pinned `verifyingContract`.
77
+ *
78
+ * Closes Threat 1.4 (misadvertised EIP-712 domain): the server's advertised
79
+ * `extra.name` / `extra.version` and `asset` are all ignored for signing
80
+ * purposes — the signer trusts only what the integrator declared here.
81
+ */
82
+ type X402AssetParam = {
83
+ /** Use a kawasekit-maintained pinned EIP-712 domain. */
84
+ readonly kind: "known";
85
+ /** The asset id to pin. See {@link KnownAssetId} for the registry. */
86
+ readonly id: KnownAssetId;
87
+ } | {
88
+ /**
89
+ * Use a caller-supplied EIP-712 domain for an asset NOT on the
90
+ * kawasekit whitelist. The name is deliberately loud — pick this
91
+ * branch only when you have separately audited the contract and its
92
+ * `eip712Domain()` output.
93
+ */
94
+ readonly kind: "unsafeOverride";
95
+ readonly domain: {
96
+ readonly name: string;
97
+ readonly version: string;
98
+ readonly verifyingContract: Address;
99
+ };
100
+ };
101
+
102
+ export { type KnownAssetDomain as K, type X402AssetParam as X, type KnownAssetId as a, type X402TokenDomain as b, getKnownAssetDomain as g, listKnownAssetIds as l };
@@ -0,0 +1,76 @@
1
+ import { PolicyGatedSignerConfigError, resolveAssetParam, signTransferWithAuthorization } from './chunk-VPRR3TNA.js';
2
+ import { evaluateSpendingPolicy } from './chunk-MTMJNYOD.js';
3
+ import { getAddress } from 'viem';
4
+
5
+ function createLocalPolicyGatedSigner(params) {
6
+ if (params.acknowledgeAdvisory !== true) {
7
+ throw new PolicyGatedSignerConfigError(
8
+ "acknowledgeAdvisory",
9
+ "a local signer is advisory (a key-holder can bypass its policy); pass `acknowledgeAdvisory: true` to construct one consciously, or use a cryptographic adapter for bounded/regulated flows"
10
+ );
11
+ }
12
+ const { account, policy, spendState } = params;
13
+ const pinned = resolveAssetParam(params.asset);
14
+ const from = getAddress(account.address);
15
+ return {
16
+ enforcement: "advisory",
17
+ from,
18
+ async sign(intent) {
19
+ if (getAddress(intent.from) !== from) {
20
+ return {
21
+ ok: false,
22
+ rejection: {
23
+ reason: "from_mismatch",
24
+ detail: `intent.from ${getAddress(intent.from)} does not equal signer.from ${from}`
25
+ }
26
+ };
27
+ }
28
+ if (getAddress(intent.token) !== pinned.verifyingContract) {
29
+ return {
30
+ ok: false,
31
+ rejection: {
32
+ reason: "token_not_allowed",
33
+ detail: `intent.token ${getAddress(intent.token)} does not equal the signer's pinned verifyingContract ${pinned.verifyingContract}`
34
+ }
35
+ };
36
+ }
37
+ const state = await spendState?.() ?? { spentPerToken: [] };
38
+ const nowSeconds = BigInt(Math.floor(Date.now() / 1e3));
39
+ const decision = evaluateSpendingPolicy(policy, intent, state, nowSeconds);
40
+ if (!decision.ok) {
41
+ return { ok: false, rejection: decision.rejection };
42
+ }
43
+ const signed = await signTransferWithAuthorization(
44
+ account,
45
+ {
46
+ name: pinned.name,
47
+ version: pinned.version,
48
+ chainId: intent.chainId,
49
+ verifyingContract: pinned.verifyingContract
50
+ },
51
+ {
52
+ from,
53
+ to: intent.to,
54
+ value: intent.value,
55
+ validAfter: intent.validAfter,
56
+ validBefore: intent.validBefore,
57
+ nonce: intent.nonce
58
+ }
59
+ );
60
+ return { ok: true, signature: signed.signature, intent };
61
+ },
62
+ describe() {
63
+ return {
64
+ enforcement: "advisory",
65
+ from,
66
+ policyId: policy.session.id,
67
+ notAfter: policy.session.notAfter,
68
+ revoked: policy.revoked
69
+ };
70
+ }
71
+ };
72
+ }
73
+
74
+ export { createLocalPolicyGatedSigner };
75
+ //# sourceMappingURL=chunk-DYTONQW2.js.map
76
+ //# sourceMappingURL=chunk-DYTONQW2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/signer/local.ts"],"names":[],"mappings":";;;;AAgEO,SAAS,6BACf,MAAA,EACgC;AAChC,EAAA,IAAI,MAAA,CAAO,wBAAwB,IAAA,EAAM;AACxC,IAAA,MAAM,IAAI,4BAAA;AAAA,MACT,qBAAA;AAAA,MACA;AAAA,KACD;AAAA,EACD;AAEA,EAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAQ,UAAA,EAAW,GAAI,MAAA;AACxC,EAAA,MAAM,MAAA,GAAS,iBAAA,CAAkB,MAAA,CAAO,KAAK,CAAA;AAC7C,EAAA,MAAM,IAAA,GAAO,UAAA,CAAW,OAAA,CAAQ,OAAO,CAAA;AAEvC,EAAA,OAAO;AAAA,IACN,WAAA,EAAa,UAAA;AAAA,IACb,IAAA;AAAA,IACA,MAAM,KAAK,MAAA,EAA4C;AACtD,MAAA,IAAI,UAAA,CAAW,MAAA,CAAO,IAAI,CAAA,KAAM,IAAA,EAAM;AACrC,QAAA,OAAO;AAAA,UACN,EAAA,EAAI,KAAA;AAAA,UACJ,SAAA,EAAW;AAAA,YACV,MAAA,EAAQ,eAAA;AAAA,YACR,QAAQ,CAAA,YAAA,EAAe,UAAA,CAAW,OAAO,IAAI,CAAC,+BAA+B,IAAI,CAAA;AAAA;AAClF,SACD;AAAA,MACD;AACA,MAAA,IAAI,UAAA,CAAW,MAAA,CAAO,KAAK,CAAA,KAAM,OAAO,iBAAA,EAAmB;AAC1D,QAAA,OAAO;AAAA,UACN,EAAA,EAAI,KAAA;AAAA,UACJ,SAAA,EAAW;AAAA,YACV,MAAA,EAAQ,mBAAA;AAAA,YACR,MAAA,EAAQ,gBAAgB,UAAA,CAAW,MAAA,CAAO,KAAK,CAAC,CAAA,sDAAA,EAAyD,OAAO,iBAAiB,CAAA;AAAA;AAClI,SACD;AAAA,MACD;AAEA,MAAA,MAAM,QAAqB,MAAM,UAAA,QAAmB,EAAE,aAAA,EAAe,EAAC,EAAE;AACxE,MAAA,MAAM,UAAA,GAAa,OAAO,IAAA,CAAK,KAAA,CAAM,KAAK,GAAA,EAAI,GAAI,GAAI,CAAC,CAAA;AACvD,MAAA,MAAM,QAAA,GAAW,sBAAA,CAAuB,MAAA,EAAQ,MAAA,EAAQ,OAAO,UAAU,CAAA;AACzE,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AACjB,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,SAAA,EAAW,SAAS,SAAA,EAAU;AAAA,MACnD;AAEA,MAAA,MAAM,SAAS,MAAM,6BAAA;AAAA,QACpB,OAAA;AAAA,QACA;AAAA,UACC,MAAM,MAAA,CAAO,IAAA;AAAA,UACb,SAAS,MAAA,CAAO,OAAA;AAAA,UAChB,SAAS,MAAA,CAAO,OAAA;AAAA,UAChB,mBAAmB,MAAA,CAAO;AAAA,SAC3B;AAAA,QACA;AAAA,UACC,IAAA;AAAA,UACA,IAAI,MAAA,CAAO,EAAA;AAAA,UACX,OAAO,MAAA,CAAO,KAAA;AAAA,UACd,YAAY,MAAA,CAAO,UAAA;AAAA,UACnB,aAAa,MAAA,CAAO,WAAA;AAAA,UACpB,OAAO,MAAA,CAAO;AAAA;AACf,OACD;AACA,MAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAA,EAAW,MAAA,CAAO,WAAW,MAAA,EAAO;AAAA,IACxD,CAAA;AAAA,IACA,QAAA,GAA8B;AAC7B,MAAA,OAAO;AAAA,QACN,WAAA,EAAa,UAAA;AAAA,QACb,IAAA;AAAA,QACA,QAAA,EAAU,OAAO,OAAA,CAAQ,EAAA;AAAA,QACzB,QAAA,EAAU,OAAO,OAAA,CAAQ,QAAA;AAAA,QACzB,SAAS,MAAA,CAAO;AAAA,OACjB;AAAA,IACD;AAAA,GACD;AACD","file":"chunk-DYTONQW2.js","sourcesContent":["/**\n * The `local` PolicyGatedSigner adapter — `enforcement: \"advisory\"`.\n *\n * Wraps a viem {@link Account} + a {@link SpendingPolicy} + a pinned EIP-712\n * domain. `sign(intent)` evaluates the policy SDK-side and, on pass, produces a\n * real EIP-3009 authorization via {@link signTransferWithAuthorization}. It is\n * **advisory** because the wrapped key can still sign anything elsewhere — the\n * gate is only reached if the caller chooses to call *this* `sign()`. Use it for\n * dev, the A1 cross-language fallback, and any flow that is explicitly not\n * bounded/regulated; the type-gate (`requireNonBypassable`) keeps it out of\n * flows that require non-bypassable enforcement.\n *\n * @packageDocumentation\n */\n\nimport type { Account } from \"viem\";\nimport { getAddress } from \"viem\";\nimport type { SpendingPolicy, SpendState } from \"../policy/spending-policy\";\nimport { evaluateSpendingPolicy } from \"../policy/spending-policy\";\nimport type { X402AssetParam } from \"../tokens/asset-domain\";\nimport { resolveAssetParam } from \"../tokens/asset-domain\";\nimport { signTransferWithAuthorization } from \"../tokens/eip3009\";\nimport { PolicyGatedSignerConfigError } from \"./errors\";\nimport type { PaymentIntent, PolicyGatedSigner, SignerDescription, SignResult } from \"./types\";\n\n/** Parameters for {@link createLocalPolicyGatedSigner}. */\nexport interface CreateLocalPolicyGatedSignerParams {\n\t/** EOA / LocalAccount that signs the EIP-3009 authorization. */\n\treadonly account: Account;\n\t/** The spending policy this signer enforces (SDK-side, advisory). */\n\treadonly policy: SpendingPolicy;\n\t/** Asset binding — pins the EIP-712 domain `name`/`version`/`verifyingContract`. */\n\treadonly asset: X402AssetParam;\n\t/**\n\t * Required literal acknowledgement that this signer is **advisory** (a\n\t * key-holder can bypass its policy). Omitting it is a compile error (TS) and\n\t * a construction-time throw (JS) — so constructing an advisory signer is a\n\t * conscious, greppable act. For bounded/regulated flows use a cryptographic\n\t * adapter instead.\n\t */\n\treadonly acknowledgeAdvisory: true;\n\t/**\n\t * Optional cumulative-spend view (read-only) the adapter evaluates\n\t * `cumulativeCap` against. `local` does not own an authoritative ledger; the\n\t * caller folds a successful spend back in (e.g. via `mergeSpendState`) before\n\t * the next call. Default: empty.\n\t */\n\treadonly spendState?: () => SpendState | Promise<SpendState>;\n}\n\n/**\n * Construct a `local` (advisory) PolicyGatedSigner.\n *\n * @example\n * ```ts\n * const signer = createLocalPolicyGatedSigner({\n * account,\n * policy: createSpendingPolicy({ session: { id, notAfter }, perToken: [{ token: JPYC, maxPerSign: 1_000n }] }),\n * asset: { kind: \"known\", id: \"jpyc-v2\" },\n * acknowledgeAdvisory: true,\n * });\n * const result = await signer.sign(intent);\n * ```\n */\nexport function createLocalPolicyGatedSigner(\n\tparams: CreateLocalPolicyGatedSignerParams,\n): PolicyGatedSigner<\"advisory\"> {\n\tif (params.acknowledgeAdvisory !== true) {\n\t\tthrow new PolicyGatedSignerConfigError(\n\t\t\t\"acknowledgeAdvisory\",\n\t\t\t\"a local signer is advisory (a key-holder can bypass its policy); pass `acknowledgeAdvisory: true` to construct one consciously, or use a cryptographic adapter for bounded/regulated flows\",\n\t\t);\n\t}\n\n\tconst { account, policy, spendState } = params;\n\tconst pinned = resolveAssetParam(params.asset);\n\tconst from = getAddress(account.address);\n\n\treturn {\n\t\tenforcement: \"advisory\",\n\t\tfrom,\n\t\tasync sign(intent: PaymentIntent): Promise<SignResult> {\n\t\t\tif (getAddress(intent.from) !== from) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\trejection: {\n\t\t\t\t\t\treason: \"from_mismatch\",\n\t\t\t\t\t\tdetail: `intent.from ${getAddress(intent.from)} does not equal signer.from ${from}`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (getAddress(intent.token) !== pinned.verifyingContract) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\trejection: {\n\t\t\t\t\t\treason: \"token_not_allowed\",\n\t\t\t\t\t\tdetail: `intent.token ${getAddress(intent.token)} does not equal the signer's pinned verifyingContract ${pinned.verifyingContract}`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst state: SpendState = (await spendState?.()) ?? { spentPerToken: [] };\n\t\t\tconst nowSeconds = BigInt(Math.floor(Date.now() / 1000));\n\t\t\tconst decision = evaluateSpendingPolicy(policy, intent, state, nowSeconds);\n\t\t\tif (!decision.ok) {\n\t\t\t\treturn { ok: false, rejection: decision.rejection };\n\t\t\t}\n\n\t\t\tconst signed = await signTransferWithAuthorization(\n\t\t\t\taccount,\n\t\t\t\t{\n\t\t\t\t\tname: pinned.name,\n\t\t\t\t\tversion: pinned.version,\n\t\t\t\t\tchainId: intent.chainId,\n\t\t\t\t\tverifyingContract: pinned.verifyingContract,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfrom,\n\t\t\t\t\tto: intent.to,\n\t\t\t\t\tvalue: intent.value,\n\t\t\t\t\tvalidAfter: intent.validAfter,\n\t\t\t\t\tvalidBefore: intent.validBefore,\n\t\t\t\t\tnonce: intent.nonce,\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn { ok: true, signature: signed.signature, intent };\n\t\t},\n\t\tdescribe(): SignerDescription {\n\t\t\treturn {\n\t\t\t\tenforcement: \"advisory\",\n\t\t\t\tfrom,\n\t\t\t\tpolicyId: policy.session.id,\n\t\t\t\tnotAfter: policy.session.notAfter,\n\t\t\t\trevoked: policy.revoked,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}