@kushbitx/sdk 0.1.5 → 0.1.6

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
@@ -62,6 +62,24 @@ if (await canSign(report, tx)) {
62
62
  }
63
63
  ```
64
64
 
65
+ ### Signature Check: sign only the typed data that was checked
66
+
67
+ Before an agent signs an EIP-712 request (an x402 payment authorisation, a Permit or a Permit2 message), pay 0.003 USDC for `POST /api/signature-check` with `{ chainId: 8453, signer, typedData }`. The report has `verdict`, `typedDataDigest` and `expiresAt`. `canSignTypedData` recomputes the EIP-712 digest locally and is true only for ALLOW on the same typed data, for the same signer, on Base (`chainId: 8453` in the request and in `typedData.domain`), before `expiresAt`.
68
+
69
+ ```js
70
+ import { canSignTypedData } from '@kushbitx/sdk';
71
+
72
+ const request = { chainId: 8453, signer: wallet, typedData }; // the exact object you are asked to sign
73
+ // ... pay for POST /api/signature-check with your x402 client, get `report` ...
74
+ if (await canSignTypedData(report, request)) {
75
+ // ALLOW, same digest, same signer, not expired: sign now
76
+ } else {
77
+ // REVIEW, BLOCK, changed typed data, another signer or network, or expired: do not sign
78
+ }
79
+ ```
80
+
81
+ `typedDataDigest(typedData)` returns the same digest on its own. Neither function uses the network or a key.
82
+
65
83
  Required checks per transaction type: `requiredChecks` in https://kushbitx.com/api/services.
66
84
 
67
85
  Current prices: https://kushbitx.com/api/services
package/kushbitx.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- // Type declarations for @kushbitx/sdk 0.1.3. The SDK never requests, stores or signs with a private key.
1
+ // Type declarations for @kushbitx/sdk 0.1.6. The SDK never requests, stores or signs with a private key.
2
2
 
3
3
  /** Paid service names accepted by the payment helpers, and the route each one calls. */
4
4
  export type PaidService =
5
5
  | 'token-risk'
6
6
  | 'preflight' | 'transaction-preflight'
7
7
  | 'payment' | 'payment-proof' | 'verify-payment'
8
- | 'kushbitx-preflight';
8
+ | 'kushbitx-preflight'
9
+ | 'signature-check';
9
10
 
10
11
  export declare const paidServices: Readonly<Record<PaidService, string>>;
11
12
 
@@ -127,6 +128,17 @@ export declare function preflightDigest(transaction: { chainId?: number | string
127
128
  /** True only for ALLOW on this exact Base transaction before expiresAt. chainId must be stated and be 8453; a missing or other chainId, or any changed field, is false. */
128
129
  export declare function canSign(report: PreflightReport | Record<string, unknown>, transaction: { chainId: number | string | bigint; from?: Address | null; to: Address; value?: string; data?: string }, now?: number): Promise<boolean>;
129
130
 
131
+ /** EIP-712 typed data: the exact object a wallet is asked to sign. */
132
+ export interface TypedData { domain: { name?: string; version?: string; chainId?: number | string | bigint; verifyingContract?: Address; salt?: string }; types: Record<string, { name: string; type: string }[]>; primaryType: string; message: Record<string, unknown> }
133
+ /** Input of Signature Check (0.003 USDC). */
134
+ export interface SignatureCheckInput { chainId: 8453; signer: Address; typedData: TypedData; expectedPayTo?: Address; expectedAmount?: string; expectedSpender?: Address }
135
+ /** Signature Check report (fields used by canSignTypedData). */
136
+ export interface SignatureCheckReport { service: 'signature-check'; verdict: 'ALLOW' | 'REVIEW' | 'BLOCK'; typedDataDigest: `0x${string}`; chainId: 8453; signer: Address; expiresAt: string; kind: string; reasonCodes: string[]; [k: string]: unknown }
137
+ /** EIP-712 digest of typed data, computed locally (no network). Equals typedDataDigest in a Signature Check report. Throws on malformed typed data. */
138
+ export declare function typedDataDigest(typedData: TypedData): `0x${string}`;
139
+ /** True only for ALLOW on this exact typed data, for this signer, before expiresAt. request.chainId and typedData.domain.chainId must both be 8453; anything else is false. */
140
+ export declare function canSignTypedData(report: SignatureCheckReport | Record<string, unknown>, request: { chainId: number | string | bigint; signer: Address; typedData: TypedData }, now?: number): Promise<boolean>;
141
+
130
142
  export declare class KushBitxClient {
131
143
  constructor(options?: ClientOptions);
132
144
  baseUrl: string;
@@ -146,6 +158,7 @@ export declare class KushBitxClient {
146
158
 
147
159
  /** Ask for the x402 challenge (HTTP 402). Nothing is charged. */
148
160
  getPaymentChallenge(service: 'kushbitx-preflight', input: PreflightInput): Promise<PaymentChallenge>;
161
+ getPaymentChallenge(service: 'signature-check', input: SignatureCheckInput): Promise<PaymentChallenge>;
149
162
  getPaymentChallenge(service: 'token-risk', input: TokenInput): Promise<PaymentChallenge>;
150
163
  getPaymentChallenge(service: 'preflight' | 'transaction-preflight', input: TransactionInput): Promise<PaymentChallenge>;
151
164
  getPaymentChallenge(service: 'payment' | 'payment-proof' | 'verify-payment', input: PaymentProofInput): Promise<PaymentChallenge>;
@@ -153,6 +166,7 @@ export declare class KushBitxClient {
153
166
  prepareRecovery(service: PaidService, input: object): Promise<RecoveryCredentials>;
154
167
  /** Send an externally signed payment (PAYMENT-SIGNATURE). Returns the report, or a pending status (HTTP 202). */
155
168
  submitPaidCheck(service: 'kushbitx-preflight', input: PreflightInput, auth: { paymentSignature: string; reportId: string; recoveryKey: string }): Promise<PreflightReport | Record<string, unknown>>;
169
+ submitPaidCheck(service: 'signature-check', input: SignatureCheckInput, auth: { paymentSignature: string; reportId: string; recoveryKey: string }): Promise<SignatureCheckReport | Record<string, unknown>>;
156
170
  submitPaidCheck(service: PaidService, input: object, auth: { paymentSignature: string; reportId: string; recoveryKey: string }): Promise<Record<string, unknown>>;
157
171
  restoreReport(args: { id: string; key: string; txHash?: string }): Promise<Record<string, unknown>>;
158
172
  }
package/kushbitx.mjs CHANGED
@@ -6,7 +6,9 @@ const PAID_SERVICES = Object.freeze({
6
6
  'payment-proof': '/api/verify-payment',
7
7
  'verify-payment': '/api/verify-payment',
8
8
  // KushBitx Preflight (0.005 USDC): one verdict before money moves. 'preflight' stays Transaction Preflight.
9
- 'kushbitx-preflight': '/api/preflight'
9
+ 'kushbitx-preflight': '/api/preflight',
10
+ // Signature Check (0.003 USDC): ALLOW, REVIEW or BLOCK for an EIP-712 signature request, bound to its digest.
11
+ 'signature-check': '/api/signature-check'
10
12
  });
11
13
 
12
14
  function required(value, name) {
@@ -164,3 +166,102 @@ export async function canSign(report, transaction, now = Date.now()) {
164
166
  return false;
165
167
  }
166
168
  }
169
+
170
+ // Signature Check: the EIP-712 digest the service returns in typedDataDigest, computed locally with no network and
171
+ // no dependency: keccak256(0x19 0x01 ‖ domainSeparator ‖ hashStruct(primaryType, message)).
172
+ const MASK64 = (1n << 64n) - 1n;
173
+ const KECCAK_RC = [0x1n, 0x8082n, 0x800000000000808an, 0x8000000080008000n, 0x808bn, 0x80000001n, 0x8000000080008081n, 0x8000000000008009n, 0x8an, 0x88n, 0x80008009n, 0x8000000an, 0x8000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n, 0x800an, 0x800000008000000an, 0x8000000080008081n, 0x8000000000008080n, 0x80000001n, 0x8000000080008008n];
174
+ const KECCAK_ROT = [0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14];
175
+ const rotl64 = (v, n) => (n === 0 ? v : ((v << BigInt(n)) | (v >> BigInt(64 - n))) & MASK64);
176
+ function keccakF(st) {
177
+ for (let round = 0; round < 24; round++) {
178
+ const c = [0, 1, 2, 3, 4].map(x => st[x] ^ st[x + 5] ^ st[x + 10] ^ st[x + 15] ^ st[x + 20]);
179
+ for (let x = 0; x < 5; x++) { const d = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1); for (let y = 0; y < 25; y += 5) st[y + x] ^= d; }
180
+ const b = new Array(25);
181
+ for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(st[x + 5 * y], KECCAK_ROT[x + 5 * y]);
182
+ for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) st[x + 5 * y] = b[x + 5 * y] ^ (~b[(x + 1) % 5 + 5 * y] & MASK64 & b[(x + 2) % 5 + 5 * y]);
183
+ st[0] ^= KECCAK_RC[round];
184
+ }
185
+ }
186
+ function keccak256(bytes) {
187
+ const rate = 136, st = new Array(25).fill(0n);
188
+ const padded = new Uint8Array(Math.floor(bytes.length / rate) * rate + rate);
189
+ padded.set(bytes); padded[bytes.length] ^= 0x01; padded[padded.length - 1] ^= 0x80;
190
+ for (let off = 0; off < padded.length; off += rate) {
191
+ for (let i = 0; i < rate / 8; i++) { let lane = 0n; for (let j = 7; j >= 0; j--) lane = (lane << 8n) | BigInt(padded[off + i * 8 + j]); st[i] ^= lane; }
192
+ keccakF(st);
193
+ }
194
+ const out = new Uint8Array(32);
195
+ for (let i = 0; i < 4; i++) for (let j = 0; j < 8; j++) out[i * 8 + j] = Number((st[i] >> BigInt(8 * j)) & 0xffn);
196
+ return out;
197
+ }
198
+ const hexBytes = hex => { const h = String(hex).replace(/^0x/i, ''); if (h.length % 2 || /[^0-9a-f]/i.test(h)) throw new Error('Invalid hex in typedData'); return Uint8Array.from(h.match(/../g) || [], b => parseInt(b, 16)); };
199
+ const concatBytes = parts => { const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; };
200
+ const utf8 = text => new TextEncoder().encode(String(text));
201
+ const word256 = n => hexBytes(BigInt.asUintN(256, BigInt(n)).toString(16).padStart(64, '0'));
202
+ function typeDeps(primary, types, found = new Set()) {
203
+ const base = primary.replace(/\[.*$/, '');
204
+ if (found.has(base) || !types[base]) return found;
205
+ found.add(base);
206
+ for (const field of types[base]) typeDeps(field.type, types, found);
207
+ return found;
208
+ }
209
+ function encodeType(primary, types) {
210
+ return [primary, ...[...typeDeps(primary, types)].filter(t => t !== primary).sort()].map(t => {
211
+ if (!Array.isArray(types[t])) throw new Error('Unknown type in typedData: ' + t);
212
+ return t + '(' + types[t].map(f => f.type + ' ' + f.name).join(',') + ')';
213
+ }).join('');
214
+ }
215
+ function encodeValue(type, value, types) {
216
+ if (types[type]) return hashStruct(type, value, types);
217
+ const array = type.match(/^(.*)\[(\d*)\]$/);
218
+ if (array) {
219
+ if (!Array.isArray(value)) throw new Error('Expected an array for ' + type);
220
+ if (array[2] !== '' && value.length !== Number(array[2])) throw new Error('Wrong array length for ' + type);
221
+ return keccak256(concatBytes(value.map(v => encodeValue(array[1], v, types))));
222
+ }
223
+ if (type === 'string') return keccak256(utf8(value));
224
+ if (type === 'bytes') return keccak256(hexBytes(value));
225
+ if (type === 'bool') return word256(value === true || value === 'true' ? 1 : 0);
226
+ if (type === 'address') { if (!/^0x[0-9a-fA-F]{40}$/.test(String(value))) throw new Error('Invalid address in typedData'); return word256(BigInt(value)); }
227
+ const bytesN = type.match(/^bytes(\d+)$/);
228
+ if (bytesN) { const b = hexBytes(value); if (b.length !== Number(bytesN[1])) throw new Error('Wrong length for ' + type); const out = new Uint8Array(32); out.set(b); return out; }
229
+ const int = type.match(/^(u?)int(\d*)$/);
230
+ if (int) { const n = BigInt(value); if (int[1] === 'u' && (n < 0n || n >= 1n << BigInt(Number(int[2] || 256)))) throw new Error('Value out of range for ' + type); return word256(n); }
231
+ throw new Error('Unsupported type in typedData: ' + type);
232
+ }
233
+ function hashStruct(primary, data, types) {
234
+ if (!data || typeof data !== 'object') throw new Error('Missing struct value for ' + primary);
235
+ return keccak256(concatBytes([keccak256(utf8(encodeType(primary, types))), ...types[primary].map(f => encodeValue(f.type, data[f.name], types))]));
236
+ }
237
+ const DOMAIN_FIELDS = [['name', 'string'], ['version', 'string'], ['chainId', 'uint256'], ['verifyingContract', 'address'], ['salt', 'bytes32']];
238
+ /** EIP-712 digest of typed data ({domain, types, primaryType, message}), as 0x-hex. Same value as typedDataDigest in a Signature Check report. */
239
+ export function typedDataDigest(typedData) {
240
+ const { domain, types, primaryType, message } = typedData || {};
241
+ if (!domain || !types || !primaryType || !message) throw new Error('typedData needs domain, types, primaryType and message');
242
+ const own = Object.fromEntries(Object.entries(types).filter(([k]) => k !== 'EIP712Domain'));
243
+ if (!own[primaryType]) throw new Error('primaryType is not defined in types');
244
+ const domainTypes = { EIP712Domain: DOMAIN_FIELDS.filter(([n]) => domain[n] !== undefined && domain[n] !== null).map(([name, type]) => ({ name, type })) };
245
+ const digest = keccak256(concatBytes([new Uint8Array([0x19, 0x01]), hashStruct('EIP712Domain', domain, domainTypes), hashStruct(primaryType, message, own)]));
246
+ return '0x' + Array.from(digest, b => b.toString(16).padStart(2, '0')).join('');
247
+ }
248
+
249
+ /**
250
+ * True only for an ALLOW Signature Check report on this exact typed data, for this signer, on Base, before expiresAt.
251
+ * request: { chainId: 8453 (stated), signer, typedData }. typedData.domain.chainId must also be 8453: a signature
252
+ * without a chain in its domain could be replayed elsewhere, so it is never accepted. Anything else: do not sign.
253
+ */
254
+ export async function canSignTypedData(report, request, now = Date.now()) {
255
+ if (!report || typeof report !== 'object' || report.verdict !== 'ALLOW' || typeof report.typedDataDigest !== 'string') return false;
256
+ if (!request || typeof request !== 'object' || request.chainId === undefined || request.chainId === null || chainIdOf(request) !== BASE_CHAIN_ID) return false;
257
+ if (report.chainId !== undefined && Number(report.chainId) !== BASE_CHAIN_ID) return false;
258
+ const domainChain = request.typedData?.domain?.chainId;
259
+ if (domainChain === undefined || domainChain === null || chainIdOf({ chainId: domainChain }) !== BASE_CHAIN_ID) return false;
260
+ if (typeof request.signer !== 'string' || typeof report.signer !== 'string' || request.signer.toLowerCase() !== report.signer.toLowerCase()) return false;
261
+ if (!report.expiresAt || !(Date.parse(report.expiresAt) > now)) return false;
262
+ try {
263
+ return report.typedDataDigest.toLowerCase() === typedDataDigest(request.typedData);
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kushbitx/sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Zero-dependency ESM client for KushBitx AgentProof services on Base",
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,6 +26,7 @@
26
26
  "payment-security",
27
27
  "spendguard",
28
28
  "preflight",
29
+ "eip712",
29
30
  "typescript"
30
31
  ],
31
32
  "homepage": "https://github.com/BHamim/kushbitx-sdk#readme",