next-form-request 0.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  // src/core/types.ts
2
4
  function isAppRouterRequest(request) {
3
5
  return "headers" in request && request.headers instanceof Headers;
@@ -62,6 +64,225 @@ var AuthorizationError = class _AuthorizationError extends Error {
62
64
  }
63
65
  };
64
66
 
67
+ // src/utils/rateLimit.ts
68
+ var MemoryRateLimitStore = class {
69
+ constructor() {
70
+ this.store = /* @__PURE__ */ new Map();
71
+ }
72
+ async get(key) {
73
+ const state = this.store.get(key);
74
+ if (!state) return null;
75
+ if (Date.now() > state.resetAt) {
76
+ this.store.delete(key);
77
+ return null;
78
+ }
79
+ return state;
80
+ }
81
+ async set(key, state, _ttlMs) {
82
+ this.store.set(key, state);
83
+ if (this.store.size > 1e3) {
84
+ this.cleanup();
85
+ }
86
+ }
87
+ async increment(key, windowMs) {
88
+ const now = Date.now();
89
+ const existing = this.store.get(key);
90
+ if (!existing || now > existing.resetAt) {
91
+ const state = {
92
+ count: 1,
93
+ resetAt: now + windowMs
94
+ };
95
+ this.store.set(key, state);
96
+ return state;
97
+ }
98
+ existing.count++;
99
+ return existing;
100
+ }
101
+ cleanup() {
102
+ const now = Date.now();
103
+ for (const [key, state] of this.store.entries()) {
104
+ if (now > state.resetAt) {
105
+ this.store.delete(key);
106
+ }
107
+ }
108
+ }
109
+ };
110
+ var defaultStore = new MemoryRateLimitStore();
111
+ function setDefaultRateLimitStore(store) {
112
+ defaultStore = store;
113
+ }
114
+ function getClientIp(request) {
115
+ if ("headers" in request && request.headers instanceof Headers) {
116
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown";
117
+ }
118
+ const req = request;
119
+ const forwarded = req.headers["x-forwarded-for"];
120
+ if (typeof forwarded === "string") {
121
+ return forwarded.split(",")[0]?.trim() || "unknown";
122
+ }
123
+ if (Array.isArray(forwarded)) {
124
+ return forwarded[0]?.split(",")[0]?.trim() || "unknown";
125
+ }
126
+ const realIp = req.headers["x-real-ip"];
127
+ if (typeof realIp === "string") {
128
+ return realIp;
129
+ }
130
+ const socket = req.socket;
131
+ return socket?.remoteAddress || "unknown";
132
+ }
133
+ async function checkRateLimit(request, config) {
134
+ const { maxAttempts, windowMs, key, store = defaultStore, skip } = config;
135
+ if (skip && await skip(request)) {
136
+ return {
137
+ allowed: true,
138
+ remaining: maxAttempts,
139
+ limit: maxAttempts,
140
+ resetAt: Date.now() + windowMs,
141
+ retryAfter: 0
142
+ };
143
+ }
144
+ const rateLimitKey = key ? await key(request) : getClientIp(request);
145
+ const fullKey = `ratelimit:${rateLimitKey}`;
146
+ const state = await store.increment(fullKey, windowMs);
147
+ const allowed = state.count <= maxAttempts;
148
+ const remaining = Math.max(0, maxAttempts - state.count);
149
+ const retryAfter = allowed ? 0 : Math.ceil((state.resetAt - Date.now()) / 1e3);
150
+ return {
151
+ allowed,
152
+ remaining,
153
+ limit: maxAttempts,
154
+ resetAt: state.resetAt,
155
+ retryAfter
156
+ };
157
+ }
158
+ var RateLimitError = class extends Error {
159
+ constructor(result, message) {
160
+ super(message || `Rate limit exceeded. Retry after ${result.retryAfter} seconds.`);
161
+ this.name = "RateLimitError";
162
+ this.retryAfter = result.retryAfter;
163
+ this.remaining = result.remaining;
164
+ this.limit = result.limit;
165
+ this.resetAt = result.resetAt;
166
+ }
167
+ /**
168
+ * Get headers to send with the rate limit response
169
+ */
170
+ getHeaders() {
171
+ return {
172
+ "X-RateLimit-Limit": String(this.limit),
173
+ "X-RateLimit-Remaining": String(this.remaining),
174
+ "X-RateLimit-Reset": String(this.resetAt),
175
+ "Retry-After": String(this.retryAfter)
176
+ };
177
+ }
178
+ };
179
+ function rateLimit(config) {
180
+ return {
181
+ key: getClientIp,
182
+ ...config
183
+ };
184
+ }
185
+
186
+ // src/utils/coerce.ts
187
+ var defaultOptions = {
188
+ booleans: true,
189
+ numbers: true,
190
+ dates: true,
191
+ nulls: true,
192
+ emptyStrings: false,
193
+ json: false
194
+ };
195
+ function isIsoDateString(value) {
196
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:?\d{2})?)?$/;
197
+ return isoDateRegex.test(value);
198
+ }
199
+ function isNumericString(value) {
200
+ if (value === "" || value === null) return false;
201
+ if (/^0\d/.test(value)) return false;
202
+ if (value.length > 15) return false;
203
+ return !isNaN(Number(value)) && isFinite(Number(value));
204
+ }
205
+ function coerceValue(value, options) {
206
+ if (typeof value !== "string") {
207
+ return value;
208
+ }
209
+ if (options.emptyStrings && value === "") {
210
+ return void 0;
211
+ }
212
+ if (options.nulls && value === "null") {
213
+ return null;
214
+ }
215
+ if (options.booleans) {
216
+ const lower = value.toLowerCase();
217
+ if (lower === "true") return true;
218
+ if (lower === "false") return false;
219
+ }
220
+ if (options.dates && isIsoDateString(value)) {
221
+ const date = new Date(value);
222
+ if (!isNaN(date.getTime())) {
223
+ return date;
224
+ }
225
+ }
226
+ if (options.numbers && isNumericString(value)) {
227
+ return Number(value);
228
+ }
229
+ if (options.json) {
230
+ const trimmed = value.trim();
231
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
232
+ try {
233
+ return JSON.parse(value);
234
+ } catch {
235
+ }
236
+ }
237
+ }
238
+ return value;
239
+ }
240
+ function coerceObject(data, options, path = "") {
241
+ const result = {};
242
+ for (const [key, value] of Object.entries(data)) {
243
+ const fieldPath = path ? `${path}.${key}` : key;
244
+ if (options.fields && options.fields[fieldPath]) {
245
+ result[key] = options.fields[fieldPath](value);
246
+ continue;
247
+ }
248
+ if (Array.isArray(value)) {
249
+ result[key] = value.map((item, index) => {
250
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
251
+ return coerceObject(item, options, `${fieldPath}.${index}`);
252
+ }
253
+ return coerceValue(item, options);
254
+ });
255
+ } else if (value !== null && typeof value === "object") {
256
+ result[key] = coerceObject(value, options, fieldPath);
257
+ } else {
258
+ result[key] = coerceValue(value, options);
259
+ }
260
+ }
261
+ return result;
262
+ }
263
+ function coerceFormData(data, options = {}) {
264
+ const mergedOptions = { ...defaultOptions, ...options };
265
+ return coerceObject(data, mergedOptions);
266
+ }
267
+ function zodCoerce(options = {}) {
268
+ return (data) => {
269
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
270
+ return coerceFormData(data, options);
271
+ }
272
+ return data;
273
+ };
274
+ }
275
+ var coercionPresets = {
276
+ /** Coerce all supported types */
277
+ all: { booleans: true, numbers: true, dates: true, nulls: true, emptyStrings: true, json: true },
278
+ /** Only coerce booleans and numbers (safest) */
279
+ safe: { booleans: true, numbers: true, dates: false, nulls: false, emptyStrings: false, json: false },
280
+ /** Coerce booleans, numbers, and dates */
281
+ standard: { booleans: true, numbers: true, dates: true, nulls: false, emptyStrings: false, json: false },
282
+ /** No coercion */
283
+ none: { booleans: false, numbers: false, dates: false, nulls: false, emptyStrings: false, json: false }
284
+ };
285
+
65
286
  // src/core/FormRequest.ts
66
287
  var FormRequest = class {
67
288
  constructor() {
@@ -102,6 +323,42 @@ var FormRequest = class {
102
323
  authorize() {
103
324
  return true;
104
325
  }
326
+ /**
327
+ * Define rate limiting for this request.
328
+ * Override this method to add rate limiting.
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * rateLimit() {
333
+ * return {
334
+ * maxAttempts: 5,
335
+ * windowMs: 60000, // 1 minute
336
+ * key: (req) => this.input('email') || 'anonymous',
337
+ * };
338
+ * }
339
+ * ```
340
+ */
341
+ rateLimit() {
342
+ return null;
343
+ }
344
+ /**
345
+ * Define coercion options for form data.
346
+ * Override this method to enable automatic type coercion.
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * coercion() {
351
+ * return {
352
+ * booleans: true, // "true" → true
353
+ * numbers: true, // "123" → 123
354
+ * dates: true, // "2024-01-01" → Date
355
+ * };
356
+ * }
357
+ * ```
358
+ */
359
+ coercion() {
360
+ return null;
361
+ }
105
362
  /**
106
363
  * Called before validation runs.
107
364
  * Use this to normalize or transform input data.
@@ -243,15 +500,27 @@ var FormRequest = class {
243
500
  * Run validation and return the validated data.
244
501
  * Throws ValidationError if validation fails.
245
502
  * Throws AuthorizationError if authorization fails.
503
+ * Throws RateLimitError if rate limit is exceeded.
246
504
  *
247
505
  * @returns The validated data with full type inference
248
506
  */
249
507
  async validate() {
508
+ const rateLimitConfig = this.rateLimit();
509
+ if (rateLimitConfig) {
510
+ const result2 = await checkRateLimit(this.request, rateLimitConfig);
511
+ if (!result2.allowed) {
512
+ throw new RateLimitError(result2, rateLimitConfig.message);
513
+ }
514
+ }
250
515
  const isAuthorized = await this.authorize();
251
516
  if (!isAuthorized) {
252
517
  await this.onAuthorizationFailed();
253
518
  throw new AuthorizationError();
254
519
  }
520
+ const coercionOptions = this.coercion();
521
+ if (coercionOptions) {
522
+ this.body = coerceFormData(this.body, coercionOptions);
523
+ }
255
524
  await this.beforeValidation();
256
525
  const validator = this.rules();
257
526
  const result = await validator.validate(this.getDataForValidation(), {
@@ -430,6 +699,59 @@ function createPagesRouterWrapper(options) {
430
699
  };
431
700
  }
432
701
 
702
+ // src/middleware/withSchema.ts
703
+ async function parseRequestBody(request) {
704
+ const contentType = request.headers.get("content-type") || "";
705
+ if (contentType.includes("application/json")) {
706
+ try {
707
+ return await request.json();
708
+ } catch {
709
+ return {};
710
+ }
711
+ }
712
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
713
+ try {
714
+ const formData = await request.formData();
715
+ const data = {};
716
+ formData.forEach((value, key) => {
717
+ if (data[key]) {
718
+ if (Array.isArray(data[key])) {
719
+ data[key].push(value);
720
+ } else {
721
+ data[key] = [data[key], value];
722
+ }
723
+ } else {
724
+ data[key] = value;
725
+ }
726
+ });
727
+ return data;
728
+ } catch {
729
+ return {};
730
+ }
731
+ }
732
+ return {};
733
+ }
734
+ function withSchema(adapter, handler) {
735
+ return async (request, context) => {
736
+ const body = await parseRequestBody(request);
737
+ const result = await adapter.validate(body);
738
+ if (!result.success) {
739
+ throw new ValidationError(result.errors || {});
740
+ }
741
+ return handler(result.data, request);
742
+ };
743
+ }
744
+ function withApiSchema(adapter, handler) {
745
+ return async (req, res) => {
746
+ const body = req.body;
747
+ const result = await adapter.validate(body);
748
+ if (!result.success) {
749
+ throw new ValidationError(result.errors || {});
750
+ }
751
+ await handler(result.data, req, res);
752
+ };
753
+ }
754
+
433
755
  // src/adapters/validators/ZodAdapter.ts
434
756
  var ZodAdapter = class {
435
757
  constructor(schema) {
@@ -481,6 +803,641 @@ var ZodAdapter = class {
481
803
  }
482
804
  };
483
805
 
484
- export { AuthorizationError, FormRequest, ValidationError, ZodAdapter, createAppRouterWrapper, createPagesRouterWrapper, isAppRouterRequest, isPagesRouterRequest, withApiRequest, withRequest };
806
+ // src/adapters/validators/YupAdapter.ts
807
+ var YupAdapter = class {
808
+ constructor(schema) {
809
+ this.schema = schema;
810
+ }
811
+ async validate(data, config) {
812
+ try {
813
+ const validated = await this.schema.validate(data, {
814
+ abortEarly: false,
815
+ stripUnknown: true
816
+ });
817
+ return {
818
+ success: true,
819
+ data: validated
820
+ };
821
+ } catch (error) {
822
+ if (this.isYupValidationError(error)) {
823
+ const errors = this.formatYupErrors(error, config);
824
+ return {
825
+ success: false,
826
+ errors
827
+ };
828
+ }
829
+ throw error;
830
+ }
831
+ }
832
+ validateSync(data, config) {
833
+ try {
834
+ const validated = this.schema.validateSync(data, {
835
+ abortEarly: false,
836
+ stripUnknown: true
837
+ });
838
+ return {
839
+ success: true,
840
+ data: validated
841
+ };
842
+ } catch (error) {
843
+ if (this.isYupValidationError(error)) {
844
+ const errors = this.formatYupErrors(error, config);
845
+ return {
846
+ success: false,
847
+ errors
848
+ };
849
+ }
850
+ throw error;
851
+ }
852
+ }
853
+ isYupValidationError(error) {
854
+ return error !== null && typeof error === "object" && "inner" in error && "name" in error && error.name === "ValidationError";
855
+ }
856
+ formatYupErrors(error, config) {
857
+ const errors = {};
858
+ const customMessages = config?.messages ?? {};
859
+ const customAttributes = config?.attributes ?? {};
860
+ const innerErrors = error.inner.length > 0 ? error.inner : [error];
861
+ for (const issue of innerErrors) {
862
+ const path = issue.path ?? "_root";
863
+ const attributeName = customAttributes[path] ?? path;
864
+ const messageKey = `${path}.${issue.type ?? "invalid"}`;
865
+ let message;
866
+ if (customMessages[messageKey]) {
867
+ message = customMessages[messageKey];
868
+ } else if (customMessages[path]) {
869
+ message = customMessages[path];
870
+ } else {
871
+ message = issue.message.replace(
872
+ new RegExp(`\\b${path}\\b`, "gi"),
873
+ attributeName
874
+ );
875
+ }
876
+ if (!errors[path]) {
877
+ errors[path] = [];
878
+ }
879
+ errors[path].push(message);
880
+ }
881
+ return errors;
882
+ }
883
+ };
884
+
885
+ // src/adapters/validators/ValibotAdapter.ts
886
+ var ValibotAdapter = class {
887
+ constructor(schema, safeParse) {
888
+ this.schema = schema;
889
+ this.safeParseFunc = safeParse;
890
+ }
891
+ async validate(data, config) {
892
+ return this.validateSync(data, config);
893
+ }
894
+ validateSync(data, config) {
895
+ const result = this.safeParseFunc(this.schema, data);
896
+ if (result.success) {
897
+ return {
898
+ success: true,
899
+ data: result.output
900
+ };
901
+ }
902
+ const errors = this.formatValibotErrors(result.issues ?? [], config);
903
+ return {
904
+ success: false,
905
+ errors
906
+ };
907
+ }
908
+ formatValibotErrors(issues, config) {
909
+ const errors = {};
910
+ const customMessages = config?.messages ?? {};
911
+ const customAttributes = config?.attributes ?? {};
912
+ for (const issue of issues) {
913
+ const pathParts = issue.path?.map((p) => {
914
+ if (typeof p === "object" && p !== null && "key" in p) {
915
+ return String(p.key);
916
+ }
917
+ return String(p);
918
+ }) ?? [];
919
+ const path = pathParts.join(".") || "_root";
920
+ const attributeName = customAttributes[path] ?? path;
921
+ const messageKey = `${path}.${issue.type ?? "invalid"}`;
922
+ let message;
923
+ if (customMessages[messageKey]) {
924
+ message = customMessages[messageKey];
925
+ } else if (customMessages[path]) {
926
+ message = customMessages[path];
927
+ } else {
928
+ message = (issue.message ?? "Validation failed").replace(
929
+ new RegExp(`\\b${path}\\b`, "gi"),
930
+ attributeName
931
+ );
932
+ }
933
+ if (!errors[path]) {
934
+ errors[path] = [];
935
+ }
936
+ errors[path].push(message);
937
+ }
938
+ return errors;
939
+ }
940
+ };
941
+
942
+ // src/adapters/validators/ArkTypeAdapter.ts
943
+ function isArkTypeErrors(result) {
944
+ return result !== null && typeof result === "object" && "summary" in result && true;
945
+ }
946
+ var ArkTypeAdapter = class {
947
+ constructor(schema) {
948
+ this.schema = schema;
949
+ }
950
+ async validate(data, config) {
951
+ return this.validateSync(data, config);
952
+ }
953
+ validateSync(data, config) {
954
+ const result = this.schema(data);
955
+ if (!isArkTypeErrors(result)) {
956
+ return {
957
+ success: true,
958
+ data: result
959
+ };
960
+ }
961
+ const errors = this.formatArkTypeErrors(result, config);
962
+ return {
963
+ success: false,
964
+ errors
965
+ };
966
+ }
967
+ formatArkTypeErrors(arkErrors, config) {
968
+ const errors = {};
969
+ const customMessages = config?.messages ?? {};
970
+ const customAttributes = config?.attributes ?? {};
971
+ const errorList = [];
972
+ if (arkErrors.errors && Array.isArray(arkErrors.errors)) {
973
+ errorList.push(...arkErrors.errors);
974
+ } else if (arkErrors[Symbol.iterator]) {
975
+ const iteratorFn = arkErrors[Symbol.iterator];
976
+ if (typeof iteratorFn === "function") {
977
+ const iterator = iteratorFn.call(arkErrors);
978
+ let result = iterator.next();
979
+ while (!result.done) {
980
+ errorList.push(result.value);
981
+ result = iterator.next();
982
+ }
983
+ }
984
+ } else {
985
+ errorList.push({
986
+ path: [],
987
+ message: arkErrors.summary
988
+ });
989
+ }
990
+ for (const issue of errorList) {
991
+ const path = issue.path.join(".") || "_root";
992
+ const attributeName = customAttributes[path] ?? path;
993
+ const messageKey = `${path}.${issue.code ?? "invalid"}`;
994
+ let message;
995
+ if (customMessages[messageKey]) {
996
+ message = customMessages[messageKey];
997
+ } else if (customMessages[path]) {
998
+ message = customMessages[path];
999
+ } else {
1000
+ message = issue.message.replace(
1001
+ new RegExp(`\\b${path}\\b`, "gi"),
1002
+ attributeName
1003
+ );
1004
+ }
1005
+ if (!errors[path]) {
1006
+ errors[path] = [];
1007
+ }
1008
+ errors[path].push(message);
1009
+ }
1010
+ return errors;
1011
+ }
1012
+ };
1013
+ function parseSize(size) {
1014
+ if (typeof size === "number") {
1015
+ return size;
1016
+ }
1017
+ const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
1018
+ if (!match) {
1019
+ throw new Error(`Invalid size format: ${size}. Use formats like "5mb", "1024kb", or "1gb"`);
1020
+ }
1021
+ const value = parseFloat(match[1]);
1022
+ const unit = match[2] || "b";
1023
+ const multipliers = {
1024
+ b: 1,
1025
+ kb: 1024,
1026
+ mb: 1024 * 1024,
1027
+ gb: 1024 * 1024 * 1024
1028
+ };
1029
+ return Math.floor(value * multipliers[unit]);
1030
+ }
1031
+ function mimeTypeMatches(mimeType, pattern) {
1032
+ if (pattern === "*" || pattern === "*/*") {
1033
+ return true;
1034
+ }
1035
+ if (pattern.endsWith("/*")) {
1036
+ const category = pattern.slice(0, -2);
1037
+ return mimeType.startsWith(category + "/");
1038
+ }
1039
+ return mimeType === pattern;
1040
+ }
1041
+ function createValidatedFile(file) {
1042
+ const extension = file.name.includes(".") ? file.name.split(".").pop()?.toLowerCase() ?? "" : "";
1043
+ return {
1044
+ name: file.name,
1045
+ size: file.size,
1046
+ type: file.type,
1047
+ file,
1048
+ extension,
1049
+ arrayBuffer: () => file.arrayBuffer(),
1050
+ text: () => file.text(),
1051
+ stream: () => file.stream()
1052
+ };
1053
+ }
1054
+ function formFile(options = {}) {
1055
+ const {
1056
+ maxSize,
1057
+ minSize,
1058
+ types,
1059
+ extensions,
1060
+ required = true
1061
+ } = options;
1062
+ const maxSizeBytes = maxSize ? parseSize(maxSize) : void 0;
1063
+ const minSizeBytes = minSize ? parseSize(minSize) : void 0;
1064
+ const fileSchema = z.instanceof(File, { message: "Expected a file" }).refine(
1065
+ (file) => {
1066
+ if (!required && file.size === 0) return true;
1067
+ return file.size > 0;
1068
+ },
1069
+ { message: "File is required" }
1070
+ ).refine(
1071
+ (file) => {
1072
+ if (!maxSizeBytes) return true;
1073
+ return file.size <= maxSizeBytes;
1074
+ },
1075
+ {
1076
+ message: maxSize ? `File size must not exceed ${typeof maxSize === "string" ? maxSize : `${maxSize} bytes`}` : "File is too large"
1077
+ }
1078
+ ).refine(
1079
+ (file) => {
1080
+ if (!minSizeBytes) return true;
1081
+ return file.size >= minSizeBytes;
1082
+ },
1083
+ {
1084
+ message: minSize ? `File size must be at least ${typeof minSize === "string" ? minSize : `${minSize} bytes`}` : "File is too small"
1085
+ }
1086
+ ).refine(
1087
+ (file) => {
1088
+ if (!types || types.length === 0) return true;
1089
+ return types.some((pattern) => mimeTypeMatches(file.type, pattern));
1090
+ },
1091
+ {
1092
+ message: types ? `File type must be one of: ${types.join(", ")}` : "Invalid file type"
1093
+ }
1094
+ ).refine(
1095
+ (file) => {
1096
+ if (!extensions || extensions.length === 0) return true;
1097
+ const fileExt = file.name.includes(".") ? file.name.split(".").pop()?.toLowerCase() : "";
1098
+ return extensions.some((ext) => ext.toLowerCase() === fileExt);
1099
+ },
1100
+ {
1101
+ message: extensions ? `File extension must be one of: ${extensions.join(", ")}` : "Invalid file extension"
1102
+ }
1103
+ ).transform(createValidatedFile);
1104
+ return fileSchema;
1105
+ }
1106
+ function formFiles(options = {}) {
1107
+ const { minFiles, maxFiles, ...fileOptions } = options;
1108
+ const singleFileSchema = formFile({ ...fileOptions, required: true });
1109
+ let arraySchema = z.array(singleFileSchema);
1110
+ if (minFiles !== void 0) {
1111
+ arraySchema = arraySchema.min(minFiles, {
1112
+ message: `At least ${minFiles} file(s) required`
1113
+ });
1114
+ }
1115
+ if (maxFiles !== void 0) {
1116
+ arraySchema = arraySchema.max(maxFiles, {
1117
+ message: `Maximum ${maxFiles} file(s) allowed`
1118
+ });
1119
+ }
1120
+ return arraySchema;
1121
+ }
1122
+
1123
+ // src/utils/errorFormatting.ts
1124
+ function parsePath(path) {
1125
+ const segments = [];
1126
+ const parts = path.split(".");
1127
+ for (const part of parts) {
1128
+ const arrayMatch = part.match(/^(.+?)\[(\d+)\]$/);
1129
+ if (arrayMatch) {
1130
+ segments.push(arrayMatch[1]);
1131
+ segments.push(parseInt(arrayMatch[2], 10));
1132
+ } else if (/^\d+$/.test(part)) {
1133
+ segments.push(parseInt(part, 10));
1134
+ } else {
1135
+ segments.push(part);
1136
+ }
1137
+ }
1138
+ return segments;
1139
+ }
1140
+ function setNestedValue(obj, path, value) {
1141
+ let current = obj;
1142
+ for (let i = 0; i < path.length - 1; i++) {
1143
+ const key = path[i];
1144
+ const nextKey = path[i + 1];
1145
+ const keyStr = String(key);
1146
+ if (!(keyStr in current)) {
1147
+ current[keyStr] = typeof nextKey === "number" ? [] : {};
1148
+ }
1149
+ current = current[keyStr];
1150
+ }
1151
+ const lastKey = String(path[path.length - 1]);
1152
+ current[lastKey] = value;
1153
+ }
1154
+ function formatErrors(errors, options = {}) {
1155
+ const { maxErrorsPerField } = options;
1156
+ const flat = {};
1157
+ const nested = {};
1158
+ const all = [];
1159
+ for (const [field, messages] of Object.entries(errors)) {
1160
+ const limitedMessages = maxErrorsPerField ? messages.slice(0, maxErrorsPerField) : messages;
1161
+ flat[field] = limitedMessages;
1162
+ all.push(...limitedMessages);
1163
+ const path = parsePath(field);
1164
+ setNestedValue(nested, path, limitedMessages);
1165
+ }
1166
+ return {
1167
+ flat,
1168
+ nested,
1169
+ all,
1170
+ count: all.length,
1171
+ has(field) {
1172
+ return field in flat && flat[field].length > 0;
1173
+ },
1174
+ get(field) {
1175
+ return flat[field] ?? [];
1176
+ },
1177
+ first(field) {
1178
+ return flat[field]?.[0];
1179
+ }
1180
+ };
1181
+ }
1182
+ function flattenErrors(nested, options = {}) {
1183
+ const { pathSeparator = ".", includeArrayIndices = true } = options;
1184
+ const result = {};
1185
+ function flatten(obj, prefix = "") {
1186
+ if (Array.isArray(obj)) {
1187
+ if (obj.every((item) => typeof item === "string")) {
1188
+ result[prefix] = obj;
1189
+ return;
1190
+ }
1191
+ obj.forEach((item, index) => {
1192
+ const key = includeArrayIndices ? `${prefix}${prefix ? pathSeparator : ""}${index}` : prefix;
1193
+ flatten(item, key);
1194
+ });
1195
+ } else if (obj !== null && typeof obj === "object") {
1196
+ for (const [key, value] of Object.entries(obj)) {
1197
+ const newPrefix = prefix ? `${prefix}${pathSeparator}${key}` : key;
1198
+ flatten(value, newPrefix);
1199
+ }
1200
+ }
1201
+ }
1202
+ flatten(nested);
1203
+ return result;
1204
+ }
1205
+ function summarizeErrors(errors) {
1206
+ const entries = Object.entries(errors);
1207
+ if (entries.length === 0) {
1208
+ return "No errors";
1209
+ }
1210
+ if (entries.length === 1) {
1211
+ const [field, messages] = entries[0];
1212
+ return `${field}: ${messages[0]}`;
1213
+ }
1214
+ const totalErrors = entries.reduce((sum, [, msgs]) => sum + msgs.length, 0);
1215
+ return `${totalErrors} validation error${totalErrors > 1 ? "s" : ""} in ${entries.length} field${entries.length > 1 ? "s" : ""}`;
1216
+ }
1217
+ function filterErrors(errors, fields) {
1218
+ const result = {};
1219
+ for (const field of fields) {
1220
+ if (errors[field]) {
1221
+ result[field] = errors[field];
1222
+ }
1223
+ for (const [key, messages] of Object.entries(errors)) {
1224
+ if (key.startsWith(`${field}.`)) {
1225
+ result[key] = messages;
1226
+ }
1227
+ }
1228
+ }
1229
+ return result;
1230
+ }
1231
+ function mergeErrors(...errorSets) {
1232
+ const result = {};
1233
+ for (const errors of errorSets) {
1234
+ for (const [field, messages] of Object.entries(errors)) {
1235
+ if (!result[field]) {
1236
+ result[field] = [];
1237
+ }
1238
+ result[field].push(...messages);
1239
+ }
1240
+ }
1241
+ return result;
1242
+ }
1243
+
1244
+ // src/utils/testing.ts
1245
+ function createMockRequest(body, options = {}) {
1246
+ const {
1247
+ method = "POST",
1248
+ headers = {},
1249
+ query = {},
1250
+ baseUrl = "http://localhost:3000/api/test"
1251
+ } = options;
1252
+ const url = new URL(baseUrl);
1253
+ for (const [key, value] of Object.entries(query)) {
1254
+ url.searchParams.set(key, value);
1255
+ }
1256
+ const requestHeaders = new Headers(headers);
1257
+ if (["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
1258
+ if (!requestHeaders.has("content-type")) {
1259
+ requestHeaders.set("content-type", "application/json");
1260
+ }
1261
+ }
1262
+ return new Request(url.toString(), {
1263
+ method,
1264
+ headers: requestHeaders,
1265
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1266
+ });
1267
+ }
1268
+ async function testFormRequest(RequestClass, body, options = {}) {
1269
+ const request = createMockRequest(body, options);
1270
+ const FormRequestWithStatic = RequestClass;
1271
+ const instance = await FormRequestWithStatic.fromAppRouter(request, options.params);
1272
+ try {
1273
+ const data = await instance.validate();
1274
+ return {
1275
+ success: true,
1276
+ data,
1277
+ instance
1278
+ };
1279
+ } catch (error) {
1280
+ if (error instanceof ValidationError) {
1281
+ return {
1282
+ success: false,
1283
+ errors: error.errors,
1284
+ instance
1285
+ };
1286
+ }
1287
+ if (error instanceof AuthorizationError) {
1288
+ return {
1289
+ success: false,
1290
+ unauthorized: true,
1291
+ instance
1292
+ };
1293
+ }
1294
+ throw error;
1295
+ }
1296
+ }
1297
+ function addMockMethod(RequestClass) {
1298
+ RequestClass.mock = async (body, options) => testFormRequest(RequestClass, body, options);
1299
+ }
1300
+ function expectValid(result) {
1301
+ if (!result.success) {
1302
+ const errorDetails = result.errors ? Object.entries(result.errors).map(([field, messages]) => `${field}: ${messages.join(", ")}`).join("\n") : result.unauthorized ? "Authorization denied" : "Unknown error";
1303
+ throw new Error(`Expected validation to pass, but it failed:
1304
+ ${errorDetails}`);
1305
+ }
1306
+ }
1307
+ function expectInvalid(result) {
1308
+ if (result.success) {
1309
+ throw new Error(`Expected validation to fail, but it passed with data: ${JSON.stringify(result.data)}`);
1310
+ }
1311
+ if (!result.errors) {
1312
+ throw new Error("Expected validation errors, but got none");
1313
+ }
1314
+ }
1315
+ function expectFieldError(result, field, messagePattern) {
1316
+ expectInvalid(result);
1317
+ const fieldErrors = result.errors[field];
1318
+ if (!fieldErrors || fieldErrors.length === 0) {
1319
+ throw new Error(
1320
+ `Expected errors for field "${field}", but found none. Errors: ${JSON.stringify(result.errors)}`
1321
+ );
1322
+ }
1323
+ if (messagePattern) {
1324
+ const hasMatch = fieldErrors.some(
1325
+ (msg) => typeof messagePattern === "string" ? msg.includes(messagePattern) : messagePattern.test(msg)
1326
+ );
1327
+ if (!hasMatch) {
1328
+ throw new Error(
1329
+ `Expected error for "${field}" to match "${messagePattern}", but got: ${fieldErrors.join(", ")}`
1330
+ );
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ // src/utils/compose.ts
1336
+ function createAuthenticatedRequest(authorizeFn) {
1337
+ class AuthenticatedFormRequest extends FormRequest {
1338
+ async authorize() {
1339
+ return authorizeFn(this);
1340
+ }
1341
+ }
1342
+ return AuthenticatedFormRequest;
1343
+ }
1344
+ function composeAuthorization(...checks) {
1345
+ return async function() {
1346
+ for (const check of checks) {
1347
+ const result = await check(this);
1348
+ if (!result) return false;
1349
+ }
1350
+ return true;
1351
+ };
1352
+ }
1353
+ var authHelpers = {
1354
+ /**
1355
+ * Check if request has a specific header
1356
+ */
1357
+ hasHeader: (headerName) => (request) => {
1358
+ return !!request.header(headerName);
1359
+ },
1360
+ /**
1361
+ * Check if request has authorization header
1362
+ */
1363
+ isAuthenticated: (request) => {
1364
+ return !!request.header("authorization");
1365
+ },
1366
+ /**
1367
+ * Check if request has a bearer token
1368
+ */
1369
+ hasBearerToken: (request) => {
1370
+ const auth = request.header("authorization");
1371
+ return typeof auth === "string" && auth.startsWith("Bearer ");
1372
+ },
1373
+ /**
1374
+ * Extract bearer token from request
1375
+ */
1376
+ getBearerToken: (request) => {
1377
+ const auth = request.header("authorization");
1378
+ if (typeof auth === "string" && auth.startsWith("Bearer ")) {
1379
+ return auth.slice(7);
1380
+ }
1381
+ return null;
1382
+ },
1383
+ /**
1384
+ * Check if request has API key
1385
+ */
1386
+ hasApiKey: (headerName = "x-api-key") => (request) => {
1387
+ return !!request.header(headerName);
1388
+ }
1389
+ };
1390
+ var hookHelpers = {
1391
+ /**
1392
+ * Compose multiple beforeValidation hooks
1393
+ */
1394
+ beforeValidation: (...hooks) => async function() {
1395
+ for (const hook of hooks) {
1396
+ await hook.call(this);
1397
+ }
1398
+ },
1399
+ /**
1400
+ * Compose multiple afterValidation hooks
1401
+ */
1402
+ afterValidation: (...hooks) => async function(data) {
1403
+ for (const hook of hooks) {
1404
+ await hook.call(this, data);
1405
+ }
1406
+ },
1407
+ /**
1408
+ * Common beforeValidation transformations
1409
+ */
1410
+ transforms: {
1411
+ /** Trim all string values */
1412
+ trimStrings: function() {
1413
+ const body = this.all();
1414
+ for (const [key, value] of Object.entries(body)) {
1415
+ if (typeof value === "string") {
1416
+ body[key] = value.trim();
1417
+ }
1418
+ }
1419
+ },
1420
+ /** Lowercase specific fields */
1421
+ lowercase: (...fields) => function() {
1422
+ for (const field of fields) {
1423
+ const value = this.input(field);
1424
+ if (typeof value === "string") {
1425
+ this.all()[field] = value.toLowerCase();
1426
+ }
1427
+ }
1428
+ },
1429
+ /** Uppercase specific fields */
1430
+ uppercase: (...fields) => function() {
1431
+ for (const field of fields) {
1432
+ const value = this.input(field);
1433
+ if (typeof value === "string") {
1434
+ this.all()[field] = value.toUpperCase();
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ };
1440
+
1441
+ export { ArkTypeAdapter, AuthorizationError, FormRequest, MemoryRateLimitStore, RateLimitError, ValibotAdapter, ValidationError, YupAdapter, ZodAdapter, addMockMethod, authHelpers, checkRateLimit, coerceFormData, coercionPresets, composeAuthorization, createAppRouterWrapper, createAuthenticatedRequest, createMockRequest, createPagesRouterWrapper, expectFieldError, expectInvalid, expectValid, filterErrors, flattenErrors, formFile, formFiles, formatErrors, hookHelpers, isAppRouterRequest, isPagesRouterRequest, mergeErrors, rateLimit, setDefaultRateLimitStore, summarizeErrors, testFormRequest, withApiRequest, withApiSchema, withRequest, withSchema, zodCoerce };
485
1442
  //# sourceMappingURL=index.mjs.map
486
1443
  //# sourceMappingURL=index.mjs.map