@mentaproject/signer-react-native 0.0.1
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 +343 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/signer.d.ts +161 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +378 -0
- package/dist/signer.js.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/base64url.d.ts +75 -0
- package/dist/utils/base64url.d.ts.map +1 -0
- package/dist/utils/base64url.js +142 -0
- package/dist/utils/base64url.js.map +1 -0
- package/dist/utils/cose.d.ts +98 -0
- package/dist/utils/cose.d.ts.map +1 -0
- package/dist/utils/cose.js +259 -0
- package/dist/utils/cose.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/passkey.d.ts +108 -0
- package/dist/utils/passkey.d.ts.map +1 -0
- package/dist/utils/passkey.js +296 -0
- package/dist/utils/passkey.js.map +1 -0
- package/package.json +71 -0
- package/src/index.ts +59 -0
- package/src/signer.ts +499 -0
- package/src/types/index.ts +140 -0
- package/src/utils/base64url.ts +161 -0
- package/src/utils/cose.ts +356 -0
- package/src/utils/index.ts +40 -0
- package/src/utils/passkey.ts +392 -0
package/src/signer.ts
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { Passkey } from "react-native-passkey";
|
|
2
|
+
import {
|
|
3
|
+
type Address,
|
|
4
|
+
type Hex,
|
|
5
|
+
type TypedData,
|
|
6
|
+
type TypedDataDefinition,
|
|
7
|
+
hashTypedData,
|
|
8
|
+
keccak256,
|
|
9
|
+
} from "viem";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
SmartAccountSigner,
|
|
13
|
+
PasskeyCredential,
|
|
14
|
+
P256PublicKey,
|
|
15
|
+
ReactNativePasskeySignerConfig,
|
|
16
|
+
SignableMessage,
|
|
17
|
+
SignatureEncodingFormat,
|
|
18
|
+
} from "./types/index.js";
|
|
19
|
+
import {
|
|
20
|
+
extractPublicKeyFromAttestation,
|
|
21
|
+
encodeUncompressedPublicKey,
|
|
22
|
+
isValidP256PublicKey,
|
|
23
|
+
COSEParseError,
|
|
24
|
+
} from "./utils/cose.js";
|
|
25
|
+
import { bytesToBase64Url, base64UrlToHex } from "./utils/base64url.js";
|
|
26
|
+
import {
|
|
27
|
+
parseWebAuthnAssertion,
|
|
28
|
+
encodeWebAuthnSignature,
|
|
29
|
+
createAssertionOptions,
|
|
30
|
+
SignatureError,
|
|
31
|
+
} from "./utils/passkey.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when passkey operations fail.
|
|
35
|
+
*/
|
|
36
|
+
export class PasskeySignerError extends Error {
|
|
37
|
+
constructor(
|
|
38
|
+
message: string,
|
|
39
|
+
public readonly cause?: unknown,
|
|
40
|
+
) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "PasskeySignerError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ReactNativePasskeySigner implements SmartAccountSigner using WebAuthn/Passkeys.
|
|
48
|
+
*
|
|
49
|
+
* This signer uses the device's biometric authentication (Face ID, Touch ID, fingerprint)
|
|
50
|
+
* to sign messages and transactions for ERC-4337 smart accounts.
|
|
51
|
+
*
|
|
52
|
+
* The public key is a P-256 (secp256r1) key stored in the device's secure enclave.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Register a new passkey
|
|
57
|
+
* const signer = await ReactNativePasskeySigner.register("user@example.com", {
|
|
58
|
+
* rpId: "example.com",
|
|
59
|
+
* rpName: "Example App",
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // Sign a message (triggers biometric prompt)
|
|
63
|
+
* const signature = await signer.signMessage({
|
|
64
|
+
* message: { raw: userOpHash },
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class ReactNativePasskeySigner implements SmartAccountSigner<"passkey"> {
|
|
69
|
+
public readonly source = "passkey" as const;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The credential ID in Base64URL format.
|
|
73
|
+
*/
|
|
74
|
+
public readonly credentialId: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The credential ID as hex.
|
|
78
|
+
*/
|
|
79
|
+
public readonly credentialIdHex: Hex;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The P-256 public key coordinates (x, y).
|
|
83
|
+
*/
|
|
84
|
+
public readonly publicKeyCoordinates: P256PublicKey;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The encoded public key (uncompressed SEC1 format: 0x04 || x || y).
|
|
88
|
+
*/
|
|
89
|
+
public readonly publicKey: Hex;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Configuration for passkey operations.
|
|
93
|
+
*/
|
|
94
|
+
private readonly config: ReactNativePasskeySignerConfig;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a new ReactNativePasskeySigner instance.
|
|
98
|
+
* Use the static `register` or `fromCredential` methods instead of calling this directly.
|
|
99
|
+
*
|
|
100
|
+
* @param credential - The passkey credential data
|
|
101
|
+
* @param config - Configuration for passkey operations
|
|
102
|
+
*/
|
|
103
|
+
private constructor(
|
|
104
|
+
credential: PasskeyCredential,
|
|
105
|
+
config: ReactNativePasskeySignerConfig,
|
|
106
|
+
) {
|
|
107
|
+
this.credentialId = credential.credentialId;
|
|
108
|
+
this.credentialIdHex = credential.credentialIdHex;
|
|
109
|
+
this.publicKeyCoordinates = credential.publicKey;
|
|
110
|
+
this.publicKey = credential.publicKeyHex;
|
|
111
|
+
this.config = {
|
|
112
|
+
...config,
|
|
113
|
+
timeout: config.timeout ?? 60000,
|
|
114
|
+
userVerification: config.userVerification ?? "required",
|
|
115
|
+
signatureFormat: config.signatureFormat ?? "kernel",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Registers a new passkey and returns a signer instance.
|
|
121
|
+
*
|
|
122
|
+
* This triggers the device's WebAuthn registration flow:
|
|
123
|
+
* 1. Shows a system prompt to create a new passkey
|
|
124
|
+
* 2. User authenticates with biometrics
|
|
125
|
+
* 3. Device generates a new P-256 key pair in the secure enclave
|
|
126
|
+
* 4. Returns the public key and credential ID
|
|
127
|
+
*
|
|
128
|
+
* @param username - The username for the passkey (displayed in system UI)
|
|
129
|
+
* @param config - Configuration for the passkey
|
|
130
|
+
* @returns A new ReactNativePasskeySigner instance
|
|
131
|
+
* @throws PasskeySignerError if registration fails
|
|
132
|
+
*/
|
|
133
|
+
public static async register(
|
|
134
|
+
username: string,
|
|
135
|
+
config: ReactNativePasskeySignerConfig,
|
|
136
|
+
): Promise<ReactNativePasskeySigner> {
|
|
137
|
+
// Generate a random user ID
|
|
138
|
+
const userId = new Uint8Array(32);
|
|
139
|
+
crypto.getRandomValues(userId);
|
|
140
|
+
const userIdB64 = bytesToBase64Url(userId);
|
|
141
|
+
|
|
142
|
+
// Generate a random challenge
|
|
143
|
+
const challenge = new Uint8Array(32);
|
|
144
|
+
crypto.getRandomValues(challenge);
|
|
145
|
+
const challengeB64 = bytesToBase64Url(challenge);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Call the native passkey creation
|
|
149
|
+
const result = await Passkey.create({
|
|
150
|
+
challenge: challengeB64,
|
|
151
|
+
rp: {
|
|
152
|
+
id: config.rpId,
|
|
153
|
+
name: config.rpName,
|
|
154
|
+
},
|
|
155
|
+
user: {
|
|
156
|
+
id: userIdB64,
|
|
157
|
+
name: username,
|
|
158
|
+
displayName: username,
|
|
159
|
+
},
|
|
160
|
+
pubKeyCredParams: [
|
|
161
|
+
{
|
|
162
|
+
type: "public-key",
|
|
163
|
+
alg: -7, // ES256 (P-256 with SHA-256)
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
authenticatorSelection: {
|
|
167
|
+
authenticatorAttachment: "platform",
|
|
168
|
+
residentKey: "required",
|
|
169
|
+
requireResidentKey: true,
|
|
170
|
+
userVerification: config.userVerification ?? "required",
|
|
171
|
+
},
|
|
172
|
+
timeout: config.timeout ?? 60000,
|
|
173
|
+
attestation: "none",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Extract the credential ID
|
|
177
|
+
const credentialId = result.id;
|
|
178
|
+
const credentialIdHex = base64UrlToHex(credentialId);
|
|
179
|
+
|
|
180
|
+
// Parse the attestation to extract the public key
|
|
181
|
+
// The attestationObject contains the COSE-encoded public key
|
|
182
|
+
if (!result.response.attestationObject) {
|
|
183
|
+
throw new PasskeySignerError(
|
|
184
|
+
"Registration response missing attestationObject",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { publicKey } = extractPublicKeyFromAttestation(
|
|
189
|
+
result.response.attestationObject,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Validate the public key
|
|
193
|
+
if (!isValidP256PublicKey(publicKey)) {
|
|
194
|
+
throw new PasskeySignerError(
|
|
195
|
+
"Invalid public key extracted from attestation",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Encode the public key in uncompressed format
|
|
200
|
+
const publicKeyHex = encodeUncompressedPublicKey(publicKey);
|
|
201
|
+
|
|
202
|
+
const credential: PasskeyCredential = {
|
|
203
|
+
credentialId,
|
|
204
|
+
credentialIdHex,
|
|
205
|
+
publicKey,
|
|
206
|
+
publicKeyHex,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return new ReactNativePasskeySigner(credential, config);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof COSEParseError) {
|
|
212
|
+
throw new PasskeySignerError(
|
|
213
|
+
`Failed to parse attestation: ${error.message}`,
|
|
214
|
+
error,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (error instanceof PasskeySignerError) {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
throw new PasskeySignerError(
|
|
221
|
+
`Passkey registration failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
222
|
+
error,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Creates a signer from an existing credential.
|
|
229
|
+
*
|
|
230
|
+
* Use this when you have previously registered a passkey and stored the credential data.
|
|
231
|
+
*
|
|
232
|
+
* @param credential - The stored credential data
|
|
233
|
+
* @param config - Configuration for passkey operations
|
|
234
|
+
* @returns A new ReactNativePasskeySigner instance
|
|
235
|
+
*/
|
|
236
|
+
public static fromCredential(
|
|
237
|
+
credential: PasskeyCredential,
|
|
238
|
+
config: ReactNativePasskeySignerConfig,
|
|
239
|
+
): ReactNativePasskeySigner {
|
|
240
|
+
if (!isValidP256PublicKey(credential.publicKey)) {
|
|
241
|
+
throw new PasskeySignerError("Invalid public key in credential");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new ReactNativePasskeySigner(credential, config);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Signs a message using the passkey.
|
|
249
|
+
*
|
|
250
|
+
* This triggers the device's biometric authentication prompt.
|
|
251
|
+
* The message can be a string or raw bytes (typically a UserOpHash).
|
|
252
|
+
*
|
|
253
|
+
* @param params - The message to sign
|
|
254
|
+
* @returns The encoded signature
|
|
255
|
+
* @throws PasskeySignerError if signing fails
|
|
256
|
+
*/
|
|
257
|
+
public async signMessage({
|
|
258
|
+
message,
|
|
259
|
+
}: {
|
|
260
|
+
message: SignableMessage;
|
|
261
|
+
}): Promise<Hex> {
|
|
262
|
+
// Convert message to bytes
|
|
263
|
+
let messageBytes: Uint8Array;
|
|
264
|
+
|
|
265
|
+
if (typeof message === "string") {
|
|
266
|
+
// For string messages, hash with EIP-191 prefix
|
|
267
|
+
const encoder = new TextEncoder();
|
|
268
|
+
const msgBytes = encoder.encode(message);
|
|
269
|
+
const prefix = `\x19Ethereum Signed Message:\n${msgBytes.length}`;
|
|
270
|
+
const prefixBytes = encoder.encode(prefix);
|
|
271
|
+
|
|
272
|
+
const combined = new Uint8Array(prefixBytes.length + msgBytes.length);
|
|
273
|
+
combined.set(prefixBytes, 0);
|
|
274
|
+
combined.set(msgBytes, prefixBytes.length);
|
|
275
|
+
|
|
276
|
+
// Hash the prefixed message
|
|
277
|
+
const hash = keccak256(combined);
|
|
278
|
+
messageBytes = this.hexToBytes(hash);
|
|
279
|
+
} else if ("raw" in message) {
|
|
280
|
+
// For raw bytes, use them directly (typically already a hash)
|
|
281
|
+
if (typeof message.raw === "string") {
|
|
282
|
+
messageBytes = this.hexToBytes(message.raw as Hex);
|
|
283
|
+
} else {
|
|
284
|
+
messageBytes = message.raw;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
throw new PasskeySignerError("Invalid message format");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Create challenge from the message bytes
|
|
291
|
+
const challenge = bytesToBase64Url(messageBytes);
|
|
292
|
+
|
|
293
|
+
return this.signWithPasskey(challenge);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Signs typed data according to EIP-712.
|
|
298
|
+
*
|
|
299
|
+
* @param typedData - The typed data to sign
|
|
300
|
+
* @returns The encoded signature
|
|
301
|
+
* @throws PasskeySignerError if signing fails
|
|
302
|
+
*/
|
|
303
|
+
public async signTypedData<
|
|
304
|
+
const TTypedData extends TypedData | Record<string, unknown>,
|
|
305
|
+
TPrimaryType extends keyof TTypedData | "EIP712Domain" = keyof TTypedData,
|
|
306
|
+
>(typedData: TypedDataDefinition<TTypedData, TPrimaryType>): Promise<Hex> {
|
|
307
|
+
// Hash the typed data according to EIP-712
|
|
308
|
+
const hash = hashTypedData(typedData);
|
|
309
|
+
|
|
310
|
+
// Convert hash to bytes for challenge
|
|
311
|
+
const messageBytes = this.hexToBytes(hash);
|
|
312
|
+
const challenge = bytesToBase64Url(messageBytes);
|
|
313
|
+
|
|
314
|
+
return this.signWithPasskey(challenge);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Returns the address derived from the public key.
|
|
319
|
+
*
|
|
320
|
+
* Note: P-256 keys don't directly map to Ethereum addresses.
|
|
321
|
+
* This returns a pseudo-address derived from the public key hash.
|
|
322
|
+
* The actual smart account address is determined by the account implementation.
|
|
323
|
+
*
|
|
324
|
+
* @returns A derived address (not a real EOA address)
|
|
325
|
+
*/
|
|
326
|
+
public async getAddress(): Promise<Address> {
|
|
327
|
+
// Derive an address from the public key by hashing it
|
|
328
|
+
// This is not a real EOA address, just an identifier
|
|
329
|
+
const hash = keccak256(this.publicKey);
|
|
330
|
+
// Take the last 20 bytes
|
|
331
|
+
return `0x${hash.slice(-40)}` as Address;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Returns the X coordinate of the public key.
|
|
336
|
+
*/
|
|
337
|
+
public getPublicKeyX(): Hex {
|
|
338
|
+
return this.publicKeyCoordinates.x;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Returns the Y coordinate of the public key.
|
|
343
|
+
*/
|
|
344
|
+
public getPublicKeyY(): Hex {
|
|
345
|
+
return this.publicKeyCoordinates.y;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Returns the full credential data for storage/restoration.
|
|
350
|
+
*/
|
|
351
|
+
public getCredential(): PasskeyCredential {
|
|
352
|
+
return {
|
|
353
|
+
credentialId: this.credentialId,
|
|
354
|
+
credentialIdHex: this.credentialIdHex,
|
|
355
|
+
publicKey: this.publicKeyCoordinates,
|
|
356
|
+
publicKeyHex: this.publicKey,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Sets the signature encoding format.
|
|
362
|
+
*
|
|
363
|
+
* @param format - The format to use for encoding signatures
|
|
364
|
+
* @returns A new signer instance with the updated format
|
|
365
|
+
*/
|
|
366
|
+
public withSignatureFormat(
|
|
367
|
+
format: SignatureEncodingFormat,
|
|
368
|
+
): ReactNativePasskeySigner {
|
|
369
|
+
return new ReactNativePasskeySigner(this.getCredential(), {
|
|
370
|
+
...this.config,
|
|
371
|
+
signatureFormat: format,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Internal method to sign a challenge with the passkey.
|
|
377
|
+
*
|
|
378
|
+
* @param challenge - The Base64URL-encoded challenge
|
|
379
|
+
* @returns The encoded signature
|
|
380
|
+
*/
|
|
381
|
+
private async signWithPasskey(challenge: string): Promise<Hex> {
|
|
382
|
+
try {
|
|
383
|
+
// Create assertion options
|
|
384
|
+
const options = createAssertionOptions(
|
|
385
|
+
challenge,
|
|
386
|
+
this.config.rpId,
|
|
387
|
+
this.credentialId,
|
|
388
|
+
this.config.userVerification,
|
|
389
|
+
this.config.timeout,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Call native passkey assertion
|
|
393
|
+
const result = await Passkey.get(options);
|
|
394
|
+
|
|
395
|
+
// Parse the assertion response
|
|
396
|
+
const webAuthnSignature = parseWebAuthnAssertion({
|
|
397
|
+
authenticatorData: result.response.authenticatorData,
|
|
398
|
+
clientDataJSON: result.response.clientDataJSON,
|
|
399
|
+
signature: result.response.signature,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Encode the signature according to the configured format
|
|
403
|
+
return encodeWebAuthnSignature(
|
|
404
|
+
webAuthnSignature,
|
|
405
|
+
this.config.signatureFormat,
|
|
406
|
+
);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof SignatureError) {
|
|
409
|
+
throw new PasskeySignerError(
|
|
410
|
+
`Signature parsing failed: ${error.message}`,
|
|
411
|
+
error,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
throw new PasskeySignerError(
|
|
415
|
+
`Passkey signing failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
416
|
+
error,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Converts a hex string to Uint8Array.
|
|
423
|
+
*/
|
|
424
|
+
private hexToBytes(hex: Hex): Uint8Array {
|
|
425
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
426
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
427
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
428
|
+
bytes[i / 2] = parseInt(cleanHex.slice(i, i + 2), 16);
|
|
429
|
+
}
|
|
430
|
+
return bytes;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Type guard to check if a value is a valid PasskeyCredential.
|
|
436
|
+
*/
|
|
437
|
+
export function isPasskeyCredential(
|
|
438
|
+
value: unknown,
|
|
439
|
+
): value is PasskeyCredential {
|
|
440
|
+
if (typeof value !== "object" || value === null) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const obj = value as Record<string, unknown>;
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
typeof obj.credentialId === "string" &&
|
|
448
|
+
typeof obj.credentialIdHex === "string" &&
|
|
449
|
+
obj.credentialIdHex.startsWith("0x") &&
|
|
450
|
+
typeof obj.publicKey === "object" &&
|
|
451
|
+
obj.publicKey !== null &&
|
|
452
|
+
typeof (obj.publicKey as Record<string, unknown>).x === "string" &&
|
|
453
|
+
typeof (obj.publicKey as Record<string, unknown>).y === "string" &&
|
|
454
|
+
typeof obj.publicKeyHex === "string" &&
|
|
455
|
+
obj.publicKeyHex.startsWith("0x")
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Serializes a PasskeyCredential to a JSON-safe object.
|
|
461
|
+
*/
|
|
462
|
+
export function serializeCredential(
|
|
463
|
+
credential: PasskeyCredential,
|
|
464
|
+
): Record<string, string> {
|
|
465
|
+
return {
|
|
466
|
+
credentialId: credential.credentialId,
|
|
467
|
+
credentialIdHex: credential.credentialIdHex,
|
|
468
|
+
publicKeyX: credential.publicKey.x,
|
|
469
|
+
publicKeyY: credential.publicKey.y,
|
|
470
|
+
publicKeyHex: credential.publicKeyHex,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Deserializes a PasskeyCredential from a JSON object.
|
|
476
|
+
*/
|
|
477
|
+
export function deserializeCredential(
|
|
478
|
+
data: Record<string, string>,
|
|
479
|
+
): PasskeyCredential {
|
|
480
|
+
if (
|
|
481
|
+
!data.credentialId ||
|
|
482
|
+
!data.credentialIdHex ||
|
|
483
|
+
!data.publicKeyX ||
|
|
484
|
+
!data.publicKeyY ||
|
|
485
|
+
!data.publicKeyHex
|
|
486
|
+
) {
|
|
487
|
+
throw new PasskeySignerError("Invalid serialized credential data");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
credentialId: data.credentialId,
|
|
492
|
+
credentialIdHex: data.credentialIdHex as Hex,
|
|
493
|
+
publicKey: {
|
|
494
|
+
x: data.publicKeyX as Hex,
|
|
495
|
+
y: data.publicKeyY as Hex,
|
|
496
|
+
},
|
|
497
|
+
publicKeyHex: data.publicKeyHex as Hex,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Address, Hex, TypedData, TypedDataDefinition } from "viem";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents the source of a SmartAccountSigner - either a custom source or a known type.
|
|
5
|
+
*/
|
|
6
|
+
export type SmartAccountSignerSource = "passkey" | "custom";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface for signing messages and typed data for Smart Accounts.
|
|
10
|
+
* Compatible with permissionless.js and viem account abstraction.
|
|
11
|
+
*/
|
|
12
|
+
export interface SmartAccountSigner<
|
|
13
|
+
TSource extends SmartAccountSignerSource = SmartAccountSignerSource,
|
|
14
|
+
> {
|
|
15
|
+
/** The type/source of the signer */
|
|
16
|
+
readonly source: TSource;
|
|
17
|
+
|
|
18
|
+
/** The public key associated with this signer (for passkeys, this is the encoded P256 key) */
|
|
19
|
+
readonly publicKey: Hex;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Signs a message and returns the signature.
|
|
23
|
+
* For passkeys, this triggers the biometric prompt.
|
|
24
|
+
*/
|
|
25
|
+
signMessage: (params: { message: SignableMessage }) => Promise<Hex>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Signs typed data according to EIP-712 and returns the signature.
|
|
29
|
+
* For passkeys, this triggers the biometric prompt.
|
|
30
|
+
*/
|
|
31
|
+
signTypedData: <
|
|
32
|
+
const TTypedData extends TypedData | Record<string, unknown>,
|
|
33
|
+
TPrimaryType extends keyof TTypedData | "EIP712Domain" = keyof TTypedData,
|
|
34
|
+
>(
|
|
35
|
+
typedData: TypedDataDefinition<TTypedData, TPrimaryType>
|
|
36
|
+
) => Promise<Hex>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the address derived from or associated with this signer.
|
|
40
|
+
* For passkeys with P256 keys, this may be a derived address or the account address.
|
|
41
|
+
*/
|
|
42
|
+
getAddress: () => Promise<Address>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Message types that can be signed.
|
|
47
|
+
* Matches viem's SignableMessage type.
|
|
48
|
+
*/
|
|
49
|
+
export type SignableMessage =
|
|
50
|
+
| string
|
|
51
|
+
| {
|
|
52
|
+
raw: Hex | Uint8Array;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configuration for WebAuthn/Passkey operations.
|
|
57
|
+
*/
|
|
58
|
+
export interface PasskeyConfig {
|
|
59
|
+
/** The Relying Party ID (usually the domain) */
|
|
60
|
+
rpId: string;
|
|
61
|
+
/** The Relying Party name for display */
|
|
62
|
+
rpName: string;
|
|
63
|
+
/** Challenge timeout in milliseconds (default: 60000) */
|
|
64
|
+
timeout?: number;
|
|
65
|
+
/** User verification requirement */
|
|
66
|
+
userVerification?: "required" | "preferred" | "discouraged";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Represents the parsed public key coordinates from a P256/secp256r1 key.
|
|
71
|
+
*/
|
|
72
|
+
export interface P256PublicKey {
|
|
73
|
+
/** X coordinate of the public key (32 bytes) */
|
|
74
|
+
x: Hex;
|
|
75
|
+
/** Y coordinate of the public key (32 bytes) */
|
|
76
|
+
y: Hex;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Credential data stored after passkey registration.
|
|
81
|
+
*/
|
|
82
|
+
export interface PasskeyCredential {
|
|
83
|
+
/** The credential ID in base64url format */
|
|
84
|
+
credentialId: string;
|
|
85
|
+
/** The raw credential ID as hex */
|
|
86
|
+
credentialIdHex: Hex;
|
|
87
|
+
/** The public key coordinates */
|
|
88
|
+
publicKey: P256PublicKey;
|
|
89
|
+
/** The encoded public key for the signer interface */
|
|
90
|
+
publicKeyHex: Hex;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* WebAuthn authenticator data structure.
|
|
95
|
+
*/
|
|
96
|
+
export interface AuthenticatorData {
|
|
97
|
+
/** RP ID hash (32 bytes) */
|
|
98
|
+
rpIdHash: Hex;
|
|
99
|
+
/** Flags byte */
|
|
100
|
+
flags: number;
|
|
101
|
+
/** Sign counter */
|
|
102
|
+
signCount: number;
|
|
103
|
+
/** Raw authenticator data */
|
|
104
|
+
raw: Hex;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parsed WebAuthn assertion response for signing.
|
|
109
|
+
*/
|
|
110
|
+
export interface WebAuthnSignature {
|
|
111
|
+
/** Raw authenticator data */
|
|
112
|
+
authenticatorData: Hex;
|
|
113
|
+
/** Client data JSON string */
|
|
114
|
+
clientDataJSON: string;
|
|
115
|
+
/** The r component of the signature (32 bytes) */
|
|
116
|
+
r: Hex;
|
|
117
|
+
/** The s component of the signature (32 bytes) */
|
|
118
|
+
s: Hex;
|
|
119
|
+
/** Challenge location in clientDataJSON */
|
|
120
|
+
challengeLocation: number;
|
|
121
|
+
/** Response type location in clientDataJSON */
|
|
122
|
+
responseTypeLocation: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Options for signature encoding format.
|
|
127
|
+
* Different smart contract implementations expect different formats.
|
|
128
|
+
*/
|
|
129
|
+
export type SignatureEncodingFormat =
|
|
130
|
+
| "kernel" // Kernel/ZeroDev format
|
|
131
|
+
| "rhinestone" // Rhinestone format
|
|
132
|
+
| "raw"; // Raw WebAuthn signature components
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Configuration for the ReactNativePasskeySigner.
|
|
136
|
+
*/
|
|
137
|
+
export interface ReactNativePasskeySignerConfig extends PasskeyConfig {
|
|
138
|
+
/** The format to use when encoding signatures */
|
|
139
|
+
signatureFormat?: SignatureEncodingFormat;
|
|
140
|
+
}
|