@defra/fcp-sfd-frontend-engine 0.6.1 → 0.8.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
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ constants: () => constants,
32
33
  mappers: () => mappers,
33
34
  presenters: () => presenters,
34
35
  schemas: () => schemas,
@@ -129,17 +130,36 @@ var mappers = {
129
130
  businessDetails: mapBusinessDetails
130
131
  };
131
132
 
132
- // src/schemas/business/business-sbi-schema.js
133
+ // src/utils/joi.js
133
134
  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"
135
+
136
+ // src/constants/patterns.js
137
+ var PHONE_NUMBER_PATTERN = /^\+?(?=[\d ()]*\d)[\d ()]*$/;
138
+ var NO_CONTROL_CHARS_PATTERN = /^[^\x00-\x1f\x7f-\x9f]*$/;
139
+
140
+ // src/utils/joi.js
141
+ var Joi = import_joi.default.extend((joi) => ({
142
+ type: "string",
143
+ base: joi.string(),
144
+ messages: {
145
+ "string.noControlChars": "Field must not contain invalid characters"
146
+ },
147
+ validate(value, helpers) {
148
+ if (typeof value === "string" && !NO_CONTROL_CHARS_PATTERN.test(value)) {
149
+ return { value, errors: helpers.error("string.noControlChars") };
150
+ }
151
+ return { value };
152
+ }
153
+ }));
154
+
155
+ // src/schemas/business/business-sbi-schema.js
156
+ var businessSbiSchema = Joi.object({
157
+ sbi: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
158
+ "string.pattern.base": "Enter the full SBI",
159
+ "string.noControlChars": "SBI must not contain invalid characters"
137
160
  })
138
161
  });
139
162
 
140
- // src/schemas/address-schema.js
141
- var import_joi2 = __toESM(require("joi"), 1);
142
-
143
163
  // src/constants/validation-fields.js
144
164
  var FIRST_NAME_MAX = 100;
145
165
  var LAST_NAME_MAX = 100;
@@ -155,53 +175,59 @@ var COUNTY_MAX = 60;
155
175
  var COUNTRY_MAX = 60;
156
176
  var POSTCODE_MAX = 8;
157
177
 
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({
178
+ // src/schemas/shared/address-schema.js
179
+ var addressSchema = Joi.object({
180
+ address1: Joi.string().trim().required().max(ADDRESS_LINE_MAX).messages({
161
181
  "string.empty": "Enter address line 1, typically the building and street",
162
182
  "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"
183
+ "any.required": "Enter address line 1, typically the building and street",
184
+ "string.noControlChars": "Address line 1 must not contain invalid characters"
164
185
  }),
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`
186
+ address2: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
187
+ "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`,
188
+ "string.noControlChars": "Address line 2 must not contain invalid characters"
167
189
  }),
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`
190
+ address3: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
191
+ "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`,
192
+ "string.noControlChars": "Address line 3 must not contain invalid characters"
170
193
  }),
171
- city: import_joi2.default.string().required().max(TOWN_CITY_MAX).messages({
194
+ city: Joi.string().trim().required().max(TOWN_CITY_MAX).messages({
172
195
  "string.empty": "Enter town or city",
173
196
  "string.max": `Town or city must be ${TOWN_CITY_MAX} characters or less`,
174
- "any.required": "Enter town or city"
197
+ "any.required": "Enter town or city",
198
+ "string.noControlChars": "Town or city must not contain invalid characters"
175
199
  }),
176
- county: import_joi2.default.string().allow("").max(COUNTY_MAX).messages({
177
- "string.max": `County must be ${COUNTY_MAX} characters or less`
200
+ county: Joi.string().trim().allow("").max(COUNTY_MAX).messages({
201
+ "string.max": `County must be ${COUNTY_MAX} characters or less`,
202
+ "string.noControlChars": "County must not contain invalid characters"
178
203
  }),
179
- postcode: import_joi2.default.string().required().max(POSTCODE_MAX).messages({
204
+ postcode: Joi.string().trim().required().max(POSTCODE_MAX).messages({
180
205
  "any.required": "Enter a postal code or zip code",
181
206
  "string.empty": "Enter a postal code or zip code",
182
- "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`
207
+ "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`,
208
+ "string.noControlChars": "Postal code or zip code must not contain invalid characters"
183
209
  }),
184
- country: import_joi2.default.string().required().max(COUNTRY_MAX).messages({
210
+ country: Joi.string().trim().required().max(COUNTRY_MAX).messages({
185
211
  "string.empty": "Enter a country",
186
212
  "string.max": `Country must be ${COUNTRY_MAX} characters or less`,
187
- "any.required": "Enter a country"
213
+ "any.required": "Enter a country",
214
+ "string.noControlChars": "Country must not contain invalid characters"
188
215
  })
189
216
  });
190
217
 
191
218
  // 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({
219
+ var businessNameSchema = Joi.object({
220
+ businessName: Joi.string().trim().required().max(BUSINESS_NAME_MAX).messages({
195
221
  "string.empty": "Enter business name",
196
222
  "string.max": `Business name must be ${BUSINESS_NAME_MAX} characters or less`,
197
- "any.required": "Enter business name"
223
+ "any.required": "Enter business name",
224
+ "string.noControlChars": "Business name must not contain invalid characters"
198
225
  })
199
226
  });
200
227
 
201
228
  // 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({
229
+ var businessEmailSchema = Joi.object({
230
+ businessEmail: Joi.string().required().max(EMAIL_MAX).email({
205
231
  minDomainSegments: 2,
206
232
  tlds: {
207
233
  allow: true
@@ -209,37 +235,34 @@ var businessEmailSchema = import_joi4.default.object({
209
235
  }).messages({
210
236
  "string.max": `Business email address must be ${EMAIL_MAX} characters or less`,
211
237
  "string.empty": "Enter business email address",
212
- "string.email": "Enter an email address, like name@example.com"
238
+ "string.email": "Enter an email address, like name@example.com",
239
+ "string.noControlChars": "Business email address must not contain invalid characters"
213
240
  })
214
241
  });
215
242
 
216
243
  // 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({
244
+ var businessPhoneSchema = Joi.object({
245
+ businessTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
225
246
  "string.min": `Business telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
226
247
  "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 +"
248
+ "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
249
+ "string.noControlChars": "Business telephone number must not contain invalid characters"
228
250
  }),
229
- businessMobile: import_joi5.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
251
+ businessMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
230
252
  "string.min": `Business mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
231
253
  "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 +"
254
+ "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
255
+ "string.noControlChars": "Business mobile phone number must not contain invalid characters"
233
256
  })
234
257
  }).or("businessTelephone", "businessMobile").messages({
235
258
  "object.missing": "Enter at least one phone number"
236
259
  });
237
260
 
238
261
  // 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"
262
+ var businessVatSchema = Joi.object({
263
+ vatNumber: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
264
+ "string.pattern.base": "Enter a VAT registration number, like 123456789",
265
+ "string.noControlChars": "VAT registration number must not contain invalid characters"
243
266
  })
244
267
  });
245
268
 
@@ -256,10 +279,10 @@ var businessSchemas = {
256
279
  };
257
280
 
258
281
  // 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"
282
+ var customerCrnSchema = Joi.object({
283
+ crn: Joi.string().pattern(/^\d{10}$/).allow("").optional().messages({
284
+ "string.pattern.base": "Enter the full CRN",
285
+ "string.noControlChars": "CRN must not contain invalid characters"
263
286
  })
264
287
  });
265
288
 
@@ -269,26 +292,25 @@ var customerSchemas = {
269
292
  };
270
293
 
271
294
  // 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({
295
+ var personalNameSchema = Joi.object({
296
+ first: Joi.string().trim().required().max(FIRST_NAME_MAX).messages({
275
297
  "string.empty": "Enter first name",
276
298
  "string.max": `First name must be ${FIRST_NAME_MAX} characters or less`,
277
- "any.required": "Enter first name"
299
+ "any.required": "Enter first name",
300
+ "string.noControlChars": "First name must not contain invalid characters"
278
301
  }),
279
- last: import_joi8.default.string().required().max(LAST_NAME_MAX).messages({
302
+ last: Joi.string().trim().required().max(LAST_NAME_MAX).messages({
280
303
  "string.empty": "Enter last name",
281
304
  "string.max": `Last name must be ${LAST_NAME_MAX} characters or less`,
282
- "any.required": "Enter last name"
305
+ "any.required": "Enter last name",
306
+ "string.noControlChars": "Last name must not contain invalid characters"
283
307
  }),
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`
308
+ middle: Joi.string().trim().allow("").max(MIDDLE_NAMES_MAX).messages({
309
+ "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`,
310
+ "string.noControlChars": "Middle names must not contain invalid characters"
286
311
  })
287
312
  });
288
313
 
289
- // src/schemas/personal/personal-dob-schema.js
290
- var import_joi9 = __toESM(require("joi"), 1);
291
-
292
314
  // src/constants/month-map.js
293
315
  var MONTH_MAP = {
294
316
  january: 1,
@@ -318,10 +340,10 @@ var MONTH_MAP = {
318
340
  };
319
341
 
320
342
  // 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("")
343
+ var personalDobSchema = Joi.object({
344
+ day: Joi.string().allow(""),
345
+ month: Joi.string().allow(""),
346
+ year: Joi.string().allow("")
325
347
  }).custom((value, helpers) => {
326
348
  const { day, month, year } = value;
327
349
  const missingFieldsError = checkMissingFields(day, month, year, helpers);
@@ -339,7 +361,9 @@ var personalDobSchema = import_joi9.default.object({
339
361
  if (fullDate.isJoiError) {
340
362
  return fullDate;
341
363
  }
342
- if (fullDate > /* @__PURE__ */ new Date()) {
364
+ const today = /* @__PURE__ */ new Date();
365
+ today.setUTCHours(0, 0, 0, 0);
366
+ if (fullDate >= today) {
343
367
  return makeError(helpers, "dob.future", ["day", "month", "year"]);
344
368
  }
345
369
  const tooOldError = checkNotTooOld(fullDate, helpers);
@@ -358,7 +382,8 @@ var personalDobSchema = import_joi9.default.object({
358
382
  "dob.yearLength": "Enter a year with 4 numbers, like 1975",
359
383
  "dob.invalid": "Date of birth must be a real date",
360
384
  "dob.future": "Date of birth must be in the past",
361
- "dob.tooOld": "Date of birth must be on or after {{#oldest}}"
385
+ "dob.tooOld": "Date of birth must be on or after {{#oldest}}",
386
+ "string.noControlChars": "Date of birth must not contain invalid characters"
362
387
  });
363
388
  var checkNotTooOld = (fullDate, helpers) => {
364
389
  const oldestDateAllowed = getOldestAllowedDate();
@@ -398,7 +423,11 @@ var getMonthNumber = (month, helpers) => {
398
423
  }
399
424
  return mapped;
400
425
  }
401
- return Number.parseInt(month, 10);
426
+ const parsedMonth = Number.parseInt(month, 10);
427
+ if (parsedMonth < 1 || parsedMonth > 12) {
428
+ return makeError(helpers, "dob.invalid", ["month"]);
429
+ }
430
+ return parsedMonth;
402
431
  };
403
432
  var checkMissingFields = (day, month, year, helpers) => {
404
433
  if (!day && !month && !year) {
@@ -432,9 +461,8 @@ var makeError = (helpers, code, fields) => {
432
461
  };
433
462
 
434
463
  // 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({
464
+ var personalEmailSchema = Joi.object({
465
+ personalEmail: Joi.string().required().max(EMAIL_MAX).email({
438
466
  minDomainSegments: 2,
439
467
  tlds: {
440
468
  allow: true,
@@ -443,22 +471,24 @@ var personalEmailSchema = import_joi10.default.object({
443
471
  }).messages({
444
472
  "string.max": `Email address must be ${EMAIL_MAX} characters or less`,
445
473
  "string.empty": "Enter a personal email address",
446
- "string.email": "Enter an email address, like name@example.com"
474
+ "string.email": "Enter an email address, like name@example.com",
475
+ "string.noControlChars": "Personal email address must not contain invalid characters"
447
476
  })
448
477
  });
449
478
 
450
479
  // 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({
480
+ var personalPhoneSchema = Joi.object({
481
+ personalTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
454
482
  "string.min": `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
455
483
  "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 +"
484
+ "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
485
+ "string.noControlChars": "Personal telephone number must not contain invalid characters"
457
486
  }),
458
- personalMobile: import_joi11.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
487
+ personalMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
459
488
  "string.min": `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
460
489
  "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 +"
490
+ "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
491
+ "string.noControlChars": "Personal mobile phone number must not contain invalid characters"
462
492
  })
463
493
  }).or("personalTelephone", "personalMobile").messages({
464
494
  "object.missing": "Enter at least one phone number"
@@ -681,8 +711,55 @@ var presenters = {
681
711
  formatCphText,
682
712
  formatBusinessAddress
683
713
  };
714
+
715
+ // src/constants/status-codes.js
716
+ var OK = 200;
717
+ var BAD_REQUEST = 400;
718
+ var UNAUTHORIZED = 401;
719
+ var FORBIDDEN = 403;
720
+ var NOT_FOUND = 404;
721
+ var NO_CONTENT = 204;
722
+ var FOUND = 302;
723
+ var INTERNAL_SERVER_ERROR = 500;
724
+ var SERVICE_UNAVAILABLE = 503;
725
+
726
+ // src/constants/index.js
727
+ var constants = {
728
+ statusCodes: {
729
+ BAD_REQUEST,
730
+ UNAUTHORIZED,
731
+ FORBIDDEN,
732
+ NOT_FOUND,
733
+ OK,
734
+ NO_CONTENT,
735
+ FOUND,
736
+ INTERNAL_SERVER_ERROR,
737
+ SERVICE_UNAVAILABLE
738
+ },
739
+ validationFields: {
740
+ FIRST_NAME_MAX,
741
+ LAST_NAME_MAX,
742
+ MIDDLE_NAMES_MAX,
743
+ BUSINESS_NAME_MAX,
744
+ EMAIL_MAX,
745
+ PHONE_NUMBER_MIN,
746
+ PHONE_NUMBER_MAX,
747
+ MAX_AGE_YEARS,
748
+ ADDRESS_LINE_MAX,
749
+ TOWN_CITY_MAX,
750
+ COUNTY_MAX,
751
+ COUNTRY_MAX,
752
+ POSTCODE_MAX
753
+ },
754
+ patterns: {
755
+ PHONE_NUMBER_PATTERN,
756
+ NO_CONTROL_CHARS_PATTERN
757
+ },
758
+ monthMap: MONTH_MAP
759
+ };
684
760
  // Annotate the CommonJS export names for ESM import in node:
685
761
  0 && (module.exports = {
762
+ constants,
686
763
  mappers,
687
764
  presenters,
688
765
  schemas,
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"
@@ -643,7 +672,54 @@ var presenters = {
643
672
  formatCphText,
644
673
  formatBusinessAddress
645
674
  };
675
+
676
+ // src/constants/status-codes.js
677
+ var OK = 200;
678
+ var BAD_REQUEST = 400;
679
+ var UNAUTHORIZED = 401;
680
+ var FORBIDDEN = 403;
681
+ var NOT_FOUND = 404;
682
+ var NO_CONTENT = 204;
683
+ var FOUND = 302;
684
+ var INTERNAL_SERVER_ERROR = 500;
685
+ var SERVICE_UNAVAILABLE = 503;
686
+
687
+ // src/constants/index.js
688
+ var constants = {
689
+ statusCodes: {
690
+ BAD_REQUEST,
691
+ UNAUTHORIZED,
692
+ FORBIDDEN,
693
+ NOT_FOUND,
694
+ OK,
695
+ NO_CONTENT,
696
+ FOUND,
697
+ INTERNAL_SERVER_ERROR,
698
+ SERVICE_UNAVAILABLE
699
+ },
700
+ validationFields: {
701
+ FIRST_NAME_MAX,
702
+ LAST_NAME_MAX,
703
+ MIDDLE_NAMES_MAX,
704
+ BUSINESS_NAME_MAX,
705
+ EMAIL_MAX,
706
+ PHONE_NUMBER_MIN,
707
+ PHONE_NUMBER_MAX,
708
+ MAX_AGE_YEARS,
709
+ ADDRESS_LINE_MAX,
710
+ TOWN_CITY_MAX,
711
+ COUNTY_MAX,
712
+ COUNTRY_MAX,
713
+ POSTCODE_MAX
714
+ },
715
+ patterns: {
716
+ PHONE_NUMBER_PATTERN,
717
+ NO_CONTROL_CHARS_PATTERN
718
+ },
719
+ monthMap: MONTH_MAP
720
+ };
646
721
  export {
722
+ constants,
647
723
  mappers,
648
724
  presenters,
649
725
  schemas,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/fcp-sfd-frontend-engine",
3
- "version": "0.6.1",
3
+ "version": "0.8.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": {
@@ -1,7 +1,9 @@
1
- export {
1
+ // Validation constraints
2
+ import {
2
3
  FIRST_NAME_MAX,
3
4
  LAST_NAME_MAX,
4
5
  MIDDLE_NAMES_MAX,
6
+ BUSINESS_NAME_MAX,
5
7
  EMAIL_MAX,
6
8
  PHONE_NUMBER_MIN,
7
9
  PHONE_NUMBER_MAX,
@@ -10,7 +12,56 @@ export {
10
12
  TOWN_CITY_MAX,
11
13
  COUNTY_MAX,
12
14
  COUNTRY_MAX,
13
- POSTCODE_MAX,
14
- PHONE_NUMBER_PATTERN,
15
- MONTH_MAP
16
- } from './validation-fields-export.js'
15
+ POSTCODE_MAX
16
+ } from './validation-fields.js'
17
+
18
+ // HTTP status codes
19
+ import {
20
+ BAD_REQUEST,
21
+ UNAUTHORIZED,
22
+ FORBIDDEN,
23
+ NOT_FOUND,
24
+ OK,
25
+ NO_CONTENT,
26
+ FOUND,
27
+ INTERNAL_SERVER_ERROR,
28
+ SERVICE_UNAVAILABLE
29
+ } from './status-codes.js'
30
+
31
+ // Format patterns and mappings
32
+ import { PHONE_NUMBER_PATTERN, NO_CONTROL_CHARS_PATTERN } from './patterns.js'
33
+ import { MONTH_MAP } from './month-map.js'
34
+
35
+ export const constants = {
36
+ statusCodes: {
37
+ BAD_REQUEST,
38
+ UNAUTHORIZED,
39
+ FORBIDDEN,
40
+ NOT_FOUND,
41
+ OK,
42
+ NO_CONTENT,
43
+ FOUND,
44
+ INTERNAL_SERVER_ERROR,
45
+ SERVICE_UNAVAILABLE
46
+ },
47
+ validationFields: {
48
+ FIRST_NAME_MAX,
49
+ LAST_NAME_MAX,
50
+ MIDDLE_NAMES_MAX,
51
+ BUSINESS_NAME_MAX,
52
+ EMAIL_MAX,
53
+ PHONE_NUMBER_MIN,
54
+ PHONE_NUMBER_MAX,
55
+ MAX_AGE_YEARS,
56
+ ADDRESS_LINE_MAX,
57
+ TOWN_CITY_MAX,
58
+ COUNTY_MAX,
59
+ COUNTRY_MAX,
60
+ POSTCODE_MAX
61
+ },
62
+ patterns: {
63
+ PHONE_NUMBER_PATTERN,
64
+ NO_CONTROL_CHARS_PATTERN
65
+ },
66
+ monthMap: MONTH_MAP
67
+ }
@@ -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]*$/
@@ -0,0 +1,9 @@
1
+ export const OK = 200
2
+ export const BAD_REQUEST = 400
3
+ export const UNAUTHORIZED = 401
4
+ export const FORBIDDEN = 403
5
+ export const NOT_FOUND = 404
6
+ export const NO_CONTENT = 204
7
+ export const FOUND = 302
8
+ export const INTERNAL_SERVER_ERROR = 500
9
+ export const SERVICE_UNAVAILABLE = 503
package/src/index.js CHANGED
@@ -2,3 +2,4 @@ export { mappers } from './mappers/mappers.js'
2
2
  export { schemas } from './schemas/schemas.js'
3
3
  export { utils } from './utils/utils.js'
4
4
  export { presenters } from './presenters/presenters.js'
5
+ export { constants } from './constants/index.js'
@@ -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'