Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonRpcCondition #606

Merged
merged 8 commits into from
Jan 6, 2025
1 change: 1 addition & 0 deletions packages/taco/src/conditions/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@

export * as contract from './contract';
export * as jsonApi from './json-api';
export * as jsonRpc from './json-rpc';
export * as rpc from './rpc';
export * as time from './time';
1 change: 0 additions & 1 deletion packages/taco/src/conditions/base/json-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export {
JsonApiConditionProps,
jsonApiConditionSchema,
JsonApiConditionType,
jsonPathSchema,
} from '../schemas/json-api';

export class JsonApiCondition extends Condition {
Expand Down
22 changes: 22 additions & 0 deletions packages/taco/src/conditions/base/json-rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Condition } from '../condition';
import {
JsonRpcConditionProps,
jsonRpcConditionSchema,
JsonRpcConditionType,
} from '../schemas/json-rpc';
import { OmitConditionType } from '../shared';

export {
JsonRpcConditionProps,
jsonRpcConditionSchema,
JsonRpcConditionType,
} from '../schemas/json-rpc';

export class JsonRpcCondition extends Condition {
constructor(value: OmitConditionType<JsonRpcConditionProps>) {
super(jsonRpcConditionSchema, {
conditionType: JsonRpcConditionType,
...value,
});
}
}
7 changes: 7 additions & 0 deletions packages/taco/src/conditions/condition-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
JsonApiConditionProps,
JsonApiConditionType,
} from './base/json-api';
import {
JsonRpcCondition,
JsonRpcConditionProps,
JsonRpcConditionType,
} from './base/json-rpc';
import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc';
import {
TimeCondition,
Expand Down Expand Up @@ -46,6 +51,8 @@ export class ConditionFactory {
return new ContractCondition(props as ContractConditionProps);
case JsonApiConditionType:
return new JsonApiCondition(props as JsonApiConditionProps);
case JsonRpcConditionType:
return new JsonRpcCondition(props as JsonRpcConditionProps);
// Logical Conditions
case CompoundConditionType:
return new CompoundCondition(props as CompoundConditionProps);
Expand Down
102 changes: 48 additions & 54 deletions packages/taco/src/conditions/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from '@nucypher/taco-auth';

import { CoreConditions, CoreContext } from '../../types';
import { CompoundConditionType } from '../compound-condition';
import { Condition, ConditionProps } from '../condition';
import { ConditionExpression } from '../condition-expr';
import {
Expand All @@ -19,7 +18,6 @@ import {
CONTEXT_PARAM_REGEXP,
USER_ADDRESS_PARAMS,
} from '../const';
import { JsonApiConditionType } from '../schemas/json-api';

export type CustomContextParam = string | number | boolean;
export type ContextParam = CustomContextParam | AuthSignature;
Expand Down Expand Up @@ -143,67 +141,63 @@ export class ConditionContext {
return !!String(param).match(CONTEXT_PARAM_FULL_MATCH_REGEXP);
}

private static findContextParameters(condition: ConditionProps) {
// First, we want to find all the parameters we need to add
const requestedParameters = new Set<string>();

// Check return value test
if (condition.returnValueTest) {
const rvt = condition.returnValueTest.value;
// Return value test can be a single parameter or an array of parameters
if (Array.isArray(rvt)) {
rvt.forEach((value) => {
if (ConditionContext.isContextParameter(value)) {
requestedParameters.add(value);
}
});
} else if (ConditionContext.isContextParameter(rvt)) {
requestedParameters.add(rvt);
} else {
// Not a context parameter, we can skip
}
}
private static findContextParameter(value: unknown): Set<string> {
const includedContextVars = new Set<string>();

// Check condition parameters
for (const param of condition.parameters ?? []) {
if (this.isContextParameter(param)) {
requestedParameters.add(param);
}
// value not set
if (!value) {
return includedContextVars;
}

// If it's a compound condition, check operands
if (condition.conditionType === CompoundConditionType) {
for (const key in condition.operands) {
const innerParams = this.findContextParameters(condition.operands[key]);
for (const param of innerParams) {
requestedParameters.add(param);
}
}
}
// If it's a JSON API condition, check url and query
if (condition.conditionType === JsonApiConditionType) {
const urlComponents = condition.endpoint
.replace('https://', '')
.split('/');
for (const param of urlComponents ?? []) {
if (this.isContextParameter(param)) {
requestedParameters.add(param);
}
}
if (condition.query) {
const queryParams = condition.query.match(CONTEXT_PARAM_REGEXP);
if (queryParams) {
for (const param of queryParams) {
requestedParameters.add(param);
if (typeof value === 'string') {
if (this.isContextParameter(value)) {
// entire string is context parameter
includedContextVars.add(String(value));
} else {
// context var could be substring; find all matches
const contextVarMatches = value.match(
// RegExp with 'g' is stateful, so new instance needed every time
new RegExp(CONTEXT_PARAM_REGEXP.source, 'g'),
);
if (contextVarMatches) {
for (const match of contextVarMatches) {
includedContextVars.add(match);
}
}
}
// always a context variable, so simply check whether defined
if (condition.authorizationToken) {
requestedParameters.add(condition.authorizationToken);
} else if (Array.isArray(value)) {
// array
value.forEach((subValue) => {
const contextVarsForValue = this.findContextParameter(subValue);
contextVarsForValue.forEach((contextVar) => {
includedContextVars.add(contextVar);
});
});
} else if (typeof value === 'object') {
// dictionary (Record<string, T> - complex object eg. Condition, ConditionVariable, ReturnValueTest etc.)
for (const [, entry] of Object.entries(value)) {
const contextVarsForValue = this.findContextParameter(entry);
contextVarsForValue.forEach((contextVar) => {
includedContextVars.add(contextVar);
});
}
}

return includedContextVars;
}

private static findContextParameters(condition: ConditionProps) {
// find all the context variables we need
const requestedParameters = new Set<string>();

// iterate through all properties in ConditionProps
const properties = Object.keys(condition) as (keyof typeof condition)[];
properties.forEach((prop) => {
this.findContextParameter(condition[prop]).forEach((contextVar) => {
requestedParameters.add(contextVar);
});
});

return requestedParameters;
}

Expand Down
36 changes: 35 additions & 1 deletion packages/taco/src/conditions/schemas/common.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { JSONPath } from '@astronautlabs/jsonpath';
import {
USER_ADDRESS_PARAM_DEFAULT,
USER_ADDRESS_PARAM_EXTERNAL_EIP4361,
} from '@nucypher/taco-auth';
import { Primitive, z, ZodLiteral } from 'zod';

import { CONTEXT_PARAM_PREFIX } from '../const';
import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from '../const';

// We want to discriminate between ContextParams and plain strings
// If a string starts with `:`, it's a ContextParam
Expand Down Expand Up @@ -55,3 +56,36 @@ function createUnionSchema<T extends readonly Primitive[]>(values: T) {
}

export default createUnionSchema;

const validateJSONPath = (jsonPath: string): boolean => {
// account for embedded context variables
if (CONTEXT_PARAM_REGEXP.test(jsonPath)) {
// skip validation
return true;
}

try {
JSONPath.parse(jsonPath);
return true;
} catch (error) {
return false;
}
};

export const jsonPathSchema = z
.string()
.refine((val) => validateJSONPath(val), {
message: 'Invalid JSONPath expression',
});

const validateHttpsURL = (url: string): boolean => {
return URL.canParse(url) && url.startsWith('https://');
};

// Use our own URL refinement check due to https://github.com/colinhacks/zod/issues/2236
export const httpsURLSchema = z
.string()
.url()
derekpierre marked this conversation as resolved.
Show resolved Hide resolved
.refine((url) => validateHttpsURL(url), {
message: 'Invalid URL',
});
29 changes: 3 additions & 26 deletions packages/taco/src/conditions/schemas/json-api.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,14 @@
import { JSONPath } from '@astronautlabs/jsonpath';
import { z } from 'zod';

import { CONTEXT_PARAM_REGEXP } from '../const';

import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common';
import { contextParamSchema } from './context';
import { returnValueTestSchema } from './return-value-test';

export const JsonApiConditionType = 'json-api';

const validateJSONPath = (jsonPath: string): boolean => {
// account for embedded context variables
if (CONTEXT_PARAM_REGEXP.test(jsonPath)) {
// skip validation
return true;
}

try {
JSONPath.parse(jsonPath);
return true;
} catch (error) {
return false;
}
};

export const jsonPathSchema = z
.string()
.refine((val) => validateJSONPath(val), {
message: 'Invalid JSONPath expression',
});

export const jsonApiConditionSchema = z.object({
export const jsonApiConditionSchema = baseConditionSchema.extend({
conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType),
endpoint: z.string().url(),
endpoint: httpsURLSchema,
parameters: z.record(z.string(), z.unknown()).optional(),
query: jsonPathSchema.optional(),
authorizationToken: contextParamSchema.optional(),
Expand Down
22 changes: 22 additions & 0 deletions packages/taco/src/conditions/schemas/json-rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';

import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common';
import { contextParamSchema } from './context';
import { returnValueTestSchema } from './return-value-test';

export const JsonRpcConditionType = 'json-rpc';

export const jsonRpcConditionSchema = baseConditionSchema.extend({
conditionType: z.literal(JsonRpcConditionType).default(JsonRpcConditionType),
endpoint: httpsURLSchema,
method: z.string(),
// list or dictionary
params: z
.union([z.array(z.unknown()), z.record(z.string(), z.unknown())])
.optional(),
query: jsonPathSchema.optional(),
authorizationToken: contextParamSchema.optional(),
returnValueTest: returnValueTestSchema,
});

export type JsonRpcConditionProps = z.infer<typeof jsonRpcConditionSchema>;
5 changes: 1 addition & 4 deletions packages/taco/src/conditions/schemas/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { BlockIdentifierSchema, EthAddressSchema } from '@nucypher/shared';
import { z } from 'zod';

import {
baseConditionSchema,
UserAddressSchema,
} from './common';
import { baseConditionSchema, UserAddressSchema } from './common';
import { contextParamSchema } from './context';
import { returnValueTestSchema } from './return-value-test';

Expand Down
2 changes: 2 additions & 0 deletions packages/taco/src/conditions/schemas/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { compoundConditionSchema } from '../compound-condition';
import { contractConditionSchema } from './contract';
import { ifThenElseConditionSchema } from './if-then-else';
import { jsonApiConditionSchema } from './json-api';
import { jsonRpcConditionSchema } from './json-rpc';
import { rpcConditionSchema } from './rpc';
import { sequentialConditionSchema } from './sequential';
import { timeConditionSchema } from './time';
Expand All @@ -16,6 +17,7 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() =>
contractConditionSchema,
compoundConditionSchema,
jsonApiConditionSchema,
jsonRpcConditionSchema,
sequentialConditionSchema,
ifThenElseConditionSchema,
]),
Expand Down
4 changes: 1 addition & 3 deletions packages/taco/test/conditions/base/condition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ describe('validation', () => {
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
chain: {
_errors: [
"Expected number, received string",
],
_errors: ['Expected number, received string'],
},
});
});
Expand Down
Loading
Loading