UNPKG

@dwp/govuk-casa

Version:

A framework for building GOVUK Collect-And-Submit-Applications

673 lines 22.5 kB
"use strict"; 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