Skip to content

Commit fbf871b

Browse files
authored
feat: upgrade to zod v4 and improve r2 file validation (#21)
- Upgrade zod dependency from v3 to v4 - Update zod imports and type usage in r2.ts - Bump drizzle-orm - Added sanitization on filename and file metadata sent in headers
1 parent 448dd81 commit fbf871b

File tree

5 files changed

+52
-17
lines changed

5 files changed

+52
-17
lines changed

examples/hono/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@cloudflare/workers-types": "^4.20250606.0",
1919
"better-auth": "^1.2.8",
2020
"better-auth-cloudflare": "file:../../",
21-
"drizzle-orm": "^0.43.1",
21+
"drizzle-orm": "^0.44.5",
2222
"hono": "^4.7.11"
2323
},
2424
"devDependencies": {

examples/opennextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"better-auth-cloudflare": "file:../../",
3232
"class-variance-authority": "^0.7.1",
3333
"clsx": "^2.1.1",
34-
"drizzle-orm": "^0.43.1",
34+
"drizzle-orm": "^0.44.5",
3535
"lucide-react": "^0.509.0",
3636
"next": "15.3.1",
3737
"react": "^19.0.0",

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"format": "prettier --write ."
3434
},
3535
"dependencies": {
36-
"drizzle-orm": "^0.43.1",
37-
"zod": "^3.24.2"
36+
"drizzle-orm": "^0.44.5",
37+
"zod": "^4.1.5"
3838
},
3939
"peerDependencies": {
4040
"better-auth": "^1.1.21"

src/client.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import type { BetterAuthClientPlugin } from "better-auth/client";
22
import type { cloudflare } from ".";
33

4+
/**
5+
* Sanitizes a string to ensure it only contains ASCII characters
6+
* This prevents ByteString conversion errors in Cloudflare Workers
7+
* if unicode or other non-ASCII characters are present
8+
*/
9+
function sanitizeHeaderValue(value: string): string {
10+
return value
11+
.split("")
12+
.map(char => {
13+
const code = char.charCodeAt(0);
14+
// Only allow ASCII characters (0-127)
15+
return code <= 127 ? char : "?";
16+
})
17+
.join("");
18+
}
19+
420
/**
521
* Cloudflare client plugin for Better Auth
622
*/
@@ -15,11 +31,11 @@ export const cloudflareClient = () => {
1531
*/
1632
uploadFile: async (file: File, metadata?: Record<string, any>) => {
1733
const headers: Record<string, string> = {
18-
"x-filename": file.name,
34+
"x-filename": sanitizeHeaderValue(file.name),
1935
};
2036

2137
if (metadata && Object.keys(metadata).length > 0) {
22-
headers["x-file-metadata"] = JSON.stringify(metadata);
38+
headers["x-file-metadata"] = sanitizeHeaderValue(JSON.stringify(metadata));
2339
}
2440

2541
return $fetch("/files/upload-raw", {

src/r2.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AuthContext } from "better-auth";
22
import { createAuthEndpoint, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
33
import type { FieldAttribute } from "better-auth/db";
4-
import { z, type ZodRawShape, type ZodTypeAny } from "zod";
4+
import { z, type ZodType } from "zod";
55
import type { FileMetadata, R2Config } from "./types";
66

77
export const R2_ERROR_CODES = {
@@ -86,10 +86,10 @@ function validateFileMetadata(record: any): record is FileMetadata {
8686
* Converts Better Auth FieldAttribute to Zod schema (same pattern as feedback plugin)
8787
*/
8888
function convertFieldAttributesToZodSchema(additionalFields: Record<string, FieldAttribute>) {
89-
const zodSchema: ZodRawShape = {};
89+
const zodSchema: Record<string, ZodType> = {};
9090

9191
for (const [key, value] of Object.entries(additionalFields)) {
92-
let fieldSchema: ZodTypeAny;
92+
let fieldSchema: ZodType;
9393

9494
if (value.type === "string") {
9595
fieldSchema = z.string();
@@ -120,7 +120,7 @@ function convertFieldAttributesToZodSchema(additionalFields: Record<string, Fiel
120120
// Zod schemas for validation
121121
export const createFileMetadataSchema = (additionalFields?: Record<string, FieldAttribute>) => {
122122
if (!additionalFields || Object.keys(additionalFields).length === 0) {
123-
return z.record(z.any()).optional();
123+
return z.record(z.string(), z.any()).optional();
124124
}
125125
return convertFieldAttributesToZodSchema(additionalFields).optional();
126126
};
@@ -141,7 +141,7 @@ export const listFilesSchema = z
141141
* Creates upload schema dynamically based on additionalFields configuration
142142
*/
143143
export const createUploadFileSchema = (additionalFields?: Record<string, FieldAttribute>) => {
144-
const baseShape: ZodRawShape = {
144+
const baseShape: Record<string, ZodType> = {
145145
file: z.instanceof(File),
146146
};
147147

@@ -151,7 +151,7 @@ export const createUploadFileSchema = (additionalFields?: Record<string, FieldAt
151151

152152
// Add additionalFields to the schema
153153
for (const [key, value] of Object.entries(additionalFields)) {
154-
let fieldSchema: ZodTypeAny;
154+
let fieldSchema: ZodType;
155155

156156
if (value.type === "string") {
157157
fieldSchema = z.string();
@@ -257,7 +257,7 @@ export const createFileValidator = (config: R2Config) => {
257257

258258
if (!result.success) {
259259
// Extract detailed error information from Zod
260-
const errorMessages = result.error.errors
260+
const errorMessages = result.error.issues
261261
.map(err => {
262262
const path = err.path.length > 0 ? `${err.path.join(".")}: ` : "";
263263
return `${path}${err.message}`;
@@ -268,7 +268,7 @@ export const createFileValidator = (config: R2Config) => {
268268
ctx?.logger?.error(`[R2]: Metadata validation failed:`, {
269269
error: detailedError,
270270
metadata,
271-
zodErrors: result.error.errors,
271+
zodErrors: result.error.issues,
272272
});
273273

274274
return error(detailedError, "INVALID_METADATA");
@@ -561,13 +561,32 @@ export const createR2Endpoints = (
561561
}
562562

563563
// Get filename and metadata from headers
564-
const filename = ctx.request?.headers?.get("x-filename");
565-
const metadataHeader = ctx.request?.headers?.get("x-file-metadata");
564+
const rawFilename = ctx.request?.headers?.get("x-filename");
565+
const rawMetadataHeader = ctx.request?.headers?.get("x-file-metadata");
566566

567-
if (!filename) {
567+
if (!rawFilename) {
568568
throw new Error("x-filename header is required");
569569
}
570570

571+
// Sanitize header values to ensure it only contains ASCII characters
572+
const filename = rawFilename
573+
.split("")
574+
.map(char => {
575+
const code = char.charCodeAt(0);
576+
return code <= 127 ? char : "?";
577+
})
578+
.join("");
579+
580+
const metadataHeader = rawMetadataHeader
581+
? rawMetadataHeader
582+
.split("")
583+
.map(char => {
584+
const code = char.charCodeAt(0);
585+
return code <= 127 ? char : "?";
586+
})
587+
.join("")
588+
: undefined;
589+
571590
// Parse metadata from headers
572591
let additionalFields: Record<string, any> = {};
573592
if (metadataHeader) {

0 commit comments

Comments
 (0)