@defra/fcp-sfd-frontend-engine 0.6.0 → 0.7.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.cjs CHANGED
@@ -129,17 +129,36 @@ var mappers = {
129
129
  businessDetails: mapBusinessDetails
130
130
  };
131
131
 
132
- // src/schemas/business/business-sbi-schema.js
132
+ // src/utils/joi.js
133
133
  var import_joi = __toESM(require("joi"), 1);
134
- var businessSbiSchema = import_joi.default.object({
135
- sbi: import_joi.default.string().pattern(/^\d{9}$/).allow("").optional().messages({
136
- "string.pattern.base": "Enter the full SBI"
134
+
135
+ // src/constants/patterns.js
136
+ var PHONE_NUMBER_PATTERN = /^\+?(?=[\d ()]*\d)[\d ()]*$/;
137
+ var NO_CONTROL_CHARS_PATTERN = /^[^\x00-\x1f\x7f-\x9f]*$/;
138
+
139
+ // src/utils/joi.js
140
+ var Joi = import_joi.default.extend((joi) => ({
141
+ type: "string",
142
+ base: joi.string(),
143
+ messages: {
144
+ "string.noControlChars": "Field must not contain invalid characters"
145
+ },
146
+ validate(value, helpers) {
147
+ if (typeof value === "string" && !NO_CONTROL_CHARS_PATTERN.test(value)) {
148
+ return { value, errors: helpers.error("string.noControlChars") };
149
+ }
150
+ return { value };
151
+ }
152
+ }));
153
+
154
+ // src/schemas/business/business-sbi-schema.js
155
+ var businessSbiSchema = Joi.object({
156
+ sbi: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
157
+ "string.pattern.base": "Enter the full SBI",
158
+ "string.noControlChars": "SBI must not contain invalid characters"
137
159
  })
138
160
  });
139
161
 
140
- // src/schemas/address-schema.js
141
- var import_joi2 = __toESM(require("joi"), 1);
142
-
143
162
  // src/constants/validation-fields.js
144
163
  var FIRST_NAME_MAX = 100;
145
164
  var LAST_NAME_MAX = 100;
@@ -155,53 +174,59 @@ var COUNTY_MAX = 60;
155
174
  var COUNTRY_MAX = 60;
156
175
  var POSTCODE_MAX = 8;
157
176
 
158
- // src/schemas/address-schema.js
159
- var addressSchema = import_joi2.default.object({
160
- address1: import_joi2.default.string().required().max(ADDRESS_LINE_MAX).messages({
177
+ // src/schemas/shared/address-schema.js
178
+ var addressSchema = Joi.object({
179
+ address1: Joi.string().trim().required().max(ADDRESS_LINE_MAX).messages({
161
180
  "string.empty": "Enter address line 1, typically the building and street",
162
181
  "string.max": `Address line 1 must be ${ADDRESS_LINE_MAX} characters or less`,
163
- "any.required": "Enter address line 1, typically the building and street"
182
+ "any.required": "Enter address line 1, typically the building and street",
183
+ "string.noControlChars": "Address line 1 must not contain invalid characters"
164
184
  }),
165
- address2: import_joi2.default.string().allow("").max(ADDRESS_LINE_MAX).messages({
166
- "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`
185
+ address2: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
186
+ "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`,
187
+ "string.noControlChars": "Address line 2 must not contain invalid characters"
167
188
  }),
168
- address3: import_joi2.default.string().allow("").max(ADDRESS_LINE_MAX).messages({
169
- "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`
189
+ address3: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
190
+ "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`,
191
+ "string.noControlChars": "Address line 3 must not contain invalid characters"
170
192
  }),
171
- city: import_joi2.default.string().required().max(TOWN_CITY_MAX).messages({
193
+ city: Joi.string().trim().required().max(TOWN_CITY_MAX).messages({
172
194
  "string.empty": "Enter town or city",
173
195
  "string.max": `Town or city must be ${TOWN_CITY_MAX} characters or less`,
174
- "any.required": "Enter town or city"
196
+ "any.required": "Enter town or city",
197
+ "string.noControlChars": "Town or city must not contain invalid characters"
175
198
  }),
176
- county: import_joi2.default.string().allow("").max(COUNTY_MAX).messages({
177
- "string.max": `County must be ${COUNTY_MAX} characters or less`
199
+ county: Joi.string().trim().allow("").max(COUNTY_MAX).messages({
200
+ "string.max": `County must be ${COUNTY_MAX} characters or less`,
201
+ "string.noControlChars": "County must not contain invalid characters"
178
202
  }),
179
- postcode: import_joi2.default.string().required().max(POSTCODE_MAX).messages({
203
+ postcode: Joi.string().trim().required().max(POSTCODE_MAX).messages({
180
204
  "any.required": "Enter a postal code or zip code",
181
205
  "string.empty": "Enter a postal code or zip code",
182
- "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`
206
+ "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`,
207
+ "string.noControlChars": "Postal code or zip code must not contain invalid characters"
183
208
  }),
184
- country: import_joi2.default.string().required().max(COUNTRY_MAX).messages({
209
+ country: Joi.string().trim().required().max(COUNTRY_MAX).messages({
185
210
  "string.empty": "Enter a country",
186
211
  "string.max": `Country must be ${COUNTRY_MAX} characters or less`,
187
- "any.required": "Enter a country"
212
+ "any.required": "Enter a country",
213
+ "string.noControlChars": "Country must not contain invalid characters"
188
214
  })
189
215
  });
190
216
 
191
217
  // src/schemas/business/business-name-schema.js
192
- var import_joi3 = __toESM(require("joi"), 1);
193
- var businessNameSchema = import_joi3.default.object({
194
- businessName: import_joi3.default.string().required().max(BUSINESS_NAME_MAX).messages({
218
+ var businessNameSchema = Joi.object({
219
+ businessName: Joi.string().trim().required().max(BUSINESS_NAME_MAX).messages({
195
220
  "string.empty": "Enter business name",
196
221
  "string.max": `Business name must be ${BUSINESS_NAME_MAX} characters or less`,
197
- "any.required": "Enter business name"
222
+ "any.required": "Enter business name",
223
+ "string.noControlChars": "Business name must not contain invalid characters"
198
224
  })
199
225
  });
200
226
 
201
227
  // src/schemas/business/business-email-schema.js
202
- var import_joi4 = __toESM(require("joi"), 1);
203
- var businessEmailSchema = import_joi4.default.object({
204
- businessEmail: import_joi4.default.string().required().max(EMAIL_MAX).email({
228
+ var businessEmailSchema = Joi.object({
229
+ businessEmail: Joi.string().required().max(EMAIL_MAX).email({
205
230
  minDomainSegments: 2,
206
231
  tlds: {
207
232
  allow: true
@@ -209,37 +234,34 @@ var businessEmailSchema = import_joi4.default.object({
209
234
  }).messages({
210
235
  "string.max": `Business email address must be ${EMAIL_MAX} characters or less`,
211
236
  "string.empty": "Enter business email address",
212
- "string.email": "Enter an email address, like name@example.com"
237
+ "string.email": "Enter an email address, like name@example.com",
238
+ "string.noControlChars": "Business email address must not contain invalid characters"
213
239
  })
214
240
  });
215
241
 
216
242
  // src/schemas/business/business-phone-schema.js
217
- var import_joi5 = __toESM(require("joi"), 1);
218
-
219
- // src/constants/patterns.js
220
- var PHONE_NUMBER_PATTERN = /^\+?[0-9 ()]+$/;
221
-
222
- // src/schemas/business/business-phone-schema.js
223
- var businessPhoneSchema = import_joi5.default.object({
224
- businessTelephone: import_joi5.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
243
+ var businessPhoneSchema = Joi.object({
244
+ businessTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
225
245
  "string.min": `Business telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
226
246
  "string.max": `Business telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
227
- "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
247
+ "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
248
+ "string.noControlChars": "Business telephone number must not contain invalid characters"
228
249
  }),
229
- businessMobile: import_joi5.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
250
+ businessMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
230
251
  "string.min": `Business mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
231
252
  "string.max": `Business mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
232
- "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
253
+ "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
254
+ "string.noControlChars": "Business mobile phone number must not contain invalid characters"
233
255
  })
234
256
  }).or("businessTelephone", "businessMobile").messages({
235
257
  "object.missing": "Enter at least one phone number"
236
258
  });
237
259
 
238
260
  // src/schemas/business/business-vat-schema.js
239
- var import_joi6 = __toESM(require("joi"), 1);
240
- var businessVatSchema = import_joi6.default.object({
241
- vatNumber: import_joi6.default.string().pattern(/^\d{9}$/).allow("").optional().messages({
242
- "string.pattern.base": "Enter a VAT registration number, like 123456789"
261
+ var businessVatSchema = Joi.object({
262
+ vatNumber: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
263
+ "string.pattern.base": "Enter a VAT registration number, like 123456789",
264
+ "string.noControlChars": "VAT registration number must not contain invalid characters"
243
265
  })
244
266
  });
245
267
 
@@ -256,10 +278,10 @@ var businessSchemas = {
256
278
  };
257
279
 
258
280
  // src/schemas/customer/customer-crn-schema.js
259
- var import_joi7 = __toESM(require("joi"), 1);
260
- var customerCrnSchema = import_joi7.default.object({
261
- crn: import_joi7.default.string().pattern(/^\d{10}$/).allow("").optional().messages({
262
- "string.pattern.base": "Enter the full CRN"
281
+ var customerCrnSchema = Joi.object({
282
+ crn: Joi.string().pattern(/^\d{10}$/).allow("").optional().messages({
283
+ "string.pattern.base": "Enter the full CRN",
284
+ "string.noControlChars": "CRN must not contain invalid characters"
263
285
  })
264
286
  });
265
287
 
@@ -269,26 +291,25 @@ var customerSchemas = {
269
291
  };
270
292
 
271
293
  // src/schemas/personal/personal-name-schema.js
272
- var import_joi8 = __toESM(require("joi"), 1);
273
- var personalNameSchema = import_joi8.default.object({
274
- first: import_joi8.default.string().required().max(FIRST_NAME_MAX).messages({
294
+ var personalNameSchema = Joi.object({
295
+ first: Joi.string().trim().required().max(FIRST_NAME_MAX).messages({
275
296
  "string.empty": "Enter first name",
276
297
  "string.max": `First name must be ${FIRST_NAME_MAX} characters or less`,
277
- "any.required": "Enter first name"
298
+ "any.required": "Enter first name",
299
+ "string.noControlChars": "First name must not contain invalid characters"
278
300
  }),
279
- last: import_joi8.default.string().required().max(LAST_NAME_MAX).messages({
301
+ last: Joi.string().trim().required().max(LAST_NAME_MAX).messages({
280
302
  "string.empty": "Enter last name",
281
303
  "string.max": `Last name must be ${LAST_NAME_MAX} characters or less`,
282
- "any.required": "Enter last name"
304
+ "any.required": "Enter last name",
305
+ "string.noControlChars": "Last name must not contain invalid characters"
283
306
  }),
284
- middle: import_joi8.default.string().allow("").max(MIDDLE_NAMES_MAX).messages({
285
- "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`
307
+ middle: Joi.string().trim().allow("").max(MIDDLE_NAMES_MAX).messages({
308
+ "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`,
309
+ "string.noControlChars": "Middle names must not contain invalid characters"
286
310
  })
287
311
  });
288
312
 
289
- // src/schemas/personal/personal-dob-schema.js
290
- var import_joi9 = __toESM(require("joi"), 1);
291
-
292
313
  // src/constants/month-map.js
293
314
  var MONTH_MAP = {
294
315
  january: 1,
@@ -318,10 +339,10 @@ var MONTH_MAP = {
318
339
  };
319
340
 
320
341
  // src/schemas/personal/personal-dob-schema.js
321
- var personalDobSchema = import_joi9.default.object({
322
- day: import_joi9.default.string().allow(""),
323
- month: import_joi9.default.string().allow(""),
324
- year: import_joi9.default.string().allow("")
342
+ var personalDobSchema = Joi.object({
343
+ day: Joi.string().allow(""),
344
+ month: Joi.string().allow(""),
345
+ year: Joi.string().allow("")
325
346
  }).custom((value, helpers) => {
326
347
  const { day, month, year } = value;
327
348
  const missingFieldsError = checkMissingFields(day, month, year, helpers);
@@ -339,7 +360,9 @@ var personalDobSchema = import_joi9.default.object({
339
360
  if (fullDate.isJoiError) {
340
361
  return fullDate;
341
362
  }
342
- if (fullDate > /* @__PURE__ */ new Date()) {
363
+ const today = /* @__PURE__ */ new Date();
364
+ today.setUTCHours(0, 0, 0, 0);
365
+ if (fullDate >= today) {
343
366
  return makeError(helpers, "dob.future", ["day", "month", "year"]);
344
367
  }
345
368
  const tooOldError = checkNotTooOld(fullDate, helpers);
@@ -358,7 +381,8 @@ var personalDobSchema = import_joi9.default.object({
358
381
  "dob.yearLength": "Enter a year with 4 numbers, like 1975",
359
382
  "dob.invalid": "Date of birth must be a real date",
360
383
  "dob.future": "Date of birth must be in the past",
361
- "dob.tooOld": "Date of birth must be on or after {{#oldest}}"
384
+ "dob.tooOld": "Date of birth must be on or after {{#oldest}}",
385
+ "string.noControlChars": "Date of birth must not contain invalid characters"
362
386
  });
363
387
  var checkNotTooOld = (fullDate, helpers) => {
364
388
  const oldestDateAllowed = getOldestAllowedDate();
@@ -398,7 +422,11 @@ var getMonthNumber = (month, helpers) => {
398
422
  }
399
423
  return mapped;
400
424
  }
401
- return Number.parseInt(month, 10);
425
+ const parsedMonth = Number.parseInt(month, 10);
426
+ if (parsedMonth < 1 || parsedMonth > 12) {
427
+ return makeError(helpers, "dob.invalid", ["month"]);
428
+ }
429
+ return parsedMonth;
402
430
  };
403
431
  var checkMissingFields = (day, month, year, helpers) => {
404
432
  if (!day && !month && !year) {
@@ -432,9 +460,8 @@ var makeError = (helpers, code, fields) => {
432
460
  };
433
461
 
434
462
  // src/schemas/personal/personal-email-schema.js
435
- var import_joi10 = __toESM(require("joi"), 1);
436
- var personalEmailSchema = import_joi10.default.object({
437
- personalEmail: import_joi10.default.string().required().max(EMAIL_MAX).email({
463
+ var personalEmailSchema = Joi.object({
464
+ personalEmail: Joi.string().required().max(EMAIL_MAX).email({
438
465
  minDomainSegments: 2,
439
466
  tlds: {
440
467
  allow: true,
@@ -443,22 +470,24 @@ var personalEmailSchema = import_joi10.default.object({
443
470
  }).messages({
444
471
  "string.max": `Email address must be ${EMAIL_MAX} characters or less`,
445
472
  "string.empty": "Enter a personal email address",
446
- "string.email": "Enter an email address, like name@example.com"
473
+ "string.email": "Enter an email address, like name@example.com",
474
+ "string.noControlChars": "Personal email address must not contain invalid characters"
447
475
  })
448
476
  });
449
477
 
450
478
  // src/schemas/personal/personal-phone-schema.js
451
- var import_joi11 = __toESM(require("joi"), 1);
452
- var personalPhoneSchema = import_joi11.default.object({
453
- personalTelephone: import_joi11.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
479
+ var personalPhoneSchema = Joi.object({
480
+ personalTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
454
481
  "string.min": `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
455
482
  "string.max": `Personal telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
456
- "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
483
+ "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
484
+ "string.noControlChars": "Personal telephone number must not contain invalid characters"
457
485
  }),
458
- personalMobile: import_joi11.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
486
+ personalMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
459
487
  "string.min": `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
460
488
  "string.max": `Personal mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
461
- "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
489
+ "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
490
+ "string.noControlChars": "Personal mobile phone number must not contain invalid characters"
462
491
  })
463
492
  }).or("personalTelephone", "personalMobile").messages({
464
493
  "object.missing": "Enter at least one phone number"
package/dist/index.js CHANGED
@@ -91,17 +91,36 @@ var mappers = {
91
91
  businessDetails: mapBusinessDetails
92
92
  };
93
93
 
94
+ // src/utils/joi.js
95
+ import BaseJoi from "joi";
96
+
97
+ // src/constants/patterns.js
98
+ var PHONE_NUMBER_PATTERN = /^\+?(?=[\d ()]*\d)[\d ()]*$/;
99
+ var NO_CONTROL_CHARS_PATTERN = /^[^\x00-\x1f\x7f-\x9f]*$/;
100
+
101
+ // src/utils/joi.js
102
+ var Joi = BaseJoi.extend((joi) => ({
103
+ type: "string",
104
+ base: joi.string(),
105
+ messages: {
106
+ "string.noControlChars": "Field must not contain invalid characters"
107
+ },
108
+ validate(value, helpers) {
109
+ if (typeof value === "string" && !NO_CONTROL_CHARS_PATTERN.test(value)) {
110
+ return { value, errors: helpers.error("string.noControlChars") };
111
+ }
112
+ return { value };
113
+ }
114
+ }));
115
+
94
116
  // src/schemas/business/business-sbi-schema.js
95
- import Joi from "joi";
96
117
  var businessSbiSchema = Joi.object({
97
118
  sbi: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
98
- "string.pattern.base": "Enter the full SBI"
119
+ "string.pattern.base": "Enter the full SBI",
120
+ "string.noControlChars": "SBI must not contain invalid characters"
99
121
  })
100
122
  });
101
123
 
102
- // src/schemas/address-schema.js
103
- import Joi2 from "joi";
104
-
105
124
  // src/constants/validation-fields.js
106
125
  var FIRST_NAME_MAX = 100;
107
126
  var LAST_NAME_MAX = 100;
@@ -117,53 +136,59 @@ var COUNTY_MAX = 60;
117
136
  var COUNTRY_MAX = 60;
118
137
  var POSTCODE_MAX = 8;
119
138
 
120
- // src/schemas/address-schema.js
121
- var addressSchema = Joi2.object({
122
- address1: Joi2.string().required().max(ADDRESS_LINE_MAX).messages({
139
+ // src/schemas/shared/address-schema.js
140
+ var addressSchema = Joi.object({
141
+ address1: Joi.string().trim().required().max(ADDRESS_LINE_MAX).messages({
123
142
  "string.empty": "Enter address line 1, typically the building and street",
124
143
  "string.max": `Address line 1 must be ${ADDRESS_LINE_MAX} characters or less`,
125
- "any.required": "Enter address line 1, typically the building and street"
144
+ "any.required": "Enter address line 1, typically the building and street",
145
+ "string.noControlChars": "Address line 1 must not contain invalid characters"
126
146
  }),
127
- address2: Joi2.string().allow("").max(ADDRESS_LINE_MAX).messages({
128
- "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`
147
+ address2: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
148
+ "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`,
149
+ "string.noControlChars": "Address line 2 must not contain invalid characters"
129
150
  }),
130
- address3: Joi2.string().allow("").max(ADDRESS_LINE_MAX).messages({
131
- "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`
151
+ address3: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
152
+ "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`,
153
+ "string.noControlChars": "Address line 3 must not contain invalid characters"
132
154
  }),
133
- city: Joi2.string().required().max(TOWN_CITY_MAX).messages({
155
+ city: Joi.string().trim().required().max(TOWN_CITY_MAX).messages({
134
156
  "string.empty": "Enter town or city",
135
157
  "string.max": `Town or city must be ${TOWN_CITY_MAX} characters or less`,
136
- "any.required": "Enter town or city"
158
+ "any.required": "Enter town or city",
159
+ "string.noControlChars": "Town or city must not contain invalid characters"
137
160
  }),
138
- county: Joi2.string().allow("").max(COUNTY_MAX).messages({
139
- "string.max": `County must be ${COUNTY_MAX} characters or less`
161
+ county: Joi.string().trim().allow("").max(COUNTY_MAX).messages({
162
+ "string.max": `County must be ${COUNTY_MAX} characters or less`,
163
+ "string.noControlChars": "County must not contain invalid characters"
140
164
  }),
141
- postcode: Joi2.string().required().max(POSTCODE_MAX).messages({
165
+ postcode: Joi.string().trim().required().max(POSTCODE_MAX).messages({
142
166
  "any.required": "Enter a postal code or zip code",
143
167
  "string.empty": "Enter a postal code or zip code",
144
- "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`
168
+ "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`,
169
+ "string.noControlChars": "Postal code or zip code must not contain invalid characters"
145
170
  }),
146
- country: Joi2.string().required().max(COUNTRY_MAX).messages({
171
+ country: Joi.string().trim().required().max(COUNTRY_MAX).messages({
147
172
  "string.empty": "Enter a country",
148
173
  "string.max": `Country must be ${COUNTRY_MAX} characters or less`,
149
- "any.required": "Enter a country"
174
+ "any.required": "Enter a country",
175
+ "string.noControlChars": "Country must not contain invalid characters"
150
176
  })
151
177
  });
152
178
 
153
179
  // src/schemas/business/business-name-schema.js
154
- import Joi3 from "joi";
155
- var businessNameSchema = Joi3.object({
156
- businessName: Joi3.string().required().max(BUSINESS_NAME_MAX).messages({
180
+ var businessNameSchema = Joi.object({
181
+ businessName: Joi.string().trim().required().max(BUSINESS_NAME_MAX).messages({
157
182
  "string.empty": "Enter business name",
158
183
  "string.max": `Business name must be ${BUSINESS_NAME_MAX} characters or less`,
159
- "any.required": "Enter business name"
184
+ "any.required": "Enter business name",
185
+ "string.noControlChars": "Business name must not contain invalid characters"
160
186
  })
161
187
  });
162
188
 
163
189
  // src/schemas/business/business-email-schema.js
164
- import Joi4 from "joi";
165
- var businessEmailSchema = Joi4.object({
166
- businessEmail: Joi4.string().required().max(EMAIL_MAX).email({
190
+ var businessEmailSchema = Joi.object({
191
+ businessEmail: Joi.string().required().max(EMAIL_MAX).email({
167
192
  minDomainSegments: 2,
168
193
  tlds: {
169
194
  allow: true
@@ -171,37 +196,34 @@ var businessEmailSchema = Joi4.object({
171
196
  }).messages({
172
197
  "string.max": `Business email address must be ${EMAIL_MAX} characters or less`,
173
198
  "string.empty": "Enter business email address",
174
- "string.email": "Enter an email address, like name@example.com"
199
+ "string.email": "Enter an email address, like name@example.com",
200
+ "string.noControlChars": "Business email address must not contain invalid characters"
175
201
  })
176
202
  });
177
203
 
178
204
  // src/schemas/business/business-phone-schema.js
179
- import Joi5 from "joi";
180
-
181
- // src/constants/patterns.js
182
- var PHONE_NUMBER_PATTERN = /^\+?[0-9 ()]+$/;
183
-
184
- // src/schemas/business/business-phone-schema.js
185
- var businessPhoneSchema = Joi5.object({
186
- businessTelephone: Joi5.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
205
+ var businessPhoneSchema = Joi.object({
206
+ businessTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
187
207
  "string.min": `Business telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
188
208
  "string.max": `Business telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
189
- "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
209
+ "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
210
+ "string.noControlChars": "Business telephone number must not contain invalid characters"
190
211
  }),
191
- businessMobile: Joi5.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
212
+ businessMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
192
213
  "string.min": `Business mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
193
214
  "string.max": `Business mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
194
- "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
215
+ "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
216
+ "string.noControlChars": "Business mobile phone number must not contain invalid characters"
195
217
  })
196
218
  }).or("businessTelephone", "businessMobile").messages({
197
219
  "object.missing": "Enter at least one phone number"
198
220
  });
199
221
 
200
222
  // src/schemas/business/business-vat-schema.js
201
- import Joi6 from "joi";
202
- var businessVatSchema = Joi6.object({
203
- vatNumber: Joi6.string().pattern(/^\d{9}$/).allow("").optional().messages({
204
- "string.pattern.base": "Enter a VAT registration number, like 123456789"
223
+ var businessVatSchema = Joi.object({
224
+ vatNumber: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
225
+ "string.pattern.base": "Enter a VAT registration number, like 123456789",
226
+ "string.noControlChars": "VAT registration number must not contain invalid characters"
205
227
  })
206
228
  });
207
229
 
@@ -218,10 +240,10 @@ var businessSchemas = {
218
240
  };
219
241
 
220
242
  // src/schemas/customer/customer-crn-schema.js
221
- import Joi7 from "joi";
222
- var customerCrnSchema = Joi7.object({
223
- crn: Joi7.string().pattern(/^\d{10}$/).allow("").optional().messages({
224
- "string.pattern.base": "Enter the full CRN"
243
+ var customerCrnSchema = Joi.object({
244
+ crn: Joi.string().pattern(/^\d{10}$/).allow("").optional().messages({
245
+ "string.pattern.base": "Enter the full CRN",
246
+ "string.noControlChars": "CRN must not contain invalid characters"
225
247
  })
226
248
  });
227
249
 
@@ -231,26 +253,25 @@ var customerSchemas = {
231
253
  };
232
254
 
233
255
  // src/schemas/personal/personal-name-schema.js
234
- import Joi8 from "joi";
235
- var personalNameSchema = Joi8.object({
236
- first: Joi8.string().required().max(FIRST_NAME_MAX).messages({
256
+ var personalNameSchema = Joi.object({
257
+ first: Joi.string().trim().required().max(FIRST_NAME_MAX).messages({
237
258
  "string.empty": "Enter first name",
238
259
  "string.max": `First name must be ${FIRST_NAME_MAX} characters or less`,
239
- "any.required": "Enter first name"
260
+ "any.required": "Enter first name",
261
+ "string.noControlChars": "First name must not contain invalid characters"
240
262
  }),
241
- last: Joi8.string().required().max(LAST_NAME_MAX).messages({
263
+ last: Joi.string().trim().required().max(LAST_NAME_MAX).messages({
242
264
  "string.empty": "Enter last name",
243
265
  "string.max": `Last name must be ${LAST_NAME_MAX} characters or less`,
244
- "any.required": "Enter last name"
266
+ "any.required": "Enter last name",
267
+ "string.noControlChars": "Last name must not contain invalid characters"
245
268
  }),
246
- middle: Joi8.string().allow("").max(MIDDLE_NAMES_MAX).messages({
247
- "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`
269
+ middle: Joi.string().trim().allow("").max(MIDDLE_NAMES_MAX).messages({
270
+ "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`,
271
+ "string.noControlChars": "Middle names must not contain invalid characters"
248
272
  })
249
273
  });
250
274
 
251
- // src/schemas/personal/personal-dob-schema.js
252
- import Joi9 from "joi";
253
-
254
275
  // src/constants/month-map.js
255
276
  var MONTH_MAP = {
256
277
  january: 1,
@@ -280,10 +301,10 @@ var MONTH_MAP = {
280
301
  };
281
302
 
282
303
  // src/schemas/personal/personal-dob-schema.js
283
- var personalDobSchema = Joi9.object({
284
- day: Joi9.string().allow(""),
285
- month: Joi9.string().allow(""),
286
- year: Joi9.string().allow("")
304
+ var personalDobSchema = Joi.object({
305
+ day: Joi.string().allow(""),
306
+ month: Joi.string().allow(""),
307
+ year: Joi.string().allow("")
287
308
  }).custom((value, helpers) => {
288
309
  const { day, month, year } = value;
289
310
  const missingFieldsError = checkMissingFields(day, month, year, helpers);
@@ -301,7 +322,9 @@ var personalDobSchema = Joi9.object({
301
322
  if (fullDate.isJoiError) {
302
323
  return fullDate;
303
324
  }
304
- if (fullDate > /* @__PURE__ */ new Date()) {
325
+ const today = /* @__PURE__ */ new Date();
326
+ today.setUTCHours(0, 0, 0, 0);
327
+ if (fullDate >= today) {
305
328
  return makeError(helpers, "dob.future", ["day", "month", "year"]);
306
329
  }
307
330
  const tooOldError = checkNotTooOld(fullDate, helpers);
@@ -320,7 +343,8 @@ var personalDobSchema = Joi9.object({
320
343
  "dob.yearLength": "Enter a year with 4 numbers, like 1975",
321
344
  "dob.invalid": "Date of birth must be a real date",
322
345
  "dob.future": "Date of birth must be in the past",
323
- "dob.tooOld": "Date of birth must be on or after {{#oldest}}"
346
+ "dob.tooOld": "Date of birth must be on or after {{#oldest}}",
347
+ "string.noControlChars": "Date of birth must not contain invalid characters"
324
348
  });
325
349
  var checkNotTooOld = (fullDate, helpers) => {
326
350
  const oldestDateAllowed = getOldestAllowedDate();
@@ -360,7 +384,11 @@ var getMonthNumber = (month, helpers) => {
360
384
  }
361
385
  return mapped;
362
386
  }
363
- return Number.parseInt(month, 10);
387
+ const parsedMonth = Number.parseInt(month, 10);
388
+ if (parsedMonth < 1 || parsedMonth > 12) {
389
+ return makeError(helpers, "dob.invalid", ["month"]);
390
+ }
391
+ return parsedMonth;
364
392
  };
365
393
  var checkMissingFields = (day, month, year, helpers) => {
366
394
  if (!day && !month && !year) {
@@ -394,9 +422,8 @@ var makeError = (helpers, code, fields) => {
394
422
  };
395
423
 
396
424
  // src/schemas/personal/personal-email-schema.js
397
- import Joi10 from "joi";
398
- var personalEmailSchema = Joi10.object({
399
- personalEmail: Joi10.string().required().max(EMAIL_MAX).email({
425
+ var personalEmailSchema = Joi.object({
426
+ personalEmail: Joi.string().required().max(EMAIL_MAX).email({
400
427
  minDomainSegments: 2,
401
428
  tlds: {
402
429
  allow: true,
@@ -405,22 +432,24 @@ var personalEmailSchema = Joi10.object({
405
432
  }).messages({
406
433
  "string.max": `Email address must be ${EMAIL_MAX} characters or less`,
407
434
  "string.empty": "Enter a personal email address",
408
- "string.email": "Enter an email address, like name@example.com"
435
+ "string.email": "Enter an email address, like name@example.com",
436
+ "string.noControlChars": "Personal email address must not contain invalid characters"
409
437
  })
410
438
  });
411
439
 
412
440
  // src/schemas/personal/personal-phone-schema.js
413
- import Joi11 from "joi";
414
- var personalPhoneSchema = Joi11.object({
415
- personalTelephone: Joi11.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
441
+ var personalPhoneSchema = Joi.object({
442
+ personalTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
416
443
  "string.min": `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
417
444
  "string.max": `Personal telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
418
- "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
445
+ "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
446
+ "string.noControlChars": "Personal telephone number must not contain invalid characters"
419
447
  }),
420
- personalMobile: Joi11.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
448
+ personalMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
421
449
  "string.min": `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
422
450
  "string.max": `Personal mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
423
- "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
451
+ "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
452
+ "string.noControlChars": "Personal mobile phone number must not contain invalid characters"
424
453
  })
425
454
  }).or("personalTelephone", "personalMobile").messages({
426
455
  "object.missing": "Enter at least one phone number"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/fcp-sfd-frontend-engine",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Shared frontend engine used by both the internal and external frontend applications.",
5
5
  "main": "./dist/index.cjs",
6
6
  "exports": {
@@ -38,11 +38,11 @@
38
38
  },
39
39
  "homepage": "https://github.com/DEFRA/fcp-sfd-frontend-engine#readme",
40
40
  "devDependencies": {
41
- "@vitest/coverage-v8": "4.1.8",
41
+ "@vitest/coverage-v8": "4.1.10",
42
42
  "eslint": "9.39.2",
43
43
  "neostandard": "0.13.0",
44
44
  "tsup": "8.5.1",
45
- "vitest": "4.1.8"
45
+ "vitest": "4.1.10"
46
46
  },
47
47
  "dependencies": {
48
48
  "joi": "18.2.1"
@@ -10,7 +10,8 @@ export {
10
10
  TOWN_CITY_MAX,
11
11
  COUNTY_MAX,
12
12
  COUNTRY_MAX,
13
- POSTCODE_MAX,
14
- PHONE_NUMBER_PATTERN,
15
- MONTH_MAP
16
- } from './validation-fields-export.js'
13
+ POSTCODE_MAX
14
+ } from './validation-fields.js'
15
+
16
+ export { PHONE_NUMBER_PATTERN } from './patterns.js'
17
+ export { MONTH_MAP } from './month-map.js'
@@ -1 +1,9 @@
1
- export const PHONE_NUMBER_PATTERN = /^\+?[0-9 ()]+$/
1
+ // Allows digits, spaces, brackets, and an optional leading +, but requires at least one digit.
2
+ // The (?=...) part checks a digit exists first for efficiency and time complexity.
3
+ export const PHONE_NUMBER_PATTERN = /^\+?(?=[\d ()]*\d)[\d ()]*$/
4
+
5
+ // Rejects control characters — C0 (0x00–0x1f), DEL (0x7f) and C1 (0x80–0x9f) — which have no valid
6
+ // use in text input fields. The Unicode C1 block contains only control characters, so this does not
7
+ // reject legitimate input — accented letters, symbols and NBSP all sit at U+00A0 and above.
8
+ // eslint-disable-next-line no-control-regex
9
+ export const NO_CONTROL_CHARS_PATTERN = /^[^\x00-\x1f\x7f-\x9f]*$/
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
  import { EMAIL_MAX } from '../../constants/validation-fields.js'
3
3
 
4
4
  export const businessEmailSchema = Joi.object({
@@ -14,6 +14,7 @@ export const businessEmailSchema = Joi.object({
14
14
  .messages({
15
15
  'string.max': `Business email address must be ${EMAIL_MAX} characters or less`,
16
16
  'string.empty': 'Enter business email address',
17
- 'string.email': 'Enter an email address, like name@example.com'
17
+ 'string.email': 'Enter an email address, like name@example.com',
18
+ 'string.noControlChars': 'Business email address must not contain invalid characters'
18
19
  })
19
20
  })
@@ -1,13 +1,15 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
  import { BUSINESS_NAME_MAX } from '../../constants/validation-fields.js'
3
3
 
4
4
  export const businessNameSchema = Joi.object({
5
5
  businessName: Joi.string()
6
+ .trim()
6
7
  .required()
7
8
  .max(BUSINESS_NAME_MAX)
8
9
  .messages({
9
10
  'string.empty': 'Enter business name',
10
11
  'string.max': `Business name must be ${BUSINESS_NAME_MAX} characters or less`,
11
- 'any.required': 'Enter business name'
12
+ 'any.required': 'Enter business name',
13
+ 'string.noControlChars': 'Business name must not contain invalid characters'
12
14
  })
13
15
  })
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
  import {
3
3
  PHONE_NUMBER_MIN,
4
4
  PHONE_NUMBER_MAX
@@ -7,6 +7,7 @@ import { PHONE_NUMBER_PATTERN } from '../../constants/patterns.js'
7
7
 
8
8
  export const businessPhoneSchema = Joi.object({
9
9
  businessTelephone: Joi.string()
10
+ .trim()
10
11
  .empty('')
11
12
  .min(PHONE_NUMBER_MIN)
12
13
  .max(PHONE_NUMBER_MAX)
@@ -14,9 +15,11 @@ export const businessPhoneSchema = Joi.object({
14
15
  .messages({
15
16
  'string.min': `Business telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
16
17
  'string.max': `Business telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
17
- 'string.pattern.base': 'Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +'
18
+ 'string.pattern.base': 'Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +',
19
+ 'string.noControlChars': 'Business telephone number must not contain invalid characters'
18
20
  }),
19
21
  businessMobile: Joi.string()
22
+ .trim()
20
23
  .empty('')
21
24
  .min(PHONE_NUMBER_MIN)
22
25
  .max(PHONE_NUMBER_MAX)
@@ -24,7 +27,8 @@ export const businessPhoneSchema = Joi.object({
24
27
  .messages({
25
28
  'string.min': `Business mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
26
29
  'string.max': `Business mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
27
- 'string.pattern.base': 'Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +'
30
+ 'string.pattern.base': 'Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +',
31
+ 'string.noControlChars': 'Business mobile phone number must not contain invalid characters'
28
32
  })
29
33
  })
30
34
  .or('businessTelephone', 'businessMobile')
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
 
3
3
  export const businessSbiSchema = Joi.object({
4
4
  sbi: Joi.string()
@@ -6,6 +6,7 @@ export const businessSbiSchema = Joi.object({
6
6
  .allow('')
7
7
  .optional()
8
8
  .messages({
9
- 'string.pattern.base': 'Enter the full SBI'
9
+ 'string.pattern.base': 'Enter the full SBI',
10
+ 'string.noControlChars': 'SBI must not contain invalid characters'
10
11
  })
11
12
  })
@@ -1,5 +1,5 @@
1
1
  import { businessSbiSchema } from './business-sbi-schema.js'
2
- import { addressSchema } from '../address-schema.js'
2
+ import { addressSchema } from '../shared/address-schema.js'
3
3
  import { businessNameSchema } from './business-name-schema.js'
4
4
  import { businessEmailSchema } from './business-email-schema.js'
5
5
  import { businessPhoneSchema } from './business-phone-schema.js'
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
 
3
3
  export const businessVatSchema = Joi.object({
4
4
  vatNumber: Joi.string()
@@ -6,6 +6,7 @@ export const businessVatSchema = Joi.object({
6
6
  .allow('')
7
7
  .optional()
8
8
  .messages({
9
- 'string.pattern.base': 'Enter a VAT registration number, like 123456789'
9
+ 'string.pattern.base': 'Enter a VAT registration number, like 123456789',
10
+ 'string.noControlChars': 'VAT registration number must not contain invalid characters'
10
11
  })
11
12
  })
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
 
3
3
  export const customerCrnSchema = Joi.object({
4
4
  crn: Joi.string()
@@ -6,6 +6,7 @@ export const customerCrnSchema = Joi.object({
6
6
  .allow('')
7
7
  .optional()
8
8
  .messages({
9
- 'string.pattern.base': 'Enter the full CRN'
9
+ 'string.pattern.base': 'Enter the full CRN',
10
+ 'string.noControlChars': 'CRN must not contain invalid characters'
10
11
  })
11
12
  })
@@ -1,5 +1,6 @@
1
- import Joi from 'joi'
2
- import { MONTH_MAP, MAX_AGE_YEARS } from '../../constants/validation-fields-export.js'
1
+ import { Joi } from '../../utils/joi.js'
2
+ import { MAX_AGE_YEARS } from '../../constants/validation-fields.js'
3
+ import { MONTH_MAP } from '../../constants/month-map.js'
3
4
 
4
5
  export const personalDobSchema = Joi.object({
5
6
  day: Joi.string().allow(''),
@@ -32,7 +33,9 @@ export const personalDobSchema = Joi.object({
32
33
  }
33
34
 
34
35
  // Date must be in the past
35
- if (fullDate > new Date()) {
36
+ const today = new Date()
37
+ today.setUTCHours(0, 0, 0, 0)
38
+ if (fullDate >= today) {
36
39
  return makeError(helpers, 'dob.future', ['day', 'month', 'year'])
37
40
  }
38
41
 
@@ -54,7 +57,8 @@ export const personalDobSchema = Joi.object({
54
57
  'dob.yearLength': 'Enter a year with 4 numbers, like 1975',
55
58
  'dob.invalid': 'Date of birth must be a real date',
56
59
  'dob.future': 'Date of birth must be in the past',
57
- 'dob.tooOld': 'Date of birth must be on or after {{#oldest}}'
60
+ 'dob.tooOld': 'Date of birth must be on or after {{#oldest}}',
61
+ 'string.noControlChars': 'Date of birth must not contain invalid characters'
58
62
  })
59
63
 
60
64
  const checkNotTooOld = (fullDate, helpers) => {
@@ -119,7 +123,7 @@ const getFullDate = (day, monthValue, year, helpers) => {
119
123
  *
120
124
  * If a valid mapping is found, the mapped number is returned.
121
125
  * If no mapping exists, an error is returned via `makeError`, as the input is likely invalid.
122
- * If the month is already numeric, it is simply parsed and returned as a number.
126
+ * If the month is already numeric, it is parsed and validated to be within the range 1–12.
123
127
  */
124
128
  const getMonthNumber = (month, helpers) => {
125
129
  if (Number.isNaN(Number(month))) {
@@ -133,7 +137,13 @@ const getMonthNumber = (month, helpers) => {
133
137
  return mapped
134
138
  }
135
139
 
136
- return Number.parseInt(month, 10)
140
+ const parsedMonth = Number.parseInt(month, 10)
141
+
142
+ if (parsedMonth < 1 || parsedMonth > 12) {
143
+ return makeError(helpers, 'dob.invalid', ['month'])
144
+ }
145
+
146
+ return parsedMonth
137
147
  }
138
148
 
139
149
  /**
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
  import { EMAIL_MAX } from '../../constants/validation-fields.js'
3
3
 
4
4
  export const personalEmailSchema = Joi.object({
@@ -15,6 +15,7 @@ export const personalEmailSchema = Joi.object({
15
15
  .messages({
16
16
  'string.max': `Email address must be ${EMAIL_MAX} characters or less`,
17
17
  'string.empty': 'Enter a personal email address',
18
- 'string.email': 'Enter an email address, like name@example.com'
18
+ 'string.email': 'Enter an email address, like name@example.com',
19
+ 'string.noControlChars': 'Personal email address must not contain invalid characters'
19
20
  })
20
21
  })
@@ -1,4 +1,4 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
  import {
3
3
  FIRST_NAME_MAX,
4
4
  LAST_NAME_MAX,
@@ -7,25 +7,31 @@ import {
7
7
 
8
8
  export const personalNameSchema = Joi.object({
9
9
  first: Joi.string()
10
+ .trim()
10
11
  .required()
11
12
  .max(FIRST_NAME_MAX)
12
13
  .messages({
13
14
  'string.empty': 'Enter first name',
14
15
  'string.max': `First name must be ${FIRST_NAME_MAX} characters or less`,
15
- 'any.required': 'Enter first name'
16
+ 'any.required': 'Enter first name',
17
+ 'string.noControlChars': 'First name must not contain invalid characters'
16
18
  }),
17
19
  last: Joi.string()
20
+ .trim()
18
21
  .required()
19
22
  .max(LAST_NAME_MAX)
20
23
  .messages({
21
24
  'string.empty': 'Enter last name',
22
25
  'string.max': `Last name must be ${LAST_NAME_MAX} characters or less`,
23
- 'any.required': 'Enter last name'
26
+ 'any.required': 'Enter last name',
27
+ 'string.noControlChars': 'Last name must not contain invalid characters'
24
28
  }),
25
29
  middle: Joi.string()
30
+ .trim()
26
31
  .allow('')
27
32
  .max(MIDDLE_NAMES_MAX)
28
33
  .messages({
29
- 'string.max': `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`
34
+ 'string.max': `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`,
35
+ 'string.noControlChars': 'Middle names must not contain invalid characters'
30
36
  })
31
37
  })
@@ -1,12 +1,10 @@
1
- import Joi from 'joi'
2
- import {
3
- PHONE_NUMBER_MIN,
4
- PHONE_NUMBER_MAX,
5
- PHONE_NUMBER_PATTERN
6
- } from '../../constants/validation-fields-export.js'
1
+ import { Joi } from '../../utils/joi.js'
2
+ import { PHONE_NUMBER_MIN, PHONE_NUMBER_MAX } from '../../constants/validation-fields.js'
3
+ import { PHONE_NUMBER_PATTERN } from '../../constants/patterns.js'
7
4
 
8
5
  export const personalPhoneSchema = Joi.object({
9
6
  personalTelephone: Joi.string()
7
+ .trim()
10
8
  .empty('')
11
9
  .min(PHONE_NUMBER_MIN)
12
10
  .max(PHONE_NUMBER_MAX)
@@ -14,9 +12,11 @@ export const personalPhoneSchema = Joi.object({
14
12
  .messages({
15
13
  'string.min': `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
16
14
  'string.max': `Personal telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
17
- 'string.pattern.base': 'Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +'
15
+ 'string.pattern.base': 'Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +',
16
+ 'string.noControlChars': 'Personal telephone number must not contain invalid characters'
18
17
  }),
19
18
  personalMobile: Joi.string()
19
+ .trim()
20
20
  .empty('')
21
21
  .min(PHONE_NUMBER_MIN)
22
22
  .max(PHONE_NUMBER_MAX)
@@ -24,7 +24,8 @@ export const personalPhoneSchema = Joi.object({
24
24
  .messages({
25
25
  'string.min': `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
26
26
  'string.max': `Personal mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
27
- 'string.pattern.base': 'Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +'
27
+ 'string.pattern.base': 'Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +',
28
+ 'string.noControlChars': 'Personal mobile phone number must not contain invalid characters'
28
29
  })
29
30
  })
30
31
  .or('personalTelephone', 'personalMobile')
@@ -1,6 +1,6 @@
1
1
  import { personalNameSchema } from './personal-name-schema.js'
2
2
  import { personalDobSchema } from './personal-dob-schema.js'
3
- import { addressSchema } from '../address-schema.js'
3
+ import { addressSchema } from '../shared/address-schema.js'
4
4
  import { personalEmailSchema } from './personal-email-schema.js'
5
5
  import { personalPhoneSchema } from './personal-phone-schema.js'
6
6
 
@@ -1,61 +1,75 @@
1
- import Joi from 'joi'
1
+ import { Joi } from '../../utils/joi.js'
2
2
  import {
3
3
  ADDRESS_LINE_MAX,
4
4
  TOWN_CITY_MAX,
5
5
  COUNTY_MAX,
6
6
  POSTCODE_MAX,
7
7
  COUNTRY_MAX
8
- } from '../constants/validation-fields.js'
8
+ } from '../../constants/validation-fields.js'
9
9
 
10
10
  export const addressSchema = Joi.object({
11
11
  address1: Joi.string()
12
+ .trim()
12
13
  .required()
13
14
  .max(ADDRESS_LINE_MAX)
14
15
  .messages({
15
16
  'string.empty': 'Enter address line 1, typically the building and street',
16
17
  'string.max': `Address line 1 must be ${ADDRESS_LINE_MAX} characters or less`,
17
- 'any.required': 'Enter address line 1, typically the building and street'
18
+ 'any.required': 'Enter address line 1, typically the building and street',
19
+ 'string.noControlChars': 'Address line 1 must not contain invalid characters'
18
20
  }),
19
21
  address2: Joi.string()
22
+ .trim()
20
23
  .allow('')
21
24
  .max(ADDRESS_LINE_MAX)
22
25
  .messages({
23
- 'string.max': `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`
26
+ 'string.max': `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`,
27
+ 'string.noControlChars': 'Address line 2 must not contain invalid characters'
24
28
  }),
25
29
  address3: Joi.string()
30
+ .trim()
26
31
  .allow('')
27
32
  .max(ADDRESS_LINE_MAX)
28
33
  .messages({
29
- 'string.max': `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`
34
+ 'string.max': `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`,
35
+ 'string.noControlChars': 'Address line 3 must not contain invalid characters'
30
36
  }),
31
37
  city: Joi.string()
38
+ .trim()
32
39
  .required()
33
40
  .max(TOWN_CITY_MAX)
34
41
  .messages({
35
42
  'string.empty': 'Enter town or city',
36
43
  'string.max': `Town or city must be ${TOWN_CITY_MAX} characters or less`,
37
- 'any.required': 'Enter town or city'
44
+ 'any.required': 'Enter town or city',
45
+ 'string.noControlChars': 'Town or city must not contain invalid characters'
38
46
  }),
39
47
  county: Joi.string()
48
+ .trim()
40
49
  .allow('')
41
50
  .max(COUNTY_MAX)
42
51
  .messages({
43
- 'string.max': `County must be ${COUNTY_MAX} characters or less`
52
+ 'string.max': `County must be ${COUNTY_MAX} characters or less`,
53
+ 'string.noControlChars': 'County must not contain invalid characters'
44
54
  }),
45
55
  postcode: Joi.string()
56
+ .trim()
46
57
  .required()
47
58
  .max(POSTCODE_MAX)
48
59
  .messages({
49
60
  'any.required': 'Enter a postal code or zip code',
50
61
  'string.empty': 'Enter a postal code or zip code',
51
- 'string.max': `Postal code or zip code must be ${POSTCODE_MAX} characters or less`
62
+ 'string.max': `Postal code or zip code must be ${POSTCODE_MAX} characters or less`,
63
+ 'string.noControlChars': 'Postal code or zip code must not contain invalid characters'
52
64
  }),
53
65
  country: Joi.string()
66
+ .trim()
54
67
  .required()
55
68
  .max(COUNTRY_MAX)
56
69
  .messages({
57
70
  'string.empty': 'Enter a country',
58
71
  'string.max': `Country must be ${COUNTRY_MAX} characters or less`,
59
- 'any.required': 'Enter a country'
72
+ 'any.required': 'Enter a country',
73
+ 'string.noControlChars': 'Country must not contain invalid characters'
60
74
  })
61
75
  })
@@ -0,0 +1,21 @@
1
+ import BaseJoi from 'joi'
2
+ import { NO_CONTROL_CHARS_PATTERN } from '../constants/patterns.js'
3
+
4
+ // Extends Joi's string type so every Joi.string() automatically rejects ASCII control
5
+ // characters (0x00–0x1f and 0x7f), which have no valid use in text input fields. Using a
6
+ // dedicated 'string.noControlChars' error keeps it distinct from the generic pattern rule
7
+ // used by numeric and phone fields. Fields may override the message via their own .messages().
8
+ export const Joi = BaseJoi.extend((joi) => ({
9
+ type: 'string',
10
+ base: joi.string(),
11
+ messages: {
12
+ 'string.noControlChars': 'Field must not contain invalid characters'
13
+ },
14
+ validate (value, helpers) {
15
+ if (typeof value === 'string' && !NO_CONTROL_CHARS_PATTERN.test(value)) {
16
+ return { value, errors: helpers.error('string.noControlChars') }
17
+ }
18
+
19
+ return { value }
20
+ }
21
+ }))
@@ -1,17 +0,0 @@
1
- export {
2
- FIRST_NAME_MAX,
3
- LAST_NAME_MAX,
4
- MIDDLE_NAMES_MAX,
5
- EMAIL_MAX,
6
- PHONE_NUMBER_MIN,
7
- PHONE_NUMBER_MAX,
8
- MAX_AGE_YEARS,
9
- ADDRESS_LINE_MAX,
10
- TOWN_CITY_MAX,
11
- COUNTY_MAX,
12
- COUNTRY_MAX,
13
- POSTCODE_MAX
14
- } from './validation-fields.js'
15
-
16
- export { PHONE_NUMBER_PATTERN } from './patterns.js'
17
- export { MONTH_MAP } from './month-map.js'