@hy_ong/zod-kit 0.2.7 → 0.2.9
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/README.md +50 -51
- package/dist/{chunk-U2PB6XEO.js → chunk-2XLAXQGM.js} +43 -1
- package/dist/{chunk-KIUO2HIR.cjs → chunk-3GAFULXI.cjs} +15 -3
- package/dist/{chunk-RHKBT3M2.js → chunk-B6XAA3UV.js} +7 -7
- package/dist/{chunk-4AQB4RSU.cjs → chunk-FVWOLTP6.cjs} +10 -10
- package/dist/{chunk-TDEXEIHH.cjs → chunk-IFSUPBIF.cjs} +43 -1
- package/dist/{chunk-MUXYP6IK.js → chunk-IZEVVOXI.js} +20 -7
- package/dist/{chunk-OGU7AIZF.js → chunk-LUFTQDGA.js} +15 -3
- package/dist/{chunk-53EEWALQ.cjs → chunk-NLEKGYTV.cjs} +14 -14
- package/dist/{chunk-RVGCMQ4J.js → chunk-S5N6EFNB.js} +5 -5
- package/dist/{chunk-V2WFT5M2.cjs → chunk-SFJZLW6P.cjs} +21 -8
- package/dist/common/date.cjs +2 -2
- package/dist/common/date.js +1 -1
- package/dist/common/datetime.cjs +2 -2
- package/dist/common/datetime.js +1 -1
- package/dist/common/file.cjs +2 -2
- package/dist/common/file.js +1 -1
- package/dist/common/url.cjs +2 -2
- package/dist/common/url.js +1 -1
- package/dist/index.cjs +8 -6
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -5
- package/dist/taiwan/tel.cjs +4 -2
- package/dist/taiwan/tel.d.cts +41 -16
- package/dist/taiwan/tel.d.ts +41 -16
- package/dist/taiwan/tel.js +3 -1
- package/package.json +10 -10
- package/src/validators/common/date.ts +5 -5
- package/src/validators/common/datetime.ts +7 -7
- package/src/validators/common/file.ts +19 -3
- package/src/validators/common/url.ts +76 -1
- package/src/validators/taiwan/tel.ts +69 -26
- package/tests/common/file.test.ts +20 -0
- package/tests/common/url.test.ts +18 -0
- package/tests/taiwan/tel.test.ts +50 -14
- package/tsconfig.json +2 -1
|
@@ -116,6 +116,81 @@ export type UrlOptions<IsRequired extends boolean = true> = {
|
|
|
116
116
|
*/
|
|
117
117
|
export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
118
118
|
|
|
119
|
+
const getNormalizedHostname = (hostname: string): string => hostname.toLowerCase().replace(/^\[(.*)]$/, "$1")
|
|
120
|
+
|
|
121
|
+
const parseIPv4 = (hostname: string): number[] | null => {
|
|
122
|
+
const parts = hostname.split(".")
|
|
123
|
+
if (parts.length !== 4) return null
|
|
124
|
+
|
|
125
|
+
const octets = parts.map((part) => {
|
|
126
|
+
if (!/^\d+$/.test(part)) return null
|
|
127
|
+
const value = Number(part)
|
|
128
|
+
return Number.isInteger(value) && value >= 0 && value <= 255 ? value : null
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return octets.every((part) => part !== null) ? octets as number[] : null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isLocalIPv4 = ([first, second]: number[]): boolean => (
|
|
135
|
+
first === 0 ||
|
|
136
|
+
first === 10 ||
|
|
137
|
+
first === 127 ||
|
|
138
|
+
(first === 169 && second === 254) ||
|
|
139
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
140
|
+
(first === 192 && second === 168)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const parseHextet = (value: string): number | null => {
|
|
144
|
+
if (!/^[0-9a-f]{1,4}$/i.test(value)) return null
|
|
145
|
+
|
|
146
|
+
const parsed = Number.parseInt(value, 16)
|
|
147
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0xffff ? parsed : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parseIPv4MappedIPv6 = (hostname: string): number[] | null => {
|
|
151
|
+
if (!hostname.startsWith("::ffff:")) return null
|
|
152
|
+
|
|
153
|
+
const mappedValue = hostname.slice("::ffff:".length)
|
|
154
|
+
const dottedIPv4 = parseIPv4(mappedValue)
|
|
155
|
+
if (dottedIPv4) return dottedIPv4
|
|
156
|
+
|
|
157
|
+
const hextets = mappedValue.split(":")
|
|
158
|
+
if (hextets.length !== 2) return null
|
|
159
|
+
|
|
160
|
+
const high = parseHextet(hextets[0])
|
|
161
|
+
const low = parseHextet(hextets[1])
|
|
162
|
+
if (high === null || low === null) return null
|
|
163
|
+
|
|
164
|
+
return [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isLocalNetworkHostname = (hostname: string): boolean => {
|
|
168
|
+
const normalizedHostname = getNormalizedHostname(hostname)
|
|
169
|
+
|
|
170
|
+
if (normalizedHostname === "localhost") return true
|
|
171
|
+
|
|
172
|
+
const ipv4 = parseIPv4(normalizedHostname)
|
|
173
|
+
if (ipv4) {
|
|
174
|
+
return isLocalIPv4(ipv4)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!normalizedHostname.includes(":")) return false
|
|
178
|
+
|
|
179
|
+
const mappedIPv4 = parseIPv4MappedIPv6(normalizedHostname)
|
|
180
|
+
if (mappedIPv4) return isLocalIPv4(mappedIPv4)
|
|
181
|
+
|
|
182
|
+
const firstHextet = parseHextet(normalizedHostname.split(":")[0])
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
normalizedHostname === "::" ||
|
|
186
|
+
normalizedHostname === "::1" ||
|
|
187
|
+
(firstHextet !== null && (
|
|
188
|
+
(firstHextet & 0xfe00) === 0xfc00 ||
|
|
189
|
+
(firstHextet & 0xffc0) === 0xfe80
|
|
190
|
+
))
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
119
194
|
/**
|
|
120
195
|
* Creates a Zod schema for URL validation with comprehensive constraints
|
|
121
196
|
*
|
|
@@ -356,7 +431,7 @@ export function url<IsRequired extends boolean = false>(required?: IsRequired, o
|
|
|
356
431
|
}
|
|
357
432
|
|
|
358
433
|
// Localhost validation
|
|
359
|
-
const isLocalhost =
|
|
434
|
+
const isLocalhost = isLocalNetworkHostname(hostname)
|
|
360
435
|
if (blockLocalhost && isLocalhost) {
|
|
361
436
|
ctx.addIssue({ code: "custom", message: getMessage("noLocalhost") })
|
|
362
437
|
return
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Taiwan
|
|
2
|
+
* @fileoverview Taiwan Telephone Number validator for Zod Kit
|
|
3
3
|
*
|
|
4
|
-
* Provides validation for Taiwan
|
|
5
|
-
*
|
|
4
|
+
* Provides validation for Taiwan telephone numbers according to the official
|
|
5
|
+
* 2024 telecom numbering plan with area code and service code support.
|
|
6
6
|
*
|
|
7
7
|
* @author Ong Hoe Yuan
|
|
8
8
|
* @version 0.0.5
|
|
@@ -27,19 +27,21 @@ export type TwTelMessages = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Configuration options for Taiwan
|
|
30
|
+
* Configuration options for Taiwan telephone validation
|
|
31
31
|
*
|
|
32
32
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
33
33
|
*
|
|
34
34
|
* @interface TwTelOptions
|
|
35
35
|
* @property {IsRequired} [required=true] - Whether the field is required
|
|
36
36
|
* @property {string[]} [whitelist] - Array of specific telephone numbers that are always allowed
|
|
37
|
+
* @property {boolean} [allowITFS=false] - Whether to allow 008 international toll-free numbers
|
|
37
38
|
* @property {Function} [transform] - Custom transformation function for telephone number
|
|
38
39
|
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
39
40
|
* @property {Record<Locale, TwTelMessages>} [i18n] - Custom error messages for different locales
|
|
40
41
|
*/
|
|
41
42
|
export type TwTelOptions<IsRequired extends boolean = true> = {
|
|
42
43
|
whitelist?: string[]
|
|
44
|
+
allowITFS?: boolean
|
|
43
45
|
transform?: (value: string) => string
|
|
44
46
|
defaultValue?: IsRequired extends true ? string : string | null
|
|
45
47
|
i18n?: Partial<Record<Locale, Partial<TwTelMessages>>>
|
|
@@ -54,16 +56,39 @@ export type TwTelOptions<IsRequired extends boolean = true> = {
|
|
|
54
56
|
*/
|
|
55
57
|
export type TwTelSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
56
58
|
|
|
59
|
+
export type ValidateTaiwanTelOptions = {
|
|
60
|
+
allowITFS?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const normalizeTaiwanTel = (value: string): string => value.replace(/[-\s]/g, "")
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates Taiwan-dialed international toll-free number format.
|
|
67
|
+
*
|
|
68
|
+
* Supported formats:
|
|
69
|
+
* - 00800 + 8-digit Global Subscriber Number (UIFN)
|
|
70
|
+
* - 00801~00809 + 1-12 digit carrier-defined subscriber number (ITFS)
|
|
71
|
+
*/
|
|
72
|
+
const validateTaiwanInternationalTollFree = (value: string): boolean => {
|
|
73
|
+
const cleanValue = normalizeTaiwanTel(value)
|
|
74
|
+
|
|
75
|
+
if (/^00800\d{8}$/.test(cleanValue)) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return /^0080[1-9]\d{1,12}$/.test(cleanValue)
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
/**
|
|
58
|
-
* Validates Taiwan
|
|
83
|
+
* Validates Taiwan telephone number format (Official 2024 rules)
|
|
59
84
|
*
|
|
60
85
|
* @param {string} value - The telephone number to validate
|
|
86
|
+
* @param {ValidateTaiwanTelOptions} [options] - Optional validation settings
|
|
61
87
|
* @returns {boolean} True if the telephone number is valid
|
|
62
88
|
*
|
|
63
89
|
* @description
|
|
64
|
-
* Validates Taiwan
|
|
65
|
-
*
|
|
66
|
-
* number patterns.
|
|
90
|
+
* Validates Taiwan telephone numbers according to the official 2024 telecom
|
|
91
|
+
* numbering plan. Supports Taiwan area codes and selected service codes.
|
|
67
92
|
*
|
|
68
93
|
* Supported area codes and formats:
|
|
69
94
|
* - 02: Taipei, New Taipei, Keelung - 8 digits
|
|
@@ -75,9 +100,11 @@ export type TwTelSchema<IsRequired extends boolean> = IsRequired extends true ?
|
|
|
75
100
|
* - 06: Tainan - 7 digits
|
|
76
101
|
* - 07: Kaohsiung - 7-8 digits
|
|
77
102
|
* - 08: Pingtung - 7 digits
|
|
78
|
-
* -
|
|
79
|
-
* - 0800:
|
|
80
|
-
* - 0809:
|
|
103
|
+
* - 070: VoIP / Internet Telephony (網路電話) - 8 digits (11 total)
|
|
104
|
+
* - 0800: Domestic recipient-paid/toll-free - 6 digits
|
|
105
|
+
* - 0809: Domestic recipient-paid/toll-free - 6 digits
|
|
106
|
+
* - 00800: Universal International Freephone Number (UIFN) when allowITFS is true
|
|
107
|
+
* - 00801~00809: International Toll-Free Service (ITFS) when allowITFS is true
|
|
81
108
|
* - 082: Kinmen - 6 digits
|
|
82
109
|
* - 0836: Matsu - 5-6 digits
|
|
83
110
|
* - 089: Taitung - 6 digits
|
|
@@ -89,9 +116,12 @@ export type TwTelSchema<IsRequired extends boolean> = IsRequired extends true ?
|
|
|
89
116
|
* validateTaiwanTel("037234567") // true (Miaoli area)
|
|
90
117
|
* validateTaiwanTel("082234567") // true (Kinmen area)
|
|
91
118
|
* validateTaiwanTel("02-2345-6789") // true (with separators)
|
|
119
|
+
* validateTaiwanTel("00801-852-747", { allowITFS: true }) // true (ITFS)
|
|
92
120
|
* ```
|
|
93
121
|
*/
|
|
94
|
-
const validateTaiwanTel = (value: string): boolean => {
|
|
122
|
+
const validateTaiwanTel = (value: string, options: ValidateTaiwanTelOptions = {}): boolean => {
|
|
123
|
+
const { allowITFS = false } = options
|
|
124
|
+
|
|
95
125
|
// Official Taiwan landline formats according to telecom numbering plan:
|
|
96
126
|
// 02: Taipei, New Taipei, Keelung - 8 digits
|
|
97
127
|
// 03: Taoyuan, Hsinchu, Yilan, Hualien - 7-8 digits
|
|
@@ -102,14 +132,18 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
102
132
|
// 06: Tainan - 7 digits
|
|
103
133
|
// 07: Kaohsiung - 7-8 digits
|
|
104
134
|
// 08: Pingtung - 7 digits
|
|
105
|
-
//
|
|
106
|
-
// 0800/0809:
|
|
135
|
+
// 070: VoIP / Internet Telephony (網路電話) - 8 digits (11 total)
|
|
136
|
+
// 0800/0809: Domestic recipient-paid/toll-free - 6 digits
|
|
107
137
|
// 082: Kinmen - 6 digits
|
|
108
138
|
// 0836: Matsu - 5-6 digits
|
|
109
139
|
// 089: Taitung - 6 digits
|
|
110
140
|
|
|
111
141
|
// Remove common separators for validation
|
|
112
|
-
const cleanValue = value
|
|
142
|
+
const cleanValue = normalizeTaiwanTel(value)
|
|
143
|
+
|
|
144
|
+
if (allowITFS && validateTaiwanInternationalTollFree(cleanValue)) {
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
113
147
|
|
|
114
148
|
// Basic format: starts with 0, then area code, then number
|
|
115
149
|
if (!/^0\d{7,10}$/.test(cleanValue)) {
|
|
@@ -118,12 +152,8 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
118
152
|
|
|
119
153
|
// Check 4-digit area codes first
|
|
120
154
|
const areaCode4 = cleanValue.substring(0, 4)
|
|
121
|
-
if (areaCode4 === "0700") {
|
|
122
|
-
// Premium rate (付費語音資訊): 0700 + 6 digits, total 10 digits
|
|
123
|
-
return cleanValue.length === 10 && /^0700\d{6}$/.test(cleanValue)
|
|
124
|
-
}
|
|
125
155
|
if (areaCode4 === "0800" || areaCode4 === "0809") {
|
|
126
|
-
//
|
|
156
|
+
// Domestic recipient-paid/toll-free: 0800/0809 + 6 digits, total 10 digits
|
|
127
157
|
return cleanValue.length === 10 && /^080[09]\d{6}$/.test(cleanValue)
|
|
128
158
|
}
|
|
129
159
|
if (areaCode4 === "0836") {
|
|
@@ -133,6 +163,10 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
133
163
|
|
|
134
164
|
// Check 3-digit area codes
|
|
135
165
|
const areaCode3 = cleanValue.substring(0, 3)
|
|
166
|
+
if (areaCode3 === "070") {
|
|
167
|
+
// VoIP / Internet Telephony (網路電話): 070 + 8 digits, total 11 digits
|
|
168
|
+
return cleanValue.length === 11 && /^070\d{8}$/.test(cleanValue)
|
|
169
|
+
}
|
|
136
170
|
if (areaCode3 === "037") {
|
|
137
171
|
// Miaoli: 037 + 6-7 digits, total 9-10 digits
|
|
138
172
|
// User number must start with 2-9 (not 0 or 1)
|
|
@@ -206,7 +240,7 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
206
240
|
}
|
|
207
241
|
|
|
208
242
|
/**
|
|
209
|
-
* Creates a Zod schema for Taiwan
|
|
243
|
+
* Creates a Zod schema for Taiwan telephone number validation
|
|
210
244
|
*
|
|
211
245
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
212
246
|
* @param {IsRequired} [required=false] - Whether the field is required
|
|
@@ -214,11 +248,13 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
214
248
|
* @returns {TwTelSchema<IsRequired>} Zod schema for telephone number validation
|
|
215
249
|
*
|
|
216
250
|
* @description
|
|
217
|
-
* Creates a comprehensive Taiwan
|
|
218
|
-
*
|
|
251
|
+
* Creates a comprehensive Taiwan telephone number validator with support for
|
|
252
|
+
* Taiwan area codes and selected service codes according to the official 2024
|
|
253
|
+
* telecom numbering plan.
|
|
219
254
|
*
|
|
220
255
|
* Features:
|
|
221
256
|
* - Complete Taiwan area code support (02, 03, 037, 04, 049, 05, 06, 07, 08, 082, 0826, 0836, 089)
|
|
257
|
+
* - Optional 008 international toll-free support (UIFN/ITFS)
|
|
222
258
|
* - Automatic separator handling (hyphens and spaces)
|
|
223
259
|
* - Area-specific number length and pattern validation
|
|
224
260
|
* - Whitelist functionality for specific allowed numbers
|
|
@@ -262,6 +298,13 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
262
298
|
* transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
|
|
263
299
|
* transformSchema.parse("02 2345 6789") // ✓ Valid (spaces removed)
|
|
264
300
|
*
|
|
301
|
+
* // With Taiwan-dialed international toll-free support
|
|
302
|
+
* const itfsSchema = twTel(false, {
|
|
303
|
+
* allowITFS: true
|
|
304
|
+
* })
|
|
305
|
+
* itfsSchema.parse("00800-2468-1668") // ✓ Valid (UIFN)
|
|
306
|
+
* itfsSchema.parse("00801-852-747") // ✓ Valid (ITFS)
|
|
307
|
+
*
|
|
265
308
|
* // With custom error messages
|
|
266
309
|
* const customSchema = twTel(false, {
|
|
267
310
|
* i18n: {
|
|
@@ -276,7 +319,7 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
276
319
|
* @see {@link validateTaiwanTel} for validation logic details
|
|
277
320
|
*/
|
|
278
321
|
export function twTel<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwTelOptions<IsRequired>, 'required'>): TwTelSchema<IsRequired> {
|
|
279
|
-
const { whitelist, transform, defaultValue, i18n } = options ?? {}
|
|
322
|
+
const { whitelist, allowITFS = false, transform, defaultValue, i18n } = options ?? {}
|
|
280
323
|
|
|
281
324
|
const isRequired = required ?? false as IsRequired
|
|
282
325
|
|
|
@@ -345,7 +388,7 @@ export function twTel<IsRequired extends boolean = false>(required?: IsRequired,
|
|
|
345
388
|
}
|
|
346
389
|
|
|
347
390
|
// Taiwan telephone format validation
|
|
348
|
-
if (!validateTaiwanTel(val)) {
|
|
391
|
+
if (!validateTaiwanTel(val, { allowITFS })) {
|
|
349
392
|
ctx.addIssue({ code: "custom", message: getMessage("invalid") })
|
|
350
393
|
return
|
|
351
394
|
}
|
|
@@ -369,4 +412,4 @@ export function twTel<IsRequired extends boolean = false>(required?: IsRequired,
|
|
|
369
412
|
* const isValid = validateTaiwanTel("0223456789") // boolean
|
|
370
413
|
* ```
|
|
371
414
|
*/
|
|
372
|
-
export { validateTaiwanTel }
|
|
415
|
+
export { validateTaiwanTel, validateTaiwanInternationalTollFree }
|
|
@@ -49,6 +49,26 @@ describe("file(true) features", () => {
|
|
|
49
49
|
expect(schema.parse(undefined)).toBe(null)
|
|
50
50
|
expect(schema.parse("")).toBe(null)
|
|
51
51
|
})
|
|
52
|
+
|
|
53
|
+
it("should not throw while creating a schema when File is unavailable", () => {
|
|
54
|
+
const originalFile = globalThis.File
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
Object.defineProperty(globalThis, "File", {
|
|
58
|
+
value: undefined,
|
|
59
|
+
configurable: true,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const schema = file(false)
|
|
63
|
+
expect(schema.parse(null)).toBe(null)
|
|
64
|
+
expect(() => schema.parse("not a file")).toThrow("Invalid file")
|
|
65
|
+
} finally {
|
|
66
|
+
Object.defineProperty(globalThis, "File", {
|
|
67
|
+
value: originalFile,
|
|
68
|
+
configurable: true,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
})
|
|
52
72
|
})
|
|
53
73
|
|
|
54
74
|
describe("file size validation", () => {
|
package/tests/common/url.test.ts
CHANGED
|
@@ -267,6 +267,15 @@ describe("url", () => {
|
|
|
267
267
|
expect(() => schema.parse("http://192.168.1.1")).toThrow()
|
|
268
268
|
})
|
|
269
269
|
|
|
270
|
+
it("should block all loopback and unspecified hosts when blockLocalhost is true", () => {
|
|
271
|
+
const schema = url(true, { blockLocalhost: true })
|
|
272
|
+
|
|
273
|
+
expect(() => schema.parse("http://127.0.0.2")).toThrow()
|
|
274
|
+
expect(() => schema.parse("http://0.0.0.0")).toThrow()
|
|
275
|
+
expect(() => schema.parse("http://[::1]")).toThrow()
|
|
276
|
+
expect(() => schema.parse("http://[::]")).toThrow()
|
|
277
|
+
})
|
|
278
|
+
|
|
270
279
|
it("should block localhost when allowLocalhost is false", () => {
|
|
271
280
|
const schema = url(true, { allowLocalhost: false })
|
|
272
281
|
expect(() => schema.parse("http://localhost:3000")).toThrow()
|
|
@@ -456,7 +465,16 @@ describe("url", () => {
|
|
|
456
465
|
expect(() => schema.parse("http://10.0.0.1")).toThrow()
|
|
457
466
|
expect(() => schema.parse("http://172.16.0.1")).toThrow()
|
|
458
467
|
expect(() => schema.parse("http://192.168.0.1")).toThrow()
|
|
468
|
+
expect(() => schema.parse("http://169.254.0.1")).toThrow()
|
|
469
|
+
expect(() => schema.parse("http://[fc00::1]")).toThrow()
|
|
470
|
+
expect(() => schema.parse("http://[fe80::1]")).toThrow()
|
|
471
|
+
expect(() => schema.parse("http://[fe81::1]")).toThrow()
|
|
472
|
+
expect(() => schema.parse("http://[febf::1]")).toThrow()
|
|
473
|
+
expect(() => schema.parse("http://[::ffff:127.0.0.1]")).toThrow()
|
|
474
|
+
expect(() => schema.parse("http://[::ffff:10.0.0.1]")).toThrow()
|
|
459
475
|
expect(schema.parse("https://8.8.8.8")).toBe("https://8.8.8.8") // Public IP
|
|
476
|
+
expect(schema.parse("http://[::ffff:8.8.8.8]")).toBe("http://[::ffff:8.8.8.8]") // IPv4-mapped public IP
|
|
477
|
+
expect(schema.parse("https://fc.example.com")).toBe("https://fc.example.com")
|
|
460
478
|
})
|
|
461
479
|
|
|
462
480
|
it("should handle edge cases with ports", () => {
|
package/tests/taiwan/tel.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest"
|
|
2
|
-
import { twTel, setLocale, validateTaiwanTel } from "../../src"
|
|
2
|
+
import { twTel, setLocale, validateTaiwanTel, validateTaiwanInternationalTollFree } from "../../src"
|
|
3
3
|
|
|
4
4
|
describe("Taiwan twTel(true) validator", () => {
|
|
5
5
|
beforeEach(() => setLocale("en-US"))
|
|
@@ -41,11 +41,11 @@ describe("Taiwan twTel(true) validator", () => {
|
|
|
41
41
|
expect(schema.parse("082661234")).toBe("082661234") // 0826-61234 (9 digits, Wuqiu)
|
|
42
42
|
expect(schema.parse("083621234")).toBe("083621234") // 0836-21234 (9 digits, Matsu)
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
expect(schema.parse("
|
|
46
|
-
expect(schema.parse("
|
|
44
|
+
// VoIP numbers (070)
|
|
45
|
+
expect(schema.parse("07012345678")).toBe("07012345678") // 070-1234-5678 (11 digits)
|
|
46
|
+
expect(schema.parse("070-1234-5678")).toBe("070-1234-5678") // with separators
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Domestic recipient-paid/toll-free numbers
|
|
49
49
|
expect(schema.parse("0800123456")).toBe("0800123456") // 0800-123-456 (10 digits)
|
|
50
50
|
expect(schema.parse("0809123456")).toBe("0809123456") // 0809-123-456 (10 digits)
|
|
51
51
|
expect(schema.parse("0800-012-345")).toBe("0800-012-345") // with separators
|
|
@@ -70,6 +70,40 @@ describe("Taiwan twTel(true) validator", () => {
|
|
|
70
70
|
expect(schema.parse("07 234-5678")).toBe("07 234-5678")
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
it("should validate Taiwan international toll-free numbers when ITFS is allowed", () => {
|
|
74
|
+
const schema = twTel(true, { allowITFS: true })
|
|
75
|
+
|
|
76
|
+
expect(schema.parse("00800-2468-1668")).toBe("00800-2468-1668")
|
|
77
|
+
expect(schema.parse("00801128000")).toBe("00801128000")
|
|
78
|
+
expect(schema.parse("00801-852-747")).toBe("00801-852-747")
|
|
79
|
+
expect(schema.parse("00809 123456789012")).toBe("00809 123456789012")
|
|
80
|
+
|
|
81
|
+
expect(validateTaiwanTel("00800-2468-1668", { allowITFS: true })).toBe(true)
|
|
82
|
+
expect(validateTaiwanTel("00801-852-747", { allowITFS: true })).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("should reject Taiwan international toll-free numbers unless ITFS is allowed", () => {
|
|
86
|
+
const schema = twTel(true)
|
|
87
|
+
|
|
88
|
+
expect(() => schema.parse("00800-2468-1668")).toThrow("Invalid Taiwan telephone format")
|
|
89
|
+
expect(() => schema.parse("00801-852-747")).toThrow("Invalid Taiwan telephone format")
|
|
90
|
+
expect(validateTaiwanTel("00801-852-747")).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("should reject invalid Taiwan international toll-free number lengths", () => {
|
|
94
|
+
const schema = twTel(true, { allowITFS: true })
|
|
95
|
+
|
|
96
|
+
expect(() => schema.parse("00800-1234-567")).toThrow("Invalid Taiwan telephone format")
|
|
97
|
+
expect(() => schema.parse("00800-1234-56789")).toThrow("Invalid Taiwan telephone format")
|
|
98
|
+
expect(() => schema.parse("00801")).toThrow("Invalid Taiwan telephone format")
|
|
99
|
+
expect(() => schema.parse("00801-1234567890123")).toThrow("Invalid Taiwan telephone format")
|
|
100
|
+
|
|
101
|
+
expect(validateTaiwanInternationalTollFree("00800-1234-5678")).toBe(true)
|
|
102
|
+
expect(validateTaiwanInternationalTollFree("00800-1234-567")).toBe(false)
|
|
103
|
+
expect(validateTaiwanInternationalTollFree("00801-123456789012")).toBe(true)
|
|
104
|
+
expect(validateTaiwanInternationalTollFree("00801-1234567890123")).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
73
107
|
it("should reject invalid Taiwan telephone numbers", () => {
|
|
74
108
|
const schema = twTel(true)
|
|
75
109
|
|
|
@@ -260,11 +294,12 @@ describe("Taiwan twTel(true) validator", () => {
|
|
|
260
294
|
expect(validateTaiwanTel("083621234")).toBe(true) // Matsu 9 digits
|
|
261
295
|
expect(validateTaiwanTel("0836212345")).toBe(true) // Matsu 10 digits
|
|
262
296
|
|
|
263
|
-
//
|
|
264
|
-
expect(validateTaiwanTel("
|
|
265
|
-
expect(validateTaiwanTel("
|
|
297
|
+
// VoIP numbers (070)
|
|
298
|
+
expect(validateTaiwanTel("07012345678")).toBe(true) // 070 VoIP (11 digits)
|
|
299
|
+
expect(validateTaiwanTel("070-1234-5678")).toBe(true) // 070 with separators
|
|
300
|
+
expect(validateTaiwanTel("07001234567")).toBe(true) // 070 starting with 0 (valid VoIP)
|
|
266
301
|
|
|
267
|
-
//
|
|
302
|
+
// Domestic recipient-paid/toll-free numbers
|
|
268
303
|
expect(validateTaiwanTel("0800123456")).toBe(true) // 0800
|
|
269
304
|
expect(validateTaiwanTel("0809123456")).toBe(true) // 0809
|
|
270
305
|
expect(validateTaiwanTel("0800-123-456")).toBe(true) // with separators
|
|
@@ -288,8 +323,9 @@ describe("Taiwan twTel(true) validator", () => {
|
|
|
288
323
|
expect(validateTaiwanTel("071234567")).toBe(false) // Invalid first digit for 07
|
|
289
324
|
expect(validateTaiwanTel("037134567")).toBe(false) // Invalid first digit for 037
|
|
290
325
|
expect(validateTaiwanTel("081234567")).toBe(false) // Invalid first digit for 08
|
|
291
|
-
expect(validateTaiwanTel("
|
|
292
|
-
expect(validateTaiwanTel("
|
|
326
|
+
expect(validateTaiwanTel("0701234567")).toBe(false) // 070 too short (10 digits)
|
|
327
|
+
expect(validateTaiwanTel("070123456789")).toBe(false) // 070 too long (12 digits)
|
|
328
|
+
expect(validateTaiwanTel("0700123456")).toBe(false) // 070 + 7 digits = 10 digits (too short)
|
|
293
329
|
expect(validateTaiwanTel("080012345")).toBe(false) // 0800 too short (9 digits)
|
|
294
330
|
expect(validateTaiwanTel("08001234567")).toBe(false) // 0800 too long (11 digits)
|
|
295
331
|
expect(validateTaiwanTel("0801123456")).toBe(false) // 0801 not valid toll-free prefix
|
|
@@ -390,9 +426,9 @@ describe("Taiwan twTel(true) validator", () => {
|
|
|
390
426
|
{ code: "08", numbers: ["084234567"] }, // Pingtung (9 digits, first digit 4) - from utility test ✓
|
|
391
427
|
{ code: "082", numbers: ["082234567"] }, // Kinmen (9 digits, first digit 2) - from utility test ✓
|
|
392
428
|
{ code: "089", numbers: ["089234567"] }, // Taitung (9 digits, first digit 2) - from utility test ✓
|
|
393
|
-
{ code: "
|
|
394
|
-
{ code: "0800", numbers: ["0800123456"] }, //
|
|
395
|
-
{ code: "0809", numbers: ["0809123456"] }, //
|
|
429
|
+
{ code: "070", numbers: ["07012345678"] }, // VoIP (11 digits)
|
|
430
|
+
{ code: "0800", numbers: ["0800123456"] }, // Domestic recipient-paid/toll-free (10 digits)
|
|
431
|
+
{ code: "0809", numbers: ["0809123456"] }, // Domestic recipient-paid/toll-free (10 digits)
|
|
396
432
|
{ code: "0826", numbers: ["082661234"] }, // Wuqiu (9 digits, first digit 6) - from utility test ✓
|
|
397
433
|
{ code: "0836", numbers: ["083621234"] }, // Matsu (9 digits, first digit 2) - from utility test ✓
|
|
398
434
|
]
|