@dwp/govuk-casa
Version:
A framework for building GOVUK Collect-And-Submit-Applications
673 lines • 22.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateI18nObject = validateI18nObject;
exports.validateI18nDirs = validateI18nDirs;
exports.validateI18nLocales = validateI18nLocales;
exports.validateI18nFallbackLng = validateI18nFallbackLng;
exports.validateMountUrl = validateMountUrl;
exports.validateSessionObject = validateSessionObject;
exports.validateViews = validateViews;
exports.validateSessionSecret = validateSessionSecret;
exports.validateSessionTtl = validateSessionTtl;
exports.validateSessionName = validateSessionName;
exports.validateSessionSecure = validateSessionSecure;
exports.validateSessionStore = validateSessionStore;
exports.validateSessionCookiePath = validateSessionCookiePath;
exports.validateErrorVisibility = validateErrorVisibility;
exports.validateSessionCookieSameSite = validateSessionCookieSameSite;
exports.validatePageHooks = validatePageHooks;
exports.validateFields = validateFields;
exports.validatePages = validatePages;
exports.validatePlan = validatePlan;
exports.validateGlobalHooks = validateGlobalHooks;
exports.validatePlugins = validatePlugins;
exports.validateEvents = validateEvents;
exports.validateHelmetConfigurator = validateHelmetConfigurator;
exports.validateFormMaxParams = validateFormMaxParams;
exports.validateFormMaxBytes = validateFormMaxBytes;
exports.validateContextIdGenerator = validateContextIdGenerator;
exports.validateGovukRebrand = validateGovukRebrand;
exports.default = ingest;
const bytes_1 = __importDefault(require("bytes"));
const field_js_1 = require("./field.js");
const Plan_js_1 = __importDefault(require("./Plan.js"));
const logger_js_1 = __importDefault(require("./logger.js"));
const utils_js_1 = require("./utils.js");
const contextIdGenerators = __importStar(require("./context-id-generators.js"));
const constants_js_1 = require("./constants.js");
/**
* @typedef {import("../casa").ConfigurationOptions} ConfigurationOptions
* @access private
*/
/**
* @typedef {import("../casa").HelmetConfigurator} HelmetConfigurator
* @access private
*/
/**
* @typedef {import("../casa").Page} Page
* @access private
*/
/**
* @typedef {import("../casa").PageHook} PageHook
* @access private
*/
/**
* @typedef {import("../casa").GlobalHook} GlobalHook
* @access private
*/
/**
* @typedef {import("../casa").IPlugin} IPlugin
* @access private
*/
/**
* @typedef {import("../casa").ContextEventHandler} ContextEventHandler
* @access private
*/
/**
* @typedef {import("../casa").ContextIdGenerator} ContextIdGenerator
* @access private
*/
const log = (0, logger_js_1.default)("lib:configuration-ingestor");
const echo = (a) => a;
/**
* Validates and sanitises i18n object.
*
* @param {object} i18n Object to validate.
* @param {Function} cb Callback function that receives the validated value.
* @returns {object} Sanitised i18n object.
* @throws {TypeError} For invalid object.
* @access private
*/
function validateI18nObject(i18n = Object.create(null), cb = echo) {
if (Object.prototype.toString.call(i18n) !== "[object Object]") {
throw new TypeError("I18n must be an object");
}
return cb(i18n);
}
/**
* Validates and sanitises i18n directory.
*
* @param {Array} dirs Array of directories.
* @returns {Array} Array of directories.
* @throws {SyntaxError} For invalid directories.
* @throws {TypeError} For invalid type.
* @access private
*/
function validateI18nDirs(dirs = []) {
if (!Array.isArray(dirs)) {
throw new TypeError("I18n directories must be an array (i18n.dirs)");
}
let i = 0;
for (const dir of dirs) {
if (typeof dir !== "string") {
throw new TypeError(`I18n directory must be a string, got ${typeof dir} (i18n.dirs[${i++}])`);
}
}
return dirs;
}
/**
* Validates and sanitises i18n locales.
*
* @param {Array} locales Array of locales.
* @returns {Array} Array of locales.
* @throws {SyntaxError} For invalid locales.
* @throws {TypeError} For invalid type.
* @access private
*/
function validateI18nLocales(locales = ["en", "cy"]) {
if (!Array.isArray(locales)) {
throw new TypeError("I18n locales must be an array (i18n.locales)");
}
let i = 0;
for (const locale of locales) {
if (typeof locale !== "string") {
throw new TypeError(`I18n locale must be a string, got ${typeof locale} (i18n.locales[${i++}])`);
}
}
return locales;
}
/**
* Validates the i18n fallback language.
*
* @param {string} [fallback] Locale
* @param {Array} locales Array of valid locales.
* @returns {string} Fallback locale
* @throws {SyntaxError} For invalid locales.
* @throws {TypeError} For invalid type.
* @access private
*/
function validateI18nFallbackLng(fallback, locales = ["en", "cy"]) {
if (fallback === undefined) {
return false;
}
if (typeof fallback !== "string") {
throw new TypeError("I18n fallback language must be a string (i18n.fallbackLng)");
}
if (!locales.includes(fallback)) {
throw new SyntaxError("I18n fallback language must be included in the locales array (i18n.fallbackLng)");
}
return fallback;
}
/**
* Validates and sanitises mount url.
*
* @param {string} mountUrl Prefix for all URLs in the browser address bar
* @returns {string | undefined} Sanitised URL.
* @throws {SyntaxError} For invalid URL.
* @access private
*/
function validateMountUrl(mountUrl) {
if (typeof mountUrl === "undefined") {
return undefined;
}
if (!mountUrl.match(/\/$/)) {
throw new SyntaxError("mountUrl must include a trailing slash (/)");
}
return mountUrl;
}
/**
* Validates and sanitises sessions object.
*
* @param {object} session Object to validate.
* @param {Function} cb Callback function that receives the validated value.
* @returns {object} Sanitised sessions object.
* @throws {TypeError} For invalid object.
* @access private
*/
function validateSessionObject(session = Object.create(null), cb = echo) {
if (session === undefined) {
return cb(session);
}
if (typeof session !== "object") {
throw new TypeError("Session config has not been specified");
}
return cb(session);
}
/**
* Validates and sanitises view directory.
*
* @param {Array} dirs Array of directories.
* @returns {Array} Array of directories.
* @throws {SyntaxError} For invalid directories.
* @throws {TypeError} For invalid type.
* @access private
*/
function validateViews(dirs = []) {
if (!Array.isArray(dirs)) {
throw new TypeError("View directories must be an array (views)");
}
let i = 0;
for (const dir of dirs) {
if (typeof dir !== "string") {
throw new TypeError(`View directory must be a string, got ${typeof dir} (views[${i++}])`);
}
}
return dirs;
}
/**
* Validates and sanitises sessions secret.
*
* @param {string} secret Session secret.
* @returns {string} Secret.
* @throws {ReferenceError} For missing value type.
* @throws {TypeError} For invalid value.
* @access private
*/
function validateSessionSecret(secret) {
if (typeof secret === "undefined") {
throw ReferenceError("Session secret is missing (session.secret)");
}
else if (typeof secret !== "string") {
throw new TypeError("Session secret must be a string (session.secret)");
}
return secret;
}
/**
* Validates and sanitises sessions ttl.
*
* @param {number} ttl Session ttl (seconds).
* @returns {number} Ttl.
* @throws {ReferenceError} For missing value type.
* @throws {TypeError} For invalid value.
* @access private
*/
function validateSessionTtl(ttl = 3600) {
if (typeof ttl !== "number") {
throw new TypeError("Session ttl must be an integer (session.ttl)");
}
return ttl;
}
/**
* Validates and sanitises sessions name.
*
* @param {string} [name] Session name. Default is `casa-session`
* @returns {string} Name.
* @throws {ReferenceError} For missing value type.
* @throws {TypeError} For invalid value.
* @access private
*/
function validateSessionName(name = "casa-session") {
if (typeof name !== "string") {
throw new TypeError("Session name must be a string (session.name)");
}
return name;
}
/**
* Validates and sanitises sessions secure flag.
*
* @param {boolean} [secure] Session secure flag.
* @returns {string} Name.
* @throws {ReferenceError} For missing value type.
* @throws {TypeError} For invalid or missing value.
* @access private
*/
function validateSessionSecure(secure) {
if (secure === undefined) {
throw new Error("Session secure flag must be explicitly defined (session.secure)");
}
if (typeof secure !== "boolean") {
throw new TypeError("Session secure flag must be boolean (session.secure)");
}
return secure;
}
/**
* Validates and sanitises sessions store.
*
* @param {Function} store Session store.
* @returns {Function} Store.
* @access private
*/
function validateSessionStore(store) {
if (typeof store === "undefined") {
log.warn("Using MemoryStore session storage, which is not suitable for production");
return null;
}
return store;
}
/**
* Validates and sanitises sessions cookie url path.
*
* @param {string} cookiePath Session cookie url path.
* @param {string} defaultPath Default path if none specified.
* @returns {string} Cookie path.
* @access private
*/
function validateSessionCookiePath(cookiePath, defaultPath = "/") {
if (typeof cookiePath === "undefined") {
return defaultPath;
}
return cookiePath;
}
/**
* Validates and sanitises sessions cookie "sameSite" flag. One of: true
* (Strict) false (will not set the flag at all) Strict Lax None
*
* @param {any} cookieSameSite Session cookie "sameSite" flag
* @param {any} defaultFlag Default path if none specified
* @returns {boolean} Cookie path
* @throws {TypeError} When invalid arguments are provided
* @access private
*/
/**
* Validates errorVisibility.
*
* @param {string} errorVisibility Sets visibility flag for page validation
* error
* @returns {symbol | Function} Flag for error visibility.
* @throws {SyntaxError} For invalid errorVisibility flag.
* @access private
*/
function validateErrorVisibility(errorVisibility = constants_js_1.CONFIG_ERROR_VISIBILITY_ONSUBMIT) {
if (errorVisibility === undefined) {
return undefined;
}
if (errorVisibility === constants_js_1.CONFIG_ERROR_VISIBILITY_ALWAYS ||
errorVisibility === constants_js_1.CONFIG_ERROR_VISIBILITY_ONSUBMIT ||
typeof errorVisibility === "function") {
return errorVisibility;
}
throw new TypeError("errorVisibility must be casa constant CONFIG_ERROR_VISIBILITY_ALWAYS | CONFIG_ERROR_VISIBILITY_ONSUBMIT or function");
}
/**
* @param {boolean | string} cookieSameSite Cookie SameSite value
* @param {boolean | string} defaultFlag Default value
* @returns {boolean | string} Validated value
*/
function validateSessionCookieSameSite(cookieSameSite, defaultFlag) {
const validValues = [true, false, "Strict", "Lax", "None"];
if (defaultFlag === undefined) {
throw new TypeError("validateSessionCookieSameSite() requires an explicit default flag");
}
else if (!validValues.includes(defaultFlag)) {
throw new TypeError("validateSessionCookieSameSite() default flag must be set to one of true, false, Strict, Lax or None (session.cookieSameSite)");
}
const value = cookieSameSite !== undefined ? cookieSameSite : defaultFlag;
if (!validValues.includes(value)) {
throw new TypeError("SameSite flag must be set to one of true, false, Strict, Lax or None (session.cookieSameSite)");
}
return value;
}
const validatePageHook = (hook, index) => {
try {
(0, utils_js_1.validateHookName)(hook.hook);
if (typeof hook.middleware !== "function") {
throw new TypeError("Hook middleware must be a function");
}
}
catch (err) {
err.message = `Page hook at index ${index} is invalid: ${err.message}`;
throw err;
}
};
/**
* @param {PageHook[]} hooks Page hook functions
* @returns {PageHook[]} Validated page hooks
*/
function validatePageHooks(hooks) {
if (!Array.isArray(hooks)) {
throw new TypeError("Hooks must be an array");
}
let i = 0;
for (const hook of hooks) {
validatePageHook(hook, i++);
}
return hooks;
}
const validateField = (field, index) => {
try {
if (!(field instanceof field_js_1.PageField)) {
throw new TypeError('Page field must be an instance of PageField (created via the "field()" function)');
}
}
catch (err) {
err.message = `Page field at index ${index} is invalid: ${err.message}`;
throw err;
}
};
/**
* @param {PageField[]} fields Page fields
* @returns {PageField[]} Validated fields
*/
function validateFields(fields) {
if (!Array.isArray(fields)) {
throw new TypeError("Page fields must be an array (page[].fields)");
}
let i = 0;
for (const field of fields) {
validateField(field, i++);
}
return fields;
}
const validatePage = (page, index) => {
try {
(0, utils_js_1.validateWaypoint)(page.waypoint);
(0, utils_js_1.validateView)(page.view);
if (page.fields !== undefined) {
validateFields(page.fields);
}
if (page.hooks !== undefined) {
validatePageHooks(page.hooks);
}
if (page.errorVisibility !== undefined) {
validateErrorVisibility(page.errorVisibility);
}
}
catch (err) {
err.message = `Page at index ${index} is invalid: ${err.message}`;
throw err;
}
};
/**
* @param {Page[]} [pages] Pages
* @returns {Page[]} Validated pages
*/
function validatePages(pages = []) {
if (!Array.isArray(pages)) {
throw new TypeError("Pages must be an array (pages)");
}
let i = 0;
for (const page of pages) {
validatePage(page, i++);
}
return pages;
}
/**
* @param {Plan} plan Plan
* @returns {Plan} Validated plan
*/
function validatePlan(plan) {
if (plan === undefined) {
return plan;
}
if (!(plan instanceof Plan_js_1.default)) {
throw new TypeError("Plan must be an instance the Plan class (plan)");
}
return plan;
}
const validateGlobalHook = (hook, index) => {
try {
(0, utils_js_1.validateHookName)(hook.hook);
if (typeof hook.middleware !== "function") {
throw new TypeError("Hook middleware must be a function");
}
if (hook.path !== undefined) {
(0, utils_js_1.validateHookPath)(hook.path);
}
}
catch (err) {
err.message = `Global hook at index ${index} is invalid: ${err.message}`;
throw err;
}
};
/**
* @param {GlobalHook[]} hooks Global hook functions
* @returns {GlobalHook[]} Validated global hooks
*/
function validateGlobalHooks(hooks) {
if (hooks === undefined) {
return [];
}
if (!Array.isArray(hooks)) {
throw new TypeError("Hooks must be an array");
}
let i = 0;
for (const hook of hooks) {
validateGlobalHook(hook, i++);
}
return hooks;
}
/**
* @param {IPlugin[]} plugins Plugins
* @returns {IPlugin[]} Validated plugins
*/
function validatePlugins(plugins) {
return plugins;
}
/**
* @param {ContextEventHandler[]} events Event handlers
* @returns {ContextEventHandler[]} Validated event handlers
*/
function validateEvents(events) {
return events;
}
/**
* Validates helmet configuration function.
*
* @param {HelmetConfigurator} helmetConfigurator Configuration function
* @returns {HelmetConfigurator} Validated configuration function
* @throws {TypeError} When passed a non-function
* @access private
*/
function validateHelmetConfigurator(helmetConfigurator) {
if (helmetConfigurator !== undefined &&
!(helmetConfigurator instanceof Function)) {
throw new TypeError("Helmet configurator must be a function");
}
return helmetConfigurator;
}
/**
* @param {number} value Max params value
* @param {number} [defaultValue] Default value
* @returns {number} Valid value
* @throws {TypeError} If not an integer
* @throws {RangeError} If out of bounds
*/
function validateFormMaxParams(value, defaultValue = 25) {
// CASA needs to send certain hidden form fields (see `sanitise-fields`
// middleware), plus some padding here.
const MIN_PARAMS = 10;
if (value === undefined) {
return defaultValue;
}
if (!Number.isInteger(value)) {
throw new TypeError("formMaxParams must be an integer");
}
if (value < MIN_PARAMS) {
throw new RangeError(`formMaxParams must be at least ${MIN_PARAMS}`);
}
return value;
}
/**
* @param {number} value Max bytes value
* @param {number} [defaultValue] Default value
* @returns {number} Valid value
* @throws {TypeError} If not an integer
* @throws {RangeError} If out of bounds
*/
function validateFormMaxBytes(value, defaultValue = 1024 * 50) {
const MIN_BYTES = 1024;
if (value === undefined) {
return defaultValue;
}
const parsedValue = bytes_1.default.parse(value);
if (!Number.isInteger(parsedValue)) {
throw new TypeError("formMaxParams must be a string or an integer");
}
if (parsedValue < MIN_BYTES) {
throw new RangeError(`formMaxBytes must be at least ${MIN_BYTES} bytes (${bytes_1.default.format(MIN_BYTES)})`);
}
return parsedValue;
}
/**
* @param {ContextIdGenerator} generator ID generator function
* @returns {ContextIdGenerator} Validated generator
* @throws {TypeError} If not a function
*/
function validateContextIdGenerator(generator) {
if (generator === undefined) {
return contextIdGenerators.uuid();
}
if (!(generator instanceof Function)) {
throw new TypeError("contextIdGenerator must be a function");
}
return generator;
}
/**
* Validates the govuk rebrand feature flag.
*
* @param {boolean} [govukRebrand] Govuk rebrand feature flag
* @returns {boolean | true} Boolean.
* @throws {TypeError} For invalid feagure flag is set.
* @access private
*/
function validateGovukRebrand(govukRebrand) {
if (govukRebrand === undefined) {
return false;
}
if (typeof govukRebrand !== "boolean") {
throw new TypeError("govukRebrand must be a boolean");
}
return govukRebrand;
}
/**
* Ingest, validate, sanitise and manipulate configuration parameters.
*
* @param {ConfigurationOptions} config Config to ingest.
* @returns {object} Immutable config object.
* @throws {Error | SyntaxError | TypeError} For invalid config values.
* @access private
*/
function ingest(config = {}) {
const parsed = {
// I18n configuration
i18n: validateI18nObject(config.i18n, (i18n) => ({
dirs: validateI18nDirs(i18n.dirs),
locales: validateI18nLocales(i18n.locales),
fallbackLng: validateI18nFallbackLng(i18n.fallbackLng, i18n.locales),
})),
// URL that will prefix all URLs in the browser address bar
mountUrl: validateMountUrl(config.mountUrl),
// flag to make validation error visible on get requests
errorVisibility: validateErrorVisibility(config.errorVisibility),
// Session
session: validateSessionObject(config.session, (session) => ({
name: validateSessionName(session.name),
secret: validateSessionSecret(session.secret),
secure: validateSessionSecure(session.secure),
ttl: validateSessionTtl(session.ttl),
store: validateSessionStore(session.store),
cookiePath: validateSessionCookiePath(session.cookiePath, "/"),
cookieSameSite: validateSessionCookieSameSite(session.cookieSameSite, "Strict"),
})),
// Views configuration
views: validateViews(config.views),
// Pages
pages: validatePages(config.pages),
// Plan
plan: validatePlan(config.plan),
// Hooks
hooks: validateGlobalHooks(config.hooks),
// Plugins
plugins: validatePlugins(config.plugins),
// Events
events: validateEvents(config.events),
// Helmet configuration
helmetConfigurator: validateHelmetConfigurator(config.helmetConfigurator),
// Form parsing
formMaxParams: validateFormMaxParams(config.formMaxParams, 25),
formMaxBytes: validateFormMaxBytes(config.formMaxBytes, 1024 * 50),
// Context ID generator
contextIdGenerator: validateContextIdGenerator(config.contextIdGenerator),
govukRebrand: validateGovukRebrand(config.govukRebrand),
};
// Freeze to modifications
Object.freeze(parsed);
return parsed;
}
//# sourceMappingURL=configuration-ingestor.js.map