From 54c9f84cb320ecd82783da34007debd45ca141ac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 13:53:56 +0000 Subject: [PATCH] fix(storage): enforce S3 max 100 versions via zod, remove native HTML validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add S3_MAX_NONCURRENT_VERSIONS constant (100) and enforce it in superRefineBucketProtection independently of plan limits - Remove HTML min attributes from both inputs so only Supabase inline errors (FormMessage via FormItemLayout) appear — no browser tooltips - Plan-specific bounds still enforced as before; the S3 cap is checked first with a distinct error message Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p --- .../BucketDataProtectionFields.schema.ts | 33 +++++++++++++------ .../Storage/BucketDataProtectionFields.tsx | 2 -- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.schema.ts b/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.schema.ts index 8279abc1c0d..4c615247fa1 100644 --- a/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.schema.ts +++ b/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.schema.ts @@ -10,6 +10,12 @@ import type { ExpirationMode, VersioningPlanLimits } from './StorageProtection.c */ const versioningNumberField = z.union([z.literal(''), z.coerce.number().int()]) +/** + * S3 lifecycle policies support at most 100 noncurrent versions per rule. + * This is an absolute ceiling independent of the org's billing plan. + */ +export const S3_MAX_NONCURRENT_VERSIONS = 100 + export const bucketProtectionFormFields = { enable_versioning: z.boolean().default(false), version_expiry_days: versioningNumberField.default(''), @@ -51,15 +57,22 @@ export const superRefineBucketProtection = ( }) } - if ( - data.max_noncurrent_versions !== '' && - (data.max_noncurrent_versions < planLimits.minVersions || - data.max_noncurrent_versions > planLimits.maxVersions) - ) { - ctx.addIssue({ - path: ['max_noncurrent_versions'], - code: z.ZodIssueCode.custom, - message: `Must be between ${planLimits.minVersions} and ${planLimits.maxVersions} versions on your plan`, - }) + if (data.max_noncurrent_versions !== '') { + if (data.max_noncurrent_versions > S3_MAX_NONCURRENT_VERSIONS) { + ctx.addIssue({ + path: ['max_noncurrent_versions'], + code: z.ZodIssueCode.custom, + message: `Cannot exceed ${S3_MAX_NONCURRENT_VERSIONS} versions (S3 lifecycle policy limit)`, + }) + } else if ( + data.max_noncurrent_versions < planLimits.minVersions || + data.max_noncurrent_versions > planLimits.maxVersions + ) { + ctx.addIssue({ + path: ['max_noncurrent_versions'], + code: z.ZodIssueCode.custom, + message: `Must be between ${planLimits.minVersions} and ${planLimits.maxVersions} versions on your plan`, + }) + } } } diff --git a/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx b/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx index 3c14a1ae66d..6133ea3013d 100644 --- a/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx +++ b/apps/studio/components/interfaces/Storage/BucketDataProtectionFields.tsx @@ -247,7 +247,6 @@ const ExpirationPolicySection = ({ { @@ -278,7 +277,6 @@ const ExpirationPolicySection = ({ {