diff --git a/api/package.json b/api/package.json index 8106bf05..173adde1 100644 --- a/api/package.json +++ b/api/package.json @@ -14,7 +14,7 @@ "license": "ISC", "description": "", "dependencies": { - "@hono/zod-validator": "^0.7.3", + "@hono/zod-validator": "^0.7.5", "@interledger/open-payments": "^7.1.3", "@noble/ed25519": "^3.0.0", "@paralleldrive/cuid2": "^2.2.2", @@ -24,7 +24,7 @@ "hono": "^4.9.8", "http-message-signatures": "^1.0.4", "httpbis-digest-headers": "^1.0.0", - "zod": "^3.25.76" + "zod": "^4.1.13" }, "types": "./src/types.ts", "devDependencies": { diff --git a/api/src/app.ts b/api/src/app.ts index 629e450f..d132f1ea 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -46,7 +46,7 @@ app.onError((error, c) => { message: 'Validation failed', code: 'VALIDATION_ERROR', details: { - issues: error.errors.map((err) => ({ + issues: error.issues.map((err) => ({ path: err.path.join('.'), message: err.message, code: err.code diff --git a/api/src/routes/get-config.ts b/api/src/routes/get-config.ts index f7562db4..590a65b3 100644 --- a/api/src/routes/get-config.ts +++ b/api/src/routes/get-config.ts @@ -1,6 +1,6 @@ import { zValidator } from '@hono/zod-validator' import { HTTPException } from 'hono/http-exception' -import { z } from 'zod' +import z from 'zod' import { ConfigStorageService } from '@shared/config-storage-service' import { AWS_PREFIX } from '@shared/defines' import { PRESET_IDS, TOOLS } from '@shared/types' @@ -26,7 +26,7 @@ app.get( zValidator( 'query', z.object({ - wa: z.string().url(), + wa: z.url(), preset: z.enum(PRESET_IDS) }) ), diff --git a/api/src/routes/probabilistic-revshare.ts b/api/src/routes/probabilistic-revshare.ts index 246d388d..1a482dfd 100644 --- a/api/src/routes/probabilistic-revshare.ts +++ b/api/src/routes/probabilistic-revshare.ts @@ -1,6 +1,6 @@ import { HTTPException } from 'hono/http-exception' import { zValidator } from '@hono/zod-validator' -import { z } from 'zod' +import z from 'zod' import type { ContentfulStatusCode } from 'hono/utils/http-status' import type { WalletAddress } from '@interledger/open-payments' import { decode, pickWeightedRandom } from '@shared/probabilistic-revenue-share' @@ -14,7 +14,7 @@ app.get( zValidator( 'param', z.object({ - payload: z.string().base64url().max(50_000).min(20) + payload: z.base64url().max(50_000).min(20) }) ), async ({ req, json }) => { diff --git a/api/src/schemas/payment.ts b/api/src/schemas/payment.ts index 44882c13..d70639b2 100644 --- a/api/src/schemas/payment.ts +++ b/api/src/schemas/payment.ts @@ -1,8 +1,8 @@ -import * as z from 'zod/v4' +import z from 'zod' export const PaymentQuoteSchema = z.object({ - senderWalletAddress: z.string().url('Invalid sender wallet address'), - receiverWalletAddress: z.string().url('Invalid receiver wallet address'), + senderWalletAddress: z.url('Invalid sender wallet address'), + receiverWalletAddress: z.url('Invalid receiver wallet address'), amount: z.number().positive('Amount must be positive'), note: z.string().optional() }) @@ -35,11 +35,11 @@ export const PaymentFinalizeSchema = z.object({ walletAddress: WalletAddressSchema, pendingGrant: z.object({ interact: z.object({ - redirect: z.string().url(), + redirect: z.url(), finish: z.string() }), continue: z.object({ - uri: z.string().url(), + uri: z.url(), access_token: z.object({ value: z.string() }), @@ -48,18 +48,18 @@ export const PaymentFinalizeSchema = z.object({ }), quote: z.object({ id: z.string(), - walletAddress: z.string().url('Invalid wallet address'), - receiver: z.string().url(), + walletAddress: z.url('Invalid wallet address'), + receiver: z.url(), receiveAmount: AmountSchema, debitAmount: AmountSchema, method: z.literal('ilp'), - createdAt: z.string().datetime(), - expiresAt: z.string().datetime().optional() + createdAt: z.iso.datetime(), + expiresAt: z.iso.datetime().optional() }), incomingPaymentGrant: z.object({ access_token: z.object({ value: z.string(), - manage: z.string().url(), + manage: z.url(), expires_in: z.number().int(), access: z.array( z.object({ @@ -73,7 +73,7 @@ export const PaymentFinalizeSchema = z.object({ access_token: z.object({ value: z.string() }), - uri: z.string().url(), + uri: z.url(), wait: z.number().int().optional() }) }), diff --git a/api/src/types.ts b/api/src/types.ts index 216db775..790b26e9 100644 --- a/api/src/types.ts +++ b/api/src/types.ts @@ -4,7 +4,7 @@ import type { PaymentQuoteSchema, WalletAddressParamSchema } from './schemas/payment.js' -import type { z } from 'zod/v4' +import type z from 'zod' export type PaymentStatusSuccess = { paymentId: string diff --git a/frontend/app/lib/types.ts b/frontend/app/lib/types.ts index e6773d0f..5c0b4a87 100644 --- a/frontend/app/lib/types.ts +++ b/frontend/app/lib/types.ts @@ -1,4 +1,4 @@ -import type { z } from 'zod' +import type z from 'zod' import type { createBannerSchema, createButtonSchema, @@ -37,7 +37,7 @@ export type SanitizedFields = Pick< > export type JSONError = { - errors: z.typeToFlattenedError> + errors: z.ZodFlattenedError> } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/frontend/app/utils/validate.client.ts b/frontend/app/utils/validate.client.ts index 76b097c3..bae34b4b 100644 --- a/frontend/app/utils/validate.client.ts +++ b/frontend/app/utils/validate.client.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import z from 'zod' import { bannerFieldsSchema, buttonFieldsSchema, @@ -6,16 +6,15 @@ import { } from './validate.shared' import type { ElementConfigType } from '@shared/types' -export const elementConfigStorageSchema = z - .object({ - versionName: z.string(), - tag: z.string().optional(), - // can be undefined initially - walletAddress: z.string().optional() - }) - .merge(buttonFieldsSchema) - .merge(bannerFieldsSchema) - .merge(widgetFieldsSchema) +export const elementConfigStorageSchema = z.object({ + versionName: z.string(), + tag: z.string().optional(), + // can be undefined initially + walletAddress: z.string().optional(), + ...buttonFieldsSchema.shape, + ...bannerFieldsSchema.shape, + ...widgetFieldsSchema.shape +}) /** * Validates configurations from localStorage. diff --git a/frontend/app/utils/validate.server.ts b/frontend/app/utils/validate.server.ts index afda1d8c..a87f9909 100644 --- a/frontend/app/utils/validate.server.ts +++ b/frontend/app/utils/validate.server.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import z from 'zod' import { checkHrefFormat, getWalletAddress, @@ -29,7 +29,7 @@ export const walletSchema = z.object({ await getWalletAddress(updatedUrl) } catch (e) { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: 'custom', message: e instanceof WalletAddressFormatError ? e.message @@ -48,29 +48,26 @@ export const fullConfigSchema = z.object({ fullconfig: z.string().min(1, { message: 'Unknown error' }) }) -export const createButtonSchema = z - .object({ - elementType: z.literal('button') - }) - .merge(buttonFieldsSchema) - .merge(walletSchema) - .merge(versionSchema) +export const createButtonSchema = z.object({ + elementType: z.literal('button'), + ...buttonFieldsSchema.shape, + ...walletSchema.shape, + ...versionSchema.shape +}) -export const createBannerSchema = z - .object({ - elementType: z.literal('banner') - }) - .merge(bannerFieldsSchema) - .merge(walletSchema) - .merge(versionSchema) +export const createBannerSchema = z.object({ + elementType: z.literal('banner'), + ...bannerFieldsSchema.shape, + ...walletSchema.shape, + ...versionSchema.shape +}) -export const createWidgetSchema = z - .object({ - elementType: z.literal('widget') - }) - .merge(widgetFieldsSchema) - .merge(walletSchema) - .merge(versionSchema) +export const createWidgetSchema = z.object({ + elementType: z.literal('widget'), + ...widgetFieldsSchema.shape, + ...walletSchema.shape, + ...versionSchema.shape +}) export const getElementSchema = (type: string) => { switch (type) { @@ -95,7 +92,10 @@ export const validateForm = async ( if (intent === 'import' || intent === 'delete') { result = await walletSchema.safeParseAsync(formData) } else if (intent === 'newversion') { - const newVersionSchema = versionSchema.merge(walletSchema) + const newVersionSchema = z.object({ + ...versionSchema.shape, + ...walletSchema.shape + }) result = await newVersionSchema.safeParseAsync(formData) } else { let currentSchema @@ -111,9 +111,13 @@ export const validateForm = async ( default: currentSchema = createBannerSchema } - result = await currentSchema - .merge(fullConfigSchema) - .safeParseAsync(Object.assign(formData, { ...{ elementType } })) + const mergedSchema = z.object({ + ...currentSchema.shape, + ...fullConfigSchema.shape + }) + result = await mergedSchema.safeParseAsync( + Object.assign(formData, { ...{ elementType } }) + ) } /* eslint-disable @typescript-eslint/no-explicit-any */ const payload = result.data as unknown as any diff --git a/frontend/app/utils/validate.shared.ts b/frontend/app/utils/validate.shared.ts index c5dd864f..114ce6ee 100644 --- a/frontend/app/utils/validate.shared.ts +++ b/frontend/app/utils/validate.shared.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import z from 'zod' import { CORNER_OPTION, BANNER_POSITION, @@ -23,7 +23,7 @@ const widgetFontSizeError = { export const buttonFieldsSchema = z.object({ buttonFontName: z.string().min(1, { message: 'Choose a font' }), buttonText: z.string().min(1, { message: 'Button label cannot be empty' }), - buttonBorder: z.nativeEnum(CORNER_OPTION), + buttonBorder: z.enum(CORNER_OPTION), buttonTextColor: z.string().min(6), buttonBackgroundColor: z.string().min(6), buttonDescriptionText: z.string().optional() @@ -48,10 +48,10 @@ export const bannerFieldsSchema = z.object({ bannerDescriptionVisible: z.coerce.boolean().optional(), bannerTextColor: z.string().min(6), bannerBackgroundColor: z.string().min(6), - bannerSlideAnimation: z.nativeEnum(SLIDE_ANIMATION), + bannerSlideAnimation: z.enum(SLIDE_ANIMATION), bannerThumbnail: z.string().optional(), - bannerPosition: z.nativeEnum(BANNER_POSITION), - bannerBorder: z.nativeEnum(CORNER_OPTION) + bannerPosition: z.enum(BANNER_POSITION), + bannerBorder: z.enum(CORNER_OPTION) }) export const widgetFieldsSchema = z.object({ @@ -71,14 +71,14 @@ export const widgetFieldsSchema = z.object({ }) .optional(), widgetDescriptionVisible: z.coerce.boolean().optional(), - widgetPosition: z.nativeEnum(WIDGET_POSITION), + widgetPosition: z.enum(WIDGET_POSITION), widgetDonateAmount: z.coerce .number() .min(0, { message: 'Donate amount must be positive' }), widgetButtonText: z .string() .min(1, { message: 'Button text cannot be empty' }), - widgetButtonBorder: z.nativeEnum(CORNER_OPTION), + widgetButtonBorder: z.enum(CORNER_OPTION), widgetButtonBackgroundColor: z.string().min(1), widgetButtonTextColor: z.string().min(1), widgetTextColor: z.string().min(1), diff --git a/frontend/package.json b/frontend/package.json index 531ed03c..cfd14d28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,7 +40,7 @@ "sanitize-html": "^2.17.0", "tailwindcss": "^3.4.18", "valtio": "^2.1.8", - "zod": "^3.25.76" + "zod": "^4.1.13" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.14.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1f46fa3..793260aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,8 +40,8 @@ importers: api: dependencies: '@hono/zod-validator': - specifier: ^0.7.3 - version: 0.7.3(hono@4.9.8)(zod@3.25.76) + specifier: ^0.7.5 + version: 0.7.5(hono@4.9.8)(zod@4.1.13) '@interledger/open-payments': specifier: ^7.1.3 version: 7.1.3 @@ -70,8 +70,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.1.13 + version: 4.1.13 devDependencies: '@cloudflare/workers-types': specifier: ^4.20251011.0 @@ -237,8 +237,8 @@ importers: specifier: ^2.1.8 version: 2.1.8(@types/react@19.2.6)(react@19.2.0) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.1.13 + version: 4.1.13 devDependencies: '@cloudflare/vite-plugin': specifier: ^1.14.1 @@ -819,8 +819,8 @@ packages: '@fontsource/titillium-web@5.2.8': resolution: {integrity: sha512-DNhXS1ib/de+LwbJiEmKgB9WAGK6MZfG9qFbJbz1JoY1f4KCt9HDK7lSUtqJPxdK47D2o0vqdWcg5SD+yphWOw==} - '@hono/zod-validator@0.7.3': - resolution: {integrity: sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ==} + '@hono/zod-validator@0.7.5': + resolution: {integrity: sha512-n4l4hutkfYU07PzRUHBOVzUEn38VSfrh+UVE5d0w4lyfWDOEhzxIupqo5iakRiJL44c3vTuFJBvcmUl8b9agIA==} peerDependencies: hono: '>=3.9.0' zod: ^3.25.0 || ^4.0.0 @@ -4075,8 +4075,8 @@ packages: zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} snapshots: @@ -4543,10 +4543,10 @@ snapshots: '@fontsource/titillium-web@5.2.8': {} - '@hono/zod-validator@0.7.3(hono@4.9.8)(zod@3.25.76)': + '@hono/zod-validator@0.7.5(hono@4.9.8)(zod@4.1.13)': dependencies: hono: 4.9.8 - zod: 3.25.76 + zod: 4.1.13 '@humanfs/core@0.19.1': {} @@ -8086,4 +8086,4 @@ snapshots: zod@3.22.3: {} - zod@3.25.76: {} + zod@4.1.13: {}