import fs from 'node:fs'; import path from 'node:path'; const CONFIG_FILE = 'qkforce.config.json'; const MAX_TARGET_URL_BYTES = 2048; const FIXED_PROFILE = Object.freeze({ id: 'qkforce-wdio-cucumber-qkta', language: 'typescript', bdd: 'cucumber', runner: 'webdriverio', adapter: 'webdriverio-cucumber-node', reporter: 'qk-test-analytics' }); const fail = message => { throw new Error(`Invalid QKForce run configuration: ${message}`); }; const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value); const rejectUnknownKeys = (value, allowed, location) => { for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${location}.${key} is not supported`); }; const validateTargetUrl = value => { if (typeof value !== 'string' || value.length === 0) fail('run.targetUrl is required'); if (Buffer.byteLength(value, 'utf8') > MAX_TARGET_URL_BYTES) fail('run.targetUrl exceeds the 2048-byte maximum'); let url; try { url = new URL(value); } catch { fail('run.targetUrl must be an absolute URL'); } if (url.username || url.password) fail('run.targetUrl must not contain credentials'); if (url.hash) fail('run.targetUrl must not contain a fragment'); if (url.search) fail('run.targetUrl must not contain a query string'); const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; if (url.protocol !== 'https:' && !(loopback && url.protocol === 'http:')) fail('run.targetUrl must use HTTPS unless it is an exact loopback development URL'); return url.toString(); }; export function validateQkforceRunConfig(candidate) { if (!isObject(candidate)) fail('the document must be a JSON object'); rejectUnknownKeys(candidate, ['schemaVersion','profile','run'], 'config'); if (candidate.schemaVersion !== 1) fail('schemaVersion must be 1'); if (!isObject(candidate.profile)) fail('profile must be an object'); rejectUnknownKeys(candidate.profile, Object.keys(FIXED_PROFILE), 'profile'); for (const [key, expected] of Object.entries(FIXED_PROFILE)) if (candidate.profile[key] !== expected) fail(`profile.${key} must be ${expected}`); if (!isObject(candidate.run)) fail('run must be an object'); rejectUnknownKeys(candidate.run, ['targetUrl'], 'run'); const targetUrl = validateTargetUrl(candidate.run.targetUrl); return Object.freeze({ schemaVersion: 1, profile: FIXED_PROFILE, run: Object.freeze({ targetUrl }), baseUrl: targetUrl }); } export function loadQkforceRunConfig(rootDirectory = process.cwd()) { const configPath = path.resolve(rootDirectory, CONFIG_FILE); let parsed; try { parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (error) { if (error && error.code === 'ENOENT') fail(`${CONFIG_FILE} is missing`); fail(`${CONFIG_FILE} is not valid JSON`); } return validateQkforceRunConfig(parsed); } export { CONFIG_FILE, FIXED_PROFILE, MAX_TARGET_URL_BYTES };