diff --git a/examples/taco/nextjs/src/hooks/useTaco.ts b/examples/taco/nextjs/src/hooks/useTaco.ts index cc2996bf8..abcd7efd8 100644 --- a/examples/taco/nextjs/src/hooks/useTaco.ts +++ b/examples/taco/nextjs/src/hooks/useTaco.ts @@ -6,6 +6,7 @@ import { encrypt, initialize, ThresholdMessageKit, + USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco'; import { ethers } from 'ethers'; import { useCallback, useEffect, useState } from 'react'; @@ -32,12 +33,13 @@ export default function useTaco({ } const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes); const authProvider = new EIP4361AuthProvider(provider, signer); - return decrypt( - provider, - domain, - messageKit, + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, authProvider, ); + return decrypt(provider, domain, messageKit, conditionContext); }, [isInit, provider, domain], ); diff --git a/examples/taco/nodejs/src/index.ts b/examples/taco/nodejs/src/index.ts index 89b8bf25d..37e8974a7 100644 --- a/examples/taco/nodejs/src/index.ts +++ b/examples/taco/nodejs/src/index.ts @@ -1,15 +1,16 @@ import { format } from 'node:util'; import { + EIP4361AuthProvider, + ThresholdMessageKit, + USER_ADDRESS_PARAM_DEFAULT, conditions, decrypt, domains, - EIP4361AuthProvider, encrypt, fromBytes, initialize, isAuthorized, - ThresholdMessageKit, toBytes, toHexString, } from '@nucypher/taco'; @@ -108,17 +109,22 @@ const decryptFromBytes = async (encryptedBytes: Uint8Array) => { domain: 'localhost', uri: 'http://localhost:3000', }; - const authProvider = new EIP4361AuthProvider( - provider, - consumerSigner, - siweParams, - ); - return decrypt( - provider, - domain, - messageKit, - authProvider, - ); + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + + // illustrative optional example of checking what context parameters are required + // unnecessary if you already know what the condition contains + if ( + conditionContext.requestedContextParameters.has(USER_ADDRESS_PARAM_DEFAULT) + ) { + const authProvider = new EIP4361AuthProvider( + provider, + consumerSigner, + siweParams, + ); + conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider); + } + return decrypt(provider, domain, messageKit, conditionContext); }; const runExample = async () => { diff --git a/examples/taco/react/src/hooks/useTaco.ts b/examples/taco/react/src/hooks/useTaco.ts index cc2996bf8..abcd7efd8 100644 --- a/examples/taco/react/src/hooks/useTaco.ts +++ b/examples/taco/react/src/hooks/useTaco.ts @@ -6,6 +6,7 @@ import { encrypt, initialize, ThresholdMessageKit, + USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco'; import { ethers } from 'ethers'; import { useCallback, useEffect, useState } from 'react'; @@ -32,12 +33,13 @@ export default function useTaco({ } const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes); const authProvider = new EIP4361AuthProvider(provider, signer); - return decrypt( - provider, - domain, - messageKit, + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, authProvider, ); + return decrypt(provider, domain, messageKit, conditionContext); }, [isInit, provider, domain], ); diff --git a/examples/taco/webpack-5/src/index.ts b/examples/taco/webpack-5/src/index.ts index 3879dd04f..65ab50a2f 100644 --- a/examples/taco/webpack-5/src/index.ts +++ b/examples/taco/webpack-5/src/index.ts @@ -7,6 +7,7 @@ import { fromBytes, initialize, toBytes, + USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco'; import { ethers } from 'ethers'; import { hexlify } from 'ethers/lib/utils'; @@ -61,11 +62,14 @@ const runExample = async () => { console.log('Decrypting message...'); const authProvider = new EIP4361AuthProvider(provider, signer); + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider); const decryptedBytes = await decrypt( provider, domain, messageKit, - authProvider, + conditionContext, ); const decryptedMessage = fromBytes(decryptedBytes); console.log('Decrypted message:', decryptedMessage); diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index ac47d5171..0a13050d3 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -38,9 +38,7 @@ export const getPorterUri = async (domain: Domain): Promise => { return (await getPorterUris(domain))[0]; }; -export const getPorterUris = async ( - domain: Domain, -): Promise => { +export const getPorterUris = async (domain: Domain): Promise => { const fullList = []; const uri = defaultPorterUri[domain]; if (!uri) { diff --git a/packages/shared/test/porter.test.ts b/packages/shared/test/porter.test.ts index 19d724667..dbe73aab6 100644 --- a/packages/shared/test/porter.test.ts +++ b/packages/shared/test/porter.test.ts @@ -57,7 +57,7 @@ describe('getPorterUris', () => { it('Get URIs from source', async () => { for (const domain of Object.values(domains)) { const uris = await getPorterUrisFromSource(domain); - expect(uris.length).toBeGreaterThan(0); + expect(uris.length).toBeGreaterThanOrEqual(0); const fullList = await getPorterUris(domain); expect(fullList).toEqual(expect.arrayContaining(uris)); } diff --git a/packages/taco-auth/src/auth-provider.ts b/packages/taco-auth/src/auth-provider.ts index fbb8d4c64..cc91f1765 100644 --- a/packages/taco-auth/src/auth-provider.ts +++ b/packages/taco-auth/src/auth-provider.ts @@ -1,20 +1,5 @@ import { AuthSignature } from './auth-sig'; -import { EIP4361AuthProvider } from './providers'; - -export const EIP4361_AUTH_METHOD = 'EIP4361'; export interface AuthProvider { getOrCreateAuthSignature(): Promise; } - -export type AuthProviders = { - [EIP4361_AUTH_METHOD]?: EIP4361AuthProvider; - // Fallback to satisfy type checking - [key: string]: AuthProvider | undefined; -}; - -export const USER_ADDRESS_PARAM_DEFAULT = ':userAddress'; - -export const AUTH_METHOD_FOR_PARAM: Record = { - [USER_ADDRESS_PARAM_DEFAULT]: EIP4361_AUTH_METHOD, -}; diff --git a/packages/taco-auth/src/auth-sig.ts b/packages/taco-auth/src/auth-sig.ts index 01e04ada6..03727842c 100644 --- a/packages/taco-auth/src/auth-sig.ts +++ b/packages/taco-auth/src/auth-sig.ts @@ -1,8 +1,10 @@ import { EthAddressSchema } from '@nucypher/shared'; import { z } from 'zod'; -import { EIP4361_AUTH_METHOD } from './auth-provider'; -import { EIP4361TypedDataSchema } from './providers'; +import { + EIP4361_AUTH_METHOD, + EIP4361TypedDataSchema, +} from './providers/eip4361/common'; export const authSignatureSchema = z.object({ signature: z.string(), diff --git a/packages/taco-auth/src/providers/eip4361/common.ts b/packages/taco-auth/src/providers/eip4361/common.ts new file mode 100644 index 000000000..e6cff1600 --- /dev/null +++ b/packages/taco-auth/src/providers/eip4361/common.ts @@ -0,0 +1,17 @@ +import { SiweMessage } from 'siwe'; +import { z } from 'zod'; + +export const EIP4361_AUTH_METHOD = 'EIP4361'; + +const isSiweMessage = (message: string): boolean => { + try { + new SiweMessage(message); + return true; + } catch { + return false; + } +}; + +export const EIP4361TypedDataSchema = z + .string() + .refine(isSiweMessage, { message: 'Invalid SIWE message' }); diff --git a/packages/taco-auth/src/providers/eip4361.ts b/packages/taco-auth/src/providers/eip4361/eip4361.ts similarity index 85% rename from packages/taco-auth/src/providers/eip4361.ts rename to packages/taco-auth/src/providers/eip4361/eip4361.ts index 8c9a475c6..e5043023a 100644 --- a/packages/taco-auth/src/providers/eip4361.ts +++ b/packages/taco-auth/src/providers/eip4361/eip4361.ts @@ -1,23 +1,12 @@ import { ethers } from 'ethers'; import { generateNonce, SiweMessage } from 'siwe'; -import { z } from 'zod'; -import { EIP4361_AUTH_METHOD } from '../auth-provider'; -import { AuthSignature } from '../auth-sig'; -import { LocalStorage } from '../storage'; +import { AuthSignature } from '../../auth-sig'; +import { LocalStorage } from '../../storage'; -const isSiweMessage = (message: string): boolean => { - try { - new SiweMessage(message); - return true; - } catch { - return false; - } -}; +import { EIP4361_AUTH_METHOD } from './common'; -export const EIP4361TypedDataSchema = z - .string() - .refine(isSiweMessage, { message: 'Invalid SIWE message' }); +export const USER_ADDRESS_PARAM_DEFAULT = ':userAddress'; export type EIP4361AuthProviderParams = { domain: string; diff --git a/packages/taco-auth/src/providers/external-eip4361.ts b/packages/taco-auth/src/providers/eip4361/external-eip4361.ts similarity index 81% rename from packages/taco-auth/src/providers/external-eip4361.ts rename to packages/taco-auth/src/providers/eip4361/external-eip4361.ts index b2c550b86..20885224a 100644 --- a/packages/taco-auth/src/providers/external-eip4361.ts +++ b/packages/taco-auth/src/providers/eip4361/external-eip4361.ts @@ -1,7 +1,11 @@ import { SiweMessage } from 'siwe'; -import { EIP4361_AUTH_METHOD } from '../auth-provider'; -import { AuthSignature } from '../auth-sig'; +import { AuthSignature } from '../../auth-sig'; + +import { EIP4361_AUTH_METHOD } from './common'; + +export const USER_ADDRESS_PARAM_EXTERNAL_EIP4361 = + ':userAddressExternalEIP4361'; export class SingleSignOnEIP4361AuthProvider { public static async fromExistingSiweInfo( @@ -22,7 +26,7 @@ export class SingleSignOnEIP4361AuthProvider { private constructor( private readonly existingSiweMessage: string, - private readonly address: string, + public readonly address: string, private readonly signature: string, ) {} diff --git a/packages/taco-auth/src/providers/index.ts b/packages/taco-auth/src/providers/index.ts index 14f5a1f3f..82912650d 100644 --- a/packages/taco-auth/src/providers/index.ts +++ b/packages/taco-auth/src/providers/index.ts @@ -1,2 +1,2 @@ -export * from './eip4361'; -export * from './external-eip4361'; +export * from './eip4361/eip4361'; +export * from './eip4361/external-eip4361'; diff --git a/packages/taco-auth/test/auth-provider.test.ts b/packages/taco-auth/test/auth-provider.test.ts index a457370d9..cf927b47b 100644 --- a/packages/taco-auth/test/auth-provider.test.ts +++ b/packages/taco-auth/test/auth-provider.test.ts @@ -7,7 +7,8 @@ import { import { SiweMessage } from 'siwe'; import { describe, expect, it } from 'vitest'; -import { EIP4361AuthProvider, EIP4361TypedDataSchema } from '../src'; +import { EIP4361AuthProvider } from '../src/providers'; +import { EIP4361TypedDataSchema } from '../src/providers/eip4361/common'; describe('auth provider', () => { const provider = fakeProvider(bobSecretKeyBytes); diff --git a/packages/taco/examples/encrypt-decrypt.ts b/packages/taco/examples/encrypt-decrypt.ts index a039efe7b..568b2b600 100644 --- a/packages/taco/examples/encrypt-decrypt.ts +++ b/packages/taco/examples/encrypt-decrypt.ts @@ -10,6 +10,7 @@ import { initialize, ThresholdMessageKit, toBytes, + USER_ADDRESS_PARAM_DEFAULT, } from '../src'; const ritualId = 1; @@ -49,11 +50,14 @@ const run = async () => { web3Provider, web3Provider.getSigner(), ); + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider); const decryptedMessage = await decrypt( web3Provider, domains.TESTNET, messageKit, - authProvider, + conditionContext, ); return decryptedMessage; }; diff --git a/packages/taco/src/conditions/condition-expr.ts b/packages/taco/src/conditions/condition-expr.ts index e62ed2b49..752ad5fcc 100644 --- a/packages/taco/src/conditions/condition-expr.ts +++ b/packages/taco/src/conditions/condition-expr.ts @@ -1,11 +1,9 @@ import { Conditions as CoreConditions } from '@nucypher/nucypher-core'; import { toJSON } from '@nucypher/shared'; -import { AuthProviders } from '@nucypher/taco-auth'; import { SemVer } from 'semver'; import { Condition } from './condition'; import { ConditionFactory } from './condition-factory'; -import { ConditionContext, CustomContextParam } from './context'; const ERR_VERSION = (provided: string, current: string) => `Version provided, ${provided}, is incompatible with current version, ${current}`; @@ -64,17 +62,6 @@ export class ConditionExpression { return ConditionExpression.fromJSON(conditions.toString()); } - public buildContext( - customParameters: Record = {}, - authProviders: AuthProviders = {}, - ): ConditionContext { - return new ConditionContext( - this.condition, - customParameters, - authProviders, - ); - } - public equals(other: ConditionExpression): boolean { return [ this.version === other.version, diff --git a/packages/taco/src/conditions/const.ts b/packages/taco/src/conditions/const.ts index 1eb809a92..4cb67d6c2 100644 --- a/packages/taco/src/conditions/const.ts +++ b/packages/taco/src/conditions/const.ts @@ -1,8 +1,8 @@ import { ChainId } from '@nucypher/shared'; -import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'; - -export const USER_ADDRESS_PARAM_EXTERNAL_EIP4361 = - ':userAddressExternalEIP4361'; +import { + USER_ADDRESS_PARAM_DEFAULT, + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, +} from '@nucypher/taco-auth'; // Only allow alphanumeric characters and underscores export const CONTEXT_PARAM_REGEXP = new RegExp('^:[a-zA-Z_][a-zA-Z0-9_]*$'); @@ -21,8 +21,3 @@ export const USER_ADDRESS_PARAMS = [ // Ordering matters, this should always be last USER_ADDRESS_PARAM_DEFAULT, ]; - -export const RESERVED_CONTEXT_PARAMS = [ - USER_ADDRESS_PARAM_DEFAULT, - // USER_ADDRESS_PARAM_EXTERNAL_EIP4361 is not reserved and can be used as a custom context parameter -]; diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 015c34afc..8b4992b8a 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -1,9 +1,12 @@ import { ThresholdMessageKit } from '@nucypher/nucypher-core'; import { toJSON } from '@nucypher/shared'; import { - AUTH_METHOD_FOR_PARAM, - AuthProviders, + AuthProvider, AuthSignature, + EIP4361AuthProvider, + SingleSignOnEIP4361AuthProvider, + USER_ADDRESS_PARAM_DEFAULT, + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, } from '@nucypher/taco-auth'; import { CoreConditions, CoreContext } from '../../types'; @@ -13,11 +16,10 @@ import { ConditionExpression } from '../condition-expr'; import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP, - RESERVED_CONTEXT_PARAMS, USER_ADDRESS_PARAMS, } from '../const'; -export type CustomContextParam = string | number | boolean | AuthSignature; +export type CustomContextParam = string | number | boolean; export type ContextParam = CustomContextParam | AuthSignature; const ERR_RESERVED_PARAM = (key: string) => @@ -28,39 +30,39 @@ const ERR_AUTH_PROVIDER_REQUIRED = (key: string) => `No matching authentication provider to satisfy ${key} context variable in condition`; const ERR_MISSING_CONTEXT_PARAMS = (params: string[]) => `Missing custom context parameter(s): ${params.join(', ')}`; -const ERR_UNKNOWN_CONTEXT_PARAMS = (params: string[]) => - `Unknown custom context parameter(s): ${params.join(', ')}`; -const ERR_NO_AUTH_PROVIDER_FOR_PARAM = (param: string) => - `No custom parameter for requested context parameter: ${param}`; +const ERR_UNKNOWN_CUSTOM_CONTEXT_PARAM = (param: string) => + `Unknown custom context parameter: ${param}`; +const ERR_INVALID_AUTH_PROVIDER_TYPE = (param: string, expected: string) => + `Invalid AuthProvider type for ${param}; expected ${expected}`; +const ERR_AUTH_PROVIDER_NOT_NEEDED_FOR_CONTEXT_PARAM = (param: string) => + `AuthProvider not necessary for context parameter: ${param}`; + +type AuthProviderType = + | typeof EIP4361AuthProvider + | typeof SingleSignOnEIP4361AuthProvider; +const EXPECTED_AUTH_PROVIDER_TYPES: Record = { + [USER_ADDRESS_PARAM_DEFAULT]: EIP4361AuthProvider, + [USER_ADDRESS_PARAM_EXTERNAL_EIP4361]: SingleSignOnEIP4361AuthProvider, +}; + +export const RESERVED_CONTEXT_PARAMS = [ + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, + USER_ADDRESS_PARAM_DEFAULT, +]; export class ConditionContext { - public requestedParameters: Set; + public requestedContextParameters: Set; + private customContextParameters: Record = {}; + private authProviders: Record = {}; - constructor( - condition: Condition, - public readonly customParameters: Record = {}, - private readonly authProviders: AuthProviders = {}, - ) { + constructor(condition: Condition) { const condProps = condition.toObj(); - this.validateContextParameters(); - this.validateCoreConditions(condProps); - this.requestedParameters = + ConditionContext.validateCoreConditions(condProps); + this.requestedContextParameters = ConditionContext.findContextParameters(condProps); - this.validateAuthProviders(this.requestedParameters); - } - - private validateContextParameters(): void { - Object.keys(this.customParameters).forEach((key) => { - if (RESERVED_CONTEXT_PARAMS.includes(key)) { - throw new Error(ERR_RESERVED_PARAM(key)); - } - if (!key.startsWith(CONTEXT_PARAM_PREFIX)) { - throw new Error(ERR_INVALID_CUSTOM_PARAM(key)); - } - }); } - private validateCoreConditions(condObject: ConditionProps) { + private static validateCoreConditions(condObject: ConditionProps) { // Checking whether the condition is compatible with the current version of the library // Intentionally ignoring the return value of the function new CoreConditions(toJSON(condObject)); @@ -71,51 +73,35 @@ export class ConditionContext { ) { // Ok, so at this point we should have all the parameters we need // If we don't, we have a problem and we should throw - const missingParameters = Array.from(this.requestedParameters).filter( - (key) => parameters[key] === undefined, - ); + const missingParameters = Array.from( + this.requestedContextParameters, + ).filter((key) => parameters[key] === undefined); if (missingParameters.length > 0) { throw new Error(ERR_MISSING_CONTEXT_PARAMS(missingParameters)); } - - // We may also have some parameters that are not used - const unknownParameters = Object.keys(parameters).filter( - (key) => - !this.requestedParameters.has(key) && - !RESERVED_CONTEXT_PARAMS.includes(key), - ); - if (unknownParameters.length > 0) { - throw new Error(ERR_UNKNOWN_CONTEXT_PARAMS(unknownParameters)); - } } private async fillContextParameters( - requestedParameters: Set, + requestedContextParameters: Set, ): Promise> { - const parameters = - await this.fillAuthContextParameters(requestedParameters); - for (const key in this.customParameters) { - parameters[key] = this.customParameters[key]; + const parameters = await this.fillAuthContextParameters( + requestedContextParameters, + ); + for (const key in this.customContextParameters) { + parameters[key] = this.customContextParameters[key]; } return parameters; } - private validateAuthProviders(requestedParameters: Set): void { - for (const param of requestedParameters) { + private validateAuthProviders(): void { + for (const param of this.requestedContextParameters) { // If it's not a user address parameter, we can skip if (!USER_ADDRESS_PARAMS.includes(param)) { continue; } - // If it's a user address parameter, we need to check if we have an auth provider - const authMethod = AUTH_METHOD_FOR_PARAM[param]; - if (!authMethod && !this.customParameters[param]) { - // If we don't have an auth method, and we don't have a custom parameter, we have a problem - throw new Error(ERR_NO_AUTH_PROVIDER_FOR_PARAM(param)); - } - - // If we have an auth method, but we don't have an auth provider, we have a problem - if (authMethod && !this.authProviders[authMethod]) { + // we don't have a corresponding auth provider, we have a problem + if (!this.authProviders[param]) { throw new Error(ERR_AUTH_PROVIDER_REQUIRED(param)); } } @@ -126,10 +112,9 @@ export class ConditionContext { ): Promise> { const entries = await Promise.all( [...requestedParameters] - .map((param) => [param, AUTH_METHOD_FOR_PARAM[param]]) - .filter(([, authMethod]) => !!authMethod) - .map(async ([param, authMethod]) => { - const maybeAuthProvider = this.authProviders[authMethod]; + .filter((param) => USER_ADDRESS_PARAMS.includes(param)) + .map(async (param) => { + const maybeAuthProvider = this.authProviders[param]; // TODO: Throw here instead of validating in the constructor? // TODO: Hide getOrCreateAuthSignature behind a more generic interface return [param, await maybeAuthProvider!.getOrCreateAuthSignature()]; @@ -138,11 +123,25 @@ export class ConditionContext { return Object.fromEntries(entries); } + private validateCustomContextParameter(customParam: string): void { + if (!ConditionContext.isContextParameter(customParam)) { + throw new Error(ERR_INVALID_CUSTOM_PARAM(customParam)); + } + + if (RESERVED_CONTEXT_PARAMS.includes(customParam)) { + throw new Error(ERR_RESERVED_PARAM(customParam)); + } + + if (!this.requestedContextParameters.has(customParam)) { + throw new Error(ERR_UNKNOWN_CUSTOM_CONTEXT_PARAM(customParam)); + } + } + private static isContextParameter(param: unknown): boolean { return !!String(param).match(CONTEXT_PARAM_REGEXP); } - public static findContextParameters(condition: ConditionProps) { + private static findContextParameters(condition: ConditionProps) { // First, we want to find all the parameters we need to add const requestedParameters = new Set(); @@ -183,6 +182,31 @@ export class ConditionContext { return requestedParameters; } + public addCustomContextParameterValues( + customContextParameters: Record, + ) { + Object.keys(customContextParameters).forEach((key) => { + this.validateCustomContextParameter(key); + this.customContextParameters[key] = customContextParameters[key]; + }); + } + + public addAuthProvider(contextParam: string, authProvider: AuthProvider) { + if (!(contextParam in EXPECTED_AUTH_PROVIDER_TYPES)) { + throw new Error( + ERR_AUTH_PROVIDER_NOT_NEEDED_FOR_CONTEXT_PARAM(contextParam), + ); + } + + if (!(authProvider instanceof EXPECTED_AUTH_PROVIDER_TYPES[contextParam])) { + throw new Error( + ERR_INVALID_AUTH_PROVIDER_TYPE(contextParam, typeof authProvider), + ); + } + + this.authProviders[contextParam] = authProvider; + } + public async toJson(): Promise { const parameters = await this.toContextParameters(); return toJSON(parameters); @@ -196,33 +220,20 @@ export class ConditionContext { public toContextParameters = async (): Promise< Record > => { + this.validateAuthProviders(); const parameters = await this.fillContextParameters( - this.requestedParameters, + this.requestedContextParameters, ); this.validateNoMissingContextParameters(parameters); return parameters; }; - public static fromConditions( - conditions: CoreConditions, - authProviders?: AuthProviders, - customParameters?: Record, - ): ConditionContext { - return new ConditionContext( - ConditionExpression.fromCoreConditions(conditions).condition, - customParameters, - authProviders, - ); - } - - public static requestedContextParameters( + public static fromMessageKit( messageKit: ThresholdMessageKit, - ): Set { + ): ConditionContext { const conditionExpr = ConditionExpression.fromCoreConditions( messageKit.acp.conditions, ); - return ConditionContext.findContextParameters( - conditionExpr.condition.toObj(), - ); + return new ConditionContext(conditionExpr.condition); } } diff --git a/packages/taco/src/conditions/shared.ts b/packages/taco/src/conditions/shared.ts index 87ea85f7c..1e63b0560 100644 --- a/packages/taco/src/conditions/shared.ts +++ b/packages/taco/src/conditions/shared.ts @@ -1,13 +1,11 @@ import { EthAddressSchema } from '@nucypher/shared'; -import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'; -import { z } from 'zod'; - import { - CONTEXT_PARAM_PREFIX, - CONTEXT_PARAM_REGEXP, - // TODO consider moving this + USER_ADDRESS_PARAM_DEFAULT, USER_ADDRESS_PARAM_EXTERNAL_EIP4361, -} from './const'; +} from '@nucypher/taco-auth'; +import { z } from 'zod'; + +import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from './const'; export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); // We want to discriminate between ContextParams and plain strings @@ -17,7 +15,7 @@ export const plainStringSchema = z.string().refine( return !str.startsWith(CONTEXT_PARAM_PREFIX); }, { - message: 'String must not be a context parameter i.e. not start with ":"', + message: `String must not be a context parameter i.e. not start with "${CONTEXT_PARAM_PREFIX}"`, }, ); diff --git a/packages/taco/src/index.ts b/packages/taco/src/index.ts index 315229937..20d7ea005 100644 --- a/packages/taco/src/index.ts +++ b/packages/taco/src/index.ts @@ -17,4 +17,6 @@ export { decrypt, encrypt, encryptWithPublicKey, isAuthorized } from './taco'; export { EIP4361AuthProvider, SingleSignOnEIP4361AuthProvider, + USER_ADDRESS_PARAM_DEFAULT, + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, } from '@nucypher/taco-auth'; diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index 647af3a52..fc166bf00 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -11,19 +11,15 @@ import { fromHexString, getPorterUris, GlobalAllowListAgent, + PorterClient, toBytes, } from '@nucypher/shared'; -import { - AuthProviders, - EIP4361_AUTH_METHOD, - EIP4361AuthProvider, -} from '@nucypher/taco-auth'; import { ethers } from 'ethers'; import { keccak256 } from 'ethers/lib/utils'; import { Condition } from './conditions/condition'; import { ConditionExpression } from './conditions/condition-expr'; -import { CustomContextParam } from './conditions/context'; +import { ConditionContext } from './conditions/context'; import { DkgClient } from './dkg'; import { retrieveAndDecrypt } from './tdec'; @@ -129,11 +125,9 @@ export const encryptWithPublicKey = async ( * @param {Domain} domain - Represents the logical network in which the decryption will be performed. * Must match the `ritualId`. * @param {ThresholdMessageKit} messageKit - The kit containing the message to be decrypted - * @param authProvider - The authentication provider that will be used to provide the authorization - * @param {string[]} [porterUris] - The URI(s) for the Porter service. If not provided, a value will be obtained + * @param {ConditionContext} context - Optional context data used for decryption time values for the condition(s) within the `messageKit`. + * @param {string[]} [porterUris] - Optional URI(s) for the Porter service. If not provided, a value will be obtained * from the Domain - * @param {Record} [customParameters] - Optional custom parameters that may be required - * depending on the condition used * * @returns {Promise} Returns Promise that resolves with a decrypted message * @@ -144,33 +138,26 @@ export const decrypt = async ( provider: ethers.providers.Provider, domain: Domain, messageKit: ThresholdMessageKit, - authProvider?: EIP4361AuthProvider, + context?: ConditionContext, porterUris?: string[], - customParameters?: Record, ): Promise => { - const porterUrisFull: string[] = porterUris ? porterUris : await getPorterUris(domain); + const porterUrisFull: string[] = porterUris + ? porterUris + : await getPorterUris(domain); + const porter = new PorterClient(porterUrisFull); const ritualId = await DkgCoordinatorAgent.getRitualIdFromPublicKey( provider, domain, messageKit.acp.publicKey, ); - const ritual = await DkgClient.getActiveRitual(provider, domain, ritualId); - const authProviders: AuthProviders = authProvider - ? { - [EIP4361_AUTH_METHOD]: authProvider, - } - : {}; return retrieveAndDecrypt( provider, domain, - porterUrisFull, + porter, messageKit, ritualId, - ritual.sharesNum, - ritual.threshold, - authProviders, - customParameters, + context, ); }; @@ -200,6 +187,7 @@ export const isAuthorized = async ( messageKit, ); +// TODO is this still valid and actually needed? should we remove this? export const registerEncrypters = async ( provider: ethers.providers.Provider, signer: ethers.Signer, diff --git a/packages/taco/src/tdec.ts b/packages/taco/src/tdec.ts index 8c7008b71..97132437d 100644 --- a/packages/taco/src/tdec.ts +++ b/packages/taco/src/tdec.ts @@ -19,12 +19,12 @@ import { PorterClient, toBytes, } from '@nucypher/shared'; -import { AuthProviders } from '@nucypher/taco-auth'; import { ethers } from 'ethers'; import { arrayify, keccak256 } from 'ethers/lib/utils'; import { ConditionExpression } from './conditions/condition-expr'; -import { ConditionContext, CustomContextParam } from './conditions/context'; +import { ConditionContext } from './conditions/context'; +import { DkgClient } from './dkg'; const ERR_DECRYPTION_FAILED = (errors: unknown) => `Threshold of responses not met; TACo decryption failed with errors: ${JSON.stringify( @@ -61,24 +61,18 @@ export const encryptMessage = async ( export const retrieveAndDecrypt = async ( provider: ethers.providers.Provider, domain: Domain, - porterUris: string[], + porter: PorterClient, thresholdMessageKit: ThresholdMessageKit, ritualId: number, - sharesNum: number, - threshold: number, - authProviders?: AuthProviders, - customParameters?: Record, + context?: ConditionContext, ): Promise => { const decryptionShares = await retrieve( provider, domain, - porterUris, + porter, thresholdMessageKit, ritualId, - sharesNum, - threshold, - authProviders, - customParameters, + context, ); const sharedSecret = combineDecryptionSharesSimple(decryptionShares); return thresholdMessageKit.decryptWithSharedSecret(sharedSecret); @@ -88,25 +82,23 @@ export const retrieveAndDecrypt = async ( const retrieve = async ( provider: ethers.providers.Provider, domain: Domain, - porterUris: string[], + porter: PorterClient, thresholdMessageKit: ThresholdMessageKit, ritualId: number, - sharesNum: number, - threshold: number, - authProviders?: AuthProviders, - customParameters?: Record, + context?: ConditionContext, ): Promise => { + const ritual = await DkgClient.getActiveRitual(provider, domain, ritualId); + const dkgParticipants = await DkgCoordinatorAgent.getParticipants( provider, domain, ritualId, - sharesNum, - ); - const conditionContext = await ConditionContext.fromConditions( - thresholdMessageKit.acp.conditions, - authProviders, - customParameters, + ritual.sharesNum, ); + const conditionContext = context + ? context + : ConditionContext.fromMessageKit(thresholdMessageKit); + const { sharedSecrets, encryptedRequests } = await makeDecryptionRequests( ritualId, conditionContext, @@ -114,12 +106,11 @@ const retrieve = async ( thresholdMessageKit, ); - const porter = new PorterClient(porterUris); const { encryptedResponses, errors } = await porter.tacoDecrypt( encryptedRequests, - threshold, + ritual.threshold, ); - if (Object.keys(encryptedResponses).length < threshold) { + if (Object.keys(encryptedResponses).length < ritual.threshold) { throw new Error(ERR_DECRYPTION_FAILED(errors)); } diff --git a/packages/taco/test/conditions/base/contract.test.ts b/packages/taco/test/conditions/base/contract.test.ts index 849ae2b5f..67e3ec26f 100644 --- a/packages/taco/test/conditions/base/contract.test.ts +++ b/packages/taco/test/conditions/base/contract.test.ts @@ -1,5 +1,5 @@ import { initialize } from '@nucypher/nucypher-core'; -import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'; +import { AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'; import { fakeAuthProviders } from '@nucypher/test-utils'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,9 +10,11 @@ import { ContractConditionType, FunctionAbiProps, } from '../../../src/conditions/base/contract'; -import { ConditionExpression } from '../../../src/conditions/condition-expr'; import { USER_ADDRESS_PARAMS } from '../../../src/conditions/const'; -import { CustomContextParam } from '../../../src/conditions/context'; +import { + ConditionContext, + CustomContextParam, +} from '../../../src/conditions/context'; import { testContractConditionObj, testFunctionAbi } from '../../test-utils'; describe('validation', () => { @@ -170,7 +172,7 @@ describe('supports various user address params', () => { ); }); -describe('supports custom function abi', () => { +describe('supports custom function abi', async () => { const contractConditionObj: ContractConditionProps = { ...testContractConditionObj, standardContractType: undefined, @@ -183,19 +185,26 @@ describe('supports custom function abi', () => { }, }; const contractCondition = new ContractCondition(contractConditionObj); - const conditionExpr = new ConditionExpression(contractCondition); const myCustomParam = ':customParam'; const customParams: Record = {}; customParams[myCustomParam] = 1234; + let authProviders: Record; beforeAll(async () => { await initialize(); + authProviders = await fakeAuthProviders(); }); it('accepts custom function abi with a custom parameter', async () => { - const asJson = await conditionExpr - .buildContext(customParams, fakeAuthProviders()) - .toJson(); + const conditionContext = new ConditionContext(contractCondition); + conditionContext.addCustomContextParameterValues(customParams); + + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); + + const asJson = await conditionContext.toJson(); expect(asJson).toBeDefined(); expect(asJson).toContain(USER_ADDRESS_PARAM_DEFAULT); expect(asJson).toContain(myCustomParam); diff --git a/packages/taco/test/conditions/conditions.test.ts b/packages/taco/test/conditions/conditions.test.ts index ad5d735b2..3be3f664a 100644 --- a/packages/taco/test/conditions/conditions.test.ts +++ b/packages/taco/test/conditions/conditions.test.ts @@ -1,4 +1,5 @@ import { ChainId } from '@nucypher/shared'; +import { AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'; import { fakeAuthProviders } from '@nucypher/test-utils'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -8,8 +9,10 @@ import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const'; import { ConditionContext } from '../../src/conditions/context'; describe('conditions', () => { + let authProviders: Record; beforeAll(async () => { await initialize(); + authProviders = await fakeAuthProviders(); }); it('creates a complex condition with custom parameters', async () => { @@ -37,11 +40,13 @@ describe('conditions', () => { expect(condition).toBeDefined(); expect(condition.requiresAuthentication()).toBeTruthy(); - const context = new ConditionContext( - condition, - { ':time': 100 }, - fakeAuthProviders(), + const context = new ConditionContext(condition); + context.addCustomContextParameterValues({ ':time': 100 }); + context.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], ); + expect(context).toBeDefined(); const asObj = await context.toContextParameters(); diff --git a/packages/taco/test/conditions/context.test.ts b/packages/taco/test/conditions/context.test.ts index fb8bce5b9..d18758554 100644 --- a/packages/taco/test/conditions/context.test.ts +++ b/packages/taco/test/conditions/context.test.ts @@ -1,10 +1,11 @@ import { initialize } from '@nucypher/nucypher-core'; import { - AuthProviders, + AuthProvider, AuthSignature, - EIP4361_AUTH_METHOD, EIP4361AuthProvider, + SingleSignOnEIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT, + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, } from '@nucypher/taco-auth'; import { fakeAuthProviders, @@ -20,12 +21,11 @@ import { ContractConditionProps, } from '../../src/conditions/base/contract'; import { RpcCondition } from '../../src/conditions/base/rpc'; -import { ConditionExpression } from '../../src/conditions/condition-expr'; import { - RESERVED_CONTEXT_PARAMS, - USER_ADDRESS_PARAM_EXTERNAL_EIP4361, -} from '../../src/conditions/const'; -import { CustomContextParam } from '../../src/conditions/context'; + ConditionContext, + CustomContextParam, +} from '../../src/conditions/context'; +import { RESERVED_CONTEXT_PARAMS } from '../../src/conditions/context/context'; import { paramOrContextParamSchema, ReturnValueTestProps, @@ -38,11 +38,10 @@ import { } from '../test-utils'; describe('context', () => { - let authProviders: AuthProviders; - + let authProviders: Record; beforeAll(async () => { await initialize(); - authProviders = fakeAuthProviders(); + authProviders = await fakeAuthProviders(); }); describe('serialization', () => { @@ -55,9 +54,11 @@ describe('context', () => { value: USER_ADDRESS_PARAM_DEFAULT, }, }); - const conditionContext = new ConditionExpression( - rpcCondition, - ).buildContext({}, authProviders); + const conditionContext = new ConditionContext(rpcCondition); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); const asJson = await conditionContext.toJson(); expect(asJson).toBeDefined(); @@ -78,69 +79,76 @@ describe('context', () => { }, }; const contractCondition = new ContractCondition(contractConditionObj); - const conditionExpr = new ConditionExpression(contractCondition); - describe('custom parameters', () => { + it('detects when a custom parameter is requested', () => { + const conditionContext = new ConditionContext(contractCondition); + expect(conditionContext.requestedContextParameters).toContain( + customParamKey, + ); + }); + it('serializes bytes as hex strings', async () => { const customParamsWithBytes: Record = {}; const customParam = toBytes('hello'); // Uint8Array is not a valid CustomContextParam, override the type: customParamsWithBytes[customParamKey] = customParam as unknown as string; - const contextAsJson = await conditionExpr - .buildContext(customParamsWithBytes) - .toJson(); + + const conditionContext = new ConditionContext(contractCondition); + conditionContext.addCustomContextParameterValues(customParamsWithBytes); + const contextAsJson = await conditionContext.toJson(); const asObj = JSON.parse(contextAsJson); expect(asObj).toBeDefined(); expect(asObj[customParamKey]).toEqual(`0x${toHexString(customParam)}`); }); - - it('detects when a custom parameter is requested', () => { - const context = conditionExpr.buildContext({}, authProviders); - expect(context.requestedParameters).toContain(customParamKey); - }); }); describe('return value test', () => { - it('accepts on a custom context parameters', async () => { - const asObj = await conditionExpr - .buildContext(customParams) - .toContextParameters(); + it('accepts only custom context parameters', async () => { + const conditionContext = new ConditionContext(contractCondition); + conditionContext.addCustomContextParameterValues(customParams); + const asObj = await conditionContext.toContextParameters(); expect(asObj).toBeDefined(); expect(asObj[customParamKey]).toEqual(1234); }); it('rejects on a missing custom context parameter', async () => { - const context = conditionExpr.buildContext({}, authProviders); - await expect(context.toContextParameters()).rejects.toThrow( + const conditionContext = new ConditionContext(contractCondition); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); + await expect(conditionContext.toContextParameters()).rejects.toThrow( `Missing custom context parameter(s): ${customParamKey}`, ); }); }); it('rejects on using reserved context parameter', () => { + const conditionContext = new ConditionContext(contractCondition); RESERVED_CONTEXT_PARAMS.forEach((reservedParam) => { const badCustomParams: Record = {}; badCustomParams[reservedParam] = 'this-will-throw'; - expect(() => conditionExpr.buildContext(badCustomParams)).toThrow( + expect(() => + conditionContext.addCustomContextParameterValues(badCustomParams), + ).toThrow( `Cannot use reserved parameter name ${reservedParam} as custom parameter`, ); }); }); - it('rejects on using a custom parameter that was not requested', async () => { + it('rejects on using a custom parameter that was not requested', () => { const badCustomParamKey = ':notRequested'; const badCustomParams: Record = {}; badCustomParams[customParamKey] = 'this-is-fine'; badCustomParams[badCustomParamKey] = 'this-will-throw'; - await expect( - conditionExpr.buildContext(badCustomParams).toContextParameters(), - ).rejects.toThrow( - `Unknown custom context parameter(s): ${badCustomParamKey}`, - ); + const conditionContext = new ConditionContext(contractCondition); + expect(() => + conditionContext.addCustomContextParameterValues(badCustomParams), + ).toThrow(`Unknown custom context parameter: ${badCustomParamKey}`); }); - it('detects when auth provider is required by parameters', () => { + it('detects when auth provider is required by parameters', async () => { const conditionObj = { ...testContractConditionObj, parameters: [USER_ADDRESS_PARAM_DEFAULT], @@ -150,14 +158,13 @@ describe('context', () => { } as ReturnValueTestProps, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - expect(conditionExpr.buildContext({}, authProviders)).toBeDefined(); - expect(() => conditionExpr.buildContext({})).toThrow( + const conditionContext = new ConditionContext(condition); + await expect(conditionContext.toContextParameters()).rejects.toThrow( `No matching authentication provider to satisfy ${USER_ADDRESS_PARAM_DEFAULT} context variable in condition`, ); }); - it('detects when signer is required by return value test', () => { + it('detects when signer is required by return value test', async () => { const conditionObj = { ...testContractConditionObj, standardContractType: 'ERC721', @@ -169,24 +176,22 @@ describe('context', () => { }, } as ContractConditionProps; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - expect(conditionExpr.buildContext({}, authProviders)).toBeDefined(); - expect(() => conditionExpr.buildContext({})).toThrow( + const conditionContext = new ConditionContext(condition); + await expect(conditionContext.toContextParameters()).rejects.toThrow( `No matching authentication provider to satisfy ${USER_ADDRESS_PARAM_DEFAULT} context variable in condition`, ); }); - it('detects when signer is not required', () => { + it('detects when signer is not required', async () => { const condition = new RpcCondition(testRpcConditionObj); - const conditionExpr = new ConditionExpression(condition); + const conditionContext = new ConditionContext(condition); expect( JSON.stringify(condition.toObj()).includes(USER_ADDRESS_PARAM_DEFAULT), ).toBe(false); - expect(conditionExpr.buildContext({}, authProviders)).toBeDefined(); - expect(conditionExpr.buildContext({})).toBeDefined(); + await expect(conditionContext.toContextParameters()).toBeDefined(); }); - it('rejects on a missing signer', () => { + it('rejects on a missing signer', async () => { const conditionObj = { ...testContractConditionObj, returnValueTest: { @@ -195,24 +200,24 @@ describe('context', () => { }, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - expect(() => conditionExpr.buildContext({}, undefined)).toThrow( + const conditionContext = new ConditionContext(condition); + await expect(conditionContext.toContextParameters()).rejects.toThrow( `No matching authentication provider to satisfy ${USER_ADDRESS_PARAM_DEFAULT} context variable in condition`, ); }); - it('rejects on a missing signer', () => { + it('rejects on a missing signer for single sign-on EIP4361', async () => { const conditionObj = { ...testContractConditionObj, returnValueTest: { ...testReturnValueTest, - value: USER_ADDRESS_PARAM_DEFAULT, + value: USER_ADDRESS_PARAM_EXTERNAL_EIP4361, }, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - expect(() => conditionExpr.buildContext({}, undefined)).toThrow( - `No matching authentication provider to satisfy ${USER_ADDRESS_PARAM_DEFAULT} context variable in condition`, + const conditionContext = new ConditionContext(condition); + await expect(conditionContext.toContextParameters()).rejects.toThrow( + `No matching authentication provider to satisfy ${USER_ADDRESS_PARAM_EXTERNAL_EIP4361} context variable in condition`, ); }); @@ -229,21 +234,24 @@ describe('context', () => { }; it('handles both custom and auth context parameters', () => { - const requestedParams = new ConditionExpression( - contractCondition, - ).buildContext({}, authProviders).requestedParameters; - expect(requestedParams).not.toContain(USER_ADDRESS_PARAM_DEFAULT); - expect(requestedParams).toContain(customParamKey); + const requestedContextParams = new ConditionContext(contractCondition) + .requestedContextParameters; + expect(requestedContextParams).not.toContain( + USER_ADDRESS_PARAM_DEFAULT, + ); + expect(requestedContextParams).toContain(customParamKey); }); - it('rejects on a missing parameter ', async () => { + it('rejects on a missing custom parameter ', async () => { const customContractCondition = new ContractCondition({ ...contractConditionObj, parameters: [USER_ADDRESS_PARAM_DEFAULT, customParamKey], }); - const conditionContext = new ConditionExpression( - customContractCondition, - ).buildContext({}, authProviders); + const conditionContext = new ConditionContext(customContractCondition); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); await expect(async () => conditionContext.toContextParameters(), @@ -257,9 +265,11 @@ describe('context', () => { ...contractConditionObj, parameters: [USER_ADDRESS_PARAM_DEFAULT, 100], }); - const conditionContext = new ConditionExpression( - customContractCondition, - ).buildContext({}, authProviders); + const conditionContext = new ConditionContext(customContractCondition); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); const asObj = await conditionContext.toContextParameters(); expect(asObj).toBeDefined(); @@ -275,9 +285,15 @@ describe('context', () => { }); const customParameters: Record = {}; customParameters[customParamKey] = falsyParam; - const conditionContext = new ConditionExpression( + + const conditionContext = new ConditionContext( customContractCondition, - ).buildContext(customParameters, authProviders); + ); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); + conditionContext.addCustomContextParameterValues(customParameters); const asObj = await conditionContext.toContextParameters(); expect(asObj).toBeDefined(); @@ -294,18 +310,23 @@ describe('context', () => { describe('No authentication provider', () => { let provider: ethers.providers.Provider; let signer: ethers.Signer; - let authProviders: AuthProviders; + let authProviders: Record; - async function testEIP4361AuthSignature(authSignature: AuthSignature) { + async function testEIP4361AuthSignature( + authSignature: AuthSignature, + expectedAddress?: string, + ) { expect(authSignature).toBeDefined(); expect(authSignature.signature).toBeDefined(); expect(authSignature.scheme).toEqual('EIP4361'); - const signerAddress = await signer.getAddress(); - expect(authSignature.address).toEqual(signerAddress); + const addressToUse = expectedAddress + ? expectedAddress + : await signer.getAddress(); + expect(authSignature.address).toEqual(addressToUse); expect(authSignature.typedData).toContain( - `localhost wants you to sign in with your Ethereum account:\n${signerAddress}`, + `localhost wants you to sign in with your Ethereum account:\n${addressToUse}`, ); expect(authSignature.typedData).toContain('URI: http://localhost:3000'); @@ -317,11 +338,11 @@ describe('No authentication provider', () => { await initialize(); provider = fakeProvider(); signer = fakeSigner(); - authProviders = fakeAuthProviders(); + authProviders = await fakeAuthProviders(); }); it('throws an error if there is no auth provider', () => { - RESERVED_CONTEXT_PARAMS.forEach((userAddressParam) => { + RESERVED_CONTEXT_PARAMS.forEach(async (userAddressParam) => { const conditionObj = { ...testContractConditionObj, returnValueTest: { @@ -330,14 +351,32 @@ describe('No authentication provider', () => { }, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - expect(() => conditionExpr.buildContext({}, {})).toThrow( + const conditionContext = new ConditionContext(condition); + await expect(conditionContext.toContextParameters()).rejects.toThrow( `No matching authentication provider to satisfy ${userAddressParam} context variable in condition`, ); }); }); - it('it supports just one provider at a time', () => { + it('rejects auth provider for not applicable context param', () => { + const conditionObj = { + ...testContractConditionObj, + returnValueTest: { + ...testReturnValueTest, + value: ':myParam', + }, + }; + const condition = new ContractCondition(conditionObj); + const conditionContext = new ConditionContext(condition); + expect(() => + conditionContext.addAuthProvider( + ':myParam', + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ), + ).toThrow('AuthProvider not necessary for context parameter: :myParam'); + }); + + it('rejects invalid auth provider for :userAddress', () => { const conditionObj = { ...testContractConditionObj, returnValueTest: { @@ -346,8 +385,50 @@ describe('No authentication provider', () => { }, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - expect(() => conditionExpr.buildContext({}, authProviders)).not.toThrow(); + const conditionContext = new ConditionContext(condition); + expect(() => + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_EXTERNAL_EIP4361], + ), + ).toThrow(`Invalid AuthProvider type for ${USER_ADDRESS_PARAM_DEFAULT}`); + }); + + it('rejects invalid auth provider for :userAddressExternalEIP4361', () => { + const conditionObj = { + ...testContractConditionObj, + returnValueTest: { + ...testReturnValueTest, + value: USER_ADDRESS_PARAM_EXTERNAL_EIP4361, + }, + }; + const condition = new ContractCondition(conditionObj); + const conditionContext = new ConditionContext(condition); + expect(() => + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ), + ).toThrow( + `Invalid AuthProvider type for ${USER_ADDRESS_PARAM_EXTERNAL_EIP4361}`, + ); + }); + + it('it supports just one provider at a time', async () => { + const conditionObj = { + ...testContractConditionObj, + returnValueTest: { + ...testReturnValueTest, + value: USER_ADDRESS_PARAM_DEFAULT, + }, + }; + const condition = new ContractCondition(conditionObj); + const conditionContext = new ConditionContext(condition); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); + expect(async () => conditionContext.toContextParameters()).not.toThrow(); }); async function makeAuthSignature(authMethod: string) { @@ -359,10 +440,13 @@ describe('No authentication provider', () => { }, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); - const builtContext = conditionExpr.buildContext({}, authProviders); - const contextVars = await builtContext.toContextParameters(); + const conditionContext = new ConditionContext(condition); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProviders[USER_ADDRESS_PARAM_DEFAULT], + ); + const contextVars = await conditionContext.toContextParameters(); const authSignature = contextVars[authMethod] as AuthSignature; expect(authSignature).toBeDefined(); @@ -384,12 +468,6 @@ describe('No authentication provider', () => { }); it('supports reusing external eip4361', async () => { - // Because we are reusing an existing SIWE auth message, we have to pass it as a custom parameter - const authMessage = await makeAuthSignature(USER_ADDRESS_PARAM_DEFAULT); - const customParams: Record = { - [USER_ADDRESS_PARAM_EXTERNAL_EIP4361]: authMessage as CustomContextParam, - }; - // Spying on the EIP4361 provider to make sure it's not called const eip4361Spy = vi.spyOn( EIP4361AuthProvider.prototype, @@ -405,21 +483,19 @@ describe('No authentication provider', () => { }, }; const condition = new ContractCondition(conditionObj); - const conditionExpr = new ConditionExpression(condition); + const conditionContext = new ConditionContext(condition); - // Make sure we remove the EIP4361 auth method from the auth providers first - delete authProviders[EIP4361_AUTH_METHOD]; // Should throw an error if we don't pass the custom parameter - expect(() => conditionExpr.buildContext({}, authProviders)).toThrow( - `No custom parameter for requested context parameter: ${USER_ADDRESS_PARAM_EXTERNAL_EIP4361}`, + await expect(conditionContext.toContextParameters()).rejects.toThrow( + `No matching authentication provider to satisfy ${USER_ADDRESS_PARAM_EXTERNAL_EIP4361} context variable in condition`, ); - // Remembering to pass in customParams here: - const builtContext = conditionExpr.buildContext( - customParams, - authProviders, + // Remembering to pass in auth provider + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, + authProviders[USER_ADDRESS_PARAM_EXTERNAL_EIP4361], ); - const contextVars = await builtContext.toContextParameters(); + const contextVars = await conditionContext.toContextParameters(); expect(eip4361Spy).not.toHaveBeenCalled(); // Now, we expect that the auth signature will be available in the context variables @@ -427,7 +503,14 @@ describe('No authentication provider', () => { USER_ADDRESS_PARAM_EXTERNAL_EIP4361 ] as AuthSignature; expect(authSignature).toBeDefined(); - await testEIP4361AuthSignature(authSignature); + await testEIP4361AuthSignature( + authSignature, + ( + authProviders[ + USER_ADDRESS_PARAM_EXTERNAL_EIP4361 + ] as SingleSignOnEIP4361AuthProvider + ).address, + ); }); }); diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index 8fd2545a0..589d7a9fa 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -21,6 +21,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import * as taco from '../src'; import { conditions, domains, toBytes } from '../src'; +import { ConditionContext } from '../src/conditions/context'; import { fakeDkgRitual, @@ -88,11 +89,13 @@ describe('taco', () => { signer, TEST_SIWE_PARAMS, ); + const conditionContext = ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider); const decryptedMessage = await taco.decrypt( provider, domains.DEVNET, messageKit, - authProvider, + conditionContext, [fakePorterUri], ); expect(decryptedMessage).toEqual(toBytes(message)); @@ -128,10 +131,8 @@ describe('taco', () => { ); expect(getFinalizedRitualSpy).toHaveBeenCalled(); - const requestedParameters = - taco.conditions.context.ConditionContext.requestedContextParameters( - messageKit, - ); + const conditionContext = ConditionContext.fromMessageKit(messageKit); + const requestedParameters = conditionContext.requestedContextParameters; expect(requestedParameters).toEqual( new Set([customParamKey, USER_ADDRESS_PARAM_DEFAULT]), ); diff --git a/packages/test-utils/src/utils.ts b/packages/test-utils/src/utils.ts index 07ed9dc1c..a19af1391 100644 --- a/packages/test-utils/src/utils.ts +++ b/packages/test-utils/src/utils.ts @@ -37,7 +37,11 @@ import { Ursula, zip, } from '@nucypher/shared'; -import { EIP4361_AUTH_METHOD, EIP4361AuthProvider } from '@nucypher/taco-auth'; +import { + EIP4361AuthProvider, + SingleSignOnEIP4361AuthProvider, + USER_ADDRESS_PARAM_DEFAULT, +} from '@nucypher/taco-auth'; import { ethers, providers, Wallet } from 'ethers'; import { expect, SpyInstance, vi } from 'vitest'; @@ -83,13 +87,11 @@ export const fakeSigner = ( } as unknown as ethers.providers.JsonRpcSigner; }; -export const fakeAuthProviders = () => { +export const fakeAuthProviders = async () => { return { - [EIP4361_AUTH_METHOD]: new EIP4361AuthProvider( - fakeProvider(), - fakeSigner(), - TEST_SIWE_PARAMS, - ), + [USER_ADDRESS_PARAM_DEFAULT]: fakeEIP4351AuthProvider(), + [':userAddressExternalEIP4361']: + await fakeSingleSignOnEIP4361AuthProvider(), }; }; @@ -111,6 +113,26 @@ export const fakeProvider = ( } as unknown as ethers.providers.Web3Provider; }; +const fakeEIP4351AuthProvider = () => { + return new EIP4361AuthProvider( + fakeProvider(), + fakeSigner(), + TEST_SIWE_PARAMS, + ); +}; + +const fakeSingleSignOnEIP4361AuthProvider = async () => { + const message = + 'localhost wants you to sign in with your Ethereum account:\n0x924c255297BF9032583dF6E06a8633dc720aB52D\n\nSign-In With Ethereum Example Statement\n\nURI: http://localhost:3000\nVersion: 1\nChain ID: 1234\nNonce: bTyXgcQxn2htgkjJn\nIssued At: 2024-07-18T16:53:39.093516Z'; + const signature = + '0x22cc163b9c37cf425997b76ebafd44a0d68043d0dc9a1dbf823e78c320924476644f28abcf0974d54b8604eff8a62a51559994537d4b8a85cdee977e02ee98921b'; + + return SingleSignOnEIP4361AuthProvider.fromExistingSiweInfo( + message, + signature, + ); +}; + const genChecksumAddress = (i: number): ChecksumAddress => `0x${'0'.repeat(40 - i.toString(16).length)}${i.toString( 16, @@ -130,9 +152,11 @@ export const fakeUrsulas = (n = 4): Ursula[] => export const mockGetUrsulas = ( ursulas: Ursula[] = fakeUrsulas(), ): SpyInstance => { - return vi.spyOn(PorterClient.prototype, 'getUrsulas').mockImplementation(async () => { - return Promise.resolve(ursulas); - }); + return vi + .spyOn(PorterClient.prototype, 'getUrsulas') + .mockImplementation(async () => { + return Promise.resolve(ursulas); + }); }; const fakeCFragResponse = (