From 525a2c562c6416302c56923bd886ab95c1057540 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Fri, 13 Dec 2024 14:29:39 -0500 Subject: [PATCH 1/6] Draft of JWT condition --- packages/taco/src/conditions/base/jwt.ts | 23 +++++++++++ packages/taco/src/conditions/schemas/jwt.ts | 20 ++++++++++ .../taco/test/conditions/base/jwt.test.ts | 40 +++++++++++++++++++ packages/taco/test/test-utils.ts | 17 +++++++- packages/test-utils/src/variables.ts | 5 +++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 packages/taco/src/conditions/base/jwt.ts create mode 100644 packages/taco/src/conditions/schemas/jwt.ts create mode 100644 packages/taco/test/conditions/base/jwt.test.ts diff --git a/packages/taco/src/conditions/base/jwt.ts b/packages/taco/src/conditions/base/jwt.ts new file mode 100644 index 000000000..92469a4cb --- /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/schemas/jwt.ts b/packages/taco/src/conditions/schemas/jwt.ts new file mode 100644 index 000000000..b37a6f81d --- /dev/null +++ b/packages/taco/src/conditions/schemas/jwt.ts @@ -0,0 +1,20 @@ +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), + public_key: z.string().optional(), + expected_issuer: z.string().optional(), + subject: contextParamSchema.optional(), + expiration_window: z.number().int().nonnegative().optional(), + issued_window: z.number().int().nonnegative().optional(), + jwtToken: contextParamSchema.default(JWT_PARAM_DEFAULT), +}); + +export type JWTConditionProps = z.infer; 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 000000000..d5b2de620 --- /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, + subject: TEST_CONTRACT_ADDR, + }; + + const result = JWTCondition.validate(jwtConditionSchema, badJWTObj); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + subject: { + _errors: ['Invalid'], + }, + }); + }); + }); +}); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index f1c7b19b1..cdf19ba60 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'; @@ -44,7 +45,11 @@ import { JsonApiConditionType, } from '../src/conditions/base/json-api'; import { - RpcConditionProps, + JWT_PARAM_DEFAULT, + JWTConditionProps, + JWTConditionType, +} from '../src/conditions/base/jwt'; +import { RpcConditionProps, RpcConditionType, } from '../src/conditions/base/rpc'; import { @@ -259,6 +264,16 @@ export const testJsonRpcConditionObj: JsonRpcConditionProps = { returnValueTest: testReturnValueTest, }; +export const testJWTConditionObj: JWTConditionProps = { + conditionType: JWTConditionType, + public_key: TEST_ECDSA_PUBLIC_KEY, + expected_issuer: '0xacbd', + subject: ':userAddress', + expiration_window: 1800, + issued_window: 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 462f56609..c6f73db2d 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 '; From 6480493b87054aae35b0ff9d4ad9bd3ed966ad61 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Mon, 16 Dec 2024 12:56:54 -0500 Subject: [PATCH 2/6] Apply RFCs from #604 --- packages/taco/src/conditions/schemas/jwt.ts | 8 ++++---- packages/taco/test/test-utils.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/taco/src/conditions/schemas/jwt.ts b/packages/taco/src/conditions/schemas/jwt.ts index b37a6f81d..9f543735d 100644 --- a/packages/taco/src/conditions/schemas/jwt.ts +++ b/packages/taco/src/conditions/schemas/jwt.ts @@ -9,11 +9,11 @@ export const JWTConditionType = 'jwt'; export const jwtConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(JWTConditionType).default(JWTConditionType), - public_key: z.string().optional(), - expected_issuer: z.string().optional(), + publicKey: z.string().optional(), + expectedIssuer: z.string().optional(), subject: contextParamSchema.optional(), - expiration_window: z.number().int().nonnegative().optional(), - issued_window: z.number().int().nonnegative().optional(), + expirationWindow: z.number().int().nonnegative().optional(), + issuedWindow: z.number().int().nonnegative().optional(), jwtToken: contextParamSchema.default(JWT_PARAM_DEFAULT), }); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index cdf19ba60..cba73d1a5 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -266,11 +266,11 @@ export const testJsonRpcConditionObj: JsonRpcConditionProps = { export const testJWTConditionObj: JWTConditionProps = { conditionType: JWTConditionType, - public_key: TEST_ECDSA_PUBLIC_KEY, - expected_issuer: '0xacbd', + publicKey: TEST_ECDSA_PUBLIC_KEY, + expectedIssuer: '0xacbd', subject: ':userAddress', - expiration_window: 1800, - issued_window: 86400, + expirationWindow: 1800, + issuedWindow: 86400, jwtToken: JWT_PARAM_DEFAULT, }; From af3926ef8e4ae67a351865bf9462b92eda490bfe Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Thu, 19 Dec 2024 13:09:25 -0500 Subject: [PATCH 3/6] Followup small changes --- examples/pre/nextjs/next-env.d.ts | 2 +- examples/taco/nextjs/next-env.d.ts | 2 +- packages/taco/src/conditions/base/index.ts | 1 + packages/taco/src/conditions/condition-factory.ts | 3 +++ packages/taco/src/conditions/schemas/utils.ts | 2 ++ .../taco/test/conditions/compound-condition.test.ts | 13 +++++++++++-- packages/taco/test/conditions/lingo.test.ts | 13 ++++++++++++- packages/taco/test/test-utils.ts | 3 ++- 8 files changed, 33 insertions(+), 6 deletions(-) diff --git a/examples/pre/nextjs/next-env.d.ts b/examples/pre/nextjs/next-env.d.ts index 4f11a03dc..40c3d6809 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 4f11a03dc..40c3d6809 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 f116a5987..6de843d86 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/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index 3295c2438..ab84ae909 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/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index e9f8f28c2..abd14f36b 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/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index dabd1e3f2..aae719596 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 8b85a06eb..699a04e8b 100644 --- a/packages/taco/test/conditions/lingo.test.ts +++ b/packages/taco/test/conditions/lingo.test.ts @@ -1,4 +1,4 @@ -import { TEST_CHAIN_ID } from '@nucypher/test-utils'; +import { TEST_CHAIN_ID, TEST_ECDSA_PUBLIC_KEY } from '@nucypher/test-utils'; import { describe, expect, it } from 'vitest'; import { ConditionExpression } from '../../src/conditions/condition-expr'; @@ -69,6 +69,16 @@ describe('check that valid lingo in python is valid in typescript', () => { value: 2, }, }; + // TODO reuse similar object from test-utils + const jwtConditionProps = { + conditionType: 'jwt', + publicKey: TEST_ECDSA_PUBLIC_KEY, + expectedIssuer: '0xacbd', + subject: ':userAddress', + expirationWindow: 1800, + issuedWindow: 86400, + jwtToken: ':jwt', + }; const sequentialConditionProps = { conditionType: 'sequential', conditionVariables: [ @@ -119,6 +129,7 @@ describe('check that valid lingo in python is valid in typescript', () => { contractConditionProps, jsonApiConditionProps, jsonRpcConditionProps, + jwtConditionProps, compoundConditionProps, sequentialConditionProps, ifThenElseConditionProps, diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index cba73d1a5..41fb2a35b 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -49,7 +49,8 @@ import { JWTConditionProps, JWTConditionType, } from '../src/conditions/base/jwt'; -import { RpcConditionProps, +import { + RpcConditionProps, RpcConditionType, } from '../src/conditions/base/rpc'; import { From 7e805ee704caf95ee484205b7c4ba9b78c12977a Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Wed, 1 Jan 2025 17:14:26 -0500 Subject: [PATCH 4/6] Reusing test condition objects from test-utils --- packages/taco/test/conditions/lingo.test.ts | 86 +++++---------------- 1 file changed, 21 insertions(+), 65 deletions(-) diff --git a/packages/taco/test/conditions/lingo.test.ts b/packages/taco/test/conditions/lingo.test.ts index 699a04e8b..3bbe46b4e 100644 --- a/packages/taco/test/conditions/lingo.test.ts +++ b/packages/taco/test/conditions/lingo.test.ts @@ -1,16 +1,16 @@ -import { TEST_CHAIN_ID, TEST_ECDSA_PUBLIC_KEY } from '@nucypher/test-utils'; +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,60 +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, - }, - }; - // TODO reuse similar object from test-utils - const jwtConditionProps = { - conditionType: 'jwt', - publicKey: TEST_ECDSA_PUBLIC_KEY, - expectedIssuer: '0xacbd', - subject: ':userAddress', - expirationWindow: 1800, - issuedWindow: 86400, - jwtToken: ':jwt', - }; const sequentialConditionProps = { conditionType: 'sequential', conditionVariables: [ { varName: 'timeValue', - condition: timeConditionProps, + condition: testTimeConditionObj, }, { varName: 'rpcValue', - condition: rpcConditionProps, + condition: testRpcConditionObj, }, { varName: 'contractValue', @@ -96,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 = { @@ -114,22 +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, - jwtConditionProps, + testJsonApiConditionObj, + testJsonRpcConditionObj, + testJWTConditionObj, compoundConditionProps, sequentialConditionProps, ifThenElseConditionProps, From 83a0945a673b8118df8796ab925d8ef5dbc6b1c3 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Fri, 3 Jan 2025 09:45:45 -0500 Subject: [PATCH 5/6] Comments couple field in JWT to match with PR in nucypher repo --- packages/taco/src/conditions/schemas/jwt.ts | 7 ++++--- packages/taco/test/conditions/base/jwt.test.ts | 4 ++-- packages/taco/test/test-utils.ts | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/taco/src/conditions/schemas/jwt.ts b/packages/taco/src/conditions/schemas/jwt.ts index 9f543735d..000e0834b 100644 --- a/packages/taco/src/conditions/schemas/jwt.ts +++ b/packages/taco/src/conditions/schemas/jwt.ts @@ -11,9 +11,10 @@ export const jwtConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(JWTConditionType).default(JWTConditionType), publicKey: z.string().optional(), expectedIssuer: z.string().optional(), - subject: contextParamSchema.optional(), - expirationWindow: z.number().int().nonnegative().optional(), - issuedWindow: z.number().int().nonnegative().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), }); diff --git a/packages/taco/test/conditions/base/jwt.test.ts b/packages/taco/test/conditions/base/jwt.test.ts index d5b2de620..8598e95e2 100644 --- a/packages/taco/test/conditions/base/jwt.test.ts +++ b/packages/taco/test/conditions/base/jwt.test.ts @@ -23,7 +23,7 @@ describe('JWTCondition', () => { it('rejects an invalid schema', () => { const badJWTObj = { ...testJWTConditionObj, - subject: TEST_CONTRACT_ADDR, + jwtToken: TEST_CONTRACT_ADDR, }; const result = JWTCondition.validate(jwtConditionSchema, badJWTObj); @@ -31,7 +31,7 @@ describe('JWTCondition', () => { expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ - subject: { + jwtToken: { _errors: ['Invalid'], }, }); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 41fb2a35b..1857142b6 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -269,9 +269,9 @@ export const testJWTConditionObj: JWTConditionProps = { conditionType: JWTConditionType, publicKey: TEST_ECDSA_PUBLIC_KEY, expectedIssuer: '0xacbd', - subject: ':userAddress', - expirationWindow: 1800, - issuedWindow: 86400, + // subject: ':userAddress', + // expirationWindow: 1800, + // issuedWindow: 86400, jwtToken: JWT_PARAM_DEFAULT, }; From 7f4414285513f637a461f3d5aba26a57d18bae88 Mon Sep 17 00:00:00 2001 From: Viktoriia Zotova Date: Tue, 7 Jan 2025 15:26:52 -0500 Subject: [PATCH 6/6] Public key for jwt condition is required property --- packages/taco/src/conditions/schemas/jwt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/schemas/jwt.ts b/packages/taco/src/conditions/schemas/jwt.ts index 000e0834b..34ef9457e 100644 --- a/packages/taco/src/conditions/schemas/jwt.ts +++ b/packages/taco/src/conditions/schemas/jwt.ts @@ -9,7 +9,7 @@ export const JWTConditionType = 'jwt'; export const jwtConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(JWTConditionType).default(JWTConditionType), - publicKey: z.string().optional(), + publicKey: z.string(), expectedIssuer: z.string().optional(), // TODO see https://github.com/nucypher/taco-web/pull/604#discussion_r1901746814 // subject: contextParamSchema.optional(),