@oleary-labs/signet-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.ts +38 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +112 -0
- package/dist/admin.js.map +1 -0
- package/dist/authkey-session.d.ts +64 -0
- package/dist/authkey-session.d.ts.map +1 -0
- package/dist/authkey-session.js +164 -0
- package/dist/authkey-session.js.map +1 -0
- package/dist/bootstrap.d.ts +30 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +60 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundler.d.ts +85 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +160 -0
- package/dist/bundler.js.map +1 -0
- package/dist/delegate.d.ts +57 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +111 -0
- package/dist/delegate.js.map +1 -0
- package/dist/frostVerify.d.ts +23 -0
- package/dist/frostVerify.d.ts.map +1 -0
- package/dist/frostVerify.js +69 -0
- package/dist/frostVerify.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/jwks.d.ts +28 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +81 -0
- package/dist/jwks.js.map +1 -0
- package/dist/jwt.d.ts +27 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +50 -0
- package/dist/jwt.js.map +1 -0
- package/dist/keygen.d.ts +26 -0
- package/dist/keygen.d.ts.map +1 -0
- package/dist/keygen.js +60 -0
- package/dist/keygen.js.map +1 -0
- package/dist/oauth.d.ts +34 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +119 -0
- package/dist/oauth.js.map +1 -0
- package/dist/request.d.ts +42 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +115 -0
- package/dist/request.js.map +1 -0
- package/dist/scopedSign.d.ts +82 -0
- package/dist/scopedSign.d.ts.map +1 -0
- package/dist/scopedSign.js +130 -0
- package/dist/scopedSign.js.map +1 -0
- package/dist/server-prover.d.ts +29 -0
- package/dist/server-prover.d.ts.map +1 -0
- package/dist/server-prover.js +54 -0
- package/dist/server-prover.js.map +1 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/userop.d.ts +104 -0
- package/dist/userop.d.ts.map +1 -0
- package/dist/userop.js +212 -0
- package/dist/userop.js.map +1 -0
- package/dist/x402.d.ts +127 -0
- package/dist/x402.d.ts.map +1 -0
- package/dist/x402.js +167 -0
- package/dist/x402.js.map +1 -0
- package/package.json +64 -0
- package/src/admin.ts +178 -0
- package/src/authkey-session.ts +241 -0
- package/src/bootstrap.ts +106 -0
- package/src/bundler.ts +256 -0
- package/src/delegate.ts +163 -0
- package/src/frostVerify.ts +79 -0
- package/src/generate-inputs.ts +158 -0
- package/src/index.ts +43 -0
- package/src/jwks.ts +92 -0
- package/src/jwt.ts +74 -0
- package/src/keygen.ts +89 -0
- package/src/oauth.ts +157 -0
- package/src/partial-sha.ts +99 -0
- package/src/proof.ts +99 -0
- package/src/request.ts +174 -0
- package/src/scopedSign.ts +184 -0
- package/src/server-prover.ts +76 -0
- package/src/session.ts +33 -0
- package/src/types.ts +63 -0
- package/src/userop.ts +368 -0
- package/src/witness.ts +132 -0
- package/src/x402.ts +275 -0
package/src/bundler.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundler RPC client for ERC-4337 UserOperations.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic — all config is passed in, no env imports.
|
|
5
|
+
* Handles serialization between the packed UserOp format (used
|
|
6
|
+
* internally and for hashing) and the unpacked v0.7 RPC format
|
|
7
|
+
* expected by signet-min-bundler.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Address, type Hex, concat } from "viem";
|
|
11
|
+
import type { PackedUserOperation } from "./userop";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface BundlerSendResult {
|
|
18
|
+
userOpHash: Hex;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UserOpReceipt {
|
|
22
|
+
userOpHash: Hex;
|
|
23
|
+
transactionHash: Hex;
|
|
24
|
+
success: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GasEstimate {
|
|
28
|
+
preVerificationGas: Hex;
|
|
29
|
+
verificationGasLimit: Hex;
|
|
30
|
+
callGasLimit: Hex;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ERC-7677 paymaster sponsorship result.
|
|
35
|
+
*
|
|
36
|
+
* The bundler returns paymaster gas limits as separate fields, but
|
|
37
|
+
* signet-min-bundler's wire format for eth_sendUserOperation expects
|
|
38
|
+
* them packed into the front of `paymasterData` (see packPaymasterData
|
|
39
|
+
* and the note on the getHash comment in internal/paymaster/paymaster.go).
|
|
40
|
+
*/
|
|
41
|
+
export interface PaymasterSponsorship {
|
|
42
|
+
paymaster: Address;
|
|
43
|
+
paymasterData: Hex;
|
|
44
|
+
paymasterVerificationGasLimit: Hex;
|
|
45
|
+
paymasterPostOpGasLimit: Hex;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ERC-7677 context object passed as the 4th parameter to
|
|
50
|
+
* pm_getPaymasterStubData / pm_getPaymasterData.
|
|
51
|
+
*/
|
|
52
|
+
export interface PaymasterContext {
|
|
53
|
+
invite_code?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Bundler RPC
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export async function sendUserOp(
|
|
61
|
+
bundlerUrl: string,
|
|
62
|
+
entryPointAddress: Address,
|
|
63
|
+
userOp: PackedUserOperation
|
|
64
|
+
): Promise<BundlerSendResult> {
|
|
65
|
+
const json = await bundlerRpc(bundlerUrl, "eth_sendUserOperation", [
|
|
66
|
+
serializeUserOp(userOp),
|
|
67
|
+
entryPointAddress,
|
|
68
|
+
]);
|
|
69
|
+
return { userOpHash: json.result };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getUserOpReceipt(
|
|
73
|
+
bundlerUrl: string,
|
|
74
|
+
userOpHash: Hex
|
|
75
|
+
): Promise<UserOpReceipt | null> {
|
|
76
|
+
const json = await bundlerRpc(bundlerUrl, "eth_getUserOperationReceipt", [
|
|
77
|
+
userOpHash,
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
if (!json.result) return null;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
userOpHash: json.result.userOpHash,
|
|
84
|
+
transactionHash: json.result.transactionHash,
|
|
85
|
+
success: json.result.success,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function estimateUserOpGas(
|
|
90
|
+
bundlerUrl: string,
|
|
91
|
+
entryPointAddress: Address,
|
|
92
|
+
userOp: PackedUserOperation
|
|
93
|
+
): Promise<GasEstimate> {
|
|
94
|
+
const json = await bundlerRpc(bundlerUrl, "eth_estimateUserOperationGas", [
|
|
95
|
+
serializeUserOp(userOp),
|
|
96
|
+
entryPointAddress,
|
|
97
|
+
]);
|
|
98
|
+
return json.result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// ERC-7677 Paymaster
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Call pm_getPaymasterStubData.
|
|
107
|
+
*
|
|
108
|
+
* Returns paymaster fields with a correctly-sized but zeroed signature,
|
|
109
|
+
* suitable for gas estimation. Call this BEFORE eth_estimateUserOperationGas
|
|
110
|
+
* so the estimate accounts for the paymaster's verification overhead.
|
|
111
|
+
*/
|
|
112
|
+
export async function getPaymasterStubData(
|
|
113
|
+
bundlerUrl: string,
|
|
114
|
+
entryPointAddress: Address,
|
|
115
|
+
chainId: number,
|
|
116
|
+
userOp: PackedUserOperation,
|
|
117
|
+
context?: PaymasterContext,
|
|
118
|
+
): Promise<PaymasterSponsorship> {
|
|
119
|
+
return paymasterCall(bundlerUrl, entryPointAddress, chainId, "pm_getPaymasterStubData", userOp, context);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Call pm_getPaymasterData.
|
|
124
|
+
*
|
|
125
|
+
* Returns a real paymaster signature. The bundler first checks the
|
|
126
|
+
* paymaster contract's shouldSponsor() policy via eth_call; if sponsorship
|
|
127
|
+
* is rejected, this throws.
|
|
128
|
+
*
|
|
129
|
+
* IMPORTANT: call AFTER gas estimation, because the paymaster signs over
|
|
130
|
+
* the gas fields. Changing gas after this call invalidates the signature.
|
|
131
|
+
*/
|
|
132
|
+
export async function getPaymasterData(
|
|
133
|
+
bundlerUrl: string,
|
|
134
|
+
entryPointAddress: Address,
|
|
135
|
+
chainId: number,
|
|
136
|
+
userOp: PackedUserOperation,
|
|
137
|
+
context?: PaymasterContext,
|
|
138
|
+
): Promise<PaymasterSponsorship> {
|
|
139
|
+
return paymasterCall(bundlerUrl, entryPointAddress, chainId, "pm_getPaymasterData", userOp, context);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pack an ERC-7677 sponsorship response into the on-chain paymasterAndData
|
|
144
|
+
* layout expected by EntryPoint v0.7:
|
|
145
|
+
*
|
|
146
|
+
* [paymaster:20][verifGasLimit:16][postOpGasLimit:16][paymasterData:rest]
|
|
147
|
+
*
|
|
148
|
+
* The paymaster's getHash function reads paymasterAndData[20:52] as the
|
|
149
|
+
* packed (verifGasLimit || postOpGasLimit) uint128 pair — so the returned
|
|
150
|
+
* layout must match exactly or the paymaster signature will not verify
|
|
151
|
+
* on-chain (AA34 signature error).
|
|
152
|
+
*
|
|
153
|
+
* NOTE on the bundler's wire format: signet-min-bundler's FromRPC
|
|
154
|
+
* concatenates the JSON `paymaster` + `paymasterData` fields directly
|
|
155
|
+
* into paymasterAndData. It does NOT re-insert gas-limit bytes. So when
|
|
156
|
+
* serializing for eth_sendUserOperation, the `paymasterData` on the wire
|
|
157
|
+
* already includes the packed gas limits.
|
|
158
|
+
*/
|
|
159
|
+
export function applyPaymasterSponsorship(
|
|
160
|
+
userOp: PackedUserOperation,
|
|
161
|
+
s: PaymasterSponsorship
|
|
162
|
+
): PackedUserOperation {
|
|
163
|
+
const verifGas = BigInt(s.paymasterVerificationGasLimit);
|
|
164
|
+
const postOpGas = BigInt(s.paymasterPostOpGasLimit);
|
|
165
|
+
const packedGas = (`0x${verifGas.toString(16).padStart(32, "0")}${postOpGas.toString(16).padStart(32, "0")}`) as Hex;
|
|
166
|
+
return {
|
|
167
|
+
...userOp,
|
|
168
|
+
paymasterAndData: concat([s.paymaster, packedGas, s.paymasterData]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Internal
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async function paymasterCall(
|
|
177
|
+
bundlerUrl: string,
|
|
178
|
+
entryPointAddress: Address,
|
|
179
|
+
chainId: number,
|
|
180
|
+
method: "pm_getPaymasterStubData" | "pm_getPaymasterData",
|
|
181
|
+
userOp: PackedUserOperation,
|
|
182
|
+
context?: PaymasterContext,
|
|
183
|
+
): Promise<PaymasterSponsorship> {
|
|
184
|
+
const json = await bundlerRpc(bundlerUrl, method, [
|
|
185
|
+
serializeUserOp(userOp),
|
|
186
|
+
entryPointAddress,
|
|
187
|
+
`0x${chainId.toString(16)}`,
|
|
188
|
+
context ?? {},
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
paymaster: json.result.paymaster as Address,
|
|
193
|
+
paymasterData: json.result.paymasterData as Hex,
|
|
194
|
+
paymasterVerificationGasLimit: json.result.paymasterVerificationGasLimit as Hex,
|
|
195
|
+
paymasterPostOpGasLimit: json.result.paymasterPostOpGasLimit as Hex,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
|
+
async function bundlerRpc(bundlerUrl: string, method: string, params: any[]): Promise<any> {
|
|
201
|
+
const res = await fetch(bundlerUrl, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const json = await res.json();
|
|
208
|
+
if (json.error) {
|
|
209
|
+
throw new Error(`${method} failed: ${json.error.message}`);
|
|
210
|
+
}
|
|
211
|
+
return json;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Serialize a PackedUserOperation to the unpacked v0.7 RPC format
|
|
216
|
+
* expected by signet-min-bundler.
|
|
217
|
+
*
|
|
218
|
+
* Paymaster handling: the bundler's FromRPC builds paymasterAndData by
|
|
219
|
+
* concatenating `paymaster` + `paymasterData` with no re-insertion of gas
|
|
220
|
+
* limits, so we send `paymasterData` as bytes 20..end of our stored
|
|
221
|
+
* paymasterAndData (which already includes the packed gas limits written
|
|
222
|
+
* by applyPaymasterSponsorship).
|
|
223
|
+
*/
|
|
224
|
+
function serializeUserOp(userOp: PackedUserOperation) {
|
|
225
|
+
const gasLimitsHex = userOp.accountGasLimits.slice(2).padStart(64, "0");
|
|
226
|
+
const verificationGasLimit = `0x${BigInt("0x" + gasLimitsHex.slice(0, 32)).toString(16)}`;
|
|
227
|
+
const callGasLimit = `0x${BigInt("0x" + gasLimitsHex.slice(32, 64)).toString(16)}`;
|
|
228
|
+
|
|
229
|
+
const gasFeesHex = (userOp.gasFees as string).slice(2).padStart(64, "0");
|
|
230
|
+
const maxPriorityFeePerGas = `0x${BigInt("0x" + gasFeesHex.slice(0, 32)).toString(16)}`;
|
|
231
|
+
const maxFeePerGas = `0x${BigInt("0x" + gasFeesHex.slice(32, 64)).toString(16)}`;
|
|
232
|
+
|
|
233
|
+
const initCodeHex = userOp.initCode.slice(2);
|
|
234
|
+
const factory = initCodeHex.length >= 40 ? `0x${initCodeHex.slice(0, 40)}` : "";
|
|
235
|
+
const factoryData = initCodeHex.length > 40 ? `0x${initCodeHex.slice(40)}` : "";
|
|
236
|
+
|
|
237
|
+
const pmHex = userOp.paymasterAndData.slice(2);
|
|
238
|
+
const paymaster = pmHex.length >= 40 ? `0x${pmHex.slice(0, 40)}` : "";
|
|
239
|
+
const paymasterData = pmHex.length > 40 ? `0x${pmHex.slice(40)}` : "";
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
sender: userOp.sender,
|
|
243
|
+
nonce: `0x${userOp.nonce.toString(16)}`,
|
|
244
|
+
factory,
|
|
245
|
+
factoryData,
|
|
246
|
+
callData: userOp.callData,
|
|
247
|
+
callGasLimit,
|
|
248
|
+
verificationGasLimit,
|
|
249
|
+
preVerificationGas: `0x${userOp.preVerificationGas.toString(16)}`,
|
|
250
|
+
maxFeePerGas,
|
|
251
|
+
maxPriorityFeePerGas,
|
|
252
|
+
paymaster,
|
|
253
|
+
paymasterData,
|
|
254
|
+
signature: userOp.signature,
|
|
255
|
+
};
|
|
256
|
+
}
|
package/src/delegate.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation token minting and delegation-based auth.
|
|
3
|
+
*
|
|
4
|
+
* Delegation tokens are JWTs signed by a parent FROST/ECDSA key that grant
|
|
5
|
+
* an agent long-lived access to a specific scoped sub-key without requiring
|
|
6
|
+
* the user's OAuth session.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. User creates parent key + scoped sub-key via keygen
|
|
10
|
+
* 2. User calls requestDelegation() → gets a JWT signed by parent key
|
|
11
|
+
* 3. Agent calls authenticateWithDelegation() → establishes session
|
|
12
|
+
* 4. Agent signs with the scoped sub-key via the session
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SessionKeypair, IdTokenClaims } from "./types";
|
|
16
|
+
import { signKeygenRequest } from "./request";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface DelegationResult {
|
|
23
|
+
token: string;
|
|
24
|
+
keyId: string;
|
|
25
|
+
parentKey: string;
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DelegationAuthResult {
|
|
30
|
+
identity: string;
|
|
31
|
+
keyId: string;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Mint delegation token (user path)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request a delegation token for a scoped sub-key.
|
|
41
|
+
*
|
|
42
|
+
* Requires an active OAuth session. The node threshold-signs a JWT using
|
|
43
|
+
* the parent key, granting the token holder access to the sub-key.
|
|
44
|
+
*
|
|
45
|
+
* @param nodeUrl - Target group node URL
|
|
46
|
+
* @param proxyEndpoint - CORS proxy URL (e.g. "/api/node/proxy")
|
|
47
|
+
* @param groupId - Group contract address
|
|
48
|
+
* @param keyId - The sub-key to delegate access to
|
|
49
|
+
* @param parentKeyId - The parent key that signs the JWT
|
|
50
|
+
* @param curve - Key curve (e.g. "ecdsa_secp256k1")
|
|
51
|
+
* @param expiresIn - Token lifetime in seconds (e.g. 2592000 for 30 days)
|
|
52
|
+
* @param sessionKeypair - Active session keypair
|
|
53
|
+
* @param claims - OAuth claims for session auth
|
|
54
|
+
* @param identity - For auth key cert sessions
|
|
55
|
+
*/
|
|
56
|
+
export async function requestDelegation(
|
|
57
|
+
nodeUrl: string,
|
|
58
|
+
proxyEndpoint: string,
|
|
59
|
+
groupId: string,
|
|
60
|
+
keySuffix: string,
|
|
61
|
+
parentKeyId: string,
|
|
62
|
+
curve: string,
|
|
63
|
+
expiresIn: number,
|
|
64
|
+
sessionKeypair: SessionKeypair,
|
|
65
|
+
claims: IdTokenClaims,
|
|
66
|
+
identity?: string,
|
|
67
|
+
): Promise<DelegationResult> {
|
|
68
|
+
// Build session-authenticated request.
|
|
69
|
+
// Try with suffix in the canonical hash — if the node resolves the key
|
|
70
|
+
// from session + suffix, the signature must cover the suffixed key ID.
|
|
71
|
+
const signReq = await signKeygenRequest(
|
|
72
|
+
sessionKeypair,
|
|
73
|
+
claims,
|
|
74
|
+
groupId,
|
|
75
|
+
keySuffix,
|
|
76
|
+
identity,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
const res = await fetch(proxyEndpoint, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"x-node-url": nodeUrl,
|
|
86
|
+
"x-node-path": "/v1/delegate",
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
group_id: groupId.toLowerCase(),
|
|
90
|
+
key_suffix: keySuffix,
|
|
91
|
+
parent_key_id: parentKeyId,
|
|
92
|
+
curve,
|
|
93
|
+
expires_in: expiresIn,
|
|
94
|
+
session_pub: signReq.session_pub,
|
|
95
|
+
request_sig: signReq.request_sig,
|
|
96
|
+
nonce: signReq.nonce,
|
|
97
|
+
timestamp: signReq.timestamp,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.text();
|
|
103
|
+
throw new Error(`Delegation failed: ${res.status} — ${body}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
return {
|
|
108
|
+
token: data.token,
|
|
109
|
+
keyId: data.key_id,
|
|
110
|
+
parentKey: data.parent_key,
|
|
111
|
+
expiresAt: data.expires_at,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Authenticate with delegation token (agent path)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Authenticate with a node using a delegation token.
|
|
121
|
+
*
|
|
122
|
+
* The agent presents a JWT signed by the parent key. The node verifies
|
|
123
|
+
* the signature and creates a session scoped to the sub-key.
|
|
124
|
+
*
|
|
125
|
+
* @param nodeUrl - Target group node URL
|
|
126
|
+
* @param proxyEndpoint - CORS proxy URL
|
|
127
|
+
* @param groupId - Group contract address
|
|
128
|
+
* @param delegationToken - The JWT from requestDelegation()
|
|
129
|
+
* @param sessionKeypair - Fresh ephemeral session keypair for the agent
|
|
130
|
+
*/
|
|
131
|
+
export async function authenticateWithDelegation(
|
|
132
|
+
nodeUrl: string,
|
|
133
|
+
proxyEndpoint: string,
|
|
134
|
+
groupId: string,
|
|
135
|
+
delegationToken: string,
|
|
136
|
+
sessionKeypair: SessionKeypair,
|
|
137
|
+
): Promise<DelegationAuthResult> {
|
|
138
|
+
const res = await fetch(proxyEndpoint, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"x-node-url": nodeUrl,
|
|
143
|
+
"x-node-path": "/v1/auth",
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
group_id: groupId.toLowerCase(),
|
|
147
|
+
delegation_token: delegationToken,
|
|
148
|
+
session_pub: sessionKeypair.publicKeyHex,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
const body = await res.text();
|
|
154
|
+
throw new Error(`Delegation auth failed: ${res.status} — ${body}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = await res.json();
|
|
158
|
+
return {
|
|
159
|
+
identity: data.identity,
|
|
160
|
+
keyId: data.key_id,
|
|
161
|
+
expiresAt: data.expires_at,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side FROST Schnorr signature verification (RFC 9591 secp256k1-SHA256-v1).
|
|
3
|
+
*
|
|
4
|
+
* Verifies a 65-byte FROST threshold signature against a compressed group
|
|
5
|
+
* public key and message. Uses the same algorithm as FROSTVerifier.sol:
|
|
6
|
+
*
|
|
7
|
+
* Verification equation: z·G = R + c·Y
|
|
8
|
+
* Challenge: c = H2(R || Y || msg) via expand_message_xmd (RFC 9380)
|
|
9
|
+
* DST: "FROST-secp256k1-SHA256-v1chal"
|
|
10
|
+
*
|
|
11
|
+
* Uses @noble/curves (already installed) for elliptic curve arithmetic
|
|
12
|
+
* and RFC 9380 hash-to-curve utilities.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
16
|
+
import { expand_message_xmd } from "@noble/curves/abstract/hash-to-curve";
|
|
17
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
18
|
+
import { concatBytes } from "@noble/hashes/utils";
|
|
19
|
+
|
|
20
|
+
const DST = new TextEncoder().encode("FROST-secp256k1-SHA256-v1chal");
|
|
21
|
+
const N = secp256k1.CURVE.n;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Verify a FROST Schnorr signature.
|
|
25
|
+
*
|
|
26
|
+
* @param message - The message bytes that were signed (typically 32 bytes)
|
|
27
|
+
* @param signature - 65-byte signature: R.x(32) || z(32) || v(1)
|
|
28
|
+
* @param groupPublicKey - 33-byte compressed secp256k1 group public key
|
|
29
|
+
* @returns true if the signature is valid
|
|
30
|
+
*/
|
|
31
|
+
export function verifyFrostSignature(
|
|
32
|
+
message: Uint8Array,
|
|
33
|
+
signature: Uint8Array,
|
|
34
|
+
groupPublicKey: Uint8Array,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (signature.length !== 65) return false;
|
|
37
|
+
if (groupPublicKey.length !== 33) return false;
|
|
38
|
+
|
|
39
|
+
const rx = signature.slice(0, 32);
|
|
40
|
+
const z = bytesToBigInt(signature.slice(32, 64));
|
|
41
|
+
const v = signature[64];
|
|
42
|
+
|
|
43
|
+
if (z === 0n || z >= N) return false;
|
|
44
|
+
|
|
45
|
+
// Reconstruct compressed R from R.x + parity
|
|
46
|
+
const rCompressed = new Uint8Array(33);
|
|
47
|
+
rCompressed[0] = v === 0 ? 0x02 : 0x03;
|
|
48
|
+
rCompressed.set(rx, 1);
|
|
49
|
+
|
|
50
|
+
// Challenge: c = H2(R_compressed || groupPublicKey || message)
|
|
51
|
+
// Uses expand_message_xmd (RFC 9380) with SHA-256, output 48 bytes, reduced mod N
|
|
52
|
+
const input = concatBytes(rCompressed, groupPublicKey, message);
|
|
53
|
+
const uniform = expand_message_xmd(input, DST, 48, sha256);
|
|
54
|
+
const c = bytesToBigInt(uniform) % N;
|
|
55
|
+
|
|
56
|
+
if (c === 0n) return false;
|
|
57
|
+
|
|
58
|
+
// Verify: z·G == R + c·Y
|
|
59
|
+
try {
|
|
60
|
+
const R = secp256k1.ProjectivePoint.fromHex(rCompressed);
|
|
61
|
+
const Y = secp256k1.ProjectivePoint.fromHex(groupPublicKey);
|
|
62
|
+
const G = secp256k1.ProjectivePoint.BASE;
|
|
63
|
+
|
|
64
|
+
const lhs = G.multiply(z);
|
|
65
|
+
const rhs = R.add(Y.multiply(c));
|
|
66
|
+
|
|
67
|
+
return lhs.equals(rhs);
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
74
|
+
let result = 0n;
|
|
75
|
+
for (const b of bytes) {
|
|
76
|
+
result = (result << 8n) | BigInt(b);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { generatePartialSHA256 } from './partial-sha';
|
|
2
|
+
|
|
3
|
+
type GenerateInputsParams = {
|
|
4
|
+
jwt: string;
|
|
5
|
+
pubkey: JsonWebKey;
|
|
6
|
+
shaPrecomputeTillKeys?: string[];
|
|
7
|
+
maxSignedDataLength: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type JWTCircuitInputs = {
|
|
11
|
+
data?: {
|
|
12
|
+
storage: number[];
|
|
13
|
+
len: number;
|
|
14
|
+
};
|
|
15
|
+
base64_decode_offset: number;
|
|
16
|
+
pubkey_modulus_limbs: string[];
|
|
17
|
+
redc_params_limbs: string[];
|
|
18
|
+
signature_limbs: string[];
|
|
19
|
+
partial_data?: {
|
|
20
|
+
storage: number[];
|
|
21
|
+
len: number;
|
|
22
|
+
};
|
|
23
|
+
partial_hash?: number[];
|
|
24
|
+
full_data_length?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Generates circuit inputs required for the jwt lib
|
|
29
|
+
* @param {Object} params - The input parameters
|
|
30
|
+
* @param {string} params.jwt - The JWT token to process (string)
|
|
31
|
+
* @param {JsonWebKey} params.pubkey - The public key to verify the signature (JsonWebKey)
|
|
32
|
+
* @param {string[]} params.shaPrecomputeTillKeys - (optional) Key(s) in the payload until which SHA should be precomputed
|
|
33
|
+
* @param {number} params.maxSignedDataLength - Maximum length of signed data (with or without partial hash) allowed by the circuit
|
|
34
|
+
*/
|
|
35
|
+
export async function generateInputs({
|
|
36
|
+
jwt,
|
|
37
|
+
pubkey,
|
|
38
|
+
shaPrecomputeTillKeys,
|
|
39
|
+
maxSignedDataLength, // when using partial hash, this will be the length of data after partial hash
|
|
40
|
+
}: GenerateInputsParams) {
|
|
41
|
+
// Parse token
|
|
42
|
+
const [headerB64, payloadB64] = jwt.split(".");
|
|
43
|
+
|
|
44
|
+
// Extract signed data as byte array
|
|
45
|
+
const signedDataString = jwt.split(".").slice(0, 2).join("."); // $header.$payload
|
|
46
|
+
const signedData = new TextEncoder().encode(signedDataString) as Uint8Array;
|
|
47
|
+
|
|
48
|
+
// Extract signature as bigint
|
|
49
|
+
const signatureBase64Url = jwt.split(".")[2];
|
|
50
|
+
const signatureBase64 = signatureBase64Url
|
|
51
|
+
.replace(/-/g, "+")
|
|
52
|
+
.replace(/_/g, "/");
|
|
53
|
+
|
|
54
|
+
const signature = new Uint8Array(
|
|
55
|
+
atob(signatureBase64)
|
|
56
|
+
.split("")
|
|
57
|
+
.map((c) => c.charCodeAt(0))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const signatureBigInt = BigInt("0x" + Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''));
|
|
61
|
+
|
|
62
|
+
// Extract pubkey modulus as bigint
|
|
63
|
+
const pubkeyBigInt = BigInt("0x" + atob(pubkey.n!.replace(/-/g, "+").replace(/_/g, "/"))
|
|
64
|
+
.split("")
|
|
65
|
+
.map(c => c.charCodeAt(0).toString(16).padStart(2, "0"))
|
|
66
|
+
.join(""));
|
|
67
|
+
const redcParam = (1n << (2n * 2048n + 4n)) / pubkeyBigInt; // something needed by the noir big-num lib
|
|
68
|
+
|
|
69
|
+
const inputs: Partial<JWTCircuitInputs> = {
|
|
70
|
+
pubkey_modulus_limbs: splitBigIntToChunks(pubkeyBigInt, 120, 18).map(s => s.toString()),
|
|
71
|
+
redc_params_limbs: splitBigIntToChunks(redcParam, 120, 18).map(s => s.toString()),
|
|
72
|
+
signature_limbs: splitBigIntToChunks(signatureBigInt, 120, 18).map(s => s.toString()),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!shaPrecomputeTillKeys || shaPrecomputeTillKeys.length === 0) {
|
|
76
|
+
// No precompute selector - no need to precompute SHA256
|
|
77
|
+
if (signedData.length > maxSignedDataLength) {
|
|
78
|
+
throw new Error("Signed data length exceeds maxSignedDataLength");
|
|
79
|
+
}
|
|
80
|
+
const signedDataPadded = new Uint8Array(maxSignedDataLength);
|
|
81
|
+
signedDataPadded.set(signedData);
|
|
82
|
+
inputs.data = {
|
|
83
|
+
storage: Array.from(signedDataPadded),
|
|
84
|
+
len: signedData.length,
|
|
85
|
+
}
|
|
86
|
+
// entire payload is base64 decode-able when not using partial hash
|
|
87
|
+
// offset in signed data is the index of payload start
|
|
88
|
+
// this can be any multiple of 4 from payload start, if you want to skip some bytes from start
|
|
89
|
+
inputs.base64_decode_offset = headerB64.length + 1;
|
|
90
|
+
} else {
|
|
91
|
+
// Precompute SHA256 of the signed data
|
|
92
|
+
// SHA256 is done in 64 byte chunks, so we can hash upto certain portion outside of circuit to save constraints
|
|
93
|
+
// Signed data is $headerB64.$payloadB64
|
|
94
|
+
// We need to find the index in B64 payload corresponding to min(hdIndex, nonceIndex) when decoded
|
|
95
|
+
// Then we find the 64 byte boundary before this index and precompute the SHA256 upto that
|
|
96
|
+
const payloadString = atob(payloadB64);
|
|
97
|
+
const indicesOfPrecomputeKeys = shaPrecomputeTillKeys.map((key) =>
|
|
98
|
+
payloadString.indexOf(`"${key}":`)
|
|
99
|
+
);
|
|
100
|
+
const smallerIndex = Math.min(...indicesOfPrecomputeKeys);
|
|
101
|
+
const smallerIndexInB64 = Math.floor((smallerIndex * 4) / 3); // 4 B64 chars = 3 bytes
|
|
102
|
+
|
|
103
|
+
const sliceStart = headerB64.length + smallerIndexInB64 + 1; // +1 for the '.'
|
|
104
|
+
|
|
105
|
+
// Precompute the SHA256 hash
|
|
106
|
+
const { partialHash, remainingData } =
|
|
107
|
+
await generatePartialSHA256(signedData, sliceStart);
|
|
108
|
+
|
|
109
|
+
// Pad to the max length configured in the circuit
|
|
110
|
+
if (remainingData.length > maxSignedDataLength) {
|
|
111
|
+
throw new Error("remainingData after partial hash exceeds maxSignedDataLength");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const remainingDataPadded = new Uint8Array(maxSignedDataLength);
|
|
115
|
+
remainingDataPadded.set(remainingData);
|
|
116
|
+
|
|
117
|
+
inputs.partial_data = {
|
|
118
|
+
storage: Array.from(remainingDataPadded),
|
|
119
|
+
len: remainingData.length,
|
|
120
|
+
};
|
|
121
|
+
inputs.partial_hash = Array.from(partialHash);
|
|
122
|
+
inputs.full_data_length = signedData.length;
|
|
123
|
+
|
|
124
|
+
// when using partial hash, the data after the partial hash might not be a valid base64
|
|
125
|
+
// we need to find an offset (1, 2, or 3) such that the remaining payload is base64 decode-able
|
|
126
|
+
// this is the number that should be added to the "payload chunk that was included in SHA precompute"
|
|
127
|
+
// to make it a multiple of 4
|
|
128
|
+
// in other words, if you trim offset number of bytes from the remaining payload, it will be base64 decode-able
|
|
129
|
+
const shaCutoffIndex = signedData.length - remainingData.length;
|
|
130
|
+
const payloadBytesInShaPrecompute = shaCutoffIndex - (headerB64.length + 1);
|
|
131
|
+
const offsetToMakeIt4x = 4 - (payloadBytesInShaPrecompute % 4);
|
|
132
|
+
inputs.base64_decode_offset = offsetToMakeIt4x;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return inputs as JWTCircuitInputs;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* Splits a BigInt into fixed-size chunks
|
|
141
|
+
* @param {bigint} bigInt - The BigInt to split
|
|
142
|
+
* @param {number} chunkSize - Size of each chunk in bits
|
|
143
|
+
* @param {number} numChunks - Number of chunks to split into
|
|
144
|
+
* @returns {bigint[]} Array of BigInt chunks
|
|
145
|
+
*/
|
|
146
|
+
export function splitBigIntToChunks(
|
|
147
|
+
bigInt: bigint,
|
|
148
|
+
chunkSize: number,
|
|
149
|
+
numChunks: number
|
|
150
|
+
) {
|
|
151
|
+
const chunks = [];
|
|
152
|
+
const mask = (1n << BigInt(chunkSize)) - 1n;
|
|
153
|
+
for (let i = 0; i < numChunks; i++) {
|
|
154
|
+
const chunk = (bigInt / (1n << (BigInt(i) * BigInt(chunkSize)))) & mask;
|
|
155
|
+
chunks.push(chunk);
|
|
156
|
+
}
|
|
157
|
+
return chunks;
|
|
158
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signet DKMS SDK
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic library for Signet protocol interactions.
|
|
5
|
+
*
|
|
6
|
+
* This barrel export includes only the core modules that have no heavy
|
|
7
|
+
* dependencies. For ZK proof generation (client-side), import directly:
|
|
8
|
+
*
|
|
9
|
+
* import { generateJWTProof } from "@oleary-labs/signet-sdk/proof"
|
|
10
|
+
* import { buildFullWitness } from "@oleary-labs/signet-sdk/witness"
|
|
11
|
+
*
|
|
12
|
+
* These require @noir-lang/noir_js, @aztec/bb.js, and
|
|
13
|
+
* @oleary-labs/signet-circuits as peer dependencies.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Core
|
|
17
|
+
export * from "./types";
|
|
18
|
+
export * from "./session";
|
|
19
|
+
export * from "./request";
|
|
20
|
+
export * from "./keygen";
|
|
21
|
+
|
|
22
|
+
// Auth (lightweight — no WASM)
|
|
23
|
+
export * from "./oauth";
|
|
24
|
+
export * from "./jwt";
|
|
25
|
+
export * from "./jwks";
|
|
26
|
+
export * from "./bootstrap";
|
|
27
|
+
export * from "./authkey-session";
|
|
28
|
+
export * from "./server-prover";
|
|
29
|
+
|
|
30
|
+
// Admin
|
|
31
|
+
export * from "./admin";
|
|
32
|
+
|
|
33
|
+
// Signing + Delegation
|
|
34
|
+
export * from "./delegate";
|
|
35
|
+
export * from "./scopedSign";
|
|
36
|
+
export * from "./frostVerify";
|
|
37
|
+
|
|
38
|
+
// x402
|
|
39
|
+
export * from "./x402";
|
|
40
|
+
|
|
41
|
+
// ERC-4337
|
|
42
|
+
export * from "./userop";
|
|
43
|
+
export * from "./bundler";
|