From 911238bd0bf9690072cf371184664b37377c3ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Wed, 6 Dec 2023 17:27:48 +0100 Subject: [PATCH] Validate signed api name (#157) * Re-order exports * Validate signed API name triggers --- .../src/validation/schema.test.ts | 18 +++++ .../airnode-feed/src/validation/schema.ts | 67 ++++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/packages/airnode-feed/src/validation/schema.test.ts b/packages/airnode-feed/src/validation/schema.test.ts index b7831cdd..2e488076 100644 --- a/packages/airnode-feed/src/validation/schema.test.ts +++ b/packages/airnode-feed/src/validation/schema.test.ts @@ -93,3 +93,21 @@ test('validates trigger references', async () => { ]) ); }); + +test('trigger must point to a valid Signed API definition', async () => { + // As a note, having unused Signed API definitions is not an error. + const invalidConfig: Config = { + ...config, + signedApis: [{ ...config.signedApis[0]!, name: 'different-name' }], + }; + + await expect(async () => configSchema.parseAsync(invalidConfig)).rejects.toStrictEqual( + new ZodError([ + { + code: 'custom', + message: 'Unable to find signed API with name: localhost', + path: ['triggers', 'signedApiUpdates', 0, 'signedApiName'], + }, + ]) + ); +}); diff --git a/packages/airnode-feed/src/validation/schema.ts b/packages/airnode-feed/src/validation/schema.ts index 8fbac0ad..ad88164f 100644 --- a/packages/airnode-feed/src/validation/schema.ts +++ b/packages/airnode-feed/src/validation/schema.ts @@ -15,17 +15,27 @@ import { z, type SuperRefinement } from 'zod'; import packageJson from '../../package.json'; +export type Config = z.infer; +export type Address = z.infer; +export type BeaconId = z.infer; +export type TemplateId = z.infer; +export type EndpointId = z.infer; + export const parameterSchema = z.strictObject({ name: z.string(), type: z.string(), value: z.string(), }); +export type Parameter = z.infer; + export const templateSchema = z.strictObject({ endpointId: config.evmIdSchema, parameters: z.array(parameterSchema), }); +export type Template = z.infer; + export const templatesSchema = z.record(config.evmIdSchema, templateSchema).superRefine((templates, ctx) => { for (const [templateId, template] of Object.entries(templates)) { // Verify that config.templates. is valid by deriving the hash of the endpointId and parameters @@ -53,11 +63,15 @@ export const templatesSchema = z.record(config.evmIdSchema, templateSchema).supe } }); +export type Templates = z.infer; + export const endpointSchema = z.strictObject({ oisTitle: z.string(), endpointName: z.string(), }); +export type Endpoint = z.infer; + export const endpointsSchema = z.record(endpointSchema).superRefine((endpoints, ctx) => { for (const [endpointId, endpoint] of Object.entries(endpoints)) { // Verify that config.endpoints. is valid @@ -77,6 +91,8 @@ export const endpointsSchema = z.record(endpointSchema).superRefine((endpoints, } }); +export type Endpoints = z.infer; + export const baseBeaconUpdateSchema = z.strictObject({ deviationThreshold: z.number(), heartbeatInterval: z.number().int(), @@ -88,6 +104,8 @@ export const beaconUpdateSchema = z }) .merge(baseBeaconUpdateSchema); +export type BeaconUpdate = z.infer; + export const signedApiUpdateSchema = z.strictObject({ signedApiName: z.string(), templateIds: z.array(config.evmIdSchema), @@ -95,10 +113,14 @@ export const signedApiUpdateSchema = z.strictObject({ updateDelay: z.number(), }); +export type SignedApiUpdate = z.infer; + export const triggersSchema = z.strictObject({ signedApiUpdates: z.array(signedApiUpdateSchema).nonempty(), }); +export type Triggers = z.infer; + const validateTemplatesReferences: SuperRefinement<{ templates: Templates; endpoints: Endpoints }> = (config, ctx) => { for (const [templateId, template] of Object.entries(config.templates)) { const endpoint = config.endpoints[template.endpointId]; @@ -203,6 +225,8 @@ export const signedApiSchema = z.strictObject({ url: z.string().url(), }); +export type SignedApi = z.infer; + export const signedApisSchema = z .array(signedApiSchema) .nonempty() @@ -219,10 +243,28 @@ export const signedApisSchema = z } }); +const validateSignedApiReferences: SuperRefinement<{ + triggers: Triggers; + signedApis: SignedApi[]; +}> = (config, ctx) => { + for (const [index, trigger] of config.triggers.signedApiUpdates.entries()) { + const api = config.signedApis.find((api) => api.name === trigger.signedApiName); + if (!api) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Unable to find signed API with name: ${trigger.signedApiName}`, + path: ['triggers', 'signedApiUpdates', index, 'signedApiName'], + }); + } + } +}; + export const oisesSchema = z.array(oisSchema); export const apisCredentialsSchema = z.array(config.apiCredentialsSchema); +export type ApisCredentials = z.infer; + export const nodeSettingsSchema = z.strictObject({ nodeVersion: z.string().refine((version) => version === packageJson.version, 'Invalid node version'), airnodeWalletMnemonic: z.string().refine((mnemonic) => ethers.utils.isValidMnemonic(mnemonic), 'Invalid mnemonic'), @@ -248,41 +290,32 @@ export const configSchema = z }) .superRefine(validateTemplatesReferences) .superRefine(validateOisReferences) - .superRefine(validateTriggerReferences); + .superRefine(validateTriggerReferences) + .superRefine(validateSignedApiReferences); export const encodedValueSchema = z.string().regex(/^0x[\dA-Fa-f]{64}$/); + export const signatureSchema = z.string().regex(/^0x[\dA-Fa-f]{130}$/); + export const signedDataSchema = z.strictObject({ timestamp: z.string(), encodedValue: encodedValueSchema, signature: signatureSchema, }); +export type SignedData = z.infer; + export const signedApiPayloadSchema = signedDataSchema.extend({ beaconId: config.evmIdSchema, airnode: config.evmAddressSchema, templateId: config.evmIdSchema, }); +export type SignedApiPayload = z.infer; + export const signedApiBatchPayloadSchema = z.array(signedApiPayloadSchema); -export type SignedApiPayload = z.infer; export type SignedApiBatchPayload = z.infer; -export type Config = z.infer; -export type Template = z.infer; -export type Templates = z.infer; -export type BeaconUpdate = z.infer; -export type SignedApiUpdate = z.infer; -export type Triggers = z.infer; -export type Address = z.infer; -export type BeaconId = z.infer; -export type TemplateId = z.infer; -export type EndpointId = z.infer; -export type SignedData = z.infer; -export type Endpoint = z.infer; -export type Endpoints = z.infer; -export type ApisCredentials = z.infer; -export type Parameter = z.infer; export const secretsSchema = z.record(z.string());