diff --git a/packages/auth-helpers/src/index.ts b/packages/auth-helpers/src/index.ts index 25de60faff..d1561cfb73 100644 --- a/packages/auth-helpers/src/index.ts +++ b/packages/auth-helpers/src/index.ts @@ -3,6 +3,7 @@ export * from './lib/generate-auth-sig'; export * from './lib/models'; export * from './lib/recap/recap-session-capability-object'; export * from './lib/recap/resource-builder'; +export * from './lib/recap/utils'; export * from './lib/resources'; export * from './lib/session-capability-object'; export * from './lib/siwe/create-siwe-message'; diff --git a/packages/auth-helpers/src/lib/recap/utils.ts b/packages/auth-helpers/src/lib/recap/utils.ts index 9c96f0c991..d73e2238ed 100644 --- a/packages/auth-helpers/src/lib/recap/utils.ts +++ b/packages/auth-helpers/src/lib/recap/utils.ts @@ -54,3 +54,55 @@ export function getRecapNamespaceAndAbility(litAbility: LIT_ABILITY_VALUES): { ); } } + +export const RESOLVED_AUTH_CONTEXT_PREFIX = 'lit-resolvedauthcontext://'; +const PAYMENT_DELEGATION_PREFIX = 'lit-paymentdelegation://'; +const PKP_PREFIX = 'lit-pkp://'; +const ACC_PREFIX = 'lit-accesscontrolcondition://'; + +/** + * Reverse mapping from Recap namespace/ability to LitAbility. + * Returns null when the recap entry only carries metadata (eg. resolved auth context). + */ +export function getLitAbilityFromRecap(params: { + recapNamespace: string; + recapAbility: string; + resourceKey: string; +}): LIT_ABILITY_VALUES | null { + const { recapNamespace, recapAbility, resourceKey } = params; + + if (recapNamespace === LIT_NAMESPACE.Threshold) { + if (recapAbility === LIT_RECAP_ABILITY.Decryption) { + return LIT_ABILITY.AccessControlConditionDecryption; + } + + if (recapAbility === LIT_RECAP_ABILITY.Execution) { + return LIT_ABILITY.LitActionExecution; + } + + if (recapAbility === LIT_RECAP_ABILITY.Signing) { + if (resourceKey.startsWith(PKP_PREFIX)) { + return LIT_ABILITY.PKPSigning; + } + if (resourceKey.startsWith(ACC_PREFIX)) { + return LIT_ABILITY.AccessControlConditionSigning; + } + } + } + + if ( + recapNamespace === LIT_NAMESPACE.Auth && + recapAbility === LIT_RECAP_ABILITY.Auth + ) { + if (resourceKey.startsWith(PAYMENT_DELEGATION_PREFIX)) { + return LIT_ABILITY.PaymentDelegation; + } + + if (resourceKey.startsWith(RESOLVED_AUTH_CONTEXT_PREFIX)) { + // Resolved auth context entries only contain metadata. + return null; + } + } + + return null; +} diff --git a/packages/auth-helpers/src/lib/resources.ts b/packages/auth-helpers/src/lib/resources.ts index a5a494a9ce..5ff1ff631e 100644 --- a/packages/auth-helpers/src/lib/resources.ts +++ b/packages/auth-helpers/src/lib/resources.ts @@ -9,8 +9,13 @@ import { import { AccessControlConditions, ILitResource } from '@lit-protocol/types'; import { formatPKPResource } from './utils'; +const RESOLVED_AUTH_CONTEXT_PREFIX = 'lit-resolvedauthcontext'; +type InternalResourcePrefix = + | LIT_RESOURCE_PREFIX_VALUES + | typeof RESOLVED_AUTH_CONTEXT_PREFIX; + abstract class LitResourceBase { - abstract resourcePrefix: LIT_RESOURCE_PREFIX_VALUES; + abstract resourcePrefix: InternalResourcePrefix; public readonly resource: string; constructor(resource: string) { @@ -158,6 +163,18 @@ export function parseLitResource(resourceKey: string): ILitResource { return new LitActionResource( resourceKey.substring(`${LIT_RESOURCE_PREFIX.LitAction}://`.length) ); + } else if (resourceKey.startsWith(RESOLVED_AUTH_CONTEXT_PREFIX)) { + const resource = resourceKey.substring( + `${RESOLVED_AUTH_CONTEXT_PREFIX}://`.length + ); + + return { + resourcePrefix: RESOLVED_AUTH_CONTEXT_PREFIX, + resource, + getResourceKey: () => `${RESOLVED_AUTH_CONTEXT_PREFIX}://${resource}`, + toString: () => `${RESOLVED_AUTH_CONTEXT_PREFIX}://${resource}`, + isValidLitAbility: () => false, + } as unknown as ILitResource; } throw new InvalidArgumentException( { diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index f89b9e2a00..49c2ff2c1d 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -89,6 +89,33 @@ export { getAuthIdByAuthMethod } from './lib/authenticators/helper/utils'; * @returns {SessionKeyPair} The generated session key pair. */ export { generateSessionKeyPair } from './lib/AuthManager/utils/generateSessionKeyPair'; +export { validateDelegationAuthSig } from './lib/AuthManager/utils/validateDelegationAuthSig'; + +/** + * Utility function to generate a PKP delegation auth signature for a given session key pair. + * The PKP will sign the session key delegation message via Lit nodes. + * This function is useful for server-side scenarios where you want to pre-generate + * PKP session materials and reuse them across multiple requests. + */ +export { generatePkpDelegationAuthSig } from './lib/AuthManager/authAdapters/generatePkpDelegationAuthSig'; + +/** + * Utility function to generate an EOA delegation auth signature for a given session key pair. + * The EOA wallet will sign the session key delegation message directly. + * This function is useful for server-side scenarios where you want to pre-generate + * EOA session materials and reuse them across multiple requests. + */ +export { generateEoaDelegationAuthSig } from './lib/AuthManager/authAdapters/generateEoaDelegationAuthSig'; + +/** + * Utility function to create a PKP auth context from pre-generated session materials. + * This is a streamlined API for server-side scenarios where session materials + * are generated once and reused across multiple requests. + * + * This function only requires the essential parameters (pkpPublicKey, sessionKeyPair, delegationAuthSig) + * and extracts auth config information from the delegation signature automatically. + */ +export { getPkpAuthContextFromPreGeneratedAdapter } from './lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter'; // ============================== Authenticators ============================== export { diff --git a/packages/auth/src/lib/AuthManager/auth-manager.ts b/packages/auth/src/lib/AuthManager/auth-manager.ts index a48663a05e..40b780c8b9 100644 --- a/packages/auth/src/lib/AuthManager/auth-manager.ts +++ b/packages/auth/src/lib/AuthManager/auth-manager.ts @@ -1,6 +1,6 @@ import { getChildLogger } from '@lit-protocol/logger'; import { AuthData, HexPrefixedSchema } from '@lit-protocol/schemas'; -// import { AuthSig, SessionKeyPair } from '@lit-protocol/types'; +import { AuthSig, SessionKeyPair } from '@lit-protocol/types'; import { z } from 'zod'; import { AuthConfigV2 } from '../authenticators/types'; import type { LitAuthStorageProvider } from '../storage/types'; @@ -11,7 +11,9 @@ import { import { getPkpAuthContextAdapter } from './authAdapters/getPkpAuthContextAdapter'; import { AuthConfigSchema } from './authContexts/BaseAuthContextType'; import { getCustomAuthContextAdapter } from './authAdapters/getCustomAuthContextAdapter'; -import { hexToBigInt, keccak256, toBytes } from 'viem'; +import { generatePkpDelegationAuthSig } from './authAdapters/generatePkpDelegationAuthSig'; +import { generateEoaDelegationAuthSig } from './authAdapters/generateEoaDelegationAuthSig'; +import { getPkpAuthContextFromPreGeneratedAdapter } from './authAdapters/getPkpAuthContextFromPreGeneratedAdapter'; export interface AuthManagerParams { storage: LitAuthStorageProvider; @@ -77,12 +79,19 @@ export const createAuthManager = (authManagerParams: AuthManagerParams) => { cache?: { delegationAuthSig?: boolean; }; - // Optional pre-generated auth materials for server-side usage - // sessionKeyPair?: SessionKeyPair; - // delegationAuthSig?: AuthSig; + sessionKeyPair?: SessionKeyPair; + delegationAuthSig?: AuthSig; }) => { return getPkpAuthContextAdapter(authManagerParams, params); }, + createPkpAuthContextFromPreGenerated: (params: { + pkpPublicKey: z.infer; + sessionKeyPair: SessionKeyPair; + delegationAuthSig: AuthSig; + authData?: AuthData; + }) => { + return getPkpAuthContextFromPreGeneratedAdapter(params); + }, createCustomAuthContext: (params: { // authData: AuthData; pkpPublicKey: z.infer; @@ -104,5 +113,22 @@ export const createAuthManager = (authManagerParams: AuthManagerParams) => { return getCustomAuthContextAdapter(authManagerParams, params); }, + generatePkpDelegationAuthSig: (params: { + pkpPublicKey: z.infer; + authData: AuthData; + sessionKeyPair: SessionKeyPair; + authConfig: AuthConfigV2; + litClient: BaseAuthContext['litClient']; + }) => { + return generatePkpDelegationAuthSig(authManagerParams, params); + }, + generateEoaDelegationAuthSig: (params: { + account: any; // ExpectedAccountOrWalletClient type + sessionKeyPair: SessionKeyPair; + authConfig: AuthConfigV2; + litClient: BaseAuthContext['litClient']; + }) => { + return generateEoaDelegationAuthSig(authManagerParams, params); + }, }; }; diff --git a/packages/auth/src/lib/AuthManager/authAdapters/generateEoaDelegationAuthSig.ts b/packages/auth/src/lib/AuthManager/authAdapters/generateEoaDelegationAuthSig.ts new file mode 100644 index 0000000000..3c9e760b57 --- /dev/null +++ b/packages/auth/src/lib/AuthManager/authAdapters/generateEoaDelegationAuthSig.ts @@ -0,0 +1,109 @@ +import { AUTH_METHOD_TYPE } from '@lit-protocol/constants'; +import { getChildLogger } from '@lit-protocol/logger'; +import { AuthConfigSchema } from '@lit-protocol/schemas'; +import { AuthSig, SessionKeyPair } from '@lit-protocol/types'; +import { z } from 'zod'; +import { AuthConfigV2 } from '../../authenticators/types'; +import { LitAuthData } from '../../types'; +import { AuthManagerParams } from '../auth-manager'; +import { + ExpectedAccountOrWalletClient, + getEoaAuthContext, +} from '../authContexts/getEoaAuthContext'; +import { processResources } from '../utils/processResources'; +import { WalletClientAuthenticator } from '../../authenticators/WalletClientAuthenticator'; +import { ViemAccountAuthenticator } from '../../authenticators/ViemAccountAuthenticator'; + +const _logger = getChildLogger({ + module: 'generateEoaDelegationAuthSig', +}); + +/** + * Generates an EOA delegation auth signature for a given session key pair. + * The EOA wallet will sign the session key delegation message directly. + * This function is useful for server-side scenarios where you want to pre-generate + * EOA session materials and reuse them across multiple requests. + * + * @param upstreamParams - Auth manager parameters including storage + * @param params - Parameters for generating the EOA delegation signature + * @returns The delegation auth signature (AuthSig) signed by the EOA wallet + */ +export async function generateEoaDelegationAuthSig( + upstreamParams: AuthManagerParams, + params: { + account: ExpectedAccountOrWalletClient; + sessionKeyPair: SessionKeyPair; + authConfig: AuthConfigV2; + litClient: { + getContext: () => Promise; + }; + } +): Promise { + _logger.info( + { + hasAccount: !!params.account, + hasSessionKeyPair: !!params.sessionKeyPair, + }, + 'generateEoaDelegationAuthSig: Starting EOA delegation signature generation' + ); + + const _resources = processResources(params.authConfig.resources); + + // Get network context from litClient for nonce + const litClientCtx = await params.litClient.getContext(); + + // Create a minimal LitAuthData structure with the provided session key pair + const litAuthData: LitAuthData = { + sessionKey: { + keyPair: params.sessionKeyPair, + expiresAt: params.authConfig.expiration!, + }, + // For EOA, we use EthWallet as the auth method type + authMethodType: AUTH_METHOD_TYPE.EthWallet, + }; + + // Determine the authenticator based on account type + let authenticatorClass; + if ( + 'account' in params.account && + params.account.account?.type === 'json-rpc' + ) { + // WalletClient + authenticatorClass = WalletClientAuthenticator; + } else { + // Viem Account + authenticatorClass = ViemAccountAuthenticator; + } + + // Create auth config for validation + const authConfigForValidation = { + ...params.authConfig, + resources: _resources, + }; + const validatedAuthConfig = AuthConfigSchema.parse(authConfigForValidation); + + // Call getEoaAuthContext which will generate the delegation signature + const authContext = await getEoaAuthContext({ + authentication: { + authenticator: authenticatorClass, + account: params.account, + }, + authConfig: validatedAuthConfig, + deps: { + nonce: litClientCtx.latestBlockhash, + authData: litAuthData, + }, + }); + + // Get the delegation signature from the auth context + const delegationAuthSig = await authContext.authNeededCallback(); + + _logger.info( + { + hasSignature: !!delegationAuthSig, + }, + 'generateEoaDelegationAuthSig: EOA delegation signature generated successfully' + ); + + return delegationAuthSig as AuthSig; +} diff --git a/packages/auth/src/lib/AuthManager/authAdapters/generatePkpDelegationAuthSig.ts b/packages/auth/src/lib/AuthManager/authAdapters/generatePkpDelegationAuthSig.ts new file mode 100644 index 0000000000..fb7ac66f8f --- /dev/null +++ b/packages/auth/src/lib/AuthManager/authAdapters/generatePkpDelegationAuthSig.ts @@ -0,0 +1,123 @@ +import { AUTH_METHOD_TYPE_VALUES, PRODUCT_IDS } from '@lit-protocol/constants'; +import { getChildLogger } from '@lit-protocol/logger'; +import { AuthData, HexPrefixedSchema } from '@lit-protocol/schemas'; +import { AuthSig, SessionKeyPair } from '@lit-protocol/types'; +import { ethers } from 'ethers'; +import { z } from 'zod'; +import { AuthConfigV2 } from '../../authenticators/types'; +import { LitAuthData } from '../../types'; +import { AuthManagerParams } from '../auth-manager'; +import { getPkpAuthContext } from '../authContexts/getPkpAuthContext'; +import { processResources } from '../utils/processResources'; + +const _logger = getChildLogger({ + module: 'generatePkpDelegationAuthSig', +}); + +/** + * Generates a PKP delegation auth signature for a given session key pair. + * The PKP will sign the session key delegation message via Lit nodes. + * This function is useful for server-side scenarios where you want to pre-generate + * PKP session materials and reuse them across multiple requests. + * + * @param upstreamParams - Auth manager parameters including storage + * @param params - Parameters for generating the PKP delegation signature + * @returns The delegation auth signature (AuthSig) signed by the PKP + */ +export async function generatePkpDelegationAuthSig( + upstreamParams: AuthManagerParams, + params: { + pkpPublicKey: z.infer; + authData: AuthData; + sessionKeyPair: SessionKeyPair; + authConfig: AuthConfigV2; + litClient: { + getContext: () => Promise; + }; + productId?: 'LIT_ACTION' | 'SIGN_SESSION_KEY'; + } +): Promise { + const defaultedProductId = params.productId ?? 'LIT_ACTION'; + + _logger.info( + { + pkpPublicKey: params.pkpPublicKey, + hasSessionKeyPair: !!params.sessionKeyPair, + productId: defaultedProductId, + }, + 'generatePkpDelegationAuthSig: Starting PKP delegation signature generation' + ); + + const _resources = processResources(params.authConfig.resources); + + // Get network context from litClient + const litClientCtx = await params.litClient.getContext(); + const latestConnectionInfo = litClientCtx.latestConnectionInfo; + const nodePrices = latestConnectionInfo.priceFeedInfo.networkPrices; + const handshakeResult = litClientCtx.handshakeResult; + const threshold = handshakeResult.threshold; + + const nodeUrls = litClientCtx.getMaxPricesForNodeProduct({ + nodePrices: nodePrices, + userMaxPrice: litClientCtx.getUserMaxPrice({ + product: defaultedProductId, + }), + productId: PRODUCT_IDS[defaultedProductId], + numRequiredNodes: threshold, + }); + + const pkpAddress = ethers.utils.computeAddress(params.pkpPublicKey); + + // Create a minimal LitAuthData structure with the provided session key pair + const litAuthData: LitAuthData = { + sessionKey: { + keyPair: params.sessionKeyPair, + expiresAt: params.authConfig.expiration!, + }, + authMethodType: params.authData.authMethodType as AUTH_METHOD_TYPE_VALUES, + }; + + // Call getPkpAuthContext which will generate the delegation signature + const authContext = await getPkpAuthContext({ + authentication: { + pkpPublicKey: params.pkpPublicKey, + authData: params.authData, + }, + authConfig: { + domain: params.authConfig.domain!, + resources: _resources, + capabilityAuthSigs: params.authConfig.capabilityAuthSigs!, + expiration: params.authConfig.expiration!, + statement: params.authConfig.statement!, + }, + deps: { + litAuthData: litAuthData, + connection: { + nonce: litClientCtx.latestBlockhash, + currentEpoch: + litClientCtx.latestConnectionInfo.epochState.currentNumber, + nodeUrls: nodeUrls, + }, + signSessionKey: litClientCtx.signSessionKey, + storage: upstreamParams.storage, + pkpAddress: pkpAddress, + }, + // Disable caching since we're explicitly generating a new signature + cache: { + delegationAuthSig: false, + }, + }); + + // Get the delegation signature from the auth context + const delegationAuthSig = await authContext.authNeededCallback(); + + _logger.info( + { + pkpAddress, + hasSignature: !!delegationAuthSig, + }, + 'generatePkpDelegationAuthSig: PKP delegation signature generated successfully' + ); + + return delegationAuthSig; +} diff --git a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts index 89297a554e..abb89663b2 100644 --- a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts +++ b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts @@ -1,19 +1,25 @@ import { AUTH_METHOD_TYPE_VALUES, PRODUCT_IDS } from '@lit-protocol/constants'; +import { getChildLogger } from '@lit-protocol/logger'; import { AuthData, HexPrefixedSchema, NodeUrlsSchema, - // SessionKeyUriSchema, + SessionKeyUriSchema, } from '@lit-protocol/schemas'; -// import { AuthSig, LitResourceAbilityRequest, SessionKeyPair } from '@lit-protocol/types'; +import { AuthSig, SessionKeyPair } from '@lit-protocol/types'; import { ethers } from 'ethers'; import { z } from 'zod'; import { AuthConfigV2 } from '../../authenticators/types'; import { AuthManagerParams } from '../auth-manager'; import { getPkpAuthContext } from '../authContexts/getPkpAuthContext'; import { processResources } from '../utils/processResources'; +import { validateDelegationAuthSig } from '../utils/validateDelegationAuthSig'; import { tryGetCachedAuthData } from '../try-getters/tryGetCachedAuthData'; +const _logger = getChildLogger({ + module: 'getPkpAuthContextAdapter', +}); + export const PkpAuthDepsSchema = z.object({ nonce: z.any(), currentEpoch: z.any(), @@ -21,40 +27,6 @@ export const PkpAuthDepsSchema = z.object({ nodeUrls: NodeUrlsSchema, }); -/** - * Validates that the provided delegation auth sig hasn't expired and contains required resources - */ -// function validateDelegationAuthSig( -// delegationAuthSig: AuthSig, -// requiredResources: LitResourceAbilityRequest[], -// sessionKeyUri: string -// ): void { -// try { -// // Parse the signed message to extract expiration and validate session key match -// const siweMessage = delegationAuthSig.signedMessage; - -// // Check expiration -// const expirationMatch = siweMessage.match(/^Expiration Time: (.*)$/m); -// if (expirationMatch && expirationMatch[1]) { -// const expiration = new Date(expirationMatch[1].trim()); -// if (expiration.getTime() <= Date.now()) { -// throw new Error(`Delegation signature has expired at ${expiration.toISOString()}`); -// } -// } - -// // Validate session key URI matches -// if (!siweMessage.includes(sessionKeyUri)) { -// throw new Error('Session key URI in delegation signature does not match provided session key pair'); -// } - -// // TODO: Add resource validation - check if delegationAuthSig has required resources -// // This would involve parsing the RECAP URN and checking against requiredResources - -// } catch (error) { -// throw new Error(`Invalid delegation signature: ${error instanceof Error ? error.message : 'Unknown error'}`); -// } -// } - export async function getPkpAuthContextAdapter( upstreamParams: AuthManagerParams, params: { @@ -67,54 +39,69 @@ export async function getPkpAuthContextAdapter( cache?: { delegationAuthSig?: boolean; }; - // Optional pre-generated auth materials - // sessionKeyPair?: SessionKeyPair; - // delegationAuthSig?: AuthSig; + // Optional pre-generated auth materials for server-side usage + sessionKeyPair?: SessionKeyPair; + delegationAuthSig?: AuthSig; } ) { const _resources = processResources(params.authConfig.resources); - // // Validate optional parameters - // if ((params.sessionKeyPair && !params.delegationAuthSig) || - // (!params.sessionKeyPair && params.delegationAuthSig)) { - // throw new Error('Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided'); - // } - - // // If pre-generated auth materials are provided, validate and use them - // if (params.sessionKeyPair && params.delegationAuthSig) { - // // Generate sessionKeyUri from the public key - // const sessionKeyUri = SessionKeyUriSchema.parse(params.sessionKeyPair.publicKey); - - // // Validate the delegation signature - // validateDelegationAuthSig( - // params.delegationAuthSig, - // _resources, - // sessionKeyUri - // ); - - // // Return auth context using provided materials - // return { - // chain: 'ethereum', - // pkpPublicKey: params.pkpPublicKey, - // authData: params.authData, - // authConfig: { - // domain: params.authConfig.domain!, - // resources: _resources, - // capabilityAuthSigs: params.authConfig.capabilityAuthSigs!, - // expiration: params.authConfig.expiration!, - // statement: params.authConfig.statement!, - // }, - // sessionKeyPair: { - // ...params.sessionKeyPair, - // sessionKeyUri, // Add the generated sessionKeyUri to match expected interface - // }, - // // Provide the pre-generated delegation signature - // authNeededCallback: async () => params.delegationAuthSig!, - // }; - // } + // Validate optional parameters + if ( + (params.sessionKeyPair && !params.delegationAuthSig) || + (!params.sessionKeyPair && params.delegationAuthSig) + ) { + throw new Error( + 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' + ); + } + + // If pre-generated auth materials are provided, validate and use them + if (params.sessionKeyPair && params.delegationAuthSig) { + _logger.info( + { + hasSessionKeyPair: true, + hasDelegationAuthSig: true, + }, + 'getPkpAuthContextAdapter: Using pre-generated session materials' + ); + + // Generate sessionKeyUri from the public key + const sessionKeyUri = SessionKeyUriSchema.parse( + 'lit:session:' + params.sessionKeyPair.publicKey + ); + + // Validate the delegation signature + validateDelegationAuthSig({ + delegationAuthSig: params.delegationAuthSig, + sessionKeyUri, + }); + + // Return auth context using provided materials + return { + chain: 'ethereum', + pkpPublicKey: params.pkpPublicKey, + authData: params.authData, + authConfig: { + domain: params.authConfig.domain!, + resources: _resources, + capabilityAuthSigs: params.authConfig.capabilityAuthSigs!, + expiration: params.authConfig.expiration!, + statement: params.authConfig.statement!, + }, + sessionKeyPair: params.sessionKeyPair, + // Provide the pre-generated delegation signature + authNeededCallback: async () => { + _logger.debug( + 'getPkpAuthContextAdapter: Returning pre-generated delegation signature' + ); + return params.delegationAuthSig!; + }, + }; + } // Original logic for generating auth materials - // TODO: πŸ‘‡ The plan is to identify if the certain operations could be wrapped inside a single function + // TODO: πŸ‘‡ The plan is to identify whether certain operations can be wrapped inside a single function // where different network modules can provide their own implementations. // TODO: ❗️THIS IS NOT TYPED - we have to fix this! diff --git a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter.spec.ts b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter.spec.ts new file mode 100644 index 0000000000..15c24bc8d4 --- /dev/null +++ b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter.spec.ts @@ -0,0 +1,105 @@ +import { getPkpAuthContextFromPreGeneratedAdapter } from './getPkpAuthContextFromPreGeneratedAdapter'; +import type { AuthSig, SessionKeyPair } from '@lit-protocol/types'; + +const baseSessionKeyPair: SessionKeyPair = { + publicKey: 'a1b2c3', + secretKey: 'deadbeef', +}; + +const baseDelegation = ({ + recap, +}: { + recap: Record; +}): AuthSig => { + const resourcesLine = `- urn:recap:${Buffer.from( + JSON.stringify(recap) + ).toString('base64url')}`; + + const signedMessage = [ + 'localhost wants you to sign in with your Ethereum account:', + '0x1234567890abcdef1234567890ABCDEF12345678', + '', + "Lit Protocol PKP session signature I further authorize the stated URI to perform the following actions on my behalf: (1) 'Threshold': 'Signing' for 'lit-pkp://*'.", + '', + 'URI: lit:session:a1b2c3', + 'Version: 1', + 'Chain ID: 1', + 'Nonce: 0xabc', + 'Issued At: 2025-01-01T00:00:00Z', + 'Expiration Time: 2099-01-01T00:00:00.000Z', + 'Resources:', + resourcesLine, + ].join('\n'); + + return { + sig: '{"ProofOfPossession":"proof"}', + algo: 'LIT_BLS', + derivedVia: 'lit.bls', + signedMessage, + address: '0x1234567890abcdef1234567890ABCDEF12345678', + }; +}; + +describe('getPkpAuthContextFromPreGeneratedAdapter', () => { + it('derives authData from recap metadata when not provided', async () => { + const recapPayload = { + att: { + 'lit-pkp://*': { + 'Threshold/Signing': [{}], + }, + 'lit-resolvedauthcontext://*': { + 'Auth/Auth': [ + { + auth_context: { + authMethodContexts: [ + { + appId: 'lit', + authMethodType: 1, + usedForSignSessionKeyRequest: true, + userId: '0xabcdef', + }, + ], + authSigAddress: null, + }, + }, + ], + }, + }, + prf: [], + }; + + const delegationAuthSig = baseDelegation({ recap: recapPayload }); + + const context = await getPkpAuthContextFromPreGeneratedAdapter({ + pkpPublicKey: '0xpkp', + sessionKeyPair: baseSessionKeyPair, + delegationAuthSig, + }); + + expect(context.authData).toBeDefined(); + expect(context.authData?.authMethodId).toBe('0xabcdef'); + expect(context.derivedAuthMetadata?.authMethodId).toBe('0xabcdef'); + expect(context.authConfig.resources.length).toBeGreaterThan(0); + }); + + it('throws when recap metadata is missing and authData not supplied', async () => { + const recapPayload = { + att: { + 'lit-pkp://*': { + 'Threshold/Signing': [{}], + }, + }, + prf: [], + }; + + const delegationAuthSig = baseDelegation({ recap: recapPayload }); + + await expect( + getPkpAuthContextFromPreGeneratedAdapter({ + pkpPublicKey: '0xpkp', + sessionKeyPair: baseSessionKeyPair, + delegationAuthSig, + }) + ).rejects.toThrow(/Failed to derive authData/); + }); +}); diff --git a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter.ts b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter.ts new file mode 100644 index 0000000000..448a797586 --- /dev/null +++ b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextFromPreGeneratedAdapter.ts @@ -0,0 +1,442 @@ +import { getChildLogger } from '@lit-protocol/logger'; +import { + RecapSessionCapabilityObject, + getLitAbilityFromRecap, + parseLitResource, +} from '@lit-protocol/auth-helpers'; +import { RESOLVED_AUTH_CONTEXT_PREFIX } from '@lit-protocol/auth-helpers'; +import { + AUTH_METHOD_TYPE, + AUTH_METHOD_TYPE_VALUES, + LIT_ABILITY_VALUES, +} from '@lit-protocol/constants'; +import { + HexPrefixedSchema, + SessionKeyUriSchema, + AuthData, + AuthDataSchema, + DefinedJson, + DefinedJsonSchema, +} from '@lit-protocol/schemas'; +import { + AuthSig, + ILitResource, + LitResourceAbilityRequest, + SessionKeyPair, +} from '@lit-protocol/types'; +import { z } from 'zod'; +import { processResources } from '../utils/processResources'; +import { validateDelegationAuthSig } from '../utils/validateDelegationAuthSig'; + +const _logger = getChildLogger({ + module: 'getPkpAuthContextFromPreGeneratedAdapter', +}); + +const AuthMethodContextSchema = z + .object({ + appId: z.string().optional(), + authMethodType: z.number(), + usedForSignSessionKeyRequest: z.boolean().optional(), + userId: z.string(), + }) + .passthrough(); + +const AuthContextSchema = z + .object({ + authMethodContexts: z.array(AuthMethodContextSchema), + authSigAddress: z.string().optional().nullable(), + }) + .passthrough(); + +function buildAuthDataFromAuthContext(authContext: unknown): AuthData | null { + const parsedContext = AuthContextSchema.safeParse(authContext); + + if (!parsedContext.success) { + _logger.debug( + { + issues: parsedContext.error.issues, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Unable to parse auth context from recap restriction' + ); + return null; + } + + const { authMethodContexts, authSigAddress } = parsedContext.data; + + if (authMethodContexts.length === 0) { + return null; + } + + const resolvedContext = + authMethodContexts.find( + (context) => context.usedForSignSessionKeyRequest === true + ) ?? authMethodContexts[0]; + + const authMethodType = resolvedContext.authMethodType; + const userId = resolvedContext.userId; + + if ( + !Object.values(AUTH_METHOD_TYPE).includes( + authMethodType as AUTH_METHOD_TYPE_VALUES + ) + ) { + _logger.warn( + { + authMethodType, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Unsupported auth method type found in delegation auth signature recap' + ); + return null; + } + + if (authMethodType !== AUTH_METHOD_TYPE.EthWallet) { + _logger.info( + { + authMethodType, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Recap metadata present but only EthWallet is auto-derived; please supply authData explicitly for other auth methods' + ); + return null; + } + + if (!userId.startsWith('0x')) { + _logger.warn( + { + userId, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Derived userId from delegation auth signature recap is not hex-prefixed' + ); + return null; + } + + const candidate = { + authMethodType: authMethodType as AUTH_METHOD_TYPE_VALUES, + authMethodId: userId, + accessToken: JSON.stringify({ + userId, + appId: resolvedContext.appId, + }), + ...(authSigAddress?.startsWith('0x') ? { publicKey: authSigAddress } : {}), + metadata: { + authContext: parsedContext.data, + }, + }; + + const parsedAuthData = AuthDataSchema.safeParse(candidate); + + if (!parsedAuthData.success) { + _logger.warn( + { + candidate, + issues: parsedAuthData.error.issues, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Unable to derive authData from delegation auth signature recap' + ); + return null; + } + + _logger.debug( + { + authMethodType: parsedAuthData.data.authMethodType, + authMethodId: parsedAuthData.data.authMethodId, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Derived auth metadata from delegation recap' + ); + + return parsedAuthData.data; +} + +function extractAuthMetadataFromRestriction( + restriction: Record +): AuthData | null { + if (!('auth_context' in restriction)) { + return null; + } + + return buildAuthDataFromAuthContext(restriction['auth_context']); +} + +function sanitiseRestrictionData( + restriction: Record +): Record | undefined { + const parsedData = Object.entries(restriction).reduce((acc, [key, value]) => { + const result = DefinedJsonSchema.safeParse(value); + if (result.success) { + acc[key] = result.data; + } + return acc; + }, {} as Record); + + return Object.keys(parsedData).length > 0 ? parsedData : undefined; +} + +function decodeRecapResource(urn: string): { + requests: LitResourceAbilityRequest[]; + derivedAuthData: AuthData | null; +} { + try { + const recap = RecapSessionCapabilityObject.decode(urn); + const attenuations = recap.attenuations; + + const requests: LitResourceAbilityRequest[] = []; + let derivedAuthData: AuthData | null = null; + + for (const [resourceKey, abilityMap] of Object.entries(attenuations)) { + if ( + !abilityMap || + typeof abilityMap !== 'object' || + Array.isArray(abilityMap) + ) { + continue; + } + + const isResolvedAuthContext = resourceKey.startsWith( + RESOLVED_AUTH_CONTEXT_PREFIX + ); + + let resource: ILitResource | null = null; + if (!isResolvedAuthContext) { + try { + resource = parseLitResource(resourceKey); + } catch (error) { + _logger.warn( + { + resourceKey, + error, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Unable to parse lit resource from recap attenuation' + ); + continue; + } + } + + for (const [abilityKey, restrictions] of Object.entries( + abilityMap as Record + )) { + const restrictionArray = Array.isArray(restrictions) + ? (restrictions as Array) + : []; + + const restriction = restrictionArray.find( + (item): item is Record => + !!item && typeof item === 'object' + ); + + let data: Record | undefined; + + if (restriction) { + const restrictionRecord = restriction as Record; + + if (!derivedAuthData) { + derivedAuthData = + extractAuthMetadataFromRestriction(restrictionRecord) || + derivedAuthData; + if (derivedAuthData) { + _logger.debug( + { + authMethodType: derivedAuthData.authMethodType, + authMethodId: derivedAuthData.authMethodId, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Reusing derived auth metadata from recap payload' + ); + } + } + + data = sanitiseRestrictionData(restrictionRecord); + } + + const [recapNamespace, recapAbility] = abilityKey.split('/'); + const litAbility = recapNamespace + ? getLitAbilityFromRecap({ + recapNamespace, + recapAbility: recapAbility ?? '', + resourceKey, + }) + : null; + + if (!litAbility || !resource) { + continue; + } + + requests.push({ + resource, + ability: litAbility, + ...(data ? { data } : {}), + }); + } + } + + return { requests, derivedAuthData }; + } catch (error) { + _logger.warn( + { + urn, + error, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Failed to decode recap resource' + ); + + return { requests: [], derivedAuthData: null }; + } +} + +/** + * Extracts auth config information from a delegation signature's SIWE message + */ +function extractAuthConfigFromDelegationAuthSig(delegationAuthSig: AuthSig): { + domain?: string; + statement?: string; + expiration?: string; + resources?: LitResourceAbilityRequest[]; + derivedAuthData?: AuthData; +} { + const siweMessage = delegationAuthSig.signedMessage; + + // Extract domain + const domainMatch = siweMessage.match(/^([^\s]+) wants you to sign in/m); + const domain = domainMatch ? domainMatch[1] : undefined; + + // Extract statement + const statementMatch = siweMessage.match(/^(.*?)(?:\n\nURI:|$)/m); + const statement = statementMatch + ? statementMatch[1].split('\n').slice(2).join('\n').trim() + : undefined; + + // Extract expiration + const expirationMatch = siweMessage.match(/^Expiration Time: (.*)$/m); + const expiration = expirationMatch ? expirationMatch[1].trim() : undefined; + + const resourceMatches = [...siweMessage.matchAll(/-\s*(urn:recap:[^\s]+)/g)]; + + const resources: LitResourceAbilityRequest[] = []; + let derivedAuthData: AuthData | undefined; + + for (const match of resourceMatches) { + const urn = match[1]; + const { requests, derivedAuthData: candidateAuthData } = + decodeRecapResource(urn); + + resources.push(...requests); + + if (!derivedAuthData && candidateAuthData) { + derivedAuthData = candidateAuthData; + } + } + + return { domain, statement, expiration, resources, derivedAuthData }; +} + +/** + * Creates a PKP auth context from pre-generated session materials. + * This is a streamlined API for server-side scenarios where session materials + * are generated once and reused across multiple requests. + */ +/** + * Builds a PKP auth context from session materials that were pre-generated elsewhere. + * + * @example + * ```ts + * const { sessionKeyPair, delegationAuthSig } = await generateMaterials(); + * const context = await getPkpAuthContextFromPreGeneratedAdapter({ + * pkpPublicKey: '0xabc...', + * sessionKeyPair, + * delegationAuthSig, + * }); + * ``` + * + * @param params.pkpPublicKey PKP public key that minted the delegation. + * @param params.sessionKeyPair Session key pair previously generated. + * @param params.delegationAuthSig Delegation signature returned from PKP nodes. + * @param params.authData Optional auth metadata from the original signer; if omitted, + * the method will attempt to reconstruct it from the RECAP payload. + * @returns An auth context that can be reused for issuing session signatures. + */ +export async function getPkpAuthContextFromPreGeneratedAdapter(params: { + pkpPublicKey: z.infer; + sessionKeyPair: SessionKeyPair; + delegationAuthSig: AuthSig; + authData?: AuthData; +}) { + _logger.info( + { + pkpPublicKey: params.pkpPublicKey, + hasSessionKeyPair: !!params.sessionKeyPair, + hasDelegationAuthSig: !!params.delegationAuthSig, + hasAuthData: !!params.authData, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Creating PKP auth context from pre-generated materials' + ); + + // Extract auth config from delegation signature + const extractedAuthConfig = extractAuthConfigFromDelegationAuthSig( + params.delegationAuthSig + ); + + // Create auth config using extracted information with sensible defaults + const resources = + extractedAuthConfig.resources && extractedAuthConfig.resources.length > 0 + ? extractedAuthConfig.resources + : processResources([['pkp-signing', '*']]); + + const authConfig = { + domain: extractedAuthConfig.domain || 'localhost', + resources, + capabilityAuthSigs: [], + expiration: + extractedAuthConfig.expiration || + new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + statement: extractedAuthConfig.statement || '', + }; + + // Generate sessionKeyUri from the public key + const sessionKeyUri = SessionKeyUriSchema.parse( + 'lit:session:' + params.sessionKeyPair.publicKey + ); + + // Validate the delegation signature + validateDelegationAuthSig({ + delegationAuthSig: params.delegationAuthSig, + sessionKeyUri, + }); + + const resolvedAuthData = + params.authData || extractedAuthConfig.derivedAuthData; + + if (!resolvedAuthData) { + _logger.error( + { + pkpPublicKey: params.pkpPublicKey, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Unable to derive authData from delegation signature and none supplied' + ); + + throw new Error( + 'Failed to derive authData from delegation signature. Provide authData explicitly when calling createPkpAuthContextFromPreGenerated.' + ); + } + + _logger.debug( + { + authMethodType: resolvedAuthData.authMethodType, + authMethodId: resolvedAuthData.authMethodId, + }, + 'getPkpAuthContextFromPreGeneratedAdapter: Using resolved auth metadata for PKP context' + ); + + // Return auth context using pre-generated materials + return { + chain: 'ethereum', + pkpPublicKey: params.pkpPublicKey, + authData: resolvedAuthData, + authConfig, + sessionKeyPair: params.sessionKeyPair, + derivedAuthMetadata: extractedAuthConfig.derivedAuthData || undefined, + // Provide the pre-generated delegation signature + authNeededCallback: async () => { + _logger.debug( + 'getPkpAuthContextFromPreGeneratedAdapter: Returning pre-generated delegation signature' + ); + return params.delegationAuthSig; + }, + }; +} diff --git a/packages/auth/src/lib/AuthManager/authContexts/getPkpAuthContext.ts b/packages/auth/src/lib/AuthManager/authContexts/getPkpAuthContext.ts index 215eddc75b..c795c14ef4 100644 --- a/packages/auth/src/lib/AuthManager/authContexts/getPkpAuthContext.ts +++ b/packages/auth/src/lib/AuthManager/authContexts/getPkpAuthContext.ts @@ -7,14 +7,16 @@ import { NodeInfoSchema, NodeUrlsSchema, SessionKeyUriSchema, + SessionKeyPairSchema, } from '@lit-protocol/schemas'; -import { NodeSet } from '@lit-protocol/types'; +import { AuthSig, NodeSet } from '@lit-protocol/types'; import { z } from 'zod'; import { LitAuthStorageProvider } from '../../storage'; import { LitAuthData, LitAuthDataSchema } from '../../types'; import { AuthConfig } from '../auth-manager'; import { tryGetCachedDelegationAuthSig } from '../try-getters/tryGetCachedDelegationAuthSig'; import { AuthConfigSchema } from './BaseAuthContextType'; +import { validateDelegationAuthSig } from '../utils/validateDelegationAuthSig'; const _logger = getChildLogger({ module: 'getPkpAuthContext', @@ -49,12 +51,17 @@ export const GetPkpAuthContextSchema = z.object({ // @depreacted - to be removed. testing only. pkpAddress: z.string(), + + // Optional pre-generated delegation signature + preGeneratedDelegationAuthSig: z.any().optional(), }), cache: z .object({ delegationAuthSig: z.boolean().optional(), }) .optional(), + sessionKeyPair: SessionKeyPairSchema.optional(), + delegationAuthSig: z.custom().optional(), }); interface PreparePkpAuthRequestBodyParams { @@ -124,19 +131,7 @@ export const getPkpAuthContext = async ( ); const _params = GetPkpAuthContextSchema.parse(params); - const _nodeInfo = NodeInfoSchema.parse(params.deps.connection.nodeUrls); - - const requestBody = await preparePkpAuthRequestBody({ - authentication: _params.authentication, - authConfig: _params.authConfig, - deps: { - litAuthData: _params.deps.litAuthData, - nodeUrls: _nodeInfo.urls, - nodeSet: _nodeInfo.nodeSet, - nonce: _params.deps.connection.nonce, - currentEpoch: _params.deps.connection.currentEpoch, - }, - }); + const _nodeInfo = NodeInfoSchema.parse(_params.deps.connection.nodeUrls); const authConfig: AuthConfig = { capabilityAuthSigs: _params.authConfig.capabilityAuthSigs, @@ -146,21 +141,69 @@ export const getPkpAuthContext = async ( resources: _params.authConfig.resources, }; - const delegationAuthSig = await tryGetCachedDelegationAuthSig({ - cache: _params.cache?.delegationAuthSig, - storage: _params.deps.storage, - address: _params.deps.pkpAddress, - expiration: _params.authConfig.expiration, - signSessionKey: () => - _params.deps.signSessionKey({ - requestBody, + const hasProvidedSessionKeyPair = !!_params.sessionKeyPair; + const hasProvidedDelegationAuthSig = !!_params.delegationAuthSig; + + if (hasProvidedSessionKeyPair !== hasProvidedDelegationAuthSig) { + throw new Error( + 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' + ); + } + + const sessionKeyPair = hasProvidedSessionKeyPair + ? _params.sessionKeyPair! + : _params.deps.litAuthData.sessionKey.keyPair; + + const sessionKeyUri = SessionKeyUriSchema.parse(sessionKeyPair.publicKey); + + let delegationAuthSig: AuthSig; + let isPreGenerated = false; + + if (hasProvidedSessionKeyPair && hasProvidedDelegationAuthSig) { + validateDelegationAuthSig({ + delegationAuthSig: _params.delegationAuthSig!, + sessionKeyUri, + }); + delegationAuthSig = _params.delegationAuthSig!; + isPreGenerated = true; + } else if (_params.deps.preGeneratedDelegationAuthSig) { + validateDelegationAuthSig({ + delegationAuthSig: _params.deps.preGeneratedDelegationAuthSig, + sessionKeyUri, + }); + delegationAuthSig = _params.deps.preGeneratedDelegationAuthSig; + isPreGenerated = true; + } else { + const requestBody = await preparePkpAuthRequestBody({ + authentication: _params.authentication, + authConfig: _params.authConfig, + deps: { + litAuthData: _params.deps.litAuthData, nodeUrls: _nodeInfo.urls, - }), - }); + nodeSet: _nodeInfo.nodeSet, + nonce: _params.deps.connection.nonce, + currentEpoch: _params.deps.connection.currentEpoch, + }, + }); + + delegationAuthSig = await tryGetCachedDelegationAuthSig({ + cache: _params.cache?.delegationAuthSig, + storage: _params.deps.storage, + address: _params.deps.pkpAddress, + expiration: _params.authConfig.expiration, + signSessionKey: () => + _params.deps.signSessionKey({ + requestBody, + nodeUrls: _nodeInfo.urls, + }), + }); + } _logger.info( { delegationAuthSig, + isPreGenerated, + usedProvidedSessionMaterials: hasProvidedSessionKeyPair, }, 'getPkpAuthContext: delegationAuthSig' ); @@ -175,6 +218,6 @@ export const getPkpAuthContext = async ( return delegationAuthSig; }, authConfig, - sessionKeyPair: _params.deps.litAuthData.sessionKey.keyPair, + sessionKeyPair, }; }; diff --git a/packages/auth/src/lib/AuthManager/utils/validateDelegationAuthSig.spec.ts b/packages/auth/src/lib/AuthManager/utils/validateDelegationAuthSig.spec.ts new file mode 100644 index 0000000000..4b0ee7e40d --- /dev/null +++ b/packages/auth/src/lib/AuthManager/utils/validateDelegationAuthSig.spec.ts @@ -0,0 +1,119 @@ +import { AuthSig } from '@lit-protocol/types'; + +import { validateDelegationAuthSig } from './validateDelegationAuthSig'; + +type ISOString = `${string}Z`; + +const DEFAULT_SESSION_KEY_HEX = + 'd63204d1dd3b133f37d813532046ef63fdba1e312a950373eb6a54c9757ce281'; +const getDefaultExpiration = (): ISOString => + new Date(Date.now() + 1000 * 60 * 60).toISOString() as ISOString; + +function createDelegationAuthSig( + params: { + sessionKeyHex?: string; + expiration?: ISOString; + } = {} +): AuthSig { + const sessionKeyHex = params.sessionKeyHex ?? DEFAULT_SESSION_KEY_HEX; + const expiration = params.expiration ?? getDefaultExpiration(); + + return { + sig: '{"ProofOfPossession":"87655294574e145befb9ba8c6392d4dd54184effd3d69388a9d0a3be88a645ca6c480c1779871f6859c0688e8ffe03121004ca9374e7c7ff41261c64ba2ee63d64b6075acba9995cdbcc0644ef6a44037e51307ea1a95e10e68199d8051d96e6"}', + algo: 'LIT_BLS', + derivedVia: 'lit.bls', + signedMessage: `localhost wants you to sign in with your Ethereum account: +0x7697f071cbe4764F596B64d95ca54Adf5e614C37 + +Lit Protocol PKP session signature I further authorize the stated URI to perform the following actions on my behalf: (1) 'Threshold': 'Decryption' for 'lit-accesscontrolcondition://*'. (2) 'Threshold': 'Execution' for 'lit-litaction://*'. (3) 'Threshold': 'Signing' for 'lit-pkp://*'. I further authorize the stated URI to perform the following actions on my behalf: (1) 'Threshold': 'Decryption' for 'lit-accesscontrolcondition://*'. (2) 'Threshold': 'Execution' for 'lit-litaction://*'. (3) 'Threshold': 'Signing' for 'lit-pkp://*'. (4) 'Auth': 'Auth' for 'lit-resolvedauthcontext://*'. + +URI: lit:session:${sessionKeyHex} +Version: 1 +Chain ID: 1 +Nonce: 0xde5eca386bb63607b24c5256e6c483a678b99e51e3104ec4d9959a9f19af0b96 +Issued At: 2025-10-15T15:27:11Z +Expiration Time: ${expiration} +Resources: +- urn:recap:eyJhdHQiOnsibGl0LWFjY2Vzc2NvbnRyb2xjb25kaXRpb246Ly8qIjp7IlRocmVzaG9sZC9EZWNyeXB0aW9uIjpbe31dfSwibGl0LWxpdGFjdGlvbjovLyoiOnsiVGhyZXNob2xkL0V4ZWN1dGlvbiI6W3t9XX0sImxpdC1wa3A6Ly8qIjp7IlRocmVzaG9sZC9TaWduaW5nIjpbe31dfSwibGl0LXJlc29sdmVkYXV0aGNvbnRleHQ6Ly8qIjp7IkF1dGgvQXV0aCI6W3siYXV0aF9jb250ZXh0Ijp7ImFjdGlvbklwZnNJZFN0YWNrIjpbXSwiYXV0aE1ldGhvZENvbnRleHRzIjpbeyJhcHBJZCI6ImxpdCIsImF1dGhNZXRob2RUeXBlIjoxLCJ1c2VkRm9yU2lnblNlc3Npb25LZXlSZXF1ZXN0Ijp0cnVlLCJ1c2VySWQiOiIweGNmZjYzMjJBMDFGMkY5MDg1NUI3YUQwMThjODNGOUEyYzEzZjg3YmYifV0sImF1dGhTaWdBZGRyZXNzIjpudWxsLCJjdXN0b21BdXRoUmVzb3VyY2UiOiIiLCJyZXNvdXJjZXMiOltdfX1dfX0sInByZiI6W119`, + address: '0x7697f071cbe4764F596B64d95ca54Adf5e614C37', + }; +} + +describe('validateDelegationAuthSig', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + it('accepts a valid delegation signature from the network', () => { + jest.useFakeTimers().setSystemTime(new Date('2025-10-15T15:30:00.000Z')); + + const delegationAuthSig = createDelegationAuthSig(); + + expect(() => + validateDelegationAuthSig({ + delegationAuthSig, + sessionKeyUri: DEFAULT_SESSION_KEY_HEX, + }) + ).not.toThrow(); + }); + + it('accepts a delegation signature when the provided URI already contains the prefix', () => { + const delegationAuthSig = createDelegationAuthSig(); + + expect(() => + validateDelegationAuthSig({ + delegationAuthSig, + sessionKeyUri: `lit:session:${DEFAULT_SESSION_KEY_HEX}`, + }) + ).not.toThrow(); + }); + + it('throws when expiration timestamp cannot be parsed', () => { + jest.useFakeTimers().setSystemTime(new Date('2025-10-15T15:30:00.000Z')); + + const invalidDelegationAuthSig = createDelegationAuthSig({ + expiration: '2025-13-32T00:00:00Z', + }); + + expect(() => + validateDelegationAuthSig({ + delegationAuthSig: invalidDelegationAuthSig, + sessionKeyUri: DEFAULT_SESSION_KEY_HEX, + }) + ).toThrowError( + 'Invalid delegation signature: Delegation signature contains an invalid expiration timestamp' + ); + }); + + it('throws when the delegation signature is expired', () => { + jest.useFakeTimers().setSystemTime(new Date('2025-10-15T15:30:00.000Z')); + + const expiredDelegationAuthSig = createDelegationAuthSig({ + expiration: '2025-10-15T15:00:00Z', + }); + + expect(() => + validateDelegationAuthSig({ + delegationAuthSig: expiredDelegationAuthSig, + sessionKeyUri: DEFAULT_SESSION_KEY_HEX, + }) + ).toThrowError( + 'Invalid delegation signature: Delegation signature has expired at 2025-10-15T15:00:00.000Z' + ); + }); + + it('throws when the session key URI does not match', () => { + const mismatchedDelegationAuthSig = createDelegationAuthSig({ + sessionKeyHex: 'another-session-key', + }); + + expect(() => + validateDelegationAuthSig({ + delegationAuthSig: mismatchedDelegationAuthSig, + sessionKeyUri: DEFAULT_SESSION_KEY_HEX, + }) + ).toThrowError( + 'Invalid delegation signature: Session key URI in delegation signature does not match provided session key pair' + ); + }); +}); diff --git a/packages/auth/src/lib/AuthManager/utils/validateDelegationAuthSig.ts b/packages/auth/src/lib/AuthManager/utils/validateDelegationAuthSig.ts new file mode 100644 index 0000000000..8a072f6026 --- /dev/null +++ b/packages/auth/src/lib/AuthManager/utils/validateDelegationAuthSig.ts @@ -0,0 +1,67 @@ +import { AuthSig } from '@lit-protocol/types'; +import { SessionKeyUriSchema } from '@lit-protocol/schemas'; +import { parseSignedMessage } from '../../authenticators/helper/session-sigs-validator'; + +/** + * Validates that the provided delegation auth sig hasn't expired and + * references the expected session key URI. + * Throws an error if validation fails. + */ +export function validateDelegationAuthSig(params: { + delegationAuthSig: AuthSig; + sessionKeyUri: string; +}) { + const { delegationAuthSig, sessionKeyUri } = params; + const expectedSessionKeyUri = SessionKeyUriSchema.parse(sessionKeyUri); + + try { + const siweMessage = delegationAuthSig.signedMessage; + const parsedMessage = parseSignedMessage(siweMessage); + + // Check expiration if it exists in the SIWE message + const expirationField = parsedMessage['Expiration Time']; + if (typeof expirationField === 'string') { + const expiration = new Date(expirationField.trim()); + if (Number.isNaN(expiration.getTime())) { + throw new Error( + 'Delegation signature contains an invalid expiration timestamp' + ); + } + if (expiration.getTime() <= Date.now()) { + throw new Error( + `Delegation signature has expired at ${expiration.toISOString()}` + ); + } + } else if (Array.isArray(expirationField)) { + throw new Error( + 'Delegation signature contains multiple expiration timestamps' + ); + } + + // Validate session key URI matches + const parsedSessionKeyUri = + typeof parsedMessage['URI'] === 'string' + ? parsedMessage['URI'].trim() + : undefined; + + if (parsedSessionKeyUri) { + if (parsedSessionKeyUri !== expectedSessionKeyUri) { + throw new Error( + 'Session key URI in delegation signature does not match provided session key pair' + ); + } + } else if (!siweMessage.includes(expectedSessionKeyUri)) { + throw new Error( + 'Session key URI in delegation signature does not match provided session key pair' + ); + } + + // TODO: Add resource validation by parsing the RECAP URN when available. + } catch (error) { + throw new Error( + `Invalid delegation signature: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ); + } +} diff --git a/packages/e2e/src/e2e.spec.ts b/packages/e2e/src/e2e.spec.ts index bd425633fc..50b5fca4fe 100644 --- a/packages/e2e/src/e2e.spec.ts +++ b/packages/e2e/src/e2e.spec.ts @@ -1,6 +1,7 @@ import { createCustomAuthContext, createPkpAuthContext, + createPkpAuthContextWithPreGeneratedMaterials, } from './helper/auth-contexts'; import { createExecuteJsTest, @@ -19,6 +20,7 @@ import { } from './helper/tests'; import { init } from './init'; import { AuthContext } from './types'; +import { createPregenDelegationServerReuseTest } from './tests/signSessionKey/pregen-delegation'; const RPC_OVERRIDE = process.env['LIT_YELLOWSTONE_PRIVATE_RPC_URL']; if (RPC_OVERRIDE) { @@ -170,9 +172,118 @@ describe('all', () => { // }); }); + describe('PKP Auth with Pre-generated Materials', () => { + console.log('πŸ” Testing PKP auth with pre-generated session materials'); + + let preGeneratedAuthContext: any; + + beforeAll(async () => { + try { + preGeneratedAuthContext = + await createPkpAuthContextWithPreGeneratedMaterials(ctx); + } catch (e) { + console.error('Failed to create pre-generated auth context:', e); + throw e; + } + }); + + describe('endpoints', () => { + it('pkpSign with pre-generated materials', () => + createPkpSignTest(ctx, () => preGeneratedAuthContext)()); + + it('executeJs with pre-generated materials', () => + createExecuteJsTest(ctx, () => preGeneratedAuthContext)()); + + it('pkpEncryptDecrypt with pre-generated materials', () => + createPkpEncryptDecryptTest(ctx, () => preGeneratedAuthContext)()); + }); + + describe('error handling', () => { + it('should reject when only sessionKeyPair is provided', async () => { + const tempAuthContext = await ctx.authManager.createPkpAuthContext({ + authData: ctx.aliceViemAccountAuthData, + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: ctx.litClient, + }); + + const sessionKeyPair = tempAuthContext.sessionKeyPair; + + await expect( + ctx.authManager.createPkpAuthContext({ + authData: ctx.aliceViemAccountAuthData, + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: ctx.litClient, + sessionKeyPair, // Only providing sessionKeyPair + // delegationAuthSig is missing + }) + ).rejects.toThrow( + 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' + ); + }); + + it('should reject when only delegationAuthSig is provided', async () => { + const tempAuthContext = await ctx.authManager.createPkpAuthContext({ + authData: ctx.aliceViemAccountAuthData, + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: ctx.litClient, + }); + + const delegationAuthSig = await tempAuthContext.authNeededCallback(); + + await expect( + ctx.authManager.createPkpAuthContext({ + authData: ctx.aliceViemAccountAuthData, + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: ctx.litClient, + // sessionKeyPair is missing + delegationAuthSig, // Only providing delegationAuthSig + }) + ).rejects.toThrow( + 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' + ); + }); + }); + + /** + * This scenario mirrors the client/server hand-off used in production: + * 1. A client generates session materials and a delegation auth sig. + * 2. The bundle travels over the wire (simulated via JSON serialisation). + * 3. A server restores those materials with a fresh AuthManager instance and + * proves it can sign with the delegated PKP using an independently created LitClient. + * Keeping this in the main e2e suite ensures we catch regressions in CI without + * relying on the ad-hoc ticket test. + */ + describe('server reuse flow', () => { + it('should sign using materials shipped over the wire', () => + createPregenDelegationServerReuseTest({ + authManager: ctx.authManager, + authData: ctx.aliceViemAccountAuthData, + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + clientLitClient: ctx.litClient, + fallbackLitClient: ctx.litClient, + resolvedNetwork: ctx.resolvedNetwork, + })()); + }); + }); + describe('EOA Native', () => { console.log('πŸ” Testing EOA native authentication and PKP minting'); - it('eoaNativeAuthFlow', () => createEoaNativeAuthFlowTest(ctx)()); }); }); diff --git a/packages/e2e/src/helper/auth-contexts.ts b/packages/e2e/src/helper/auth-contexts.ts index b9dd6e31d3..ec2be18755 100644 --- a/packages/e2e/src/helper/auth-contexts.ts +++ b/packages/e2e/src/helper/auth-contexts.ts @@ -1,5 +1,69 @@ +import { generateSessionKeyPair } from '@lit-protocol/auth'; import { init } from '../init'; +/** + * Creates a PKP authentication context with pre-generated session materials + * This simulates a server-side use case where session key pair and delegation + * signature are generated once and reused for multiple requests + */ +export const createPkpAuthContextWithPreGeneratedMaterials = async ( + ctx: Awaited> +) => { + console.log('πŸ” Creating PKP Auth Context with Pre-generated Materials'); + try { + // Step 1: Generate a session key pair directly + console.log(' πŸ“ Step 1: Generating session key pair...'); + const sessionKeyPair = generateSessionKeyPair(); + + // Step 2: Generate PKP delegation signature for the session key pair + console.log(' πŸ“ Step 2: Generating PKP delegation signature...'); + const delegationAuthSig = + await ctx.authManager.generatePkpDelegationAuthSig({ + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + authData: ctx.aliceViemAccountAuthData, + sessionKeyPair, + authConfig: { + resources: [ + ['pkp-signing', '*'], + ['lit-action-execution', '*'], + ['access-control-condition-decryption', '*'], + ], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: ctx.litClient, + }); + + console.log(' πŸ“ Session materials generated:', { + hasSessionKeyPair: !!sessionKeyPair, + hasDelegationAuthSig: !!delegationAuthSig, + sessionKeyPublicKey: sessionKeyPair?.publicKey?.substring(0, 20) + '...', + }); + + // Step 3: Create auth context using the pre-generated materials + // Using the dedicated function for pre-generated materials with a clean, minimal signature + console.log( + ' πŸ“ Step 3: Creating auth context with pre-generated materials...' + ); + const authContextWithPreGenerated = + await ctx.authManager.createPkpAuthContextFromPreGenerated({ + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + sessionKeyPair, + delegationAuthSig, + // Optional: can provide authData if needed, otherwise minimal default is used + authData: ctx.aliceViemAccountAuthData, + }); + + console.log('βœ… PKP Auth Context with Pre-generated Materials created'); + return authContextWithPreGenerated; + } catch (e) { + console.error( + '❌ Error creating PKP Auth Context with Pre-generated Materials', + e + ); + throw e; + } +}; + /** * Creates a PKP authentication context */ @@ -70,3 +134,72 @@ export const createCustomAuthContext: ( throw e; } }; + +/** + * Creates an EOA authentication context with pre-generated session materials + * This demonstrates how to pre-generate EOA session materials for server-side use + */ +export const createEoaAuthContextWithPreGeneratedMaterials = async ( + ctx: Awaited> +) => { + console.log('πŸ” Creating EOA Auth Context with Pre-generated Materials'); + try { + // Step 1: Generate a session key pair directly + console.log(' πŸ“ Step 1: Generating session key pair...'); + const sessionKeyPair = generateSessionKeyPair(); + + // Step 2: Generate EOA delegation signature for the session key pair + console.log(' πŸ“ Step 2: Generating EOA delegation signature...'); + const delegationAuthSig = + await ctx.authManager.generateEoaDelegationAuthSig({ + account: ctx.aliceViemAccount, + sessionKeyPair, + authConfig: { + resources: [ + ['pkp-signing', '*'], + ['lit-action-execution', '*'], + ['access-control-condition-decryption', '*'], + ], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: ctx.litClient, + }); + + console.log(' πŸ“ EOA session materials generated:', { + hasSessionKeyPair: !!sessionKeyPair, + hasDelegationAuthSig: !!delegationAuthSig, + sessionKeyPublicKey: sessionKeyPair?.publicKey?.substring(0, 20) + '...', + }); + + // Step 3: Create EOA auth context using the pre-generated materials + console.log( + ' πŸ“ Step 3: Creating EOA auth context with pre-generated materials...' + ); + const authContextWithPreGenerated = + await ctx.authManager.createEoaAuthContext({ + authConfig: { + resources: [ + ['pkp-signing', '*'], + ['lit-action-execution', '*'], + ['access-control-condition-decryption', '*'], + ], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + config: { + account: ctx.aliceViemAccount, + }, + litClient: ctx.litClient, + // Note: EOA auth contexts don't currently support pre-generated materials + // This demonstrates the pattern for when it's implemented + }); + + console.log('βœ… EOA Auth Context with Pre-generated Materials created'); + return authContextWithPreGenerated; + } catch (e) { + console.error( + '❌ Error creating EOA Auth Context with Pre-generated Materials', + e + ); + throw e; + } +}; diff --git a/packages/e2e/src/helper/network.ts b/packages/e2e/src/helper/network.ts new file mode 100644 index 0000000000..4b5ee86309 --- /dev/null +++ b/packages/e2e/src/helper/network.ts @@ -0,0 +1,171 @@ +import { z } from 'zod'; + +/** + * Canonical metadata for Lit e2e network targets. + * - `importName` feeds `@lit-protocol/networks` dynamic imports. + * - `type` lets higher level helpers branch on local vs live behaviour. + * + * @example + * ```ts + * NETWORKS['naga-dev']; + * // { importName: 'nagaDev', type: 'live' } + * ``` + */ +export const NETWORKS = { + 'naga-dev': { importName: 'nagaDev', type: 'live' }, + 'naga-test': { importName: 'nagaTest', type: 'live' }, + 'naga-local': { importName: 'nagaLocal', type: 'local' }, + 'naga-staging': { importName: 'nagaStaging', type: 'live' }, +} as const; + +export type NetworkName = keyof typeof NETWORKS; + +export type NetworkConfig = (typeof NETWORKS)[NetworkName]; + +export type NetworkType = NetworkConfig['type']; + +export type NetworkImportName = NetworkConfig['importName']; + +const NETWORK_NAME_VALUES = Object.keys(NETWORKS) as NetworkName[]; + +const NETWORK_NAME_TUPLE = NETWORK_NAME_VALUES as [ + NetworkName, + ...NetworkName[] +]; + +/** + * Shared schema so callers can parse env/config values consistently. + * + * @example + * ```ts + * NetworkNameSchema.parse('naga-local'); + * // 'naga-local' + * ``` + */ +export const NetworkNameSchema = z.enum(NETWORK_NAME_TUPLE); + +export const DEFAULT_NETWORK_NAME: NetworkName = 'naga-dev'; + +/** + * Ordered list of network identifiers. Useful when presenting choices to users. + * + * @example + * ```ts + * SUPPORTED_NETWORK_NAMES; + * // ['naga-dev', 'naga-test', 'naga-local', 'naga-staging'] + * ``` + */ +export const SUPPORTED_NETWORK_NAMES = [...NETWORK_NAME_VALUES] as const; + +type NetworksModule = typeof import('@lit-protocol/networks'); + +export type LitNetworkModule = NetworksModule[NetworkImportName]; + +/** + * Type guard used when consuming untyped sources such as env variables. + * + * @example + * ```ts + * isNetworkName('naga-test'); + * // true + * + * isNetworkName('unknown-network'); + * // false + * ``` + */ +export function isNetworkName(value: unknown): value is NetworkName { + return ( + typeof value === 'string' && + Object.prototype.hasOwnProperty.call(NETWORKS, value) + ); +} + +/** + * Normalises any caller-provided identifier to the canonical network tuple used + * by init flows and tests. Always returns a full `NETWORKS` entry alongside the + * resolved name, so callers can keep a single source of truth for network metadata. + * + * @example + * ```ts + * getNetworkConfig('naga-test'); + * // { name: 'naga-test', importName: 'nagaTest', type: 'live' } + * + * getNetworkConfig(); + * // { name: 'naga-dev', importName: 'nagaDev', type: 'live' } + * ``` + */ +export function getNetworkConfig(network?: string): { + name: NetworkName; + importName: NetworkImportName; + type: NetworkType; +} { + const candidate = (network ?? DEFAULT_NETWORK_NAME) as string; + + if (!isNetworkName(candidate)) { + throw new Error( + `Unsupported network "${network}". Supported networks: ${SUPPORTED_NETWORK_NAMES.join( + ', ' + )}` + ); + } + + const name: NetworkName = candidate; + const { importName, type } = NETWORKS[name]; + + return { name, importName, type }; +} + +/** + * Convenience wrapper used where only the `importName` string matters. + * + * @example + * ```ts + * resolveNetworkImportName('naga-local'); + * // 'nagaLocal' + * ``` + */ +export function resolveNetworkImportName(network?: string): NetworkImportName { + return getNetworkConfig(network).importName; +} + +export type ResolveNetworkOptions = { + network?: string; + rpcUrlOverride?: string; +}; + +export type ResolvedNetwork = { + name: NetworkName; + importName: NetworkImportName; + type: NetworkType; + networkModule: LitNetworkModule; +}; + +/** + * Fully resolves a Lit network by combining metadata with the backing module. + * + * @example + * ```ts + * const { name, networkModule } = await resolveNetwork({ + * network: 'naga-local', + * rpcUrlOverride: 'http://127.0.0.1:8545', + * }); + * // name === 'naga-local' + * // networkModule is the hydrated Lit network module with overrides applied + * ``` + */ +export async function resolveNetwork( + options: ResolveNetworkOptions = {} +): Promise { + const { network, rpcUrlOverride } = options; + const { name, importName, type } = getNetworkConfig(network); + + const networksModule: NetworksModule = await import('@lit-protocol/networks'); + const baseNetworkModule = networksModule[importName]; + + const networkModule = + rpcUrlOverride && typeof baseNetworkModule.withOverrides === 'function' + ? baseNetworkModule.withOverrides({ rpcUrl: rpcUrlOverride }) + : baseNetworkModule; + + return { name, importName, type, networkModule }; +} diff --git a/packages/e2e/src/init.ts b/packages/e2e/src/init.ts index 4ffe403c7c..0ca6529f4f 100644 --- a/packages/e2e/src/init.ts +++ b/packages/e2e/src/init.ts @@ -5,6 +5,12 @@ import { } from '@lit-protocol/auth'; import { createLitClient, utils as litUtils } from '@lit-protocol/lit-client'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import { + NetworkName, + NetworkNameSchema, + ResolvedNetwork, + resolveNetwork, +} from './helper/network'; import { z } from 'zod'; import { fundAccount } from './helper/fundAccount'; import { getOrCreatePkp } from './helper/pkp-utils'; @@ -18,15 +24,6 @@ import { // import { createPkpAuthContext } from './helper/auth-contexts'; -const SupportedNetworkSchema = z.enum([ - 'naga-dev', - 'naga-test', - 'naga-local', - 'naga-staging', -]); - -type SupportedNetwork = z.infer; - const LogLevelSchema = z.enum(['silent', 'info', 'debug']); type LogLevel = z.infer; @@ -38,27 +35,44 @@ const LIVE_NETWORK_LEDGER_DEPOSIT_AMOUNT = '2'; const EVE_VALIDATION_IPFS_CID = 'QmcxWmo3jefFsPUnskJXYBwsJYtiFuMAH1nDQEs99AwzDe'; -export const init = async ( - network?: SupportedNetwork, - logLevel?: LogLevel -): Promise<{ +type BaseInitResult = { litClient: LitClientInstance; authManager: AuthManagerInstance; localMasterAccount: ViemAccount; aliceViemAccount: ViemAccount; aliceViemAccountAuthData: AuthData; aliceViemAccountPkp: PKPData; + aliceEoaAuthContext: AuthContext; + masterDepositForUser: (userAddress: string) => Promise; + resolvedNetwork: ResolvedNetwork; +}; + +type FullInitResult = BaseInitResult & { bobViemAccount: ViemAccount; bobViemAccountAuthData: AuthData; bobViemAccountPkp: PKPData; - aliceEoaAuthContext: AuthContext; alicePkpAuthContext: AuthContext; eveViemAccount: ViemAccount; eveCustomAuthData: CustomAuthData; eveViemAccountPkp: PKPData; eveValidationIpfsCid: `Qm${string}`; - masterDepositForUser: (userAddress: string) => Promise; -}> => { +}; + +async function initInternal( + mode: 'fast', + network?: NetworkName, + logLevel?: LogLevel +): Promise; +async function initInternal( + mode: 'full', + network?: NetworkName, + logLevel?: LogLevel +): Promise; +async function initInternal( + mode: 'fast' | 'full', + network?: NetworkName, + logLevel?: LogLevel +): Promise { /** * ==================================== * Prepare accounts for testing @@ -75,33 +89,25 @@ export const init = async ( aliceViemAccount ); - const bobViemAccount = privateKeyToAccount(generatePrivateKey()); - const bobViemAccountAuthData = await ViemAccountAuthenticator.authenticate( - bobViemAccount - ); - - const eveViemAccount = privateKeyToAccount(generatePrivateKey()); - /** * ==================================== * Environment settings * ==================================== */ - const _network = network || process.env['NETWORK']; - const _logLevel = logLevel || process.env['LOG_LEVEL']; - process.env['LOG_LEVEL'] = _logLevel; + const networkInput = network ?? process.env['NETWORK']; + const _logLevel = logLevel ?? process.env['LOG_LEVEL']; + if (_logLevel) { + process.env['LOG_LEVEL'] = _logLevel; + } - if (!_network) { + if (!networkInput) { throw new Error( - `❌ Network not specified. Please set the NETWORK environment variable or pass a network parameter. Available networks: ${SupportedNetworkSchema.options.join( + `❌ Network not specified. Please set the NETWORK environment variable or pass a network parameter. Available networks: ${NetworkNameSchema.options.join( ', ' )}` ); } - console.log('βœ… Using network:', _network); - console.log('βœ… Using log level:', _logLevel); - /** * ==================================== * Network configuration and setup @@ -109,30 +115,21 @@ export const init = async ( * ❗️ If it's on live chain, we will fund it with the master account. (set in the .env file) * ==================================== */ - - // Network configuration map - const networkConfig = { - 'naga-dev': { importName: 'nagaDev', type: 'live' }, - 'naga-test': { importName: 'nagaTest', type: 'live' }, - 'naga-local': { importName: 'nagaLocal', type: 'local' }, - 'naga-staging': { importName: 'nagaStaging', type: 'live' }, - } as const; - - const config = networkConfig[_network as keyof typeof networkConfig]; - if (!config) { - throw new Error(`❌ Invalid network: ${_network}`); - } - - // Dynamic import of network module - const networksModule = await import('@lit-protocol/networks'); - const _baseNetworkModule = networksModule[config.importName]; - // Optional RPC override from env const rpcOverride = process.env['LIT_YELLOWSTONE_PRIVATE_RPC_URL']; - const _networkModule = - rpcOverride && typeof _baseNetworkModule.withOverrides === 'function' - ? _baseNetworkModule.withOverrides({ rpcUrl: rpcOverride }) - : _baseNetworkModule; + const resolvedNetwork = await resolveNetwork({ + network: networkInput, + rpcUrlOverride: rpcOverride, + }); + + const { + name: resolvedNetworkName, + type: networkType, + networkModule, + } = resolvedNetwork; + + console.log('βœ… Using network:', resolvedNetworkName); + console.log('βœ… Using log level:', _logLevel); if (rpcOverride) { console.log( @@ -142,34 +139,47 @@ export const init = async ( } // Fund accounts based on network type - const isLocal = config.type === 'local'; + const isLocal = networkType === 'local'; const masterAccount = isLocal ? localMasterAccount : liveMasterAccount; const fundingAmount = isLocal ? LOCAL_NETWORK_FUNDING_AMOUNT : LIVE_NETWORK_FUNDING_AMOUNT; // Fund accounts sequentially to avoid nonce conflicts with same sponsor - await fundAccount(aliceViemAccount, masterAccount, _networkModule, { + await fundAccount(aliceViemAccount, masterAccount, networkModule, { ifLessThan: fundingAmount, thenFundWith: fundingAmount, }); - await fundAccount(bobViemAccount, masterAccount, _networkModule, { - ifLessThan: fundingAmount, - thenFundWith: fundingAmount, - }); + let bobViemAccount: ViemAccount | undefined; + let bobViemAccountAuthData: AuthData | undefined; + let eveViemAccount: ViemAccount | undefined; - await fundAccount(eveViemAccount, masterAccount, _networkModule, { - ifLessThan: fundingAmount, - thenFundWith: fundingAmount, - }); + if (mode === 'full') { + bobViemAccount = privateKeyToAccount(generatePrivateKey()); + bobViemAccountAuthData = await ViemAccountAuthenticator.authenticate( + bobViemAccount + ); + + eveViemAccount = privateKeyToAccount(generatePrivateKey()); + + await fundAccount(bobViemAccount, masterAccount, networkModule, { + ifLessThan: fundingAmount, + thenFundWith: fundingAmount, + }); + + await fundAccount(eveViemAccount, masterAccount, networkModule, { + ifLessThan: fundingAmount, + thenFundWith: fundingAmount, + }); + } /** * ==================================== * Initialise the LitClient * ==================================== */ - const litClient = await createLitClient({ network: _networkModule }); + const litClient = await createLitClient({ network: networkModule }); /** * ==================================== @@ -204,20 +214,73 @@ export const init = async ( const authManager = createAuthManager({ storage: storagePlugins.localStorageNode({ appName: 'my-local-testing-app', - networkName: _network, + networkName: resolvedNetworkName, storagePath: './.e2e/lit-auth-local', }), }); + const createAliceEoaAuthContext = () => + authManager.createEoaAuthContext({ + config: { + account: aliceViemAccount, + }, + authConfig: { + statement: 'I authorize the Lit Protocol to execute this Lit Action.', + domain: 'example.com', + resources: [ + ['lit-action-execution', '*'], + ['pkp-signing', '*'], + ['access-control-condition-decryption', '*'], + ], + capabilityAuthSigs: [], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: litClient, + }); + + const aliceViemAccountPkp = await getOrCreatePkp( + litClient, + aliceViemAccountAuthData, + aliceViemAccount + ); + + if (mode === 'fast') { + await masterDepositForUser(aliceViemAccount.address); + await masterDepositForUser(aliceViemAccountPkp.ethAddress); + + const aliceEoaAuthContext = await createAliceEoaAuthContext(); + + console.log('βœ… Initialised components (fast)'); + + const baseResult: BaseInitResult = { + litClient, + authManager, + localMasterAccount, + aliceViemAccount, + aliceViemAccountAuthData, + aliceViemAccountPkp, + aliceEoaAuthContext, + masterDepositForUser, + resolvedNetwork, + }; + + return baseResult; + } + + if (!bobViemAccount || !bobViemAccountAuthData || !eveViemAccount) { + throw new Error('❌ Failed to prepare accounts for full init'); + } + /** * ==================================== * Get or create PKPs for Alice and Bob * ==================================== */ - const [aliceViemAccountPkp, bobViemAccountPkp] = await Promise.all([ - getOrCreatePkp(litClient, aliceViemAccountAuthData, aliceViemAccount), - getOrCreatePkp(litClient, bobViemAccountAuthData, bobViemAccount), - ]); + const bobViemAccountPkp = await getOrCreatePkp( + litClient, + bobViemAccountAuthData, + bobViemAccount + ); // Use custom auth to create a PKP for Eve const uniqueDappName = 'e2e-test-dapp'; @@ -257,23 +320,7 @@ export const init = async ( * Create the auth context * ==================================== */ - const aliceEoaAuthContext = await authManager.createEoaAuthContext({ - config: { - account: aliceViemAccount, - }, - authConfig: { - statement: 'I authorize the Lit Protocol to execute this Lit Action.', - domain: 'example.com', - resources: [ - ['lit-action-execution', '*'], - ['pkp-signing', '*'], - ['access-control-condition-decryption', '*'], - ], - capabilityAuthSigs: [], - expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), - }, - litClient: litClient, - }); + const aliceEoaAuthContext = await createAliceEoaAuthContext(); console.log('βœ… Initialised components'); @@ -300,52 +347,58 @@ export const init = async ( const alicePkpViemAccount = await litClient.getPkpViemAccount({ pkpPublicKey: aliceViemAccountPkp.pubkey, authContext: alicePkpAuthContext, - chainConfig: _networkModule.getChainConfig(), + chainConfig: networkModule.getChainConfig(), }); - await fundAccount(alicePkpViemAccount, masterAccount, _networkModule, { + await fundAccount(alicePkpViemAccount, masterAccount, networkModule, { ifLessThan: LOCAL_NETWORK_FUNDING_AMOUNT, thenFundWith: LOCAL_NETWORK_FUNDING_AMOUNT, }); /** * ==================================== - * Depositing to Lit Ledger for differen + * Depositing to Lit Ledger for different accounts * ==================================== */ - - // Deposit to the PKP Viem account Ledger await masterDepositForUser(alicePkpViemAccount.address); - // const alicePkpViemAccountPermissionsManager = await litClient.getPKPPermissionsManager({ - // pkpIdentifier: { - // tokenId: aliceViemAccountPkp.tokenId, - // }, - // account: alicePkpViemAccount, - // }); - - /** - * ==================================== - * Return the initialised components - * ==================================== - */ - return { + const baseResult: BaseInitResult = { litClient, authManager, localMasterAccount, aliceViemAccount, aliceViemAccountAuthData, aliceViemAccountPkp, + aliceEoaAuthContext, + masterDepositForUser, + resolvedNetwork, + }; + + const fullResult: FullInitResult = { + ...baseResult, bobViemAccount, bobViemAccountAuthData, bobViemAccountPkp, + alicePkpAuthContext, eveViemAccount, eveCustomAuthData, eveViemAccountPkp, eveValidationIpfsCid: EVE_VALIDATION_IPFS_CID, - aliceEoaAuthContext, - alicePkpAuthContext, - masterDepositForUser, - // alicePkpViemAccountPermissionsManager }; + + return fullResult; +} + +export const init = async ( + network?: NetworkName, + logLevel?: LogLevel +): Promise => { + return initInternal('full', network, logLevel); +}; + +export const initFast = async ( + network?: NetworkName, + logLevel?: LogLevel +): Promise => { + return initInternal('fast', network, logLevel); }; diff --git a/packages/e2e/src/tests/signSessionKey/pregen-delegation.ts b/packages/e2e/src/tests/signSessionKey/pregen-delegation.ts new file mode 100644 index 0000000000..6850f229f0 --- /dev/null +++ b/packages/e2e/src/tests/signSessionKey/pregen-delegation.ts @@ -0,0 +1,108 @@ +import { + createAuthManager, + generateSessionKeyPair, + storagePlugins, + validateDelegationAuthSig, +} from '@lit-protocol/auth'; +import { createLitClient } from '@lit-protocol/lit-client'; +import { ResolvedNetwork } from '../../helper/network'; +import { AuthData } from '@lit-protocol/schemas'; +import { AuthManagerInstance, LitClientInstance } from '../../types'; + +type PregenDelegationParams = { + authManager: AuthManagerInstance; + authData: AuthData; + pkpPublicKey: string; + clientLitClient: LitClientInstance; + fallbackLitClient?: LitClientInstance; + resolvedNetwork: ResolvedNetwork; +}; + +export const createPregenDelegationServerReuseTest = ( + params: PregenDelegationParams +) => { + return async () => { + const { + authManager, + authData, + pkpPublicKey, + clientLitClient, + fallbackLitClient, + resolvedNetwork, + } = params; + + const sessionKeyPair = generateSessionKeyPair(); + const delegationAuthSig = await authManager.generatePkpDelegationAuthSig({ + pkpPublicKey, + authData, + sessionKeyPair, + authConfig: { + resources: [ + ['pkp-signing', '*'], + ['lit-action-execution', '*'], + ['access-control-condition-decryption', '*'], + ], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: clientLitClient, + }); + + const envelope = JSON.stringify({ + pkpPublicKey, + payload: Buffer.from( + JSON.stringify({ sessionKeyPair, delegationAuthSig }), + 'utf8' + ).toString('base64url'), + }); + + const parsedEnvelope = JSON.parse(envelope) as { + pkpPublicKey: string; + payload: string; + }; + + const decodedPayload = JSON.parse( + Buffer.from(parsedEnvelope.payload, 'base64url').toString('utf8') + ) as { + sessionKeyPair: typeof sessionKeyPair; + delegationAuthSig: typeof delegationAuthSig; + }; + + validateDelegationAuthSig({ + delegationAuthSig: decodedPayload.delegationAuthSig, + sessionKeyUri: decodedPayload.sessionKeyPair.publicKey, + }); + + let litClient: LitClientInstance; + + try { + litClient = await createLitClient({ + network: resolvedNetwork.networkModule, + }); + } catch { + litClient = fallbackLitClient ?? clientLitClient; + } + + const serverAuthManager = createAuthManager({ + storage: storagePlugins.localStorageNode({ + appName: 'e2e-server-reuse', + networkName: resolvedNetwork.name, + storagePath: './.e2e/server-reuse-storage', + }), + }); + + const authContext = + await serverAuthManager.createPkpAuthContextFromPreGenerated({ + pkpPublicKey: parsedEnvelope.pkpPublicKey, + sessionKeyPair: decodedPayload.sessionKeyPair, + delegationAuthSig: decodedPayload.delegationAuthSig, + }); + + const result = await litClient.chain.ethereum.pkpSign({ + authContext, + pubKey: parsedEnvelope.pkpPublicKey, + toSign: 'hello from server reuse', + }); + + expect(result).toBeTruthy(); + }; +}; diff --git a/packages/e2e/src/tickets/jss36-pregen-delegation.spec.ts b/packages/e2e/src/tickets/jss36-pregen-delegation.spec.ts new file mode 100644 index 0000000000..8494cde320 --- /dev/null +++ b/packages/e2e/src/tickets/jss36-pregen-delegation.spec.ts @@ -0,0 +1,20 @@ +import { initFast } from '../init'; +import { createPregenDelegationServerReuseTest } from '../tests/signSessionKey/pregen-delegation'; + +describe('PKP Auth with Pre-generated Materials', () => { + let ctx: Awaited>; + + beforeAll(async () => { + ctx = await initFast(); + }); + + test('Try to pregen', async () => + createPregenDelegationServerReuseTest({ + authManager: ctx.authManager, + authData: ctx.aliceViemAccountAuthData, + pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, + clientLitClient: ctx.litClient, + fallbackLitClient: ctx.litClient, + resolvedNetwork: ctx.resolvedNetwork, + })()); +}); diff --git a/packages/e2e/tsconfig.spec.json b/packages/e2e/tsconfig.spec.json new file mode 100644 index 0000000000..56fd568062 --- /dev/null +++ b/packages/e2e/tsconfig.spec.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/ClaimRequestSchema.ts b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/ClaimRequestSchema.ts index fd37ccb942..45e8293e2b 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/ClaimRequestSchema.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/ClaimRequestSchema.ts @@ -1,4 +1,3 @@ -import { t } from 'elysia'; import { z } from 'zod'; import { toBigInt, @@ -21,16 +20,3 @@ export type ClaimRequestRaw = z.input; export type ClaimRequestTransformed = z.infer; // ✨ Elysia Schema -export const tClaimRequestSchema = t.Object({ - derivedKeyId: t.String(), - signatures: t.Array( - t.Object({ - r: t.String(), - s: t.String(), - v: t.Number(), - }) - ), - authMethodType: t.Number(), - authMethodId: t.String(), - authMethodPubkey: t.String(), -}); diff --git a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/MintRequestSchema.ts b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/MintRequestSchema.ts index 313d06d947..3560d2effa 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/MintRequestSchema.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/schemas/MintRequestSchema.ts @@ -1,4 +1,3 @@ -import { t } from 'elysia'; import { z } from 'zod'; import { toBigInt, @@ -26,12 +25,3 @@ export type MintRequestRaw = z.input; export type MintRequestTransformed = z.infer; // ✨ Elysia Schema -export const tMintRequestSchema = t.Object({ - keyType: t.Number(), - permittedAuthMethodTypes: t.Array(t.Number()), - permittedAuthMethodIds: t.Array(t.String()), - permittedAuthMethodPubkeys: t.Array(t.String()), - permittedAuthMethodScopes: t.Array(t.Array(t.Number())), - addPkpEthAddressAsPermittedAddress: t.Boolean(), - sendPkpToItself: t.Boolean(), -});