@hy_ong/zod-kit 0.0.4 → 0.0.6

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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,319 @@
1
+ /**
2
+ * @fileoverview Number validator for Zod Kit
3
+ *
4
+ * Provides comprehensive number validation with type constraints, range validation,
5
+ * precision control, and advanced parsing features including comma-separated numbers.
6
+ *
7
+ * @author Ong Hoe Yuan
8
+ * @version 0.0.5
9
+ */
10
+
11
+ import { z, ZodNullable, ZodNumber } from "zod"
12
+ import { t } from "../../i18n"
13
+ import { getLocale, type Locale } from "../../config"
14
+
15
+ /**
16
+ * Type definition for number validation error messages
17
+ *
18
+ * @interface NumberMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [invalid] - Message when value is not a valid number
21
+ * @property {string} [integer] - Message when integer is required but float provided
22
+ * @property {string} [float] - Message when float is required but integer provided
23
+ * @property {string} [min] - Message when number is below minimum value
24
+ * @property {string} [max] - Message when number exceeds maximum value
25
+ * @property {string} [positive] - Message when positive number is required
26
+ * @property {string} [negative] - Message when negative number is required
27
+ * @property {string} [nonNegative] - Message when non-negative number is required
28
+ * @property {string} [nonPositive] - Message when non-positive number is required
29
+ * @property {string} [multipleOf] - Message when number is not a multiple of specified value
30
+ * @property {string} [finite] - Message when finite number is required
31
+ * @property {string} [precision] - Message when number has too many decimal places
32
+ */
33
+ export type NumberMessages = {
34
+ required?: string
35
+ invalid?: string
36
+ integer?: string
37
+ float?: string
38
+ min?: string
39
+ max?: string
40
+ positive?: string
41
+ negative?: string
42
+ nonNegative?: string
43
+ nonPositive?: string
44
+ multipleOf?: string
45
+ finite?: string
46
+ precision?: string
47
+ }
48
+
49
+ /**
50
+ * Configuration options for number validation
51
+ *
52
+ * @template IsRequired - Whether the field is required (affects return type)
53
+ *
54
+ * @interface NumberOptions
55
+ * @property {IsRequired} [required=true] - Whether the field is required
56
+ * @property {number} [min] - Minimum allowed value
57
+ * @property {number} [max] - Maximum allowed value
58
+ * @property {number | null} [defaultValue] - Default value when input is empty
59
+ * @property {"integer" | "float" | "both"} [type="both"] - Type constraint for the number
60
+ * @property {boolean} [positive] - Whether number must be positive (> 0)
61
+ * @property {boolean} [negative] - Whether number must be negative (< 0)
62
+ * @property {boolean} [nonNegative] - Whether number must be non-negative (>= 0)
63
+ * @property {boolean} [nonPositive] - Whether number must be non-positive (<= 0)
64
+ * @property {number} [multipleOf] - Number must be a multiple of this value
65
+ * @property {number} [precision] - Maximum number of decimal places allowed
66
+ * @property {boolean} [finite=true] - Whether to reject Infinity and -Infinity
67
+ * @property {Function} [transform] - Custom transformation function for number values
68
+ * @property {boolean} [parseCommas=false] - Whether to parse comma-separated numbers (e.g., "1,234")
69
+ * @property {Record<Locale, NumberMessages>} [i18n] - Custom error messages for different locales
70
+ */
71
+ export type NumberOptions<IsRequired extends boolean = true> = {
72
+ required?: IsRequired
73
+ min?: number
74
+ max?: number
75
+ defaultValue?: IsRequired extends true ? number : number | null
76
+ type?: "integer" | "float" | "both"
77
+ positive?: boolean
78
+ negative?: boolean
79
+ nonNegative?: boolean
80
+ nonPositive?: boolean
81
+ multipleOf?: number
82
+ precision?: number
83
+ finite?: boolean
84
+ transform?: (value: number) => number
85
+ parseCommas?: boolean // Parse "1,234" as 1234
86
+ i18n?: Record<Locale, NumberMessages>
87
+ }
88
+
89
+ /**
90
+ * Type alias for number validation schema based on required flag
91
+ *
92
+ * @template IsRequired - Whether the field is required
93
+ * @typedef NumberSchema
94
+ * @description Returns ZodNumber if required, ZodNullable<ZodNumber> if optional
95
+ */
96
+ export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber>
97
+
98
+ /**
99
+ * Creates a Zod schema for number validation with comprehensive constraints
100
+ *
101
+ * @template IsRequired - Whether the field is required (affects return type)
102
+ * @param {NumberOptions<IsRequired>} [options] - Configuration options for number validation
103
+ * @returns {NumberSchema<IsRequired>} Zod schema for number validation
104
+ *
105
+ * @description
106
+ * Creates a comprehensive number validator with type constraints, range validation,
107
+ * precision control, and advanced parsing features including comma-separated numbers.
108
+ *
109
+ * Features:
110
+ * - Type constraints (integer, float, or both)
111
+ * - Range validation (min/max)
112
+ * - Sign constraints (positive, negative, non-negative, non-positive)
113
+ * - Multiple-of validation
114
+ * - Precision control (decimal places)
115
+ * - Finite number validation
116
+ * - Comma-separated number parsing
117
+ * - Custom transformation functions
118
+ * - Comprehensive internationalization
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * // Basic number validation
123
+ * const basicSchema = number()
124
+ * basicSchema.parse(42) // ✓ Valid
125
+ * basicSchema.parse("42") // ✓ Valid (converted to number)
126
+ *
127
+ * // Integer only
128
+ * const integerSchema = number({ type: "integer" })
129
+ * integerSchema.parse(42) // ✓ Valid
130
+ * integerSchema.parse(42.5) // ✗ Invalid
131
+ *
132
+ * // Range validation
133
+ * const rangeSchema = number({ min: 0, max: 100 })
134
+ * rangeSchema.parse(50) // ✓ Valid
135
+ * rangeSchema.parse(150) // ✗ Invalid
136
+ *
137
+ * // Positive numbers only
138
+ * const positiveSchema = number({ positive: true })
139
+ * positiveSchema.parse(5) // ✓ Valid
140
+ * positiveSchema.parse(-5) // ✗ Invalid
141
+ *
142
+ * // Multiple of constraint
143
+ * const multipleSchema = number({ multipleOf: 5 })
144
+ * multipleSchema.parse(10) // ✓ Valid
145
+ * multipleSchema.parse(7) // ✗ Invalid
146
+ *
147
+ * // Precision control
148
+ * const precisionSchema = number({ precision: 2 })
149
+ * precisionSchema.parse(3.14) // ✓ Valid
150
+ * precisionSchema.parse(3.14159) // ✗ Invalid
151
+ *
152
+ * // Comma-separated parsing
153
+ * const commaSchema = number({ parseCommas: true })
154
+ * commaSchema.parse("1,234.56") // ✓ Valid (parsed as 1234.56)
155
+ *
156
+ * // Optional with default
157
+ * const optionalSchema = number({
158
+ * required: false,
159
+ * defaultValue: 0
160
+ * })
161
+ * ```
162
+ *
163
+ * @throws {z.ZodError} When validation fails with specific error messages
164
+ * @see {@link NumberOptions} for all available configuration options
165
+ */
166
+ export function number<IsRequired extends boolean = true>(options?: NumberOptions<IsRequired>): NumberSchema<IsRequired> {
167
+ const {
168
+ required = true,
169
+ min,
170
+ max,
171
+ defaultValue,
172
+ type = "both",
173
+ positive,
174
+ negative,
175
+ nonNegative,
176
+ nonPositive,
177
+ multipleOf,
178
+ precision,
179
+ finite = true,
180
+ transform,
181
+ parseCommas = false,
182
+ i18n,
183
+ } = options ?? {}
184
+
185
+ // Helper function to get custom message or fallback to default i18n
186
+ const getMessage = (key: keyof NumberMessages, params?: Record<string, any>) => {
187
+ if (i18n) {
188
+ const currentLocale = getLocale()
189
+ const customMessages = i18n[currentLocale]
190
+ if (customMessages && customMessages[key]) {
191
+ const template = customMessages[key]!
192
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
193
+ }
194
+ }
195
+ return t(`common.number.${key}`, params)
196
+ }
197
+
198
+ // Set appropriate default value based on required flag
199
+ const actualDefaultValue = defaultValue ?? null
200
+
201
+ const schema = z
202
+ .preprocess(
203
+ (val) => {
204
+ if (val === "" || val === undefined || val === null) {
205
+ return actualDefaultValue
206
+ }
207
+
208
+ // Handle string input
209
+ if (typeof val === "string") {
210
+ let processedVal = val.trim()
211
+
212
+ // Parse comma-separated numbers like "1,234.56"
213
+ if (parseCommas) {
214
+ processedVal = processedVal.replace(/,/g, "")
215
+ }
216
+
217
+ const parsed = Number(processedVal)
218
+
219
+ // Return NaN as is so it can be caught in refine
220
+ if (isNaN(parsed)) {
221
+ return parsed
222
+ }
223
+
224
+ if (transform) {
225
+ return transform(parsed)
226
+ }
227
+
228
+ return parsed
229
+ }
230
+
231
+ // Handle existing numbers (including Infinity)
232
+ if (typeof val === "number") {
233
+ if (transform && Number.isFinite(val)) {
234
+ return transform(val)
235
+ }
236
+ return val
237
+ }
238
+
239
+ return val
240
+ },
241
+ z.union([z.number(), z.null(), z.nan(), z.custom<number>((val) => val === Infinity || val === -Infinity)])
242
+ )
243
+ .refine((val) => {
244
+ // Required check first
245
+ if (required && val === null) {
246
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
247
+ }
248
+
249
+ if (val === null) return true
250
+
251
+ // Type validation for invalid inputs (NaN)
252
+ if (typeof val === "number" && isNaN(val)) {
253
+ if (type === "integer") {
254
+ throw new z.ZodError([{ code: "custom", message: getMessage("integer"), path: [] }])
255
+ } else if (type === "float") {
256
+ throw new z.ZodError([{ code: "custom", message: getMessage("float"), path: [] }])
257
+ } else {
258
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
259
+ }
260
+ }
261
+
262
+ // Invalid number check for non-numbers
263
+ if (typeof val !== "number") {
264
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
265
+ }
266
+
267
+ // Finite check
268
+ if (finite && !Number.isFinite(val)) {
269
+ throw new z.ZodError([{ code: "custom", message: getMessage("finite"), path: [] }])
270
+ }
271
+
272
+ // Type validation for valid numbers
273
+ if (type === "integer" && !Number.isInteger(val)) {
274
+ throw new z.ZodError([{ code: "custom", message: getMessage("integer"), path: [] }])
275
+ }
276
+ if (type === "float" && Number.isInteger(val)) {
277
+ throw new z.ZodError([{ code: "custom", message: getMessage("float"), path: [] }])
278
+ }
279
+
280
+ // Sign checks (more specific, should come first)
281
+ if (positive && val <= 0) {
282
+ throw new z.ZodError([{ code: "custom", message: getMessage("positive"), path: [] }])
283
+ }
284
+ if (negative && val >= 0) {
285
+ throw new z.ZodError([{ code: "custom", message: getMessage("negative"), path: [] }])
286
+ }
287
+ if (nonNegative && val < 0) {
288
+ throw new z.ZodError([{ code: "custom", message: getMessage("nonNegative"), path: [] }])
289
+ }
290
+ if (nonPositive && val > 0) {
291
+ throw new z.ZodError([{ code: "custom", message: getMessage("nonPositive"), path: [] }])
292
+ }
293
+
294
+ // Range checks
295
+ if (min !== undefined && val < min) {
296
+ throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
297
+ }
298
+ if (max !== undefined && val > max) {
299
+ throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
300
+ }
301
+
302
+ // Multiple of check
303
+ if (multipleOf !== undefined && val % multipleOf !== 0) {
304
+ throw new z.ZodError([{ code: "custom", message: getMessage("multipleOf", { multipleOf }), path: [] }])
305
+ }
306
+
307
+ // Precision check
308
+ if (precision !== undefined) {
309
+ const decimalPlaces = (val.toString().split(".")[1] || "").length
310
+ if (decimalPlaces > precision) {
311
+ throw new z.ZodError([{ code: "custom", message: getMessage("precision", { precision }), path: [] }])
312
+ }
313
+ }
314
+
315
+ return true
316
+ })
317
+
318
+ return schema as unknown as NumberSchema<IsRequired>
319
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * @fileoverview Password validator for Zod Kit
3
+ *
4
+ * Provides comprehensive password validation with strength analysis, character requirements,
5
+ * security checks, and protection against common weak passwords.
6
+ *
7
+ * @author Ong Hoe Yuan
8
+ * @version 0.0.5
9
+ */
10
+
11
+ import { z, ZodNullable, ZodString } from "zod"
12
+ import { t } from "../../i18n"
13
+ import { getLocale, type Locale } from "../../config"
14
+
15
+ /**
16
+ * Type definition for password validation error messages
17
+ *
18
+ * @interface PasswordMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [min] - Message when password is too short
21
+ * @property {string} [max] - Message when password is too long
22
+ * @property {string} [uppercase] - Message when uppercase letters are required
23
+ * @property {string} [lowercase] - Message when lowercase letters are required
24
+ * @property {string} [digits] - Message when digits are required
25
+ * @property {string} [special] - Message when special characters are required
26
+ * @property {string} [noRepeating] - Message when repeating characters are forbidden
27
+ * @property {string} [noSequential] - Message when sequential characters are forbidden
28
+ * @property {string} [noCommonWords] - Message when common passwords are forbidden
29
+ * @property {string} [minStrength] - Message when password strength is insufficient
30
+ * @property {string} [excludes] - Message when password contains forbidden strings
31
+ * @property {string} [includes] - Message when password doesn't contain required string
32
+ * @property {string} [invalid] - Message when password doesn't match custom regex
33
+ */
34
+ export type PasswordMessages = {
35
+ required?: string
36
+ min?: string
37
+ max?: string
38
+ uppercase?: string
39
+ lowercase?: string
40
+ digits?: string
41
+ special?: string
42
+ noRepeating?: string
43
+ noSequential?: string
44
+ noCommonWords?: string
45
+ minStrength?: string
46
+ excludes?: string
47
+ includes?: string
48
+ invalid?: string
49
+ }
50
+
51
+ /**
52
+ * Password strength levels used for validation
53
+ *
54
+ * @typedef {"weak" | "medium" | "strong" | "very-strong"} PasswordStrength
55
+ * @description
56
+ * - weak: Basic passwords with minimal requirements
57
+ * - medium: Passwords with some character variety
58
+ * - strong: Passwords with good character variety and length
59
+ * - very-strong: Passwords with excellent character variety, length, and complexity
60
+ */
61
+ export type PasswordStrength = "weak" | "medium" | "strong" | "very-strong"
62
+
63
+ /**
64
+ * Configuration options for password validation
65
+ *
66
+ * @template IsRequired - Whether the field is required (affects return type)
67
+ *
68
+ * @interface PasswordOptions
69
+ * @property {IsRequired} [required=true] - Whether the field is required
70
+ * @property {number} [min] - Minimum length of password
71
+ * @property {number} [max] - Maximum length of password
72
+ * @property {boolean} [uppercase] - Whether uppercase letters are required
73
+ * @property {boolean} [lowercase] - Whether lowercase letters are required
74
+ * @property {boolean} [digits] - Whether digits are required
75
+ * @property {boolean} [special] - Whether special characters are required
76
+ * @property {boolean} [noRepeating] - Whether to forbid repeating characters (3+ in a row)
77
+ * @property {boolean} [noSequential] - Whether to forbid sequential characters (abc, 123)
78
+ * @property {boolean} [noCommonWords] - Whether to forbid common weak passwords
79
+ * @property {PasswordStrength} [minStrength] - Minimum required password strength
80
+ * @property {string | string[]} [excludes] - String(s) that must not be included
81
+ * @property {string} [includes] - String that must be included in password
82
+ * @property {RegExp} [regex] - Custom regex pattern for validation
83
+ * @property {Function} [transform] - Custom transformation function for password
84
+ * @property {string | null} [defaultValue] - Default value when input is empty
85
+ * @property {Record<Locale, PasswordMessages>} [i18n] - Custom error messages for different locales
86
+ */
87
+ export type PasswordOptions<IsRequired extends boolean = true> = {
88
+ required?: IsRequired
89
+ min?: number
90
+ max?: number
91
+ uppercase?: boolean
92
+ lowercase?: boolean
93
+ digits?: boolean
94
+ special?: boolean
95
+ noRepeating?: boolean
96
+ noSequential?: boolean
97
+ noCommonWords?: boolean
98
+ minStrength?: PasswordStrength
99
+ excludes?: string | string[]
100
+ includes?: string
101
+ regex?: RegExp
102
+ transform?: (value: string) => string
103
+ defaultValue?: IsRequired extends true ? string : string | null
104
+ i18n?: Record<Locale, PasswordMessages>
105
+ }
106
+
107
+ /**
108
+ * Type alias for password validation schema based on required flag
109
+ *
110
+ * @template IsRequired - Whether the field is required
111
+ * @typedef PasswordSchema
112
+ * @description Returns ZodString if required, ZodNullable<ZodString> if optional
113
+ */
114
+ export type PasswordSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
115
+
116
+ /**
117
+ * List of common weak passwords to check against
118
+ *
119
+ * @constant {string[]} COMMON_PASSWORDS
120
+ * @description Contains frequently used weak passwords that should be avoided
121
+ */
122
+ const COMMON_PASSWORDS = [
123
+ "password",
124
+ "123456",
125
+ "123456789",
126
+ "12345678",
127
+ "12345",
128
+ "1234567",
129
+ "admin",
130
+ "qwerty",
131
+ "abc123",
132
+ "password123",
133
+ "letmein",
134
+ "welcome",
135
+ "monkey",
136
+ "dragon",
137
+ "sunshine",
138
+ "princess",
139
+ ]
140
+
141
+ /**
142
+ * Calculates password strength based on various criteria
143
+ *
144
+ * @param {string} password - The password to analyze
145
+ * @returns {PasswordStrength} The calculated strength level
146
+ *
147
+ * @description
148
+ * Analyzes password strength using multiple factors:
149
+ * - Length bonuses (8+, 12+, 16+ characters)
150
+ * - Character variety (lowercase, uppercase, digits, special characters)
151
+ * - Deductions for repeating or sequential patterns
152
+ *
153
+ * Scoring system:
154
+ * - 0-2 points: weak
155
+ * - 3-4 points: medium
156
+ * - 5-6 points: strong
157
+ * - 7+ points: very-strong
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * calculatePasswordStrength("password") // "weak"
162
+ * calculatePasswordStrength("Password123") // "medium"
163
+ * calculatePasswordStrength("MyStr0ng!P@ssw0rd") // "very-strong"
164
+ * ```
165
+ */
166
+ const calculatePasswordStrength = (password: string): PasswordStrength => {
167
+ let score = 0
168
+
169
+ // Length bonus
170
+ if (password.length >= 8) score += 1
171
+ if (password.length >= 12) score += 1
172
+ if (password.length >= 16) score += 1
173
+
174
+ // Character variety
175
+ if (/[a-z]/.test(password)) score += 1
176
+ if (/[A-Z]/.test(password)) score += 1
177
+ if (/[0-9]/.test(password)) score += 1
178
+ if (/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) score += 1
179
+
180
+ // Deductions
181
+ if (/(.)\1{2,}/.test(password)) score -= 1 // Repeating characters
182
+ if (/(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password)) score -= 1 // Sequential
183
+
184
+ if (score <= 2) return "weak"
185
+ if (score <= 4) return "medium"
186
+ if (score <= 6) return "strong"
187
+ return "very-strong"
188
+ }
189
+
190
+ /**
191
+ * Creates a Zod schema for password validation with comprehensive security checks
192
+ *
193
+ * @template IsRequired - Whether the field is required (affects return type)
194
+ * @param {PasswordOptions<IsRequired>} [options] - Configuration options for password validation
195
+ * @returns {PasswordSchema<IsRequired>} Zod schema for password validation
196
+ *
197
+ * @description
198
+ * Creates a comprehensive password validator with strength analysis, character requirements,
199
+ * security checks, and protection against common weak passwords.
200
+ *
201
+ * Features:
202
+ * - Length validation (min/max)
203
+ * - Character requirements (uppercase, lowercase, digits, special)
204
+ * - Security checks (no repeating, no sequential patterns)
205
+ * - Common password detection
206
+ * - Strength analysis with configurable minimum levels
207
+ * - Content inclusion/exclusion
208
+ * - Custom regex patterns
209
+ * - Custom transformation functions
210
+ * - Comprehensive internationalization
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // Basic password validation
215
+ * const basicSchema = password()
216
+ * basicSchema.parse("MyPassword123!") // ✓ Valid
217
+ *
218
+ * // Strong password requirements
219
+ * const strongSchema = password({
220
+ * min: 12,
221
+ * uppercase: true,
222
+ * lowercase: true,
223
+ * digits: true,
224
+ * special: true,
225
+ * minStrength: "strong"
226
+ * })
227
+ *
228
+ * // No common passwords
229
+ * const secureSchema = password({
230
+ * noCommonWords: true,
231
+ * noRepeating: true,
232
+ * noSequential: true
233
+ * })
234
+ * secureSchema.parse("password123") // ✗ Invalid (common password)
235
+ * secureSchema.parse("aaa123") // ✗ Invalid (repeating characters)
236
+ * secureSchema.parse("abc123") // ✗ Invalid (sequential characters)
237
+ *
238
+ * // Custom requirements
239
+ * const customSchema = password({
240
+ * min: 8,
241
+ * includes: "@", // Must contain @
242
+ * excludes: ["admin", "user"], // Cannot contain these words
243
+ * regex: /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)/ // Custom pattern
244
+ * })
245
+ *
246
+ * // Minimum strength requirement
247
+ * const strengthSchema = password({ minStrength: "very-strong" })
248
+ * strengthSchema.parse("weak") // ✗ Invalid (insufficient strength)
249
+ * strengthSchema.parse("MyVeryStr0ng!P@ssw0rd2024") // ✓ Valid
250
+ *
251
+ * // Optional with default
252
+ * const optionalSchema = password({
253
+ * required: false,
254
+ * defaultValue: null
255
+ * })
256
+ * ```
257
+ *
258
+ * @throws {z.ZodError} When validation fails with specific error messages
259
+ * @see {@link PasswordOptions} for all available configuration options
260
+ * @see {@link PasswordStrength} for strength level definitions
261
+ * @see {@link calculatePasswordStrength} for strength calculation logic
262
+ */
263
+ export function password<IsRequired extends boolean = true>(options?: PasswordOptions<IsRequired>): PasswordSchema<IsRequired> {
264
+ const {
265
+ required = true,
266
+ min,
267
+ max,
268
+ uppercase,
269
+ lowercase,
270
+ digits,
271
+ special,
272
+ noRepeating,
273
+ noSequential,
274
+ noCommonWords,
275
+ minStrength,
276
+ excludes,
277
+ includes,
278
+ regex,
279
+ transform,
280
+ defaultValue,
281
+ i18n,
282
+ } = options ?? {}
283
+
284
+ // Set appropriate default value based on required flag
285
+ const actualDefaultValue = defaultValue ?? (required ? "" : null)
286
+
287
+ // Helper function to get custom message or fallback to default i18n
288
+ const getMessage = (key: keyof PasswordMessages, params?: Record<string, any>) => {
289
+ if (i18n) {
290
+ const currentLocale = getLocale()
291
+ const customMessages = i18n[currentLocale]
292
+ if (customMessages && customMessages[key]) {
293
+ const template = customMessages[key]!
294
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
295
+ }
296
+ }
297
+ return t(`common.password.${key}`, params)
298
+ }
299
+
300
+ // Preprocessing function
301
+ const preprocessFn = (val: unknown) => {
302
+ if (val === "" || val === null || val === undefined) {
303
+ return actualDefaultValue
304
+ }
305
+
306
+ let processed = String(val)
307
+ if (transform) {
308
+ processed = transform(processed)
309
+ }
310
+
311
+ return processed
312
+ }
313
+
314
+ const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
315
+
316
+ const schema = baseSchema.refine((val) => {
317
+ if (val === null) return true
318
+
319
+ // Required check
320
+ if (required && (val === "" || val === "null" || val === "undefined")) {
321
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
322
+ }
323
+
324
+ // Length checks
325
+ if (val !== null && min !== undefined && val.length < min) {
326
+ throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
327
+ }
328
+ if (val !== null && max !== undefined && val.length > max) {
329
+ throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
330
+ }
331
+
332
+ // Character requirements
333
+ if (val !== null && uppercase && !/[A-Z]/.test(val)) {
334
+ throw new z.ZodError([{ code: "custom", message: getMessage("uppercase"), path: [] }])
335
+ }
336
+ if (val !== null && lowercase && !/[a-z]/.test(val)) {
337
+ throw new z.ZodError([{ code: "custom", message: getMessage("lowercase"), path: [] }])
338
+ }
339
+ if (val !== null && digits && !/[0-9]/.test(val)) {
340
+ throw new z.ZodError([{ code: "custom", message: getMessage("digits"), path: [] }])
341
+ }
342
+ if (val !== null && special && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(val)) {
343
+ throw new z.ZodError([{ code: "custom", message: getMessage("special"), path: [] }])
344
+ }
345
+
346
+ // Advanced security checks
347
+ if (val !== null && noRepeating && /(.)\1{2,}/.test(val)) {
348
+ throw new z.ZodError([{ code: "custom", message: getMessage("noRepeating"), path: [] }])
349
+ }
350
+ if (val !== null && noSequential && /(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(val)) {
351
+ throw new z.ZodError([{ code: "custom", message: getMessage("noSequential"), path: [] }])
352
+ }
353
+ if (val !== null && noCommonWords && COMMON_PASSWORDS.some((common) => val.toLowerCase().includes(common.toLowerCase()))) {
354
+ throw new z.ZodError([{ code: "custom", message: getMessage("noCommonWords"), path: [] }])
355
+ }
356
+ if (val !== null && minStrength) {
357
+ const strength = calculatePasswordStrength(val)
358
+ const strengthLevels = ["weak", "medium", "strong", "very-strong"]
359
+ const currentLevel = strengthLevels.indexOf(strength)
360
+ const requiredLevel = strengthLevels.indexOf(minStrength)
361
+ if (currentLevel < requiredLevel) {
362
+ throw new z.ZodError([{ code: "custom", message: getMessage("minStrength", { minStrength }), path: [] }])
363
+ }
364
+ }
365
+
366
+ // Content checks
367
+ if (val !== null && includes !== undefined && !val.includes(includes)) {
368
+ throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
369
+ }
370
+ if (val !== null && excludes !== undefined) {
371
+ const excludeList = Array.isArray(excludes) ? excludes : [excludes]
372
+ for (const exclude of excludeList) {
373
+ if (val.includes(exclude)) {
374
+ throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
375
+ }
376
+ }
377
+ }
378
+ if (val !== null && regex !== undefined && !regex.test(val)) {
379
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid", { regex }), path: [] }])
380
+ }
381
+
382
+ return true
383
+ })
384
+
385
+ return schema as unknown as PasswordSchema<IsRequired>
386
+ }