@kushbitx/sdk 0.1.5 → 0.1.7
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 +30 -0
- package/kushbitx.d.ts +22 -2
- package/kushbitx.mjs +171 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -62,6 +62,34 @@ 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
|
+
|
|
83
|
+
**Signed verdicts (0.1.7).** An ALLOW from Signature Check carries `signedVerdict`: an EIP-712 `SignatureVerdict` signed by a KushBitx key, valid at most 60 seconds. When it is present, `canSignTypedData` also verifies it: same `typedDataDigest` and signer, chainId 8453, ALLOW, not expired, valid signature. For full trust, pass the active signer addresses from https://kushbitx.com/.well-known/kushbitx-preflight-signers.json:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const signers = (await (await fetch('https://kushbitx.com/.well-known/kushbitx-preflight-signers.json')).json())
|
|
87
|
+
.signers.filter(s => s.status === 'active').map(s => s.address);
|
|
88
|
+
if (await canSignTypedData(report, request, Date.now(), { signers, requireSigned: true })) { /* sign now */ }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`verifySignedVerdict(signedVerdict, { primaryType, signers })` checks any KushBitx signed verdict on its own. Always pass `primaryType`: a `SignatureVerdict` (Signature Check) is never a `PreflightVerdict` (Preflight). `recoverAddress(digest, signature)` is the dependency-free secp256k1 recovery it uses.
|
|
92
|
+
|
|
65
93
|
Required checks per transaction type: `requiredChecks` in https://kushbitx.com/api/services.
|
|
66
94
|
|
|
67
95
|
Current prices: https://kushbitx.com/api/services
|
|
@@ -99,6 +127,8 @@ import { KushBitxClient } from 'https://kushbitx.com/sdk/kushbitx.mjs';
|
|
|
99
127
|
- Tools: https://kushbitx.com/#tools
|
|
100
128
|
- SpendGuard: https://kushbitx.com/spendguard
|
|
101
129
|
- Integration example: https://kushbitx.com/examples/agent.mjs
|
|
130
|
+
- Clodds skill (optional, external): [integrations/clodds/kushbitx-preflight](integrations/clodds/kushbitx-preflight). Copy the folder into .clodds/skills/ to run KushBitx Preflight before signing Base transactions. Not part of CloddsBot and not endorsed by its maintainers.
|
|
131
|
+
- Franklin PreSpend hook (optional, external): [integrations/franklin/kushbitx-token-risk](integrations/franklin/kushbitx-token-risk). Screens the token a Base0xSwap buys with KushBitx Token Risk before the swap runs. It sees token names, not the transaction; read its limits first. Not part of Franklin and not endorsed by BlockRun.
|
|
102
132
|
- Contact: Admin@kushbitx.com
|
|
103
133
|
|
|
104
134
|
## Community contributions
|
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,23 @@ 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[]; signedVerdict?: SignedVerdict | null; signedVerdictNote?: 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
|
+
/** A KushBitx signed verdict (ALLOW only). Preflight: primaryType PreflightVerdict. Signature Check: primaryType SignatureVerdict. Never interchangeable. */
|
|
140
|
+
export interface SignedVerdict { scheme: "eip712"; primaryType?: "PreflightVerdict" | "SignatureVerdict"; keyId: string; signer: Address; rulesVersion: string; typedData: TypedData; digest: `0x${string}`; signature: `0x${string}`; verify?: string; [k: string]: unknown }
|
|
141
|
+
/** Address that produced a 65-byte r,s,v signature over a 32-byte digest, or null (high s and v other than 27/28 are rejected). No dependency, no network. */
|
|
142
|
+
export declare function recoverAddress(digest: string, signature: string): Address | null;
|
|
143
|
+
/** Verifies a KushBitx signedVerdict: domain KushBitx Preflight / 1 / 8453, primaryType, recomputed digest and signature. Pass signers (active addresses from /.well-known/kushbitx-preflight-signers.json) for full trust. Never throws. */
|
|
144
|
+
export declare function verifySignedVerdict(signedVerdict: SignedVerdict | unknown, options?: { primaryType?: "PreflightVerdict" | "SignatureVerdict"; signers?: string[] }): { valid: boolean; signer: Address | null; reason: string | null };
|
|
145
|
+
/** True only for ALLOW on this exact typed data, for this signer, before expiresAt. request.chainId and typedData.domain.chainId must both be 8453. When the report carries signedVerdict, it must verify too (SignatureVerdict, same digest and signer, not expired); options.signers restricts the key; options.requireSigned makes it mandatory. Anything else is false. */
|
|
146
|
+
export declare function canSignTypedData(report: SignatureCheckReport | Record<string, unknown>, request: { chainId: number | string | bigint; signer: Address; typedData: TypedData }, now?: number, options?: { signers?: string[]; requireSigned?: boolean }): Promise<boolean>;
|
|
147
|
+
|
|
130
148
|
export declare class KushBitxClient {
|
|
131
149
|
constructor(options?: ClientOptions);
|
|
132
150
|
baseUrl: string;
|
|
@@ -146,6 +164,7 @@ export declare class KushBitxClient {
|
|
|
146
164
|
|
|
147
165
|
/** Ask for the x402 challenge (HTTP 402). Nothing is charged. */
|
|
148
166
|
getPaymentChallenge(service: 'kushbitx-preflight', input: PreflightInput): Promise<PaymentChallenge>;
|
|
167
|
+
getPaymentChallenge(service: 'signature-check', input: SignatureCheckInput): Promise<PaymentChallenge>;
|
|
149
168
|
getPaymentChallenge(service: 'token-risk', input: TokenInput): Promise<PaymentChallenge>;
|
|
150
169
|
getPaymentChallenge(service: 'preflight' | 'transaction-preflight', input: TransactionInput): Promise<PaymentChallenge>;
|
|
151
170
|
getPaymentChallenge(service: 'payment' | 'payment-proof' | 'verify-payment', input: PaymentProofInput): Promise<PaymentChallenge>;
|
|
@@ -153,6 +172,7 @@ export declare class KushBitxClient {
|
|
|
153
172
|
prepareRecovery(service: PaidService, input: object): Promise<RecoveryCredentials>;
|
|
154
173
|
/** Send an externally signed payment (PAYMENT-SIGNATURE). Returns the report, or a pending status (HTTP 202). */
|
|
155
174
|
submitPaidCheck(service: 'kushbitx-preflight', input: PreflightInput, auth: { paymentSignature: string; reportId: string; recoveryKey: string }): Promise<PreflightReport | Record<string, unknown>>;
|
|
175
|
+
submitPaidCheck(service: 'signature-check', input: SignatureCheckInput, auth: { paymentSignature: string; reportId: string; recoveryKey: string }): Promise<SignatureCheckReport | Record<string, unknown>>;
|
|
156
176
|
submitPaidCheck(service: PaidService, input: object, auth: { paymentSignature: string; reportId: string; recoveryKey: string }): Promise<Record<string, unknown>>;
|
|
157
177
|
restoreReport(args: { id: string; key: string; txHash?: string }): Promise<Record<string, unknown>>;
|
|
158
178
|
}
|
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,171 @@ 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
|
+
// secp256k1 public-key recovery (no dependency), used only to verify KushBitx signed verdicts.
|
|
250
|
+
const SECP_P = 2n ** 256n - 2n ** 32n - 977n;
|
|
251
|
+
const SECP_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
252
|
+
const SECP_G = [0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n];
|
|
253
|
+
const modp = (a, m = SECP_P) => { const r = a % m; return r >= 0n ? r : r + m; };
|
|
254
|
+
function modInv(a, m) { let [x, y, u, v] = [0n, 1n, m, modp(a, m)]; while (v !== 0n) { const q = u / v; [x, y] = [y, x - q * y]; [u, v] = [v, u - q * v]; } if (u !== 1n) throw new Error('no inverse'); return modp(x, m); }
|
|
255
|
+
function modPow(b, e, m) { let r = 1n; b = modp(b, m); while (e > 0n) { if (e & 1n) r = r * b % m; b = b * b % m; e >>= 1n; } return r; }
|
|
256
|
+
// Jacobian coordinates [X, Y, Z]; null is the point at infinity.
|
|
257
|
+
function jDouble(P) { if (!P || P[1] === 0n) return null; const [X, Y, Z] = P; const S = modp(4n * X * Y * Y), M = modp(3n * X * X), X3 = modp(M * M - 2n * S); return [X3, modp(M * (S - X3) - 8n * Y ** 4n), modp(2n * Y * Z)]; }
|
|
258
|
+
function jAdd(P, Q) {
|
|
259
|
+
if (!P) return Q; if (!Q) return P;
|
|
260
|
+
const [X1, Y1, Z1] = P, [X2, Y2, Z2] = Q, Z1s = modp(Z1 * Z1), Z2s = modp(Z2 * Z2);
|
|
261
|
+
const U1 = modp(X1 * Z2s), U2 = modp(X2 * Z1s), S1 = modp(Y1 * Z2s * Z2), S2 = modp(Y2 * Z1s * Z1);
|
|
262
|
+
if (U1 === U2) return S1 === S2 ? jDouble(P) : null;
|
|
263
|
+
const H = modp(U2 - U1), R = modp(S2 - S1), H2 = modp(H * H), H3 = modp(H2 * H), X3 = modp(R * R - H3 - 2n * U1 * H2);
|
|
264
|
+
return [X3, modp(R * (U1 * H2 - X3) - S1 * H3), modp(H * Z1 * Z2)];
|
|
265
|
+
}
|
|
266
|
+
function jMul(P, k) { let R = null, A = P; while (k > 0n) { if (k & 1n) R = jAdd(R, A); A = jDouble(A); k >>= 1n; } return R; }
|
|
267
|
+
function toAffine(P) { if (!P) return null; const zi = modInv(P[2], SECP_P), zi2 = modp(zi * zi); return [modp(P[0] * zi2), modp(P[1] * zi2 * zi)]; }
|
|
268
|
+
const hex32 = n => n.toString(16).padStart(64, '0');
|
|
269
|
+
/** Address that produced a 65-byte r,s,v signature over a 32-byte digest, or null. Rejects high s and v other than 27/28. */
|
|
270
|
+
export function recoverAddress(digestHex, signatureHex) {
|
|
271
|
+
try {
|
|
272
|
+
const sig = String(signatureHex).replace(/^0x/i, ''), dig = String(digestHex).replace(/^0x/i, '');
|
|
273
|
+
if (!/^[0-9a-f]{130}$/i.test(sig) || !/^[0-9a-f]{64}$/i.test(dig)) return null;
|
|
274
|
+
const r = BigInt('0x' + sig.slice(0, 64)), s = BigInt('0x' + sig.slice(64, 128)), v = parseInt(sig.slice(128), 16);
|
|
275
|
+
if ((v !== 27 && v !== 28) || r <= 0n || r >= SECP_N || s <= 0n || s > SECP_N / 2n) return null;
|
|
276
|
+
const y2 = modp(r ** 3n + 7n); let y = modPow(y2, (SECP_P + 1n) / 4n, SECP_P);
|
|
277
|
+
if (modp(y * y) !== y2) return null;
|
|
278
|
+
if ((y & 1n) !== BigInt(v - 27)) y = SECP_P - y;
|
|
279
|
+
const e = BigInt('0x' + dig) % SECP_N, rInv = modInv(r, SECP_N);
|
|
280
|
+
const Q = toAffine(jAdd(jMul([SECP_G[0], SECP_G[1], 1n], modp(-e * rInv, SECP_N)), jMul([r, y, 1n], modp(s * rInv, SECP_N))));
|
|
281
|
+
if (!Q) return null;
|
|
282
|
+
const hash = keccak256(hexBytes(hex32(Q[0]) + hex32(Q[1])));
|
|
283
|
+
return '0x' + Array.from(hash.slice(12), b => b.toString(16).padStart(2, '0')).join('');
|
|
284
|
+
} catch { return null; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const VERDICT_PRIMARY_TYPES = ['PreflightVerdict', 'SignatureVerdict'];
|
|
288
|
+
/**
|
|
289
|
+
* Verifies a KushBitx signedVerdict (Preflight or Signature Check): the domain is KushBitx Preflight / 1 / 8453, the
|
|
290
|
+
* primaryType is the one expected, the EIP-712 digest recomputes, and the signature recovers to signedVerdict.signer.
|
|
291
|
+
* options.signers: addresses listed as active at /.well-known/kushbitx-preflight-signers.json; pass them for full trust.
|
|
292
|
+
* options.primaryType: 'PreflightVerdict' or 'SignatureVerdict' (always pass it; the two are never interchangeable).
|
|
293
|
+
* Returns { valid, signer, reason }. Never throws.
|
|
294
|
+
*/
|
|
295
|
+
export function verifySignedVerdict(signedVerdict, options = {}) {
|
|
296
|
+
const fail = reason => ({ valid: false, signer: null, reason });
|
|
297
|
+
try {
|
|
298
|
+
if (!signedVerdict || typeof signedVerdict !== 'object') return fail('no signedVerdict');
|
|
299
|
+
const td = signedVerdict.typedData, d = td?.domain;
|
|
300
|
+
if (!td || !d || d.name !== 'KushBitx Preflight' || String(d.version) !== '1' || chainIdOf({ chainId: d.chainId }) !== BASE_CHAIN_ID) return fail('wrong domain');
|
|
301
|
+
if (!VERDICT_PRIMARY_TYPES.includes(td.primaryType) || (options.primaryType && td.primaryType !== options.primaryType)) return fail('wrong primaryType');
|
|
302
|
+
const digest = typedDataDigest(td);
|
|
303
|
+
if (String(signedVerdict.digest || '').toLowerCase() !== digest) return fail('digest does not match the typed data');
|
|
304
|
+
const signer = recoverAddress(digest, signedVerdict.signature);
|
|
305
|
+
if (!signer || signer !== String(signedVerdict.signer || '').toLowerCase()) return fail('bad signature');
|
|
306
|
+
if (Array.isArray(options.signers) && !options.signers.map(a => String(a).toLowerCase()).includes(signer)) return fail('signer not in the trusted list');
|
|
307
|
+
return { valid: true, signer, reason: null };
|
|
308
|
+
} catch { return fail('malformed signedVerdict'); }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* True only for an ALLOW Signature Check report on this exact typed data, for this signer, on Base, before expiresAt.
|
|
313
|
+
* request: { chainId: 8453 (stated), signer, typedData }. typedData.domain.chainId must also be 8453: a signature
|
|
314
|
+
* without a chain in its domain could be replayed elsewhere, so it is never accepted. Anything else: do not sign.
|
|
315
|
+
* When the report carries signedVerdict, it must also verify: a SignatureVerdict for the same typedDataDigest and
|
|
316
|
+
* signer, chainId 8453, ALLOW, not expired, with a valid KushBitx signature. options.signers (active addresses from
|
|
317
|
+
* /.well-known/kushbitx-preflight-signers.json) restricts the key; options.requireSigned makes signedVerdict mandatory.
|
|
318
|
+
*/
|
|
319
|
+
export async function canSignTypedData(report, request, now = Date.now(), options = {}) {
|
|
320
|
+
if (!report || typeof report !== 'object' || report.verdict !== 'ALLOW' || typeof report.typedDataDigest !== 'string') return false;
|
|
321
|
+
if (!request || typeof request !== 'object' || request.chainId === undefined || request.chainId === null || chainIdOf(request) !== BASE_CHAIN_ID) return false;
|
|
322
|
+
if (report.chainId !== undefined && Number(report.chainId) !== BASE_CHAIN_ID) return false;
|
|
323
|
+
const domainChain = request.typedData?.domain?.chainId;
|
|
324
|
+
if (domainChain === undefined || domainChain === null || chainIdOf({ chainId: domainChain }) !== BASE_CHAIN_ID) return false;
|
|
325
|
+
if (typeof request.signer !== 'string' || typeof report.signer !== 'string' || request.signer.toLowerCase() !== report.signer.toLowerCase()) return false;
|
|
326
|
+
if (!report.expiresAt || !(Date.parse(report.expiresAt) > now)) return false;
|
|
327
|
+
let local;
|
|
328
|
+
try { local = typedDataDigest(request.typedData); } catch { return false; }
|
|
329
|
+
if (report.typedDataDigest.toLowerCase() !== local) return false;
|
|
330
|
+
const sv = report.signedVerdict;
|
|
331
|
+
if (sv === undefined || sv === null) return !options.requireSigned;
|
|
332
|
+
const m = sv.typedData?.message;
|
|
333
|
+
if (!m || String(m.typedDataDigest || '').toLowerCase() !== local || String(m.signer || '').toLowerCase() !== request.signer.toLowerCase()) return false;
|
|
334
|
+
if (Number(m.chainId) !== BASE_CHAIN_ID || Number(m.verdict) !== 1 || !(Number(m.expiresAt) * 1000 > now)) return false;
|
|
335
|
+
return verifySignedVerdict(sv, { primaryType: 'SignatureVerdict', signers: options.signers }).valid;
|
|
336
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kushbitx/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|