@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.
Files changed (95) hide show
  1. package/dist/admin.d.ts +38 -0
  2. package/dist/admin.d.ts.map +1 -0
  3. package/dist/admin.js +112 -0
  4. package/dist/admin.js.map +1 -0
  5. package/dist/authkey-session.d.ts +64 -0
  6. package/dist/authkey-session.d.ts.map +1 -0
  7. package/dist/authkey-session.js +164 -0
  8. package/dist/authkey-session.js.map +1 -0
  9. package/dist/bootstrap.d.ts +30 -0
  10. package/dist/bootstrap.d.ts.map +1 -0
  11. package/dist/bootstrap.js +60 -0
  12. package/dist/bootstrap.js.map +1 -0
  13. package/dist/bundler.d.ts +85 -0
  14. package/dist/bundler.d.ts.map +1 -0
  15. package/dist/bundler.js +160 -0
  16. package/dist/bundler.js.map +1 -0
  17. package/dist/delegate.d.ts +57 -0
  18. package/dist/delegate.d.ts.map +1 -0
  19. package/dist/delegate.js +111 -0
  20. package/dist/delegate.js.map +1 -0
  21. package/dist/frostVerify.d.ts +23 -0
  22. package/dist/frostVerify.d.ts.map +1 -0
  23. package/dist/frostVerify.js +69 -0
  24. package/dist/frostVerify.js.map +1 -0
  25. package/dist/index.d.ts +32 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +38 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jwks.d.ts +28 -0
  30. package/dist/jwks.d.ts.map +1 -0
  31. package/dist/jwks.js +81 -0
  32. package/dist/jwks.js.map +1 -0
  33. package/dist/jwt.d.ts +27 -0
  34. package/dist/jwt.d.ts.map +1 -0
  35. package/dist/jwt.js +50 -0
  36. package/dist/jwt.js.map +1 -0
  37. package/dist/keygen.d.ts +26 -0
  38. package/dist/keygen.d.ts.map +1 -0
  39. package/dist/keygen.js +60 -0
  40. package/dist/keygen.js.map +1 -0
  41. package/dist/oauth.d.ts +34 -0
  42. package/dist/oauth.d.ts.map +1 -0
  43. package/dist/oauth.js +119 -0
  44. package/dist/oauth.js.map +1 -0
  45. package/dist/request.d.ts +42 -0
  46. package/dist/request.d.ts.map +1 -0
  47. package/dist/request.js +115 -0
  48. package/dist/request.js.map +1 -0
  49. package/dist/scopedSign.d.ts +82 -0
  50. package/dist/scopedSign.d.ts.map +1 -0
  51. package/dist/scopedSign.js +130 -0
  52. package/dist/scopedSign.js.map +1 -0
  53. package/dist/server-prover.d.ts +29 -0
  54. package/dist/server-prover.d.ts.map +1 -0
  55. package/dist/server-prover.js +54 -0
  56. package/dist/server-prover.js.map +1 -0
  57. package/dist/session.d.ts +14 -0
  58. package/dist/session.d.ts.map +1 -0
  59. package/dist/session.js +29 -0
  60. package/dist/session.js.map +1 -0
  61. package/dist/types.d.ts +56 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +5 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/userop.d.ts +104 -0
  66. package/dist/userop.d.ts.map +1 -0
  67. package/dist/userop.js +212 -0
  68. package/dist/userop.js.map +1 -0
  69. package/dist/x402.d.ts +127 -0
  70. package/dist/x402.d.ts.map +1 -0
  71. package/dist/x402.js +167 -0
  72. package/dist/x402.js.map +1 -0
  73. package/package.json +64 -0
  74. package/src/admin.ts +178 -0
  75. package/src/authkey-session.ts +241 -0
  76. package/src/bootstrap.ts +106 -0
  77. package/src/bundler.ts +256 -0
  78. package/src/delegate.ts +163 -0
  79. package/src/frostVerify.ts +79 -0
  80. package/src/generate-inputs.ts +158 -0
  81. package/src/index.ts +43 -0
  82. package/src/jwks.ts +92 -0
  83. package/src/jwt.ts +74 -0
  84. package/src/keygen.ts +89 -0
  85. package/src/oauth.ts +157 -0
  86. package/src/partial-sha.ts +99 -0
  87. package/src/proof.ts +99 -0
  88. package/src/request.ts +174 -0
  89. package/src/scopedSign.ts +184 -0
  90. package/src/server-prover.ts +76 -0
  91. package/src/session.ts +33 -0
  92. package/src/types.ts +63 -0
  93. package/src/userop.ts +368 -0
  94. package/src/witness.ts +132 -0
  95. 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
+ }
@@ -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";