diff --git a/examples/pre/nextjs/next-env.d.ts b/examples/pre/nextjs/next-env.d.ts index 4f11a03d..40c3d680 100644 --- a/examples/pre/nextjs/next-env.d.ts +++ b/examples/pre/nextjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/taco/nextjs/next-env.d.ts b/examples/taco/nextjs/next-env.d.ts index 4f11a03d..40c3d680 100644 --- a/examples/taco/nextjs/next-env.d.ts +++ b/examples/taco/nextjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/taco/src/conditions/base/index.ts b/packages/taco/src/conditions/base/index.ts index f116a598..6de843d8 100644 --- a/packages/taco/src/conditions/base/index.ts +++ b/packages/taco/src/conditions/base/index.ts @@ -4,5 +4,6 @@ export * as contract from './contract'; export * as jsonApi from './json-api'; export * as jsonRpc from './json-rpc'; +export * as jwt from './jwt'; export * as rpc from './rpc'; export * as time from './time'; diff --git a/packages/taco/src/conditions/base/jwt.ts b/packages/taco/src/conditions/base/jwt.ts new file mode 100644 index 00000000..92469a4c --- /dev/null +++ b/packages/taco/src/conditions/base/jwt.ts @@ -0,0 +1,23 @@ +import { Condition } from '../condition'; +import { + JWTConditionProps, + jwtConditionSchema, + JWTConditionType, +} from '../schemas/jwt'; +import { OmitConditionType } from '../shared'; + +export { + JWT_PARAM_DEFAULT, + JWTConditionProps, + jwtConditionSchema, + JWTConditionType, +} from '../schemas/jwt'; + +export class JWTCondition extends Condition { + constructor(value: OmitConditionType) { + super(jwtConditionSchema, { + conditionType: JWTConditionType, + ...value, + }); + } +} diff --git a/packages/taco/src/conditions/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index 3295c243..ab84ae90 100644 --- a/packages/taco/src/conditions/condition-factory.ts +++ b/packages/taco/src/conditions/condition-factory.ts @@ -13,6 +13,7 @@ import { JsonRpcConditionProps, JsonRpcConditionType, } from './base/json-rpc'; +import { JWTCondition, JWTConditionProps, JWTConditionType } from './base/jwt'; import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc'; import { TimeCondition, @@ -53,6 +54,8 @@ export class ConditionFactory { return new JsonApiCondition(props as JsonApiConditionProps); case JsonRpcConditionType: return new JsonRpcCondition(props as JsonRpcConditionProps); + case JWTConditionType: + return new JWTCondition(props as JWTConditionProps); // Logical Conditions case CompoundConditionType: return new CompoundCondition(props as CompoundConditionProps); diff --git a/packages/taco/src/conditions/schemas/jwt.ts b/packages/taco/src/conditions/schemas/jwt.ts new file mode 100644 index 00000000..34ef9457 --- /dev/null +++ b/packages/taco/src/conditions/schemas/jwt.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +import { baseConditionSchema } from './common'; +import { contextParamSchema } from './context'; + +export const JWT_PARAM_DEFAULT = ':jwtToken'; + +export const JWTConditionType = 'jwt'; + +export const jwtConditionSchema = baseConditionSchema.extend({ + conditionType: z.literal(JWTConditionType).default(JWTConditionType), + publicKey: z.string(), + expectedIssuer: z.string().optional(), + // TODO see https://github.com/nucypher/taco-web/pull/604#discussion_r1901746814 + // subject: contextParamSchema.optional(), + // expirationWindow: z.number().int().nonnegative().optional(), + // issuedWindow: z.number().int().nonnegative().optional(), + jwtToken: contextParamSchema.default(JWT_PARAM_DEFAULT), +}); + +export type JWTConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index e9f8f28c..abd14f36 100644 --- a/packages/taco/src/conditions/schemas/utils.ts +++ b/packages/taco/src/conditions/schemas/utils.ts @@ -6,6 +6,7 @@ import { contractConditionSchema } from './contract'; import { ifThenElseConditionSchema } from './if-then-else'; import { jsonApiConditionSchema } from './json-api'; import { jsonRpcConditionSchema } from './json-rpc'; +import { jwtConditionSchema } from './jwt'; import { rpcConditionSchema } from './rpc'; import { sequentialConditionSchema } from './sequential'; import { timeConditionSchema } from './time'; @@ -18,6 +19,7 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() => compoundConditionSchema, jsonApiConditionSchema, jsonRpcConditionSchema, + jwtConditionSchema, sequentialConditionSchema, ifThenElseConditionSchema, ]), diff --git a/packages/taco/test/conditions/base/jwt.test.ts b/packages/taco/test/conditions/base/jwt.test.ts new file mode 100644 index 00000000..8598e95e --- /dev/null +++ b/packages/taco/test/conditions/base/jwt.test.ts @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { TEST_CONTRACT_ADDR } from '@nucypher/test-utils'; +import { describe, expect, it } from 'vitest'; + +import { + JWTCondition, + jwtConditionSchema, +} from '../../../src/conditions/base/jwt'; +import { testJWTConditionObj } from '../../test-utils'; + +describe('JWTCondition', () => { + describe('validation', () => { + it('accepts a valid schema', () => { + const result = JWTCondition.validate( + jwtConditionSchema, + testJWTConditionObj, + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(testJWTConditionObj); + }); + + it('rejects an invalid schema', () => { + const badJWTObj = { + ...testJWTConditionObj, + jwtToken: TEST_CONTRACT_ADDR, + }; + + const result = JWTCondition.validate(jwtConditionSchema, badJWTObj); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + jwtToken: { + _errors: ['Invalid'], + }, + }); + }); + }); +}); diff --git a/packages/taco/test/conditions/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index dabd1e3f..aae71959 100644 --- a/packages/taco/test/conditions/compound-condition.test.ts +++ b/packages/taco/test/conditions/compound-condition.test.ts @@ -11,6 +11,7 @@ import { } from '../../src/conditions/compound-condition'; import { testContractConditionObj, + testJWTConditionObj, testRpcConditionObj, testSequentialConditionObj, testTimeConditionObj, @@ -167,7 +168,11 @@ describe('validation', () => { testRpcConditionObj, { operator: 'or', - operands: [testTimeConditionObj, testContractConditionObj], + operands: [ + testTimeConditionObj, + testContractConditionObj, + testJWTConditionObj, + ], }, testSequentialConditionObj, ], @@ -187,7 +192,11 @@ describe('validation', () => { { conditionType: CompoundConditionType, operator: 'or', - operands: [testTimeConditionObj, testContractConditionObj], + operands: [ + testTimeConditionObj, + testContractConditionObj, + testJWTConditionObj, + ], }, testSequentialConditionObj, ], diff --git a/packages/taco/test/conditions/lingo.test.ts b/packages/taco/test/conditions/lingo.test.ts index 8b85a06e..3bbe46b4 100644 --- a/packages/taco/test/conditions/lingo.test.ts +++ b/packages/taco/test/conditions/lingo.test.ts @@ -2,15 +2,15 @@ import { TEST_CHAIN_ID } from '@nucypher/test-utils'; import { describe, expect, it } from 'vitest'; import { ConditionExpression } from '../../src/conditions/condition-expr'; +import { + testJsonApiConditionObj, + testJsonRpcConditionObj, + testJWTConditionObj, + testRpcConditionObj, + testTimeConditionObj, +} from '../test-utils'; describe('check that valid lingo in python is valid in typescript', () => { - const timeConditionProps = { - conditionType: 'time', - method: 'blocktime', - chain: TEST_CHAIN_ID, - returnValueTest: { value: 0, comparator: '>' }, - }; - const contractConditionProps = { conditionType: 'contract', chain: TEST_CHAIN_ID, @@ -35,50 +35,16 @@ describe('check that valid lingo in python is valid in typescript', () => { value: true, }, }; - const rpcConditionProps = { - conditionType: 'rpc', - chain: TEST_CHAIN_ID, - method: 'eth_getBalance', - parameters: ['0x3d2Bed3259b165EB02A7F0D0753e7a01912A68f8', 'latest'], - returnValueTest: { - comparator: '>=', - value: 10000000000000, - }, - }; - const jsonApiConditionProps = { - conditionType: 'json-api', - endpoint: 'https://api.example.com/data', - query: '$.store.book[0].price', - parameters: { - ids: 'ethereum', - vs_currencies: 'usd', - }, - returnValueTest: { - comparator: '==', - value: 2, - }, - }; - const jsonRpcConditionProps = { - conditionType: 'json-rpc', - endpoint: 'https://math.example.com/', - method: 'subtract', - params: [42, 23], - query: '$.value', - returnValueTest: { - comparator: '==', - value: 2, - }, - }; const sequentialConditionProps = { conditionType: 'sequential', conditionVariables: [ { varName: 'timeValue', - condition: timeConditionProps, + condition: testTimeConditionObj, }, { varName: 'rpcValue', - condition: rpcConditionProps, + condition: testRpcConditionObj, }, { varName: 'contractValue', @@ -86,15 +52,15 @@ describe('check that valid lingo in python is valid in typescript', () => { }, { varName: 'jsonValue', - condition: jsonApiConditionProps, + condition: testJsonApiConditionObj, }, ], }; const ifThenElseConditionProps = { conditionType: 'if-then-else', - ifCondition: jsonRpcConditionProps, - thenCondition: jsonApiConditionProps, - elseCondition: timeConditionProps, + ifCondition: testJsonRpcConditionObj, + thenCondition: testJsonApiConditionObj, + elseCondition: testTimeConditionObj, }; const compoundConditionProps = { @@ -104,21 +70,22 @@ describe('check that valid lingo in python is valid in typescript', () => { contractConditionProps, ifThenElseConditionProps, sequentialConditionProps, - rpcConditionProps, + testRpcConditionObj, { conditionType: 'compound', operator: 'not', - operands: [timeConditionProps], + operands: [testTimeConditionObj], }, ], }; it.each([ - rpcConditionProps, - timeConditionProps, + testRpcConditionObj, + testTimeConditionObj, contractConditionProps, - jsonApiConditionProps, - jsonRpcConditionProps, + testJsonApiConditionObj, + testJsonRpcConditionObj, + testJWTConditionObj, compoundConditionProps, sequentialConditionProps, ifThenElseConditionProps, diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index f1c7b19b..1857142b 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -31,6 +31,7 @@ import { fakeTDecFlow, TEST_CHAIN_ID, TEST_CONTRACT_ADDR, + TEST_ECDSA_PUBLIC_KEY, } from '@nucypher/test-utils'; import { SpyInstance, vi } from 'vitest'; @@ -43,6 +44,11 @@ import { JsonApiConditionProps, JsonApiConditionType, } from '../src/conditions/base/json-api'; +import { + JWT_PARAM_DEFAULT, + JWTConditionProps, + JWTConditionType, +} from '../src/conditions/base/jwt'; import { RpcConditionProps, RpcConditionType, @@ -259,6 +265,16 @@ export const testJsonRpcConditionObj: JsonRpcConditionProps = { returnValueTest: testReturnValueTest, }; +export const testJWTConditionObj: JWTConditionProps = { + conditionType: JWTConditionType, + publicKey: TEST_ECDSA_PUBLIC_KEY, + expectedIssuer: '0xacbd', + // subject: ':userAddress', + // expirationWindow: 1800, + // issuedWindow: 86400, + jwtToken: JWT_PARAM_DEFAULT, +}; + export const testRpcConditionObj: RpcConditionProps = { conditionType: RpcConditionType, chain: TEST_CHAIN_ID, diff --git a/packages/test-utils/src/variables.ts b/packages/test-utils/src/variables.ts index 462f5660..c6f73db2 100644 --- a/packages/test-utils/src/variables.ts +++ b/packages/test-utils/src/variables.ts @@ -19,3 +19,8 @@ export const TEST_SIWE_PARAMS = { domain: 'localhost', uri: 'http://localhost:3000', }; + +export const TEST_ECDSA_PUBLIC_KEY = + '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXHVxB7s5SR7I9cWwry' + + '/JkECIReka\nCwG3uOLCYbw5gVzn4dRmwMyYUJFcQWuFSfECRK+uQOOXD0YSEucBq0p5tA==\n-----END PUBLIC ' + + 'KEY-----\n ';