@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.
Files changed (37) hide show
  1. package/README.md +50 -51
  2. package/dist/{chunk-U2PB6XEO.js → chunk-2XLAXQGM.js} +43 -1
  3. package/dist/{chunk-KIUO2HIR.cjs → chunk-3GAFULXI.cjs} +15 -3
  4. package/dist/{chunk-RHKBT3M2.js → chunk-B6XAA3UV.js} +7 -7
  5. package/dist/{chunk-4AQB4RSU.cjs → chunk-FVWOLTP6.cjs} +10 -10
  6. package/dist/{chunk-TDEXEIHH.cjs → chunk-IFSUPBIF.cjs} +43 -1
  7. package/dist/{chunk-MUXYP6IK.js → chunk-IZEVVOXI.js} +20 -7
  8. package/dist/{chunk-OGU7AIZF.js → chunk-LUFTQDGA.js} +15 -3
  9. package/dist/{chunk-53EEWALQ.cjs → chunk-NLEKGYTV.cjs} +14 -14
  10. package/dist/{chunk-RVGCMQ4J.js → chunk-S5N6EFNB.js} +5 -5
  11. package/dist/{chunk-V2WFT5M2.cjs → chunk-SFJZLW6P.cjs} +21 -8
  12. package/dist/common/date.cjs +2 -2
  13. package/dist/common/date.js +1 -1
  14. package/dist/common/datetime.cjs +2 -2
  15. package/dist/common/datetime.js +1 -1
  16. package/dist/common/file.cjs +2 -2
  17. package/dist/common/file.js +1 -1
  18. package/dist/common/url.cjs +2 -2
  19. package/dist/common/url.js +1 -1
  20. package/dist/index.cjs +8 -6
  21. package/dist/index.d.cts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +7 -5
  24. package/dist/taiwan/tel.cjs +4 -2
  25. package/dist/taiwan/tel.d.cts +41 -16
  26. package/dist/taiwan/tel.d.ts +41 -16
  27. package/dist/taiwan/tel.js +3 -1
  28. package/package.json +10 -10
  29. package/src/validators/common/date.ts +5 -5
  30. package/src/validators/common/datetime.ts +7 -7
  31. package/src/validators/common/file.ts +19 -3
  32. package/src/validators/common/url.ts +76 -1
  33. package/src/validators/taiwan/tel.ts +69 -26
  34. package/tests/common/file.test.ts +20 -0
  35. package/tests/common/url.test.ts +18 -0
  36. package/tests/taiwan/tel.test.ts +50 -14
  37. 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 = hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
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 Landline Telephone Number validator for Zod Kit
2
+ * @fileoverview Taiwan Telephone Number validator for Zod Kit
3
3
  *
4
- * Provides validation for Taiwan landline telephone numbers according to the
5
- * official 2024 telecom numbering plan with comprehensive area code support.
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 landline telephone validation
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 landline telephone number format (Official 2024 rules)
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 landline telephone numbers according to the official 2024
65
- * telecom numbering plan. Supports all Taiwan area codes and their specific
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
- * - 0700: Premium rate (付費語音資訊) - 6 digits
79
- * - 0800: Toll-free - 6 digits
80
- * - 0809: Toll-free - 6 digits
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
- // 0700: Premium rate (付費語音資訊) - 6 digits
106
- // 0800/0809: Toll-free - 6 digits
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.replace(/[-\s]/g, "")
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
- // Toll-free: 0800/0809 + 6 digits, total 10 digits
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 landline telephone number validation
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 landline telephone number validator with support for
218
- * all Taiwan area codes according to the official 2024 telecom numbering plan.
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", () => {
@@ -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", () => {
@@ -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
- // Premium rate numbers
45
- expect(schema.parse("0700123456")).toBe("0700123456") // 0700-123-456 (10 digits)
46
- expect(schema.parse("0700-123-456")).toBe("0700-123-456") // with separators
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
- // Toll-free numbers
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
- // Premium rate numbers
264
- expect(validateTaiwanTel("0700123456")).toBe(true) // 0700
265
- expect(validateTaiwanTel("0700-123-456")).toBe(true) // 0700 with separators
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
- // Toll-free numbers
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("070012345")).toBe(false) // 0700 too short (9 digits)
292
- expect(validateTaiwanTel("07001234567")).toBe(false) // 0700 too long (11 digits)
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: "0700", numbers: ["0700123456"] }, // Premium rate (10 digits)
394
- { code: "0800", numbers: ["0800123456"] }, // Toll-free (10 digits)
395
- { code: "0809", numbers: ["0809123456"] }, // Toll-free (10 digits)
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
  ]
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
4
  "module": "ESNext",
5
- "moduleResolution": "Node",
5
+ "moduleResolution": "Bundler",
6
+ "ignoreDeprecations": "6.0",
6
7
  "esModuleInterop": true,
7
8
  "declaration": true,
8
9
  "outDir": "./dist",