@kushbitx/sdk 0.1.4 → 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 +19 -1
- package/kushbitx.d.ts +16 -2
- package/kushbitx.mjs +102 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Zero-dependency ESM client and runnable Node.js example for the complete KushBit
|
|
|
12
12
|
| Token-risk report | 0.001 USDC | Receive structured token-risk evidence with missing checks identified. |
|
|
13
13
|
| Transaction preflight | 0.001 USDC | Inspect an unsigned Base transaction before signing. |
|
|
14
14
|
| Payment verification | 0.00288 USDC | Verify an existing Base USDC payment against the expected recipient and amount. |
|
|
15
|
-
| KushBitx Preflight | 0.005 USDC | One verdict (ALLOW, REVIEW, BLOCK) before money moves, fail-closed: transaction simulation, approval and transfer decoding, token and recipient screening, optional protected SpendGuard policy. Does not detect prompt injection
|
|
15
|
+
| KushBitx Preflight | 0.005 USDC | One verdict (ALLOW, REVIEW, BLOCK) before money moves, fail-closed: transaction simulation, approval and transfer decoding, token and recipient screening, optional protected SpendGuard policy. Does not detect prompt injection; Protected SpendGuard can cap what an agent pays if your signer enforces it, but does not detect prompt injection either. Service name `kushbitx-preflight`. |
|
|
16
16
|
|
|
17
17
|
Paid endpoints use x402 and require explicit payment. There is no subscription. A delivered report can be `INCOMPLETE` when evidence is unavailable and may still incur the displayed fee.
|
|
18
18
|
|
|
@@ -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.
|
|
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.
|
|
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",
|