@@ -209,8 +209,23 @@ export function webAuthn(options: webAuthn.Parameters) {
209209 let account : Account . RootAccount | undefined
210210 let accessKey : Account . AccessKeyAccount | undefined
211211
212+ const defaultAccessKeyOptions = {
213+ expiry : Math . floor (
214+ ( Date . now ( ) + 24 * 60 * 60 * 1000 ) / 1000 , // one day
215+ ) ,
216+ strict : false ,
217+ }
218+ const accessKeyOptions = ( ( ) => {
219+ if ( typeof options . grantAccessKey === 'object' )
220+ return { ...defaultAccessKeyOptions , ...options . grantAccessKey }
221+ if ( options . grantAccessKey === true ) return defaultAccessKeyOptions
222+ return undefined
223+ } ) ( )
224+
212225 const idbStorage = Storage . idb < {
213- [ key : `accessKey:${string } `] : WebCryptoP256 . createKeyPair . ReturnType
226+ [ key : `accessKey:${string } `] : WebCryptoP256 . createKeyPair . ReturnType & {
227+ keyAuthorization : KeyAuthorization . KeyAuthorization
228+ }
214229 } > ( )
215230
216231 type Properties = {
@@ -251,10 +266,18 @@ export function webAuthn(options: webAuthn.Parameters) {
251266 account = Account . fromWebAuthnP256 ( credential )
252267 } ,
253268 async connect ( parameters = { } ) {
254- const { grantAccessKey = false } = options
255269 const capabilities =
256270 'capabilities' in parameters ? ( parameters . capabilities ?? { } ) : { }
257271
272+ if (
273+ accessKeyOptions ?. strict &&
274+ accessKeyOptions . expiry &&
275+ accessKeyOptions . expiry < Date . now ( ) / 1000
276+ )
277+ throw new Error (
278+ `\`grantAccessKey.expiry = ${ accessKeyOptions . expiry } \` is in the past (${ new Date ( accessKeyOptions . expiry * 1000 ) . toLocaleString ( ) } ). Please provide a valid expiry.` ,
279+ )
280+
258281 // We are going to need to find:
259282 // - a WebAuthn `credential` to instantiate an account
260283 // - optionally, a `keyPair` to use as the access key for the account
@@ -286,7 +309,7 @@ export function webAuthn(options: webAuthn.Parameters) {
286309
287310 // Get key pair (access key) to use for the account.
288311 const keyPair = await ( async ( ) => {
289- if ( ! grantAccessKey ) return undefined
312+ if ( ! accessKeyOptions ) return undefined
290313 return await WebCryptoP256 . createKeyPair ( )
291314 } ) ( )
292315
@@ -303,15 +326,15 @@ export function webAuthn(options: webAuthn.Parameters) {
303326 if ( credential ) {
304327 // Get key pair (access key) to use for the account.
305328 const keyPair = await ( async ( ) => {
306- if ( ! grantAccessKey ) return undefined
329+ if ( ! accessKeyOptions ) return undefined
307330 const address = Address . fromPublicKey (
308331 PublicKey . fromHex ( credential . publicKey ) ,
309332 )
310333 return await idbStorage . getItem ( `accessKey:${ address } ` )
311334 } ) ( )
312335
313- // If the access key policy is lax , return the credential and key pair (if exists).
314- if ( grantAccessKey === 'lax' ) return { credential, keyPair }
336+ // If the access key provisioning is not in strict mode , return the credential and key pair (if exists).
337+ if ( ! accessKeyOptions ?. strict ) return { credential, keyPair }
315338
316339 // If a key pair is found, return the credential and key pair.
317340 if ( keyPair ) return { credential, keyPair }
@@ -328,21 +351,25 @@ export function webAuthn(options: webAuthn.Parameters) {
328351 {
329352 // Get key pair (access key) to use for the account.
330353 const keyPair = await ( async ( ) => {
331- if ( ! grantAccessKey ) return undefined
354+ if ( ! accessKeyOptions ) return undefined
332355 return await WebCryptoP256 . createKeyPair ( )
333356 } ) ( )
334357
335358 // If we are provisioning an access key, we will need to sign a key authorization.
336359 // We will need the hash (digest) to sign, and the address of the access key to construct the key authorization.
337- const { accessKeyAddress , hash } = await ( async ( ) => {
360+ const { hash , keyAuthorization_unsigned } = await ( async ( ) => {
338361 if ( ! keyPair )
339362 return { accessKeyAddress : undefined , hash : undefined }
340363 const accessKeyAddress = Address . fromPublicKey ( keyPair . publicKey )
341- const hash = KeyAuthorization . getSignPayload ( {
364+ const keyAuthorization_unsigned = KeyAuthorization . from ( {
365+ ...accessKeyOptions ,
342366 address : accessKeyAddress ,
343367 type : 'p256' ,
344368 } )
345- return { accessKeyAddress, hash, keyPair }
369+ const hash = KeyAuthorization . getSignPayload (
370+ keyAuthorization_unsigned ,
371+ )
372+ return { keyAuthorization_unsigned, hash }
346373 } ) ( )
347374
348375 // If no active credential, we will attempt to load the last active credential from storage.
@@ -363,16 +390,15 @@ export function webAuthn(options: webAuthn.Parameters) {
363390 rpId : options . getOptions ?. rpId ?? options . rpId ,
364391 } )
365392
366- const keyAuthorization = accessKeyAddress
393+ const keyAuthorization = keyAuthorization_unsigned
367394 ? KeyAuthorization . from ( {
368- address : accessKeyAddress ,
395+ ... keyAuthorization_unsigned ,
369396 signature : SignatureEnvelope . from ( {
370397 metadata : credential . metadata ,
371398 signature : credential . signature ,
372399 publicKey : PublicKey . fromHex ( credential . publicKey ) ,
373400 type : 'webAuthn' ,
374401 } ) ,
375- type : 'p256' ,
376402 } )
377403 : undefined
378404
@@ -399,19 +425,42 @@ export function webAuthn(options: webAuthn.Parameters) {
399425 storage : Storage . from ( config . storage as never ) ,
400426 } )
401427
428+ // If we are reconnecting, check if the access key is expired.
429+ if ( parameters . isReconnecting ) {
430+ if (
431+ 'keyAuthorization' in keyPair &&
432+ keyPair . keyAuthorization . expiry &&
433+ keyPair . keyAuthorization . expiry < Date . now ( ) / 1000
434+ ) {
435+ // remove any pending key authorizations from storage.
436+ await account ?. storage . removeItem ( 'pendingKeyAuthorization' )
437+
438+ const message = `Access key expired (on ${ new Date ( keyPair . keyAuthorization . expiry * 1000 ) . toLocaleString ( ) } ).`
439+ accessKey = undefined
440+
441+ // if in strict mode, disconnect and throw an error.
442+ if ( accessKeyOptions ?. strict ) {
443+ await this . disconnect ( )
444+ throw new Error ( message )
445+ }
446+ // otherwise, fall back to the root account.
447+ console . warn ( `${ message } Falling back to passkey.` )
448+ }
449+ }
402450 // If we are not reconnecting, orchestrate the provisioning of the access key.
403- if ( ! parameters . isReconnecting ) {
451+ else {
404452 const keyAuth =
405- keyAuthorization ?? ( await account . signKeyAuthorization ( accessKey ) )
453+ keyAuthorization ??
454+ ( await account . signKeyAuthorization ( accessKey , accessKeyOptions ) )
406455
407456 await account . storage . setItem ( 'pendingKeyAuthorization' , keyAuth )
408457 await idbStorage . setItem (
409458 `accessKey:${ account . address . toLowerCase ( ) } ` ,
410- keyPair ,
459+ { ... keyPair , keyAuthorization : keyAuth } ,
411460 )
412461 }
413- // If we are granting an access key, throw an error if the access key is not provisioned.
414- } else if ( grantAccessKey === true ) {
462+ // If we are granting an access key and it is in strict mode , throw an error if the access key is not provisioned.
463+ } else if ( accessKeyOptions ?. strict ) {
415464 await config . storage ?. removeItem ( 'webAuthn.activeCredential' )
416465 throw new Error ( 'access key not found' )
417466 }
@@ -430,6 +479,7 @@ export function webAuthn(options: webAuthn.Parameters) {
430479 } ,
431480 async disconnect ( ) {
432481 await config . storage ?. removeItem ( 'webAuthn.activeCredential' )
482+ config . emitter . emit ( 'disconnect' )
433483 account = undefined
434484 } ,
435485 async getAccounts ( ) {
@@ -476,8 +526,36 @@ export function webAuthn(options: webAuthn.Parameters) {
476526 const transport = transports [ chain . id ]
477527 if ( ! transport ) throw new ChainNotConfiguredError ( )
478528
529+ const targetAccount = await ( async ( ) => {
530+ if ( ! accessKey ) return account
531+
532+ const item = await idbStorage . getItem (
533+ `accessKey:${ accessKey . address . toLowerCase ( ) } ` ,
534+ )
535+ if (
536+ item ?. keyAuthorization . expiry &&
537+ item . keyAuthorization . expiry < Date . now ( ) / 1000
538+ ) {
539+ // remove any pending key authorizations from storage.
540+ await account ?. storage . removeItem ( 'pendingKeyAuthorization' )
541+
542+ const message = `Access key expired (on ${ new Date ( item . keyAuthorization . expiry * 1000 ) . toLocaleString ( ) } ).`
543+
544+ // if in strict mode, disconnect and throw an error.
545+ if ( accessKeyOptions ?. strict ) {
546+ await this . disconnect ( )
547+ throw new Error ( message )
548+ }
549+
550+ // otherwise, fall back to the root account.
551+ console . warn ( `${ message } Falling back to passkey.` )
552+ return account
553+ }
554+ return accessKey
555+ } ) ( )
556+
479557 return createClient ( {
480- account : accessKey ?? account ,
558+ account : targetAccount ,
481559 chain : chain as Chain ,
482560 transport : walletNamespaceCompat ( transport ) ,
483561 } )
@@ -503,19 +581,14 @@ export namespace webAuthn {
503581 | Pick < WebAuthnP256 . getCredential . Parameters , 'getFn' | 'rpId' >
504582 | undefined
505583 /**
506- * Whether or not to grant an access key upon connection.
507- *
508- * - `true`: The account MUST have an access key provisioned.
509- * On failure, the connection will fail.
510- * - `"lax"`: The account MAY have an access key provisioned.
511- * On failure, the connection will succeed, but the access key will not be provisioned
512- * and must be provisioned manually if the user wants to enforce access keys.
513- * - `false`: The account WILL NOT have an access key provisioned. The access key must be
514- * provisioned manually if the user wants to enforce access keys.
515- *
516- * @default false
584+ * Whether or not to grant an access key upon connection, and optionally, expiry + limits to assign to the key.
517585 */
518- grantAccessKey ?: boolean | 'lax'
586+ grantAccessKey ?:
587+ | boolean
588+ | ( Pick < KeyAuthorization . KeyAuthorization , 'expiry' | 'limits' > & {
589+ /** Whether or not to throw an error and disconnect if the access key is not provisioned or is expired. */
590+ strict ?: boolean | undefined
591+ } )
519592 /** Public key manager. */
520593 keyManager : KeyManager . KeyManager
521594 /** The RP ID to use for WebAuthn. */
0 commit comments