diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e183508289..cfecf70745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,27 +70,6 @@ jobs: - name: Install dependencies run: npm ci - run: npm run lint - check-definitions: - name: Check Definitions - timeout-minutes: 5 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Cache Node.js modules - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}- - - name: Install dependencies - run: npm ci - - name: CI Definitions Check - run: npm run ci:definitionsCheck check-circular: name: Circular Dependencies timeout-minutes: 5 diff --git a/ci/definitionsCheck.js b/ci/definitionsCheck.js deleted file mode 100644 index b4b9e88d0a..0000000000 --- a/ci/definitionsCheck.js +++ /dev/null @@ -1,27 +0,0 @@ -const fs = require('fs').promises; -const { exec } = require('child_process'); -const util = require('util'); -(async () => { - const core = await import('@actions/core'); - const [currentDefinitions, currentDocs] = await Promise.all([ - fs.readFile('./src/Options/Definitions.js', 'utf8'), - fs.readFile('./src/Options/docs.js', 'utf8'), - ]); - const execute = util.promisify(exec); - await execute('npm run definitions'); - const [newDefinitions, newDocs] = await Promise.all([ - fs.readFile('./src/Options/Definitions.js', 'utf8'), - fs.readFile('./src/Options/docs.js', 'utf8'), - ]); - if (currentDefinitions !== newDefinitions || currentDocs !== newDocs) { - // eslint-disable-next-line no-console - console.error( - '\x1b[31m%s\x1b[0m', - 'Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.' - ); - core.error('Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.'); - process.exit(1); - } else { - process.exit(0); - } -})(); diff --git a/package-lock.json b/package-lock.json index a7dba4ccaf..a0716f9fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,8 @@ "uuid": "11.1.0", "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.2" + "ws": "8.18.2", + "zod": "^4.3.6" }, "bin": { "parse-server": "bin/parse-server" @@ -22862,6 +22863,15 @@ "zen-observable": "0.8.15" } }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "spec/dependencies/mock-files-adapter": { "version": "1.0.0", "dev": true @@ -38671,6 +38681,11 @@ "requires": { "zen-observable": "0.8.15" } + }, + "zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==" } } } diff --git a/package.json b/package.json index 12a2d31b35..d67d149ce1 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "uuid": "11.1.0", "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.2" + "ws": "8.18.2", + "zod": "4.3.6" }, "devDependencies": { "@actions/core": "3.0.0", @@ -112,9 +113,7 @@ "scripts": { "ci:check": "node ./ci/ciCheck.js", "ci:checkNodeEngine": "node ./ci/nodeEngineCheck.js", - "ci:definitionsCheck": "node ./ci/definitionsCheck.js", - "definitions": "node ./resources/buildConfigDefinitions.js && prettier --write 'src/Options/*.js'", - "docs": "jsdoc -c ./jsdoc-conf.json", + "docs": "node resources/generateDocs.js && jsdoc -c ./jsdoc-conf.json", "lint": "eslint --cache ./ --flag unstable_config_lookup_from_file", "lint-fix": "eslint --fix --cache ./ --flag unstable_config_lookup_from_file", "build": "babel src/ -d lib/ --copy-files --extensions '.ts,.js'", diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js deleted file mode 100644 index 4220215a10..0000000000 --- a/resources/buildConfigDefinitions.js +++ /dev/null @@ -1,399 +0,0 @@ -/** - * Parse Server Configuration Builder - * - * This module builds the definitions file (src/Options/Definitions.js) - * from the src/Options/index.js options interfaces. - * The Definitions.js module is responsible for the default values as well - * as the mappings for the CLI. - * - * To rebuild the definitions file, run - * `$ node resources/buildConfigDefinitions.js` - */ -const parsers = require('../src/Options/parsers'); - -/** The types of nested options. */ -const nestedOptionTypes = [ - 'CustomPagesOptions', - 'DatabaseOptions', - 'FileUploadOptions', - 'IdempotencyOptions', - 'Object', - 'PagesCustomUrlsOptions', - 'PagesOptions', - 'PagesRoute', - 'PasswordPolicyOptions', - 'RequestComplexityOptions', - 'SecurityOptions', - 'SchemaOptions', - 'LogLevels', -]; - -/** The prefix of environment variables for nested options. */ -const nestedOptionEnvPrefix = { - AccountLockoutOptions: 'PARSE_SERVER_ACCOUNT_LOCKOUT_', - DatabaseOptionsClientMetadata: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_', - CustomPagesOptions: 'PARSE_SERVER_CUSTOM_PAGES_', - DatabaseOptions: 'PARSE_SERVER_DATABASE_', - FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_', - IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', - LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_', - LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_', - LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_', - LogLevel: 'PARSE_SERVER_LOG_LEVEL_', - LogLevels: 'PARSE_SERVER_LOG_LEVELS_', - PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', - PagesOptions: 'PARSE_SERVER_PAGES_', - PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', - ParseServerOptions: 'PARSE_SERVER_', - PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', - RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_', - RequestComplexityOptions: 'PARSE_SERVER_REQUEST_COMPLEXITY_', - SchemaOptions: 'PARSE_SERVER_SCHEMA_', - SecurityOptions: 'PARSE_SERVER_SECURITY_', -}; - -function last(array) { - return array[array.length - 1]; -} - -const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; -function toENV(key) { - let str = ''; - let previousIsUpper = false; - for (let i = 0; i < key.length; i++) { - const char = key[i]; - if (letters.indexOf(char) >= 0) { - if (!previousIsUpper) { - str += '_'; - previousIsUpper = true; - } - } else { - previousIsUpper = false; - } - str += char; - } - return str.toUpperCase(); -} - -function getCommentValue(comment) { - if (!comment) { - return; - } - return comment.value.trim(); -} - -function getENVPrefix(iface) { - if (nestedOptionEnvPrefix[iface.id.name]) { - return nestedOptionEnvPrefix[iface.id.name]; - } -} - -function processProperty(property, iface) { - const firstComment = getCommentValue(last(property.leadingComments || [])); - const name = property.key.name; - const prefix = getENVPrefix(iface); - - if (!firstComment) { - return; - } - const lines = firstComment.split('\n').map(line => line.trim()); - let help = ''; - let envLine; - let defaultLine; - lines.forEach(line => { - if (line.indexOf(':ENV:') === 0) { - envLine = line; - } else if (line.indexOf(':DEFAULT:') === 0) { - defaultLine = line; - } else { - help += line; - } - }); - let env; - if (envLine) { - env = envLine.split(' ')[1]; - } else { - env = prefix + toENV(name); - } - let defaultValue; - if (defaultLine) { - const defaultArray = defaultLine.split(' '); - defaultArray.shift(); - defaultValue = defaultArray.join(' '); - } - let type = property.value.type; - let isRequired = true; - if (type == 'NullableTypeAnnotation') { - isRequired = false; - type = property.value.typeAnnotation.type; - } - return { - name, - env, - help, - type, - defaultValue, - types: property.value.types, - typeAnnotation: property.value.typeAnnotation, - required: isRequired, - }; -} - -function doInterface(iface) { - return iface.body.properties - .sort((a, b) => a.key.name.localeCompare(b.key.name)) - .map(prop => processProperty(prop, iface)) - .filter(e => e !== undefined); -} - -function mapperFor(elt, t) { - const p = t.identifier('parsers'); - const wrap = identifier => t.memberExpression(p, identifier); - - if (t.isNumberTypeAnnotation(elt)) { - return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]); - } else if (t.isArrayTypeAnnotation(elt)) { - return wrap(t.identifier('arrayParser')); - } else if (t.isAnyTypeAnnotation(elt)) { - return wrap(t.identifier('objectParser')); - } else if (t.isBooleanTypeAnnotation(elt)) { - return wrap(t.identifier('booleanParser')); - } else if (t.isObjectTypeAnnotation(elt)) { - return wrap(t.identifier('objectParser')); - } else if (t.isUnionTypeAnnotation(elt)) { - const unionTypes = elt.typeAnnotation?.types || elt.types; - if (unionTypes?.some(type => t.isBooleanTypeAnnotation(type)) && unionTypes?.some(type => t.isFunctionTypeAnnotation(type))) { - return wrap(t.identifier('booleanOrFunctionParser')); - } - } else if (t.isGenericTypeAnnotation(elt)) { - const type = elt.typeAnnotation.id.name; - if (type == 'Adapter') { - return wrap(t.identifier('moduleOrObjectParser')); - } - if (type == 'NumberOrBoolean') { - return wrap(t.identifier('numberOrBooleanParser')); - } - if (type == 'NumberOrString') { - return t.callExpression(wrap(t.identifier('numberOrStringParser')), [t.stringLiteral(elt.name)]); - } - if (type === 'StringOrStringArray') { - return wrap(t.identifier('arrayParser')); - } - return wrap(t.identifier('objectParser')); - } -} - -function parseDefaultValue(elt, value, t) { - let literalValue; - if (t.isStringTypeAnnotation(elt)) { - if (value == '""' || value == "''") { - literalValue = t.stringLiteral(''); - } else { - literalValue = t.stringLiteral(value); - } - } else if (t.isNumberTypeAnnotation(elt)) { - literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); - } else if (t.isArrayTypeAnnotation(elt)) { - const array = parsers.objectParser(value); - literalValue = t.arrayExpression( - array.map(value => { - if (typeof value == 'string') { - return t.stringLiteral(value); - } else if (typeof value == 'number') { - return t.numericLiteral(value); - } else if (typeof value == 'object') { - const object = parsers.objectParser(value); - const props = Object.entries(object).map(([k, v]) => { - if (typeof v == 'string') { - return t.objectProperty(t.identifier(k), t.stringLiteral(v)); - } else if (typeof v == 'number') { - return t.objectProperty(t.identifier(k), t.numericLiteral(v)); - } else if (typeof v == 'boolean') { - return t.objectProperty(t.identifier(k), t.booleanLiteral(v)); - } - }); - return t.objectExpression(props); - } else { - throw new Error('Unable to parse array'); - } - }) - ); - } else if (t.isAnyTypeAnnotation(elt)) { - literalValue = t.arrayExpression([]); - } else if (t.isBooleanTypeAnnotation(elt)) { - literalValue = t.booleanLiteral(parsers.booleanParser(value)); - } else if (t.isGenericTypeAnnotation(elt)) { - const type = elt.typeAnnotation.id.name; - if (type == 'NumberOrBoolean') { - literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); - } - if (type == 'NumberOrString') { - literalValue = t.numericLiteral(parsers.numberOrStringParser('')(value)); - } - - if (nestedOptionTypes.includes(type)) { - const object = parsers.objectParser(value); - const props = Object.keys(object).map(key => { - return t.objectProperty(key, object[value]); - }); - literalValue = t.objectExpression(props); - } - if (type == 'ProtectedFields') { - const prop = t.objectProperty( - t.stringLiteral('_User'), - t.objectPattern([ - t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')])), - ]) - ); - literalValue = t.objectExpression([prop]); - } - } - return literalValue; -} - -function inject(t, list) { - let comments = ''; - const results = list - .map(elt => { - if (!elt.name) { - return; - } - const props = ['env', 'help'] - .map(key => { - if (elt[key]) { - return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key])); - } - }) - .filter(e => e !== undefined); - if (elt.required) { - props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true))); - } - const action = mapperFor(elt, t); - if (action) { - props.push(t.objectProperty(t.stringLiteral('action'), action)); - } - - if (t.isGenericTypeAnnotation(elt)) { - if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) { - props.push( - t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name)) - ); - } - } else if (t.isArrayTypeAnnotation(elt)) { - const elementType = elt.typeAnnotation.elementType; - if (t.isGenericTypeAnnotation(elementType)) { - if (elementType.id.name in nestedOptionEnvPrefix) { - props.push( - t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]')) - ); - } - } - } - if (elt.defaultValue) { - let parsedValue = parseDefaultValue(elt, elt.defaultValue, t); - if (!parsedValue) { - for (const type of elt.typeAnnotation.types) { - elt.type = type.type; - parsedValue = parseDefaultValue(elt, elt.defaultValue, t); - if (parsedValue) { - break; - } - } - } - if (parsedValue) { - props.push(t.objectProperty(t.stringLiteral('default'), parsedValue)); - } else { - throw new Error(`Unable to parse value for ${elt.name} `); - } - } - let type = elt.type.replace('TypeAnnotation', ''); - if (type === 'Generic') { - type = elt.typeAnnotation.id.name; - } - if (type === 'Array') { - type = elt.typeAnnotation.elementType.id - ? `${elt.typeAnnotation.elementType.id.name}[]` - : `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`; - } - if (type === 'NumberOrBoolean') { - type = 'Number|Boolean'; - } - if (type === 'NumberOrString') { - type = 'Number|String'; - } - if (type === 'Adapter') { - const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name; - type = `Adapter<${adapterType}>`; - } - if (type === 'StringOrStringArray') { - type = 'String|String[]'; - } - comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`; - const obj = t.objectExpression(props); - return t.objectProperty(t.stringLiteral(elt.name), obj); - }) - .filter(elt => { - return elt != undefined; - }); - return { results, comments }; -} - -const makeRequire = function (variableName, module, t) { - const decl = t.variableDeclarator( - t.identifier(variableName), - t.callExpression(t.identifier('require'), [t.stringLiteral(module)]) - ); - return t.variableDeclaration('var', [decl]); -}; -let docs = ``; -const plugin = function (babel) { - const t = babel.types; - const moduleExports = t.memberExpression(t.identifier('module'), t.identifier('exports')); - return { - visitor: { - ImportDeclaration: function (path) { - path.remove(); - }, - Program: function (path) { - // Inject the parser's loader - path.unshiftContainer('body', makeRequire('parsers', './parsers', t)); - }, - ExportDeclaration: function (path) { - // Export declaration on an interface - if ( - path.node && - path.node.declaration && - path.node.declaration.type == 'InterfaceDeclaration' - ) { - const { results, comments } = inject(t, doInterface(path.node.declaration)); - const id = path.node.declaration.id.name; - const exports = t.memberExpression(moduleExports, t.identifier(id)); - docs += `/**\n * @interface ${id}\n${comments} */\n\n`; - path.replaceWith(t.assignmentExpression('=', exports, t.objectExpression(results))); - } - }, - }, - }; -}; - -const auxiliaryCommentBefore = ` -**** GENERATED CODE **** -This code has been generated by resources/buildConfigDefinitions.js -Do not edit manually, but update Options/index.js -`; - -// Only run the transformation when executed directly, not when imported by tests -if (require.main === module) { - const babel = require('@babel/core'); - const res = babel.transformFileSync('./src/Options/index.js', { - plugins: [plugin, '@babel/transform-flow-strip-types'], - babelrc: false, - auxiliaryCommentBefore, - sourceMaps: false, - }); - require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n'); - require('fs').writeFileSync('./src/Options/docs.js', docs); -} - -// Export mapperFor for testing -module.exports = { mapperFor }; diff --git a/resources/generateDocs.js b/resources/generateDocs.js new file mode 100644 index 0000000000..637fd9ca3b --- /dev/null +++ b/resources/generateDocs.js @@ -0,0 +1,193 @@ +/** + * Generates lib/Options/docs.js from Zod schema metadata. + * This replaces the old buildConfigDefinitions.js docs generation. + * + * Run: npm run build && node resources/generateDocs.js + * Or via: npm run docs (called automatically before jsdoc) + * Note: `npm run build` must be run first so that lib/ is up-to-date. + */ +const { getAllOptionMeta } = require('../lib/Options/schemaUtils'); +const { ParseServerOptionsSchema } = require('../lib/Options/schemas/ParseServerOptions'); +const { SchemaOptionsSchema } = require('../lib/Options/schemas/SchemaOptions'); +const { AccountLockoutOptionsSchema } = require('../lib/Options/schemas/AccountLockoutOptions'); +const { PasswordPolicyOptionsSchema } = require('../lib/Options/schemas/PasswordPolicyOptions'); +const { FileUploadOptionsSchema } = require('../lib/Options/schemas/FileUploadOptions'); +const { IdempotencyOptionsSchema } = require('../lib/Options/schemas/IdempotencyOptions'); +const { SecurityOptionsSchema } = require('../lib/Options/schemas/SecurityOptions'); +const { RequestComplexityOptionsSchema } = require('../lib/Options/schemas/RequestComplexityOptions'); +const { + PagesOptionsSchema, + CustomPagesOptionsSchema, + PagesCustomUrlsOptionsSchema, + PagesRouteSchema, +} = require('../lib/Options/schemas/PagesOptions'); +const { + LiveQueryOptionsSchema, + LiveQueryServerOptionsSchema, +} = require('../lib/Options/schemas/LiveQueryOptions'); +const { RateLimitOptionsSchema } = require('../lib/Options/schemas/RateLimitOptions'); +const { LogLevelsSchema } = require('../lib/Options/schemas/LogLevels'); +const { + DatabaseOptionsSchema, + DatabaseOptionsClientMetadataSchema, + LogClientEventSchema, + LogLevelSchema, +} = require('../lib/Options/schemas/DatabaseOptions'); +const fs = require('fs'); +const path = require('path'); + +const { z } = require('zod'); + +/** + * Maps a Zod schema field to a JSDoc type string. + */ +function getJSDocType(schema) { + if (!schema) { + return '*'; + } + + // Unwrap wrappers + if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) { + return getJSDocType(schema.unwrap()); + } + if (schema instanceof z.ZodDefault) { + return getJSDocType(schema.removeDefault()); + } + + // Primitives + if (schema instanceof z.ZodString) { + return 'String'; + } + if (schema instanceof z.ZodNumber) { + return 'Number'; + } + if (schema instanceof z.ZodBoolean) { + return 'Boolean'; + } + + // Arrays + if (schema instanceof z.ZodArray) { + const inner = getJSDocType(schema.element); + if (inner === '*') { + return 'Array'; + } + return `${inner}[]`; + } + + // Objects — check if it's a named schema we know + if (schema instanceof z.ZodObject) { + return 'Object'; + } + + // Records + if (schema instanceof z.ZodRecord) { + return 'Object'; + } + + // Unions + if (schema instanceof z.ZodUnion) { + const options = schema._zod?.def?.options || schema._def?.options || []; + const types = options + .map(opt => getJSDocType(opt)) + .filter((t, i, arr) => arr.indexOf(t) === i); // dedupe + return types.join('|'); + } + + // Custom (functions, adapters) + if (schema._def && (schema._def.type === 'custom' || schema._def.typeName === 'ZodCustom')) { + return 'Function'; + } + + // Refinements (Zod v4 uses _def.type === 'effects') + const defType = schema._def?.type || schema._def?.typeName; + if (defType === 'effects' || defType === 'ZodEffects') { + return getJSDocType(schema._def.schema); + } + + return '*'; +} + +/** + * For nested object schemas, try to find the interface name from our known map. + */ +const schemaNameMap = new Map(); + +function getTypeName(schema) { + // Unwrap to the core type + let core = schema; + if (core instanceof z.ZodOptional || core instanceof z.ZodNullable) { + core = core.unwrap(); + } + if (core instanceof z.ZodDefault) { + core = core.removeDefault(); + } + const coreType = core._def?.type || core._def?.typeName; + if (coreType === 'effects' || coreType === 'ZodEffects') { + core = core._def.schema; + } + + // Check if this is a known named schema + const name = schemaNameMap.get(core); + if (name) { + return name; + } + + // For arrays of known schemas + if (core instanceof z.ZodArray) { + const elemName = schemaNameMap.get(core.element); + if (elemName) { + return `${elemName}[]`; + } + } + + return getJSDocType(schema); +} + +function generateJSDoc(name, schema) { + const meta = getAllOptionMeta(schema); + const shape = schema.shape || {}; + let doc = `/**\n * @interface ${name}\n`; + for (const key of Object.keys(shape).sort()) { + const fieldMeta = meta.get(key); + const help = fieldMeta?.help || ''; + // Use docType override if available, otherwise derive from Zod schema + const type = fieldMeta?.docType || getTypeName(shape[key]); + doc += ` * @property {${type}} ${key} ${help}\n`; + } + doc += ` */\n`; + return doc; +} + +const schemas = [ + ['SchemaOptions', SchemaOptionsSchema], + ['ParseServerOptions', ParseServerOptionsSchema], + ['AccountLockoutOptions', AccountLockoutOptionsSchema], + ['PasswordPolicyOptions', PasswordPolicyOptionsSchema], + ['FileUploadOptions', FileUploadOptionsSchema], + ['IdempotencyOptions', IdempotencyOptionsSchema], + ['SecurityOptions', SecurityOptionsSchema], + ['RequestComplexityOptions', RequestComplexityOptionsSchema], + ['PagesOptions', PagesOptionsSchema], + ['PagesRoute', PagesRouteSchema], + ['PagesCustomUrlsOptions', PagesCustomUrlsOptionsSchema], + ['CustomPagesOptions', CustomPagesOptionsSchema], + ['LiveQueryOptions', LiveQueryOptionsSchema], + ['LiveQueryServerOptions', LiveQueryServerOptionsSchema], + ['RateLimitOptions', RateLimitOptionsSchema], + ['LogLevels', LogLevelsSchema], + ['DatabaseOptions', DatabaseOptionsSchema], + ['DatabaseOptionsClientMetadata', DatabaseOptionsClientMetadataSchema], + ['LogClientEvent', LogClientEventSchema], + ['LogLevel', LogLevelSchema], +]; + +// Register named schemas so nested references resolve to interface names +for (const [name, schema] of schemas) { + schemaNameMap.set(schema, name); +} + +const output = schemas.map(([name, schema]) => generateJSDoc(name, schema)).join('\n'); +const outPath = path.resolve(__dirname, '../lib/Options/docs.js'); +fs.writeFileSync(outPath, output); +// eslint-disable-next-line no-console +console.log(`Generated ${outPath}`); diff --git a/spec/AccountLockoutPolicy.spec.js b/spec/AccountLockoutPolicy.spec.js index da8048adab..bc77717b1f 100644 --- a/spec/AccountLockoutPolicy.spec.js +++ b/spec/AccountLockoutPolicy.spec.js @@ -1,7 +1,8 @@ 'use strict'; const Config = require('../lib/Config'); -const Definitions = require('../lib/Options/Definitions'); +const { AccountLockoutOptionsSchema } = require('../lib/Options/schemas/AccountLockoutOptions'); +const accountLockoutDefaults = AccountLockoutOptionsSchema.parse({}); const request = require('../lib/request'); const loginWithWrongCredentialsShouldFail = function (username, password) { @@ -390,7 +391,7 @@ describe('lockout with password reset option', () => { const parseConfig = Config.get(Parse.applicationId); expect(parseConfig.accountLockout.unlockOnPasswordReset).toBe( - Definitions.AccountLockoutOptions.unlockOnPasswordReset.default + accountLockoutDefaults.unlockOnPasswordReset ); }); diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index e131a6def5..67fa1ec7ea 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -1,215 +1,7 @@ 'use strict'; -let commander; -const definitions = require('../lib/cli/definitions/parse-server').default; -const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server').default; const path = require('path'); const { spawn } = require('child_process'); -const testDefinitions = { - arg0: 'PROGRAM_ARG_0', - arg1: { - env: 'PROGRAM_ARG_1', - required: true, - }, - arg2: { - env: 'PROGRAM_ARG_2', - action: function (value) { - const intValue = parseInt(value); - if (!Number.isInteger(intValue)) { - throw 'arg2 is invalid'; - } - return intValue; - }, - }, - arg3: {}, - arg4: { - default: 'arg4Value', - }, -}; - -describe('commander additions', () => { - beforeEach(() => { - const command = require('../lib/cli/utils/commander').default; - commander = new command.constructor(); - commander.storeOptionsAsProperties(); - commander.allowExcessArguments(); - }); - afterEach(done => { - commander.options = []; - delete commander.arg0; - delete commander.arg1; - delete commander.arg2; - delete commander.arg3; - delete commander.arg4; - done(); - }); - - it('should load properly definitions from args', done => { - commander.loadDefinitions(testDefinitions); - commander.parse([ - 'node', - './CLI.spec.js', - '--arg0', - 'arg0Value', - '--arg1', - 'arg1Value', - '--arg2', - '2', - '--arg3', - 'some', - ]); - expect(commander.arg0).toEqual('arg0Value'); - expect(commander.arg1).toEqual('arg1Value'); - expect(commander.arg2).toEqual(2); - expect(commander.arg3).toEqual('some'); - expect(commander.arg4).toEqual('arg4Value'); - done(); - }); - - it('should load properly definitions from env', done => { - commander.loadDefinitions(testDefinitions); - commander.parse([], { - PROGRAM_ARG_0: 'arg0ENVValue', - PROGRAM_ARG_1: 'arg1ENVValue', - PROGRAM_ARG_2: '3', - }); - expect(commander.arg0).toEqual('arg0ENVValue'); - expect(commander.arg1).toEqual('arg1ENVValue'); - expect(commander.arg2).toEqual(3); - expect(commander.arg4).toEqual('arg4Value'); - done(); - }); - - it('should load properly use args over env', () => { - commander.loadDefinitions(testDefinitions); - commander.parse(['node', './CLI.spec.js', '--arg0', 'arg0Value', '--arg4', ''], { - PROGRAM_ARG_0: 'arg0ENVValue', - PROGRAM_ARG_1: 'arg1ENVValue', - PROGRAM_ARG_2: '4', - PROGRAM_ARG_4: 'arg4ENVValue', - }); - expect(commander.arg0).toEqual('arg0Value'); - expect(commander.arg1).toEqual('arg1ENVValue'); - expect(commander.arg2).toEqual(4); - expect(commander.arg4).toEqual(''); - }); - - it('should fail in action as port is invalid', done => { - commander.loadDefinitions(testDefinitions); - expect(() => { - commander.parse(['node', './CLI.spec.js', '--arg0', 'arg0Value'], { - PROGRAM_ARG_0: 'arg0ENVValue', - PROGRAM_ARG_1: 'arg1ENVValue', - PROGRAM_ARG_2: 'hello', - }); - }).toThrow('arg2 is invalid'); - done(); - }); - - it('should not override config.json', done => { - spyOn(console, 'log').and.callFake(() => {}); - commander.loadDefinitions(testDefinitions); - commander.parse( - ['node', './CLI.spec.js', '--arg0', 'arg0Value', './spec/configs/CLIConfig.json'], - { - PROGRAM_ARG_0: 'arg0ENVValue', - PROGRAM_ARG_1: 'arg1ENVValue', - } - ); - const options = commander.getOptions(); - expect(options.arg2).toBe(8888); - expect(options.arg3).toBe('hello'); //config value - expect(options.arg4).toBe('/1'); - done(); - }); - - it('should fail with invalid values in JSON', done => { - commander.loadDefinitions(testDefinitions); - expect(() => { - commander.parse( - ['node', './CLI.spec.js', '--arg0', 'arg0Value', './spec/configs/CLIConfigFail.json'], - { - PROGRAM_ARG_0: 'arg0ENVValue', - PROGRAM_ARG_1: 'arg1ENVValue', - } - ); - }).toThrow('arg2 is invalid'); - done(); - }); - - it('should fail when too many apps are set', done => { - commander.loadDefinitions(testDefinitions); - expect(() => { - commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigFailTooManyApps.json']); - }).toThrow('Multiple apps are not supported'); - done(); - }); - - it('should load config from apps', done => { - spyOn(console, 'log').and.callFake(() => {}); - commander.loadDefinitions(testDefinitions); - commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigApps.json']); - const options = commander.getOptions(); - expect(options.arg1).toBe('my_app'); - expect(options.arg2).toBe(8888); - expect(options.arg3).toBe('hello'); //config value - expect(options.arg4).toBe('/1'); - done(); - }); - - it('should fail when passing an invalid arguement', done => { - commander.loadDefinitions(testDefinitions); - expect(() => { - commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigUnknownArg.json']); - }).toThrow('error: unknown option myArg'); - done(); - }); -}); - -describe('definitions', () => { - it('should have valid types', () => { - for (const key in definitions) { - const definition = definitions[key]; - expect(typeof definition).toBe('object'); - if (typeof definition.env !== 'undefined') { - expect(typeof definition.env).toBe('string'); - } - expect(typeof definition.help).toBe('string'); - if (typeof definition.required !== 'undefined') { - expect(typeof definition.required).toBe('boolean'); - } - if (typeof definition.action !== 'undefined') { - expect(typeof definition.action).toBe('function'); - } - } - }); - - it('should throw when using deprecated facebookAppIds', () => { - expect(() => { - definitions.facebookAppIds.action(); - }).toThrow(); - }); -}); - -describe('LiveQuery definitions', () => { - it('should have valid types', () => { - for (const key in liveQueryDefinitions) { - const definition = liveQueryDefinitions[key]; - expect(typeof definition).toBe('object'); - if (typeof definition.env !== 'undefined') { - expect(typeof definition.env).toBe('string'); - } - expect(typeof definition.help).toBe('string', `help for ${key} should be a string`); - if (typeof definition.required !== 'undefined') { - expect(typeof definition.required).toBe('boolean'); - } - if (typeof definition.action !== 'undefined') { - expect(typeof definition.action).toBe('function'); - } - } - }); -}); - describe('execution', () => { const binPath = path.resolve(__dirname, '../bin/parse-server'); let childProcess; diff --git a/spec/Idempotency.spec.js b/spec/Idempotency.spec.js index 14d0469b86..f9e54774ef 100644 --- a/spec/Idempotency.spec.js +++ b/spec/Idempotency.spec.js @@ -1,6 +1,7 @@ 'use strict'; const Config = require('../lib/Config'); -const Definitions = require('../lib/Options/Definitions'); +const { IdempotencyOptionsSchema } = require('../lib/Options/schemas/IdempotencyOptions'); +const idempotencyDefaults = IdempotencyOptionsSchema.parse({}); const request = require('../lib/request'); const rest = require('../lib/rest'); const auth = require('../lib/Auth'); @@ -261,10 +262,10 @@ describe('Idempotency', () => { it('should use default configuration when none is set', async () => { await setup({}); expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe( - Definitions.IdempotencyOptions.ttl.default + idempotencyDefaults.ttl ); - expect(Config.get(Parse.applicationId).idempotencyOptions.paths).toBe( - Definitions.IdempotencyOptions.paths.default + expect(Config.get(Parse.applicationId).idempotencyOptions.paths).toEqual( + idempotencyDefaults.paths ); }); diff --git a/spec/Options/loaders/cliLoader.spec.js b/spec/Options/loaders/cliLoader.spec.js new file mode 100644 index 0000000000..c697c923df --- /dev/null +++ b/spec/Options/loaders/cliLoader.spec.js @@ -0,0 +1,111 @@ +const { Command } = require('commander'); +const { z } = require('zod'); +const { option } = require('../../../lib/Options/schemaUtils'); +const { registerSchemaOptions, extractCliOptions } = require('../../../lib/Options/loaders/cliLoader'); + +describe('cliLoader', () => { + let program; + let schema; + + beforeEach(() => { + program = new Command(); + program.exitOverride(); // Prevent process.exit in tests + program.allowExcessArguments(); // Match Parse Server's commander setup + program.configureOutput({ + writeOut: () => {}, + writeErr: () => {}, + }); + + schema = z.object({ + appId: option(z.string(), { + env: 'PARSE_SERVER_APP_ID', + help: 'Your Parse Application ID', + }), + port: option(z.number().default(1337), { + env: 'PARSE_SERVER_PORT', + help: 'Port to run on', + }), + verbose: option(z.boolean().default(false), { + env: 'PARSE_SERVER_VERBOSE', + help: 'Enable verbose logging', + }), + allowHeaders: option(z.array(z.string()).optional(), { + env: 'PARSE_SERVER_ALLOW_HEADERS', + help: 'Allowed headers', + }), + }); + }); + + describe('registerSchemaOptions()', () => { + it('registers options on a Commander program', () => { + registerSchemaOptions(program, schema); + + // Commander should have the options registered + const options = program.options; + const optionNames = options.map(o => o.long); + expect(optionNames).toContain('--appId'); + expect(optionNames).toContain('--port'); + expect(optionNames).toContain('--verbose'); + expect(optionNames).toContain('--allowHeaders'); + }); + + it('marks required options with angle brackets', () => { + registerSchemaOptions(program, schema); + const appIdOpt = program.options.find(o => o.long === '--appId'); + expect(appIdOpt.flags).toContain(''); + }); + + it('marks optional options with square brackets', () => { + registerSchemaOptions(program, schema); + const portOpt = program.options.find(o => o.long === '--port'); + expect(portOpt.flags).toContain('[port]'); + }); + }); + + describe('extractCliOptions()', () => { + it('extracts parsed CLI options', () => { + registerSchemaOptions(program, schema); + program.parse(['node', 'test', '--appId', 'myApp', '--port', '8080'], { from: 'user' }); + + const result = extractCliOptions(program, schema); + expect(result.appId).toBe('myApp'); + expect(result.port).toBe(8080); + }); + + it('skips options not provided on CLI', () => { + registerSchemaOptions(program, schema); + program.parse(['node', 'test', '--appId', 'myApp'], { from: 'user' }); + + const result = extractCliOptions(program, schema); + expect(result.appId).toBe('myApp'); + expect(result.port).toBeUndefined(); + }); + + it('coerces boolean values', () => { + registerSchemaOptions(program, schema); + program.parse(['node', 'test', '--appId', 'x', '--verbose', 'true'], { from: 'user' }); + + const result = extractCliOptions(program, schema); + expect(result.verbose).toBe(true); + }); + + it('treats boolean flags without explicit values as true', () => { + registerSchemaOptions(program, schema); + program.parse(['node', 'test', '--appId', 'x', '--verbose'], { from: 'user' }); + + const result = extractCliOptions(program, schema); + expect(result.verbose).toBe(true); + }); + + it('coerces CSV arrays', () => { + registerSchemaOptions(program, schema); + program.parse( + ['node', 'test', '--appId', 'x', '--allowHeaders', 'X-Custom,X-Other'], + { from: 'user' } + ); + + const result = extractCliOptions(program, schema); + expect(result.allowHeaders).toEqual(['X-Custom', 'X-Other']); + }); + }); +}); diff --git a/spec/Options/loaders/envLoader.spec.js b/spec/Options/loaders/envLoader.spec.js new file mode 100644 index 0000000000..b38af16acf --- /dev/null +++ b/spec/Options/loaders/envLoader.spec.js @@ -0,0 +1,260 @@ +const { z } = require('zod'); +const { option } = require('../../../lib/Options/schemaUtils'); +const { loadFromEnv } = require('../../../lib/Options/loaders/envLoader'); + +describe('envLoader', () => { + it('loads flat env vars into options', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APP_ID', help: 'App ID' }), + port: option(z.number().default(1337), { env: 'PARSE_SERVER_PORT', help: 'Port' }), + }); + + const env = { + PARSE_SERVER_APP_ID: 'myApp', + PARSE_SERVER_PORT: '8080', + }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ appId: 'myApp', port: 8080 }); + }); + + it('skips env vars that are not set', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APP_ID', help: 'App ID' }), + port: option(z.number().default(1337), { env: 'PARSE_SERVER_PORT', help: 'Port' }), + }); + + const env = { PARSE_SERVER_APP_ID: 'myApp' }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ appId: 'myApp' }); + expect(result.port).toBeUndefined(); + }); + + it('skips empty string env vars', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APP_ID', help: 'App ID' }), + }); + + const result = loadFromEnv(schema, { PARSE_SERVER_APP_ID: '' }); + expect(result).toEqual({}); + }); + + it('loads nested env vars into nested objects', () => { + const innerSchema = z.object({ + strict: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_STRICT', + help: 'Strict mode', + }), + ttl: option(z.number().default(5000), { + env: 'PARSE_SERVER_SCHEMA_TTL', + help: 'TTL', + }), + }); + + const schema = z.object({ + schema: option(innerSchema.optional(), { + env: null, + help: 'Schema options', + }), + }); + + const env = { + PARSE_SERVER_SCHEMA_STRICT: 'true', + PARSE_SERVER_SCHEMA_TTL: '3000', + }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ + schema: { strict: true, ttl: 3000 }, + }); + }); + + it('coerces boolean env vars', () => { + const schema = z.object({ + verbose: option(z.boolean().default(false), { env: 'VERBOSE', help: 'Verbose' }), + }); + + expect(loadFromEnv(schema, { VERBOSE: 'true' })).toEqual({ verbose: true }); + expect(loadFromEnv(schema, { VERBOSE: '1' })).toEqual({ verbose: true }); + expect(loadFromEnv(schema, { VERBOSE: 'false' })).toEqual({ verbose: false }); + }); + + it('coerces array env vars from CSV', () => { + const schema = z.object({ + ips: option(z.array(z.string()).default([]), { + env: 'PARSE_SERVER_IPS', + help: 'IPs', + }), + }); + + const result = loadFromEnv(schema, { PARSE_SERVER_IPS: '127.0.0.1,::1' }); + expect(result).toEqual({ ips: ['127.0.0.1', '::1'] }); + }); + + it('coerces array env vars from JSON', () => { + const schema = z.object({ + ips: option(z.array(z.string()).default([]), { + env: 'PARSE_SERVER_IPS', + help: 'IPs', + }), + }); + + const result = loadFromEnv(schema, { PARSE_SERVER_IPS: '["10.0.0.1","10.0.0.2"]' }); + expect(result).toEqual({ ips: ['10.0.0.1', '10.0.0.2'] }); + }); + + it('coerces object env vars from JSON', () => { + const schema = z.object({ + push: option(z.record(z.string(), z.unknown()).optional(), { + env: 'PARSE_SERVER_PUSH', + help: 'Push config', + }), + }); + + const result = loadFromEnv(schema, { + PARSE_SERVER_PUSH: '{"ios":{"pfx":"path/to/cert"}}', + }); + expect(result).toEqual({ push: { ios: { pfx: 'path/to/cert' } } }); + }); + + it('loads grouped/nested env vars matching Parse Server patterns', () => { + const accountLockoutSchema = z.object({ + duration: option(z.number().optional(), { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', + help: 'Duration in minutes', + }), + threshold: option(z.number().optional(), { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', + help: 'Number of failed attempts', + }), + unlockOnPasswordReset: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET', + help: 'Unlock on reset', + }), + }); + + const idempotencySchema = z.object({ + paths: option(z.array(z.string()).default([]), { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', + help: 'Paths', + }), + ttl: option(z.number().default(300), { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', + help: 'TTL in seconds', + }), + }); + + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APPLICATION_ID', help: 'App ID' }), + accountLockout: option(accountLockoutSchema.optional(), { + env: null, + help: 'Account lockout options', + }), + idempotencyOptions: option(idempotencySchema.optional(), { + env: null, + help: 'Idempotency options', + }), + }); + + const env = { + PARSE_SERVER_APPLICATION_ID: 'myApp', + PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION: '5', + PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD: '3', + PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET: 'true', + PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS: '.*', + PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL: '600', + }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ + appId: 'myApp', + accountLockout: { + duration: 5, + threshold: 3, + unlockOnPasswordReset: true, + }, + idempotencyOptions: { + paths: ['.*'], + ttl: 600, + }, + }); + }); + + it('handles mixed flat and nested env vars', () => { + const securitySchema = z.object({ + enableCheck: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK', + help: 'Enable check', + }), + }); + + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APP_ID', help: 'App ID' }), + verbose: option(z.boolean().default(false), { env: 'PARSE_SERVER_VERBOSE', help: 'Verbose' }), + security: option(securitySchema.optional(), { env: null, help: 'Security' }), + }); + + const env = { + PARSE_SERVER_APP_ID: 'myApp', + PARSE_SERVER_VERBOSE: 'true', + PARSE_SERVER_SECURITY_ENABLE_CHECK: 'true', + }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ + appId: 'myApp', + verbose: true, + security: { enableCheck: true }, + }); + }); + + it('handles deeply nested schemas (3 levels)', () => { + const routeSchema = z.object({ + path: option(z.string().default('/parse'), { + env: 'PARSE_SERVER_PAGES_ROUTE_PATH', + help: 'Route path', + }), + }); + + const pagesSchema = z.object({ + enableRouter: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PAGES_ENABLE_ROUTER', + help: 'Enable router', + }), + pagesRoute: option(routeSchema.optional(), { env: null, help: 'Pages route' }), + }); + + const schema = z.object({ + pages: option(pagesSchema.optional(), { env: null, help: 'Pages' }), + }); + + const env = { + PARSE_SERVER_PAGES_ENABLE_ROUTER: 'true', + PARSE_SERVER_PAGES_ROUTE_PATH: '/custom', + }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ + pages: { + enableRouter: true, + pagesRoute: { path: '/custom' }, + }, + }); + }); + + it('ignores env vars not in schema', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APP_ID', help: 'App ID' }), + }); + + const env = { + PARSE_SERVER_APP_ID: 'myApp', + UNRELATED_VAR: 'ignored', + HOME: '/home/user', + }; + + const result = loadFromEnv(schema, env); + expect(result).toEqual({ appId: 'myApp' }); + }); +}); diff --git a/spec/Options/loaders/fileLoader.spec.js b/spec/Options/loaders/fileLoader.spec.js new file mode 100644 index 0000000000..d4d0ff0ff9 --- /dev/null +++ b/spec/Options/loaders/fileLoader.spec.js @@ -0,0 +1,64 @@ +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const { loadFromFile } = require('../../../lib/Options/loaders/fileLoader'); + +describe('fileLoader', () => { + const configDir = path.join(__dirname, '../../configs'); + + it('loads a direct config object from JSON', () => { + const result = loadFromFile(path.join(configDir, 'CLIConfig.json')); + expect(result.arg1).toBe('my_app'); + expect(result.arg2).toBe('8888'); + }); + + it('loads config from apps array format', () => { + const result = loadFromFile(path.join(configDir, 'CLIConfigApps.json')); + expect(result.arg1).toBe('my_app'); + }); + + it('throws for multiple apps', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-test-')); + const multiAppPath = path.join(tempDir, 'CLIConfigMultipleApps.json'); + const multiApp = { apps: [{ arg1: 'a' }, { arg1: 'b' }] }; + fs.writeFileSync(multiAppPath, JSON.stringify(multiApp)); + try { + expect(() => loadFromFile(multiAppPath)).toThrow('Multiple apps are not supported'); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it('throws for empty apps array', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-test-')); + const emptyAppsPath = path.join(tempDir, 'CLIConfigEmptyApps.json'); + fs.writeFileSync(emptyAppsPath, JSON.stringify({ apps: [] })); + try { + expect(() => loadFromFile(emptyAppsPath)).toThrow( + 'The "apps" array must contain at least one configuration' + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it('throws for apps that is not an array', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-test-')); + const nonArrayAppsPath = path.join(tempDir, 'CLIConfigNonArrayApps.json'); + fs.writeFileSync(nonArrayAppsPath, JSON.stringify({ apps: {} })); + try { + expect(() => loadFromFile(nonArrayAppsPath)).toThrow( + 'The "apps" property must be an array' + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it('resolves relative paths', () => { + const absolutePath = path.join(configDir, 'CLIConfig.json'); + const relativePath = path.relative(process.cwd(), absolutePath); + const result = loadFromFile(relativePath); + expect(result.arg1).toBe('my_app'); + }); +}); diff --git a/spec/Options/loaders/mergeConfig.spec.js b/spec/Options/loaders/mergeConfig.spec.js new file mode 100644 index 0000000000..5d87009538 --- /dev/null +++ b/spec/Options/loaders/mergeConfig.spec.js @@ -0,0 +1,63 @@ +const { mergeConfigs } = require('../../../lib/Options/loaders/mergeConfig'); + +describe('mergeConfig', () => { + it('merges flat objects with later sources winning', () => { + const result = mergeConfigs( + { appId: 'fromFile', port: 1337 }, + { port: 8080 }, + { appId: 'fromCli' } + ); + expect(result).toEqual({ appId: 'fromCli', port: 8080 }); + }); + + it('deep merges nested objects', () => { + const result = mergeConfigs( + { schema: { strict: false, definitions: [] } }, + { schema: { strict: true } } + ); + expect(result).toEqual({ + schema: { strict: true, definitions: [] }, + }); + }); + + it('overwrites arrays instead of merging them', () => { + const result = mergeConfigs( + { ips: ['127.0.0.1'] }, + { ips: ['10.0.0.1', '10.0.0.2'] } + ); + expect(result).toEqual({ ips: ['10.0.0.1', '10.0.0.2'] }); + }); + + it('overwrites primitives with objects', () => { + const result = mergeConfigs( + { value: 'string' }, + { value: { nested: true } } + ); + expect(result).toEqual({ value: { nested: true } }); + }); + + it('skips undefined values in sources', () => { + const result = mergeConfigs( + { appId: 'original', port: 1337 }, + { appId: undefined, port: 8080 } + ); + expect(result).toEqual({ appId: 'original', port: 8080 }); + }); + + it('returns empty object when no sources provided', () => { + expect(mergeConfigs()).toEqual({}); + }); + + it('handles single source', () => { + const result = mergeConfigs({ appId: 'test' }); + expect(result).toEqual({ appId: 'test' }); + }); + + it('deep merges multiple levels', () => { + const result = mergeConfigs( + { a: { b: { c: 1, d: 2 } } }, + { a: { b: { c: 3 }, e: 4 } } + ); + expect(result).toEqual({ a: { b: { c: 3, d: 2 }, e: 4 } }); + }); +}); diff --git a/spec/Options/loaders/runner-zod.spec.js b/spec/Options/loaders/runner-zod.spec.js new file mode 100644 index 0000000000..18369296e8 --- /dev/null +++ b/spec/Options/loaders/runner-zod.spec.js @@ -0,0 +1,186 @@ +const { z } = require('zod'); +const { option } = require('../../../lib/Options/schemaUtils'); +const path = require('path'); + +// We test the core logic of the Zod runner by importing and using +// the individual loaders that it composes, since the runner itself +// calls process.argv/process.env/process.exit which are hard to test. + +const { loadFromEnv } = require('../../../lib/Options/loaders/envLoader'); +const { loadFromFile } = require('../../../lib/Options/loaders/fileLoader'); +const { mergeConfigs } = require('../../../lib/Options/loaders/mergeConfig'); +const { registerSchemaOptions } = require('../../../lib/Options/loaders/cliLoader'); +const { Command } = require('commander'); + +describe('Zod runner integration', () => { + const testSchema = z.object({ + appId: option(z.string(), { + env: 'PARSE_SERVER_APPLICATION_ID', + help: 'Your Parse Application ID', + }), + masterKey: option(z.string(), { + env: 'PARSE_SERVER_MASTER_KEY', + help: 'Your Parse Master Key', + }), + port: option(z.number().default(1337), { + env: 'PORT', + help: 'Port to run on', + }), + verbose: option(z.boolean().default(false), { + env: 'VERBOSE', + help: 'Enable verbose logging', + }), + databaseURI: option(z.string().default('mongodb://localhost:27017/parse'), { + env: 'PARSE_SERVER_DATABASE_URI', + help: 'Database URI', + }), + schema: option( + z.object({ + strict: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_STRICT', + help: 'Strict schema mode', + }), + }).loose().optional(), + { env: null, help: 'Schema options' } + ), + }).loose(); + + describe('full config merge pipeline', () => { + it('merges file + env + CLI with correct priority', () => { + // Simulate: file has port=8080, env has port=9090, CLI has port not set + const fileOptions = { appId: 'fromFile', port: 8080, databaseURI: 'mongodb://file' }; + const envOptions = loadFromEnv(testSchema, { + PARSE_SERVER_APPLICATION_ID: 'fromEnv', + PORT: '9090', + }); + const cliOptions = { masterKey: 'fromCli' }; + + // file < env < CLI + const merged = mergeConfigs(fileOptions, envOptions, cliOptions); + + expect(merged.appId).toBe('fromEnv'); // env wins over file + expect(merged.port).toBe(9090); // env wins over file + expect(merged.databaseURI).toBe('mongodb://file'); // only in file + expect(merged.masterKey).toBe('fromCli'); // only in CLI + }); + + it('CLI args win over env vars', () => { + const envOptions = loadFromEnv(testSchema, { PORT: '9090' }); + const cliOptions = { port: 3000 }; + + const merged = mergeConfigs({}, envOptions, cliOptions); + expect(merged.port).toBe(3000); + }); + + it('applies Zod defaults after merge', () => { + const merged = mergeConfigs( + { appId: 'test', masterKey: 'key' } + ); + + const result = testSchema.parse(merged); + expect(result.port).toBe(1337); // default + expect(result.verbose).toBe(false); // default + expect(result.databaseURI).toBe('mongodb://localhost:27017/parse'); // default + }); + + it('loads and merges nested env vars (solving #7151)', () => { + const envOptions = loadFromEnv(testSchema, { + PARSE_SERVER_APPLICATION_ID: 'myApp', + PARSE_SERVER_MASTER_KEY: 'myKey', + PARSE_SERVER_SCHEMA_STRICT: 'true', + }); + + const result = testSchema.parse(envOptions); + expect(result.appId).toBe('myApp'); + expect(result.schema.strict).toBe(true); + }); + }); + + describe('Commander option registration', () => { + it('registers Zod schema options on Commander', () => { + const program = new Command(); + program.exitOverride(); + program.allowExcessArguments(); + + registerSchemaOptions(program, testSchema); + + const optionNames = program.options.map(o => o.long); + expect(optionNames).toContain('--appId'); + expect(optionNames).toContain('--masterKey'); + expect(optionNames).toContain('--port'); + expect(optionNames).toContain('--verbose'); + }); + + it('parses CLI args with type coercion', () => { + const program = new Command(); + program.exitOverride(); + program.allowExcessArguments(); + + registerSchemaOptions(program, testSchema); + program.parse(['node', 'test', '--appId', 'myApp', '--port', '8080'], { from: 'user' }); + + const opts = program.opts(); + expect(opts.appId).toBe('myApp'); + expect(opts.port).toBe(8080); + }); + }); + + describe('config file loading', () => { + it('loads config from JSON file', () => { + const configPath = path.join(__dirname, '../../configs/CLIConfig.json'); + const options = loadFromFile(configPath); + expect(options.arg1).toBe('my_app'); + }); + + it('loads config from apps array', () => { + const configPath = path.join(__dirname, '../../configs/CLIConfigApps.json'); + const options = loadFromFile(configPath); + expect(options.arg1).toBe('my_app'); + }); + }); + + describe('end-to-end with ParseServerOptionsSchema', () => { + const { ParseServerOptionsSchema } = require('../../../lib/Options/schemas/ParseServerOptions'); + + it('loads Parse Server config from env vars', () => { + const envOptions = loadFromEnv(ParseServerOptionsSchema, { + PARSE_SERVER_APPLICATION_ID: 'testApp', + PARSE_SERVER_MASTER_KEY: 'testKey', + PARSE_SERVER_MAINTENANCE_KEY: 'testMaintKey', + PARSE_SERVER_URL: 'http://localhost:1337/parse', + PORT: '8080', + PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'true', + // Nested env vars (#7151) + PARSE_SERVER_SCHEMA_STRICT: 'true', + PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL: '600', + }); + + const result = ParseServerOptionsSchema.parse(envOptions); + expect(result.appId).toBe('testApp'); + expect(result.port).toBe(8080); + expect(result.allowClientClassCreation).toBe(true); + expect(result.schema.strict).toBe(true); + expect(result.idempotencyOptions.ttl).toBe(600); + }); + + it('merges file config with env overrides', () => { + const fileConfig = { + appId: 'fromFile', + masterKey: 'fileKey', + maintenanceKey: 'fileMaint', + serverURL: 'http://localhost:1337/parse', + port: 1337, + }; + + const envOptions = loadFromEnv(ParseServerOptionsSchema, { + PORT: '9090', + }); + + const merged = mergeConfigs(fileConfig, envOptions); + const result = ParseServerOptionsSchema.parse(merged); + + expect(result.appId).toBe('fromFile'); // from file + expect(result.port).toBe(9090); // env overrides file + }); + }); +}); diff --git a/spec/Options/loaders/schemaUtils.spec.js b/spec/Options/loaders/schemaUtils.spec.js new file mode 100644 index 0000000000..8f9ab59d34 --- /dev/null +++ b/spec/Options/loaders/schemaUtils.spec.js @@ -0,0 +1,150 @@ +const { z } = require('zod'); +const { + option, + getOptionMeta, + getAllOptionMeta, + buildEnvMap, + coerceValue, +} = require('../../../lib/Options/schemaUtils'); + +describe('schemaUtils', () => { + describe('option() and getOptionMeta()', () => { + it('attaches and retrieves metadata from a Zod schema', () => { + const meta = { env: 'MY_ENV_VAR', help: 'Some help text' }; + const schema = option(z.string(), meta); + expect(getOptionMeta(schema)).toBe(meta); + }); + + it('returns undefined for schemas without metadata', () => { + const schema = z.string(); + expect(getOptionMeta(schema)).toBeUndefined(); + }); + + it('preserves the original Zod schema behavior', () => { + const schema = option(z.string(), { env: 'TEST', help: 'test' }); + expect(schema.parse('hello')).toBe('hello'); + expect(() => schema.parse(123)).toThrow(); + }); + }); + + describe('getAllOptionMeta()', () => { + it('extracts all metadata from an object schema', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'APP_ID', help: 'App ID' }), + port: option(z.number(), { env: 'PORT', help: 'Port number' }), + noMeta: z.string(), + }); + + const allMeta = getAllOptionMeta(schema); + expect(allMeta.size).toBe(2); + expect(allMeta.get('appId').env).toBe('APP_ID'); + expect(allMeta.get('port').env).toBe('PORT'); + expect(allMeta.has('noMeta')).toBe(false); + }); + }); + + describe('buildEnvMap()', () => { + it('builds a flat env map for simple schemas', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'PARSE_SERVER_APP_ID', help: 'App ID' }), + port: option(z.number().default(1337), { env: 'PARSE_SERVER_PORT', help: 'Port' }), + }); + + const envMap = buildEnvMap(schema); + expect(envMap.size).toBe(2); + expect(envMap.get('PARSE_SERVER_APP_ID').path).toEqual(['appId']); + expect(envMap.get('PARSE_SERVER_PORT').path).toEqual(['port']); + }); + + it('builds a nested env map for nested object schemas', () => { + const innerSchema = z.object({ + strict: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_STRICT', + help: 'Strict mode', + }), + }); + + const schema = z.object({ + schema: option(innerSchema.optional(), { + env: 'PARSE_SERVER_SCHEMA', + help: 'Schema options', + }), + }); + + const envMap = buildEnvMap(schema); + expect(envMap.get('PARSE_SERVER_SCHEMA_STRICT').path).toEqual(['schema', 'strict']); + }); + + it('skips fields with no env metadata', () => { + const schema = z.object({ + appId: option(z.string(), { env: 'APP_ID', help: 'App ID' }), + secret: option(z.string(), { env: null, help: 'No env' }), + noMeta: z.string(), + }); + + const envMap = buildEnvMap(schema); + expect(envMap.size).toBe(1); + expect(envMap.has('APP_ID')).toBe(true); + }); + }); + + describe('coerceValue()', () => { + it('coerces string to number for ZodNumber', () => { + expect(coerceValue('42', z.number())).toBe(42); + }); + + it('coerces float strings to numbers for ZodNumber', () => { + expect(coerceValue('1.5', z.number())).toBe(1.5); + }); + + it('throws for non-numeric string with ZodNumber', () => { + expect(() => coerceValue('abc', z.number())).toThrow('Expected a number'); + }); + + it('coerces string to boolean for ZodBoolean', () => { + expect(coerceValue('true', z.boolean())).toBe(true); + expect(coerceValue('1', z.boolean())).toBe(true); + expect(coerceValue('false', z.boolean())).toBe(false); + expect(coerceValue('0', z.boolean())).toBe(false); + }); + + it('throws for unrecognized boolean string', () => { + expect(() => coerceValue('tru', z.boolean())).toThrow('Expected a boolean'); + }); + + it('coerces CSV string to array for ZodArray', () => { + const result = coerceValue('a,b,c', z.array(z.string())); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('coerces JSON array string to array for ZodArray', () => { + const result = coerceValue('["a","b"]', z.array(z.string())); + expect(result).toEqual(['a', 'b']); + }); + + it('coerces JSON string to object for ZodObject', () => { + const result = coerceValue('{"key":"val"}', z.object({ key: z.string() })); + expect(result).toEqual({ key: 'val' }); + }); + + it('returns string as-is for ZodString', () => { + expect(coerceValue('hello', z.string())).toBe('hello'); + }); + + it('handles optional wrappers', () => { + expect(coerceValue('42', z.number().optional())).toBe(42); + expect(coerceValue('true', z.boolean().default(false))).toBe(true); + }); + + it('handles union types by trying each branch', () => { + const schema = z.union([z.number(), z.string()]); + expect(coerceValue('42', schema)).toBe(42); + expect(coerceValue('hello', schema)).toBe('hello'); + }); + + it('skips function branches in unions', () => { + const schema = z.union([z.string(), z.function()]); + expect(coerceValue('hello', schema)).toBe('hello'); + }); + }); +}); diff --git a/spec/Options/schemaFeatures.spec.js b/spec/Options/schemaFeatures.spec.js new file mode 100644 index 0000000000..2e623fb963 --- /dev/null +++ b/spec/Options/schemaFeatures.spec.js @@ -0,0 +1,234 @@ +const { z } = require('zod'); +const { + option, + getDynamicKeys, + getOptionGroups, + warnInapplicableOptions, + getAllOptionMeta, +} = require('../../lib/Options/schemaUtils'); +const { ParseServerOptionsSchema } = require('../../lib/Options/schemas/ParseServerOptions'); + +describe('Phase 4: Advanced Features', () => { + + describe('Dynamic keys (#9052)', () => { + it('identifies dynamic keys from schema metadata', () => { + const dynamicKeys = getDynamicKeys(ParseServerOptionsSchema); + expect(dynamicKeys).toContain('masterKey'); + expect(dynamicKeys).toContain('publicServerURL'); + }); + + it('does not include non-dynamic keys', () => { + const dynamicKeys = getDynamicKeys(ParseServerOptionsSchema); + expect(dynamicKeys).not.toContain('appId'); + expect(dynamicKeys).not.toContain('port'); + expect(dynamicKeys).not.toContain('databaseURI'); + }); + + it('works with custom schemas', () => { + const schema = z.object({ + staticKey: option(z.string(), { env: 'A', help: 'static' }), + dynamicKey: option(z.union([z.string(), z.custom(v => typeof v === 'function')]), { + env: 'B', help: 'dynamic', dynamic: true, + }), + }); + const keys = getDynamicKeys(schema); + expect(keys).toEqual(['dynamicKey']); + }); + }); + + describe('Option groups (#7069)', () => { + it('returns logical option groups', () => { + const groups = getOptionGroups(); + expect(groups.length).toBeGreaterThan(0); + + const groupNames = groups.map(g => g.name); + expect(groupNames).toContain('Core'); + expect(groupNames).toContain('Security'); + expect(groupNames).toContain('Users & Auth'); + expect(groupNames).toContain('Database'); + expect(groupNames).toContain('GraphQL'); + expect(groupNames).toContain('LiveQuery'); + expect(groupNames).toContain('API Behavior'); + }); + + it('Core group contains essential fields', () => { + const groups = getOptionGroups(); + const core = groups.find(g => g.name === 'Core'); + expect(core.keys).toContain('appId'); + expect(core.keys).toContain('masterKey'); + expect(core.keys).toContain('serverURL'); + expect(core.keys).toContain('port'); + expect(core.keys).toContain('databaseURI'); + }); + + it('Security group contains security fields', () => { + const groups = getOptionGroups(); + const security = groups.find(g => g.name === 'Security'); + expect(security.keys).toContain('masterKeyIps'); + expect(security.keys).toContain('enforcePrivateUsers'); + expect(security.keys).toContain('security'); + }); + + it('all group keys exist in ParseServerOptions schema', () => { + const groups = getOptionGroups(); + const schemaKeys = Object.keys(ParseServerOptionsSchema.shape); + + for (const group of groups) { + for (const key of group.keys) { + expect(schemaKeys).toContain(key); + } + } + }); + + it('covers every schema key and contains no unknown keys', () => { + const groups = getOptionGroups(); + const schemaKeys = new Set(Object.keys(ParseServerOptionsSchema.shape)); + + // Collect all keys referenced by any group + const groupedKeys = new Set(); + for (const group of groups) { + for (const key of group.keys) { + groupedKeys.add(key); + } + } + + // Every schema key must appear in at least one group + const missingFromGroups = [...schemaKeys].filter(k => !groupedKeys.has(k)); + expect(missingFromGroups).toEqual([]); + + // No group key should reference a key absent from the schema + const extraInGroups = [...groupedKeys].filter(k => !schemaKeys.has(k)); + expect(extraInGroups).toEqual([]); + }); + + it('each group has name, description, and keys', () => { + const groups = getOptionGroups(); + for (const group of groups) { + expect(typeof group.name).toBe('string'); + expect(group.name.length).toBeGreaterThan(0); + expect(typeof group.description).toBe('string'); + expect(group.description.length).toBeGreaterThan(0); + expect(Array.isArray(group.keys)).toBe(true); + expect(group.keys.length).toBeGreaterThan(0); + } + }); + }); + + describe('Startup method applicability (#8432/#8300)', () => { + it('cluster is marked as CLI-only', () => { + const meta = getAllOptionMeta(ParseServerOptionsSchema); + const clusterMeta = meta.get('cluster'); + expect(clusterMeta.applicableTo).toEqual(['cli']); + }); + + it('startLiveQueryServer is marked as CLI-only', () => { + const meta = getAllOptionMeta(ParseServerOptionsSchema); + const lqMeta = meta.get('startLiveQueryServer'); + expect(lqMeta.applicableTo).toEqual(['cli']); + }); + + it('warnInapplicableOptions warns for CLI-only options in API context', () => { + const warnings = []; + const logger = msg => warnings.push(msg); + + warnInapplicableOptions( + { cluster: 2, appId: 'test' }, + 'api', + ParseServerOptionsSchema, + logger + ); + + expect(warnings.length).toBe(1); + expect(warnings[0]).toContain('cluster'); + expect(warnings[0]).toContain('cli'); + }); + + it('warnInapplicableOptions does not warn in correct context', () => { + const warnings = []; + const logger = msg => warnings.push(msg); + + warnInapplicableOptions( + { cluster: 2 }, + 'cli', + ParseServerOptionsSchema, + logger + ); + + expect(warnings.length).toBe(0); + }); + + it('warnInapplicableOptions does not warn for options without applicableTo', () => { + const warnings = []; + const logger = msg => warnings.push(msg); + + warnInapplicableOptions( + { appId: 'test', port: 1337, masterKey: 'key' }, + 'api', + ParseServerOptionsSchema, + logger + ); + + expect(warnings.length).toBe(0); + }); + + it('warnInapplicableOptions ignores undefined option values', () => { + const warnings = []; + const logger = msg => warnings.push(msg); + + warnInapplicableOptions( + { cluster: undefined }, + 'api', + ParseServerOptionsSchema, + logger + ); + + expect(warnings.length).toBe(0); + }); + }); + + describe('Config.js uses schema-driven dynamic keys', () => { + it('asyncKeys are derived from schema metadata', () => { + // Verify that Config.js now uses getDynamicKeys instead of hardcoded array + const dynamicKeys = getDynamicKeys(ParseServerOptionsSchema); + expect(dynamicKeys).toContain('publicServerURL'); + expect(dynamicKeys).toContain('masterKey'); + }); + + it('transformConfiguration renames function-valued dynamic keys with underscore prefix', () => { + const Config = require('../../lib/Config'); + const dynamicFn = () => 'https://example.com'; + const config = { + appId: 'test-app', + publicServerURL: dynamicFn, + masterKey: 'static-value', + }; + + Config.transformConfiguration(config); + + // Function-valued dynamic key should be renamed to _publicServerURL + expect(config._publicServerURL).toBe(dynamicFn); + expect(config.publicServerURL).toBeUndefined(); + + // Non-function dynamic key should remain unchanged + expect(config.masterKey).toBe('static-value'); + expect(config._masterKey).toBeUndefined(); + + // Non-dynamic key should remain unchanged + expect(config.appId).toBe('test-app'); + }); + + it('transformConfiguration leaves non-dynamic function values untouched', () => { + const Config = require('../../lib/Config'); + const customFn = () => 'value'; + const config = { + appId: customFn, + }; + + Config.transformConfiguration(config); + + // appId is not a dynamic key, so it should not be renamed even if it is a function + expect(config.appId).toBe(customFn); + expect(config._appId).toBeUndefined(); + }); + }); +}); diff --git a/spec/Options/schemas/schemas.spec.js b/spec/Options/schemas/schemas.spec.js new file mode 100644 index 0000000000..aa53156ecd --- /dev/null +++ b/spec/Options/schemas/schemas.spec.js @@ -0,0 +1,225 @@ +const { buildEnvMap } = require('../../../lib/Options/schemaUtils'); + +// Import all Zod schemas +const { ParseServerOptionsSchema } = require('../../../lib/Options/schemas/ParseServerOptions'); +const { SchemaOptionsSchema } = require('../../../lib/Options/schemas/SchemaOptions'); +const { AccountLockoutOptionsSchema } = require('../../../lib/Options/schemas/AccountLockoutOptions'); +const { PasswordPolicyOptionsSchema } = require('../../../lib/Options/schemas/PasswordPolicyOptions'); +const { FileUploadOptionsSchema } = require('../../../lib/Options/schemas/FileUploadOptions'); +const { IdempotencyOptionsSchema } = require('../../../lib/Options/schemas/IdempotencyOptions'); +const { SecurityOptionsSchema } = require('../../../lib/Options/schemas/SecurityOptions'); +const { RequestComplexityOptionsSchema } = require('../../../lib/Options/schemas/RequestComplexityOptions'); +const { + PagesOptionsSchema, + CustomPagesOptionsSchema, +} = require('../../../lib/Options/schemas/PagesOptions'); +const { + LiveQueryOptionsSchema, + LiveQueryServerOptionsSchema, +} = require('../../../lib/Options/schemas/LiveQueryOptions'); +const { RateLimitOptionsSchema } = require('../../../lib/Options/schemas/RateLimitOptions'); +const { LogLevelsSchema } = require('../../../lib/Options/schemas/LogLevels'); +const { DatabaseOptionsSchema } = require('../../../lib/Options/schemas/DatabaseOptions'); + +describe('ParseServerOptionsSchema', () => { + it('validates a minimal valid config', () => { + const result = ParseServerOptionsSchema.safeParse({ + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + databaseURI: 'mongodb://localhost:27017/parse', + }); + expect(result.success).toBe(true); + }); + + it('rejects config missing required fields', () => { + const result = ParseServerOptionsSchema.safeParse({}); + expect(result.success).toBe(false); + const paths = result.error.issues.map(i => i.path[0]); + expect(paths).toContain('appId'); + expect(paths).toContain('masterKey'); + expect(paths).toContain('serverURL'); + }); + + it('applies correct defaults', () => { + const result = ParseServerOptionsSchema.parse({ + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + }); + + expect(result.port).toBe(1337); + expect(result.defaultLimit).toBe(100); + expect(result.cacheMaxSize).toBe(10000); + expect(result.cacheTTL).toBe(5000); + expect(result.sessionLength).toBe(31536000); + expect(result.objectIdSize).toBe(10); + expect(result.host).toBe('0.0.0.0'); + expect(result.mountPath).toBe('/parse'); + expect(result.graphQLPath).toBe('/graphql'); + expect(result.maxUploadSize).toBe('20mb'); + expect(result.collectionPrefix).toBe(''); + expect(result.databaseURI).toBe('mongodb://localhost:27017/parse'); + expect(result.masterKeyIps).toEqual(['127.0.0.1', '::1']); + expect(result.maintenanceKeyIps).toEqual(['127.0.0.1', '::1']); + expect(result.readOnlyMasterKeyIps).toEqual(['0.0.0.0/0', '::0']); + expect(result.enforcePrivateUsers).toBe(true); + expect(result.revokeSessionOnPasswordReset).toBe(true); + expect(result.allowClientClassCreation).toBe(false); + expect(result.verifyServerUrl).toBe(true); + expect(result.directAccess).toBe(true); + expect(result.rateLimit).toEqual([]); + expect(result.trustProxy).toEqual([]); + }); + + it('accepts masterKey as a function', () => { + const fn = () => 'dynamicKey'; + const result = ParseServerOptionsSchema.safeParse({ + appId: 'myApp', + masterKey: fn, + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + }); + expect(result.success).toBe(true); + expect(result.data.masterKey).toBe(fn); + }); + + it('accepts verifyUserEmails as a function', () => { + const fn = () => true; + const result = ParseServerOptionsSchema.safeParse({ + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + verifyUserEmails: fn, + }); + expect(result.success).toBe(true); + expect(result.data.verifyUserEmails).toBe(fn); + }); + + it('accepts nested schema options', () => { + const result = ParseServerOptionsSchema.safeParse({ + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + schema: { strict: true, definitions: [] }, + accountLockout: { duration: 5, threshold: 3 }, + idempotencyOptions: { ttl: 600, paths: ['.*'] }, + }); + expect(result.success).toBe(true); + expect(result.data.schema.strict).toBe(true); + expect(result.data.accountLockout.duration).toBe(5); + expect(result.data.idempotencyOptions.ttl).toBe(600); + }); + + it('accepts rateLimit as array of RateLimitOptions', () => { + const result = ParseServerOptionsSchema.safeParse({ + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + rateLimit: [ + { requestPath: '/login', requestCount: 10, requestTimeWindow: 60000 }, + ], + }); + expect(result.success).toBe(true); + expect(result.data.rateLimit).toHaveLength(1); + expect(result.data.rateLimit[0].requestPath).toBe('/login'); + }); + + it('allows unknown keys with passthrough', () => { + const result = ParseServerOptionsSchema.safeParse({ + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + unknownOption: 'value', + }); + expect(result.success).toBe(true); + expect(result.data.unknownOption).toBe('value'); + }); +}); + +describe('Nested schemas', () => { + it('SchemaOptions applies defaults', () => { + const result = SchemaOptionsSchema.parse({}); + expect(result.strict).toBe(false); + expect(result.deleteExtraFields).toBe(false); + expect(result.lockSchemas).toBe(false); + expect(result.definitions).toEqual([]); + }); + + it('FileUploadOptions applies defaults', () => { + const result = FileUploadOptionsSchema.parse({}); + expect(result.enableForAnonymousUser).toBe(false); + expect(result.enableForAuthenticatedUser).toBe(true); + expect(result.enableForPublic).toBe(false); + expect(result.allowedFileUrlDomains).toEqual(['*']); + }); + + it('SecurityOptions applies defaults', () => { + const result = SecurityOptionsSchema.parse({}); + expect(result.enableCheck).toBe(false); + expect(result.enableCheckLog).toBe(false); + }); + + it('IdempotencyOptions applies defaults', () => { + const result = IdempotencyOptionsSchema.parse({}); + expect(result.paths).toEqual([]); + expect(result.ttl).toBe(300); + }); + + it('RequestComplexityOptions applies defaults', () => { + const result = RequestComplexityOptionsSchema.parse({}); + expect(result.graphQLDepth).toBe(-1); + expect(result.queryDepth).toBe(-1); + expect(result.includeDepth).toBe(-1); + }); + + it('RateLimitOptions requires requestPath', () => { + const result = RateLimitOptionsSchema.safeParse({}); + expect(result.success).toBe(false); + }); + + it('DatabaseOptions applies defaults', () => { + const result = DatabaseOptionsSchema.parse({}); + expect(result.allowPublicExplain).toBe(false); + expect(result.batchSize).toBe(1000); + expect(result.enableSchemaHooks).toBe(false); + expect(result.createIndexUserEmail).toBe(true); + }); +}); + +describe('Env var map coverage', () => { + it('ParseServerOptions env map contains key env vars', () => { + const envMap = buildEnvMap(ParseServerOptionsSchema); + // Flat env vars + expect(envMap.has('PARSE_SERVER_APPLICATION_ID')).toBe(true); + expect(envMap.has('PARSE_SERVER_MASTER_KEY')).toBe(true); + expect(envMap.has('PARSE_SERVER_URL')).toBe(true); + expect(envMap.has('PORT')).toBe(true); + expect(envMap.has('VERBOSE')).toBe(true); + expect(envMap.has('JSON_LOGS')).toBe(true); + + // Nested env vars (solving #7151) + expect(envMap.has('PARSE_SERVER_SCHEMA_STRICT')).toBe(true); + expect(envMap.has('PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL')).toBe(true); + expect(envMap.has('PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS')).toBe(true); + expect(envMap.has('PARSE_SERVER_SECURITY_ENABLE_CHECK')).toBe(true); + expect(envMap.has('PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION')).toBe(true); + expect(envMap.has('PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE')).toBe(true); + expect(envMap.has('PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC')).toBe(true); + }); + + it('nested env vars map to correct paths', () => { + const envMap = buildEnvMap(ParseServerOptionsSchema); + + expect(envMap.get('PARSE_SERVER_SCHEMA_STRICT').path).toEqual(['schema', 'strict']); + expect(envMap.get('PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL').path).toEqual(['idempotencyOptions', 'ttl']); + expect(envMap.get('PARSE_SERVER_SECURITY_ENABLE_CHECK').path).toEqual(['security', 'enableCheck']); + expect(envMap.get('PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION').path).toEqual(['accountLockout', 'duration']); + }); +}); diff --git a/spec/Options/validateConfig.spec.js b/spec/Options/validateConfig.spec.js new file mode 100644 index 0000000000..1a5611d21c --- /dev/null +++ b/spec/Options/validateConfig.spec.js @@ -0,0 +1,195 @@ +const { validateConfig } = require('../../lib/Options/validateConfig'); + +const validConfig = { + appId: 'myApp', + masterKey: 'myMasterKey', + maintenanceKey: 'myMaintenanceKey', + serverURL: 'http://localhost:1337/parse', + databaseURI: 'mongodb://localhost:27017/parse', +}; + +describe('validateConfig', () => { + describe('basic validation', () => { + it('validates a minimal valid config and applies defaults', () => { + const result = validateConfig({ ...validConfig }); + expect(result.appId).toBe('myApp'); + expect(result.port).toBe(1337); + expect(result.defaultLimit).toBe(100); + expect(result.masterKeyIps).toEqual(['127.0.0.1', '::1']); + expect(result.rateLimit).toEqual([]); + }); + + it('throws on missing required fields', () => { + expect(() => validateConfig({})).toThrow('Parse Server configuration error'); + }); + + it('throws on missing appId', () => { + const config = { ...validConfig }; + delete config.appId; + expect(() => validateConfig(config)).toThrow('appId'); + }); + + it('throws on missing masterKey', () => { + const config = { ...validConfig }; + delete config.masterKey; + expect(() => validateConfig(config)).toThrow('masterKey'); + }); + }); + + describe('cross-field validations', () => { + it('throws when masterKey equals readOnlyMasterKey', () => { + expect(() => + validateConfig({ ...validConfig, readOnlyMasterKey: 'myMasterKey' }) + ).toThrow('masterKey and readOnlyMasterKey should be different'); + }); + + it('throws when masterKey equals maintenanceKey', () => { + expect(() => + validateConfig({ ...validConfig, maintenanceKey: 'myMasterKey' }) + ).toThrow('masterKey and maintenanceKey should be different'); + }); + + it('validates account lockout duration', () => { + expect(() => + validateConfig({ ...validConfig, accountLockout: { duration: 0, threshold: 3 } }) + ).toThrow('Account lockout duration should be greater than 0'); + }); + + it('validates account lockout threshold', () => { + expect(() => + validateConfig({ ...validConfig, accountLockout: { duration: 5, threshold: 0 } }) + ).toThrow('Account lockout threshold should be greater than 0'); + }); + + it('validates IP addresses in masterKeyIps', () => { + expect(() => + validateConfig({ ...validConfig, masterKeyIps: ['invalid-ip'] }) + ).toThrow('invalid IP address'); + }); + + it('accepts valid IP addresses', () => { + const result = validateConfig({ + ...validConfig, + masterKeyIps: ['127.0.0.1', '10.0.0.0/8'], + }); + expect(result.masterKeyIps).toEqual(['127.0.0.1', '10.0.0.0/8']); + }); + + it('validates default limit must be positive', () => { + expect(() => validateConfig({ ...validConfig, defaultLimit: 0 })).toThrow( + 'Default limit must be a value greater than 0' + ); + }); + + it('validates max limit must be positive', () => { + expect(() => validateConfig({ ...validConfig, maxLimit: -1 })).toThrow( + 'Max limit must be a value greater than 0' + ); + }); + + it('validates session length when expiring sessions', () => { + expect(() => + validateConfig({ ...validConfig, expireInactiveSessions: true, sessionLength: 0 }) + ).toThrow('Session length must be a value greater than 0'); + }); + + it('validates idempotency TTL must be positive', () => { + expect(() => + validateConfig({ ...validConfig, idempotencyOptions: { ttl: 0, paths: [] } }) + ).toThrow('idempotency TTL value must be greater than 0'); + }); + + it('validates publicServerURL must start with http', () => { + expect(() => + validateConfig({ ...validConfig, publicServerURL: 'ftp://example.com' }) + ).toThrow('publicServerURL must start with http:// or https://'); + }); + + it('accepts publicServerURL as function', () => { + const result = validateConfig({ + ...validConfig, + publicServerURL: () => 'http://example.com', + }); + expect(typeof result.publicServerURL).toBe('function'); + }); + + it('validates password policy constraints', () => { + expect(() => + validateConfig({ + ...validConfig, + passwordPolicy: { maxPasswordAge: -1 }, + }) + ).toThrow('passwordPolicy.maxPasswordAge'); + }); + + it('validates password policy resetTokenReuseIfValid requires duration', () => { + expect(() => + validateConfig({ + ...validConfig, + passwordPolicy: { resetTokenReuseIfValid: true }, + }) + ).toThrow('You cannot use resetTokenReuseIfValid without resetTokenValidityDuration'); + }); + + it('validates allow headers must be array of strings', () => { + expect(() => + validateConfig({ ...validConfig, allowHeaders: ['valid', ''] }) + ).toThrow('Allow headers must not contain empty strings'); + }); + + it('validates requestComplexity values', () => { + expect(() => + validateConfig({ + ...validConfig, + requestComplexity: { queryDepth: 0 }, + }) + ).toThrow('positive integer or -1'); + }); + }); + + describe('type coercion and defaults', () => { + it('preserves function values for masterKey', () => { + const fn = () => 'dynamicKey'; + const result = validateConfig({ + ...validConfig, + masterKey: fn, + }); + expect(result.masterKey).toBe(fn); + }); + + it('preserves function values for verifyUserEmails', () => { + const fn = () => true; + const result = validateConfig({ + ...validConfig, + verifyUserEmails: fn, + }); + expect(result.verifyUserEmails).toBe(fn); + }); + + it('applies nested option defaults', () => { + const result = validateConfig({ + ...validConfig, + security: {}, + }); + expect(result.security.enableCheck).toBe(false); + expect(result.security.enableCheckLog).toBe(false); + }); + + it('applies idempotency defaults', () => { + const result = validateConfig({ + ...validConfig, + idempotencyOptions: {}, + }); + expect(result.idempotencyOptions.paths).toEqual([]); + expect(result.idempotencyOptions.ttl).toBe(300); + }); + + it('allows unknown keys via passthrough', () => { + const result = validateConfig({ + ...validConfig, + customKey: 'customValue', + }); + expect(result.customKey).toBe('customValue'); + }); + }); +}); diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index f25035ec2c..19c7999464 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -7,7 +7,8 @@ const mustache = require('mustache'); const Utils = require('../lib/Utils'); const { Page } = require('../lib/Page'); const Config = require('../lib/Config'); -const Definitions = require('../lib/Options/Definitions'); +const { PagesOptionsSchema } = require('../lib/Options/schemas/PagesOptions'); +const pagesDefaults = PagesOptionsSchema.parse({}); const UserController = require('../lib/Controllers/UserController').UserController; const { PagesRouter, @@ -206,29 +207,29 @@ describe('Pages Router', () => { it('uses default configuration when none is set', async () => { await reconfigureServerWithPagesConfig({}); expect(Config.get(Parse.applicationId).pages.enableLocalization).toBe( - Definitions.PagesOptions.enableLocalization.default + pagesDefaults.enableLocalization ); expect(Config.get(Parse.applicationId).pages.localizationJsonPath).toBe( - Definitions.PagesOptions.localizationJsonPath.default + pagesDefaults.localizationJsonPath ); expect(Config.get(Parse.applicationId).pages.localizationFallbackLocale).toBe( - Definitions.PagesOptions.localizationFallbackLocale.default + pagesDefaults.localizationFallbackLocale ); expect(Config.get(Parse.applicationId).pages.placeholders).toBe( - Definitions.PagesOptions.placeholders.default + pagesDefaults.placeholders ); expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe( - Definitions.PagesOptions.forceRedirect.default + pagesDefaults.forceRedirect ); expect(Config.get(Parse.applicationId).pages.pagesPath).toBeUndefined(); expect(Config.get(Parse.applicationId).pages.pagesEndpoint).toBe( - Definitions.PagesOptions.pagesEndpoint.default + pagesDefaults.pagesEndpoint ); expect(Config.get(Parse.applicationId).pages.customUrls).toBe( - Definitions.PagesOptions.customUrls.default + pagesDefaults.customUrls ); expect(Config.get(Parse.applicationId).pages.customRoutes).toBe( - Definitions.PagesOptions.customRoutes.default + pagesDefaults.customRoutes ); }); diff --git a/spec/SecurityCheck.spec.js b/spec/SecurityCheck.spec.js index 6b752ff972..637189b699 100644 --- a/spec/SecurityCheck.spec.js +++ b/spec/SecurityCheck.spec.js @@ -3,7 +3,8 @@ const Utils = require('../lib/Utils'); const Config = require('../lib/Config'); const request = require('../lib/request'); -const Definitions = require('../lib/Options/Definitions'); +const { SecurityOptionsSchema } = require('../lib/Options/schemas/SecurityOptions'); +const securityDefaults = SecurityOptionsSchema.parse({}); const { Check, CheckState } = require('../lib/Security/Check'); const CheckGroup = require('../lib/Security/CheckGroup'); const CheckRunner = require('../lib/Security/CheckRunner'); @@ -82,10 +83,10 @@ describe('Security Check', () => { it('uses default configuration when none is set', async () => { await reconfigureServerWithSecurityConfig({}); expect(Config.get(Parse.applicationId).security.enableCheck).toBe( - Definitions.SecurityOptions.enableCheck.default + securityDefaults.enableCheck ); expect(Config.get(Parse.applicationId).security.enableCheckLog).toBe( - Definitions.SecurityOptions.enableCheckLog.default + securityDefaults.enableCheckLog ); }); diff --git a/spec/buildConfigDefinitions.spec.js b/spec/buildConfigDefinitions.spec.js deleted file mode 100644 index bc15793a04..0000000000 --- a/spec/buildConfigDefinitions.spec.js +++ /dev/null @@ -1,219 +0,0 @@ -const t = require('@babel/types'); -const { mapperFor } = require('../resources/buildConfigDefinitions'); - -describe('buildConfigDefinitions', () => { - describe('mapperFor', () => { - it('should return objectParser for ObjectTypeAnnotation', () => { - const mockElement = { - type: 'ObjectTypeAnnotation', - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('objectParser'); - }); - - it('should return objectParser for AnyTypeAnnotation', () => { - const mockElement = { - type: 'AnyTypeAnnotation', - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('objectParser'); - }); - - it('should return arrayParser for ArrayTypeAnnotation', () => { - const mockElement = { - type: 'ArrayTypeAnnotation', - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('arrayParser'); - }); - - it('should return booleanParser for BooleanTypeAnnotation', () => { - const mockElement = { - type: 'BooleanTypeAnnotation', - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('booleanParser'); - }); - - it('should return numberParser call expression for NumberTypeAnnotation', () => { - const mockElement = { - type: 'NumberTypeAnnotation', - name: 'testNumber', - }; - - const result = mapperFor(mockElement, t); - - expect(t.isCallExpression(result)).toBe(true); - expect(result.callee.property.name).toBe('numberParser'); - expect(result.arguments[0].value).toBe('testNumber'); - }); - - it('should return moduleOrObjectParser for Adapter GenericTypeAnnotation', () => { - const mockElement = { - type: 'GenericTypeAnnotation', - typeAnnotation: { - id: { - name: 'Adapter', - }, - }, - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('moduleOrObjectParser'); - }); - - it('should return numberOrBooleanParser for NumberOrBoolean GenericTypeAnnotation', () => { - const mockElement = { - type: 'GenericTypeAnnotation', - typeAnnotation: { - id: { - name: 'NumberOrBoolean', - }, - }, - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('numberOrBooleanParser'); - }); - - it('should return numberOrStringParser call expression for NumberOrString GenericTypeAnnotation', () => { - const mockElement = { - type: 'GenericTypeAnnotation', - name: 'testString', - typeAnnotation: { - id: { - name: 'NumberOrString', - }, - }, - }; - - const result = mapperFor(mockElement, t); - - expect(t.isCallExpression(result)).toBe(true); - expect(result.callee.property.name).toBe('numberOrStringParser'); - expect(result.arguments[0].value).toBe('testString'); - }); - - it('should return arrayParser for StringOrStringArray GenericTypeAnnotation', () => { - const mockElement = { - type: 'GenericTypeAnnotation', - typeAnnotation: { - id: { - name: 'StringOrStringArray', - }, - }, - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('arrayParser'); - }); - - it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (nullable)', () => { - const mockElement = { - type: 'UnionTypeAnnotation', - typeAnnotation: { - types: [ - { type: 'BooleanTypeAnnotation' }, - { type: 'FunctionTypeAnnotation' }, - ], - }, - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('booleanOrFunctionParser'); - }); - - it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (non-nullable)', () => { - const mockElement = { - type: 'UnionTypeAnnotation', - types: [ - { type: 'BooleanTypeAnnotation' }, - { type: 'FunctionTypeAnnotation' }, - ], - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('booleanOrFunctionParser'); - }); - - it('should return undefined for UnionTypeAnnotation without boolean', () => { - const mockElement = { - type: 'UnionTypeAnnotation', - typeAnnotation: { - types: [ - { type: 'StringTypeAnnotation' }, - { type: 'NumberTypeAnnotation' }, - ], - }, - }; - - const result = mapperFor(mockElement, t); - - expect(result).toBeUndefined(); - }); - - it('should return undefined for UnionTypeAnnotation with boolean but without function', () => { - const mockElement = { - type: 'UnionTypeAnnotation', - typeAnnotation: { - types: [ - { type: 'BooleanTypeAnnotation' }, - { type: 'VoidTypeAnnotation' }, - ], - }, - }; - - const result = mapperFor(mockElement, t); - - expect(result).toBeUndefined(); - }); - - it('should return objectParser for unknown GenericTypeAnnotation', () => { - const mockElement = { - type: 'GenericTypeAnnotation', - typeAnnotation: { - id: { - name: 'UnknownType', - }, - }, - }; - - const result = mapperFor(mockElement, t); - - expect(t.isMemberExpression(result)).toBe(true); - expect(result.object.name).toBe('parsers'); - expect(result.property.name).toBe('objectParser'); - }); - }); -}); diff --git a/spec/index.spec.js b/spec/index.spec.js index 988f35cc3e..43678cd750 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -467,8 +467,11 @@ describe('server', () => { }); it('should set default masterKeyIps for IPv4 and IPv6 localhost', () => { - const definitions = require('../lib/Options/Definitions.js'); - expect(definitions.ParseServerOptions.masterKeyIps.default).toEqual(['127.0.0.1', '::1']); + const { ParseServerOptionsSchema } = require('../lib/Options/schemas/ParseServerOptions'); + const defaults = ParseServerOptionsSchema.parse({ + appId: 'x', masterKey: 'x', maintenanceKey: 'x', serverURL: 'http://x', + }); + expect(defaults.masterKeyIps).toEqual(['127.0.0.1', '::1']); }); it('should load a middleware', done => { diff --git a/spec/parsers.spec.js b/spec/parsers.spec.js deleted file mode 100644 index a844016ba7..0000000000 --- a/spec/parsers.spec.js +++ /dev/null @@ -1,101 +0,0 @@ -const { - numberParser, - numberOrBoolParser, - numberOrStringParser, - booleanParser, - booleanOrFunctionParser, - objectParser, - arrayParser, - moduleOrObjectParser, - nullParser, -} = require('../lib/Options/parsers'); - -describe('parsers', () => { - it('parses correctly with numberParser', () => { - const parser = numberParser('key'); - expect(parser(2)).toEqual(2); - expect(parser('2')).toEqual(2); - expect(() => { - parser('string'); - }).toThrow(); - }); - - it('parses correctly with numberOrStringParser', () => { - const parser = numberOrStringParser('key'); - expect(parser('100d')).toEqual('100d'); - expect(parser(100)).toEqual(100); - expect(() => { - parser(undefined); - }).toThrow(); - }); - - it('parses correctly with numberOrBoolParser', () => { - const parser = numberOrBoolParser('key'); - expect(parser(true)).toEqual(true); - expect(parser(false)).toEqual(false); - expect(parser('true')).toEqual(true); - expect(parser('false')).toEqual(false); - expect(parser(1)).toEqual(1); - expect(parser('1')).toEqual(1); - }); - - it('parses correctly with booleanParser', () => { - const parser = booleanParser; - expect(parser(true)).toEqual(true); - expect(parser(false)).toEqual(false); - expect(parser('true')).toEqual(true); - expect(parser('false')).toEqual(false); - expect(parser(1)).toEqual(true); - expect(parser(2)).toEqual(false); - }); - - it('parses correctly with booleanOrFunctionParser', () => { - const parser = booleanOrFunctionParser; - // Preserves functions - const fn = () => true; - expect(parser(fn)).toBe(fn); - const asyncFn = async () => false; - expect(parser(asyncFn)).toBe(asyncFn); - // Parses booleans and string booleans like booleanParser - expect(parser(true)).toEqual(true); - expect(parser(false)).toEqual(false); - expect(parser('true')).toEqual(true); - expect(parser('false')).toEqual(false); - expect(parser('1')).toEqual(true); - expect(parser(1)).toEqual(true); - expect(parser(0)).toEqual(false); - }); - - it('parses correctly with objectParser', () => { - const parser = objectParser; - expect(parser({ hello: 'world' })).toEqual({ hello: 'world' }); - expect(parser('{"hello": "world"}')).toEqual({ hello: 'world' }); - expect(() => { - parser('string'); - }).toThrow(); - }); - - it('parses correctly with moduleOrObjectParser', () => { - const parser = moduleOrObjectParser; - expect(parser({ hello: 'world' })).toEqual({ hello: 'world' }); - expect(parser('{"hello": "world"}')).toEqual({ hello: 'world' }); - expect(parser('string')).toEqual('string'); - }); - - it('parses correctly with arrayParser', () => { - const parser = arrayParser; - expect(parser([1, 2, 3])).toEqual([1, 2, 3]); - expect(parser('{"hello": "world"}')).toEqual(['{"hello": "world"}']); - expect(parser('1,2,3')).toEqual(['1', '2', '3']); - expect(() => { - parser(1); - }).toThrow(); - }); - - it('parses correctly with nullParser', () => { - const parser = nullParser; - expect(parser('null')).toEqual(null); - expect(parser(1)).toEqual(1); - expect(parser('blabla')).toEqual('blabla'); - }); -}); diff --git a/src/Config.js b/src/Config.js index 924fce3ee8..69ffe15eed 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,29 +2,14 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. -import { isBoolean, isString } from 'lodash'; -import { pathToRegexp } from 'path-to-regexp'; -import net from 'net'; import AppCache from './cache'; import DatabaseController from './Controllers/DatabaseController'; -import { logLevels as validLogLevels } from './Controllers/LoggerController'; import { version } from '../package.json'; -import { - AccountLockoutOptions, - DatabaseOptions, - FileUploadOptions, - IdempotencyOptions, - LiveQueryOptions, - LogLevels, - PagesOptions, - ParseServerOptions, - SchemaOptions, - RequestComplexityOptions, - SecurityOptions, -} from './Options/Definitions'; -import ParseServer from './cloud-code/Parse.Server'; -import Deprecator from './Deprecator/Deprecator'; -import Utils from './Utils'; +import { getDynamicKeys } from './Options/schemaUtils'; +import { ParseServerOptionsSchema } from './Options/schemas/ParseServerOptions'; +import { ControllerValidator } from './Options/validators/ControllerValidator'; + +const controllerValidator = new ControllerValidator(); function removeTrailingSlash(str) { if (!str) { @@ -39,7 +24,7 @@ function removeTrailingSlash(str) { /** * Config keys that need to be loaded asynchronously. */ -const asyncKeys = ['publicServerURL']; +const asyncKeys = getDynamicKeys(ParseServerOptionsSchema); export class Config { static get(applicationId: string, mount: string) { @@ -78,13 +63,13 @@ export class Config { }) ); - const cachedConfig = AppCache.get(this.appId); + const cachedConfig = AppCache.get(this.applicationId); if (cachedConfig) { const updatedConfig = { ...cachedConfig }; asyncKeys.forEach(key => { updatedConfig[key] = this[key]; }); - AppCache.put(this.appId, updatedConfig); + AppCache.put(this.applicationId, updatedConfig); } } @@ -98,507 +83,24 @@ export class Config { } static put(serverConfiguration) { - Config.validateOptions(serverConfiguration); - Config.validateControllers(serverConfiguration); + controllerValidator.validate(serverConfiguration); Config.transformConfiguration(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; } - static validateOptions({ - customPages, - publicServerURL, - revokeSessionOnPasswordReset, - expireInactiveSessions, - sessionLength, - defaultLimit, - maxLimit, - accountLockout, - passwordPolicy, - masterKeyIps, - masterKey, - maintenanceKey, - maintenanceKeyIps, - readOnlyMasterKey, - readOnlyMasterKeyIps, - allowHeaders, - idempotencyOptions, - fileUpload, - pages, - security, - enforcePrivateUsers, - enableInsecureAuthAdapters, - schema, - requestKeywordDenylist, - allowExpiredAuthDataToken, - logLevels, - rateLimit, - databaseOptions, - extendSessionOnUse, - allowClientClassCreation, - requestComplexity, - liveQuery, - }) { - if (masterKey === readOnlyMasterKey) { - throw new Error('masterKey and readOnlyMasterKey should be different'); - } - - if (masterKey === maintenanceKey) { - throw new Error('masterKey and maintenanceKey should be different'); - } - - this.validateAccountLockoutPolicy(accountLockout); - this.validatePasswordPolicy(passwordPolicy); - this.validateFileUploadOptions(fileUpload); - - if (typeof revokeSessionOnPasswordReset !== 'boolean') { - throw 'revokeSessionOnPasswordReset must be a boolean value'; - } - - if (typeof extendSessionOnUse !== 'boolean') { - throw 'extendSessionOnUse must be a boolean value'; - } - - this.validatePublicServerURL({ publicServerURL }); - this.validateSessionConfiguration(sessionLength, expireInactiveSessions); - this.validateIps('masterKeyIps', masterKeyIps); - this.validateIps('maintenanceKeyIps', maintenanceKeyIps); - this.validateIps('readOnlyMasterKeyIps', readOnlyMasterKeyIps); - this.validateDefaultLimit(defaultLimit); - this.validateMaxLimit(maxLimit); - this.validateAllowHeaders(allowHeaders); - this.validateIdempotencyOptions(idempotencyOptions); - this.validatePagesOptions(pages); - this.validateSecurityOptions(security); - this.validateSchemaOptions(schema); - this.validateEnforcePrivateUsers(enforcePrivateUsers); - this.validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters); - this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken); - this.validateRequestKeywordDenylist(requestKeywordDenylist); - this.validateRateLimit(rateLimit); - this.validateLogLevels(logLevels); - this.validateDatabaseOptions(databaseOptions); - this.validateCustomPages(customPages); - this.validateAllowClientClassCreation(allowClientClassCreation); - this.validateRequestComplexity(requestComplexity); - this.validateLiveQueryOptions(liveQuery); - } - - static validateCustomPages(customPages) { - if (!customPages) { return; } - - if (Object.prototype.toString.call(customPages) !== '[object Object]') { - throw Error('Parse Server option customPages must be an object.'); - } - } - - static validateControllers({ - verifyUserEmails, - userController, - appName, - publicServerURL, - _publicServerURL, - emailVerifyTokenValidityDuration, - emailVerifyTokenReuseIfValid, - emailVerifySuccessOnInvalidEmail, - }) { - const emailAdapter = userController.adapter; - if (verifyUserEmails) { - this.validateEmailConfiguration({ - emailAdapter, - appName, - publicServerURL: publicServerURL || _publicServerURL, - emailVerifyTokenValidityDuration, - emailVerifyTokenReuseIfValid, - emailVerifySuccessOnInvalidEmail, - }); - } - } - - static validateRequestKeywordDenylist(requestKeywordDenylist) { - if (requestKeywordDenylist === undefined) { - requestKeywordDenylist = requestKeywordDenylist.default; - } else if (!Array.isArray(requestKeywordDenylist)) { - throw 'Parse Server option requestKeywordDenylist must be an array.'; - } - } - - static validateEnforcePrivateUsers(enforcePrivateUsers) { - if (typeof enforcePrivateUsers !== 'boolean') { - throw 'Parse Server option enforcePrivateUsers must be a boolean.'; - } - } - - static validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken) { - if (typeof allowExpiredAuthDataToken !== 'boolean') { - throw 'Parse Server option allowExpiredAuthDataToken must be a boolean.'; - } - } - - static validateAllowClientClassCreation(allowClientClassCreation) { - if (typeof allowClientClassCreation !== 'boolean') { - throw 'Parse Server option allowClientClassCreation must be a boolean.'; - } - } - - static validateSecurityOptions(security) { - if (Object.prototype.toString.call(security) !== '[object Object]') { - throw 'Parse Server option security must be an object.'; - } - if (security.enableCheck === undefined) { - security.enableCheck = SecurityOptions.enableCheck.default; - } else if (!isBoolean(security.enableCheck)) { - throw 'Parse Server option security.enableCheck must be a boolean.'; - } - if (security.enableCheckLog === undefined) { - security.enableCheckLog = SecurityOptions.enableCheckLog.default; - } else if (!isBoolean(security.enableCheckLog)) { - throw 'Parse Server option security.enableCheckLog must be a boolean.'; - } - } - - static validateSchemaOptions(schema: SchemaOptions) { - if (!schema) { return; } - if (Object.prototype.toString.call(schema) !== '[object Object]') { - throw 'Parse Server option schema must be an object.'; - } - if (schema.definitions === undefined) { - schema.definitions = SchemaOptions.definitions.default; - } else if (!Array.isArray(schema.definitions)) { - throw 'Parse Server option schema.definitions must be an array.'; - } - if (schema.strict === undefined) { - schema.strict = SchemaOptions.strict.default; - } else if (!isBoolean(schema.strict)) { - throw 'Parse Server option schema.strict must be a boolean.'; - } - if (schema.deleteExtraFields === undefined) { - schema.deleteExtraFields = SchemaOptions.deleteExtraFields.default; - } else if (!isBoolean(schema.deleteExtraFields)) { - throw 'Parse Server option schema.deleteExtraFields must be a boolean.'; - } - if (schema.recreateModifiedFields === undefined) { - schema.recreateModifiedFields = SchemaOptions.recreateModifiedFields.default; - } else if (!isBoolean(schema.recreateModifiedFields)) { - throw 'Parse Server option schema.recreateModifiedFields must be a boolean.'; - } - if (schema.lockSchemas === undefined) { - schema.lockSchemas = SchemaOptions.lockSchemas.default; - } else if (!isBoolean(schema.lockSchemas)) { - throw 'Parse Server option schema.lockSchemas must be a boolean.'; - } - if (schema.beforeMigration === undefined) { - schema.beforeMigration = null; - } else if (schema.beforeMigration !== null && typeof schema.beforeMigration !== 'function') { - throw 'Parse Server option schema.beforeMigration must be a function.'; - } - if (schema.afterMigration === undefined) { - schema.afterMigration = null; - } else if (schema.afterMigration !== null && typeof schema.afterMigration !== 'function') { - throw 'Parse Server option schema.afterMigration must be a function.'; - } - } - - static validatePagesOptions(pages) { - if (Object.prototype.toString.call(pages) !== '[object Object]') { - throw 'Parse Server option pages must be an object.'; - } - if (pages.enableLocalization === undefined) { - pages.enableLocalization = PagesOptions.enableLocalization.default; - } else if (!isBoolean(pages.enableLocalization)) { - throw 'Parse Server option pages.enableLocalization must be a boolean.'; - } - if (pages.localizationJsonPath === undefined) { - pages.localizationJsonPath = PagesOptions.localizationJsonPath.default; - } else if (!isString(pages.localizationJsonPath)) { - throw 'Parse Server option pages.localizationJsonPath must be a string.'; - } - if (pages.localizationFallbackLocale === undefined) { - pages.localizationFallbackLocale = PagesOptions.localizationFallbackLocale.default; - } else if (!isString(pages.localizationFallbackLocale)) { - throw 'Parse Server option pages.localizationFallbackLocale must be a string.'; - } - if (pages.placeholders === undefined) { - pages.placeholders = PagesOptions.placeholders.default; - } else if ( - Object.prototype.toString.call(pages.placeholders) !== '[object Object]' && - typeof pages.placeholders !== 'function' - ) { - throw 'Parse Server option pages.placeholders must be an object or a function.'; - } - if (pages.forceRedirect === undefined) { - pages.forceRedirect = PagesOptions.forceRedirect.default; - } else if (!isBoolean(pages.forceRedirect)) { - throw 'Parse Server option pages.forceRedirect must be a boolean.'; - } - if (pages.pagesPath !== undefined && !isString(pages.pagesPath)) { - throw 'Parse Server option pages.pagesPath must be a string.'; - } - if (pages.pagesEndpoint === undefined) { - pages.pagesEndpoint = PagesOptions.pagesEndpoint.default; - } else if (!isString(pages.pagesEndpoint)) { - throw 'Parse Server option pages.pagesEndpoint must be a string.'; - } - if (pages.customUrls === undefined) { - pages.customUrls = PagesOptions.customUrls.default; - } else if (Object.prototype.toString.call(pages.customUrls) !== '[object Object]') { - throw 'Parse Server option pages.customUrls must be an object.'; - } - if (pages.customRoutes === undefined) { - pages.customRoutes = PagesOptions.customRoutes.default; - } else if (!Array.isArray(pages.customRoutes)) { - throw 'Parse Server option pages.customRoutes must be an array.'; - } - if (pages.encodePageParamHeaders === undefined) { - pages.encodePageParamHeaders = PagesOptions.encodePageParamHeaders.default; - } else if (!isBoolean(pages.encodePageParamHeaders)) { - throw 'Parse Server option pages.encodePageParamHeaders must be a boolean.'; - } - } - - static validateIdempotencyOptions(idempotencyOptions) { - if (!idempotencyOptions) { - return; - } - if (idempotencyOptions.ttl === undefined) { - idempotencyOptions.ttl = IdempotencyOptions.ttl.default; - } else if (!isNaN(idempotencyOptions.ttl) && idempotencyOptions.ttl <= 0) { - throw 'idempotency TTL value must be greater than 0 seconds'; - } else if (isNaN(idempotencyOptions.ttl)) { - throw 'idempotency TTL value must be a number'; - } - if (!idempotencyOptions.paths) { - idempotencyOptions.paths = IdempotencyOptions.paths.default; - } else if (!Array.isArray(idempotencyOptions.paths)) { - throw 'idempotency paths must be of an array of strings'; - } - } - - static validateAccountLockoutPolicy(accountLockout) { - if (accountLockout) { - if ( - typeof accountLockout.duration !== 'number' || - accountLockout.duration <= 0 || - accountLockout.duration > 99999 - ) { - throw 'Account lockout duration should be greater than 0 and less than 100000'; - } - - if ( - !Number.isInteger(accountLockout.threshold) || - accountLockout.threshold < 1 || - accountLockout.threshold > 999 - ) { - throw 'Account lockout threshold should be an integer greater than 0 and less than 1000'; - } - - if (accountLockout.unlockOnPasswordReset === undefined) { - accountLockout.unlockOnPasswordReset = AccountLockoutOptions.unlockOnPasswordReset.default; - } else if (!isBoolean(accountLockout.unlockOnPasswordReset)) { - throw 'Parse Server option accountLockout.unlockOnPasswordReset must be a boolean.'; - } - } - } - - static validatePasswordPolicy(passwordPolicy) { - if (passwordPolicy) { - if ( - passwordPolicy.maxPasswordAge !== undefined && - (typeof passwordPolicy.maxPasswordAge !== 'number' || passwordPolicy.maxPasswordAge < 0) - ) { - throw 'passwordPolicy.maxPasswordAge must be a positive number'; - } - - if ( - passwordPolicy.resetTokenValidityDuration !== undefined && - (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || - passwordPolicy.resetTokenValidityDuration <= 0) - ) { - throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; - } - - if (passwordPolicy.validatorPattern) { - if (typeof passwordPolicy.validatorPattern === 'string') { - passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern); - } else if (!Utils.isRegExp(passwordPolicy.validatorPattern)) { - throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.'; - } - } - - if ( - passwordPolicy.validatorCallback && - typeof passwordPolicy.validatorCallback !== 'function' - ) { - throw 'passwordPolicy.validatorCallback must be a function.'; - } - - if ( - passwordPolicy.doNotAllowUsername && - typeof passwordPolicy.doNotAllowUsername !== 'boolean' - ) { - throw 'passwordPolicy.doNotAllowUsername must be a boolean value.'; - } - - if ( - passwordPolicy.maxPasswordHistory && - (!Number.isInteger(passwordPolicy.maxPasswordHistory) || - passwordPolicy.maxPasswordHistory <= 0 || - passwordPolicy.maxPasswordHistory > 20) - ) { - throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'; - } - - if ( - passwordPolicy.resetTokenReuseIfValid && - typeof passwordPolicy.resetTokenReuseIfValid !== 'boolean' - ) { - throw 'resetTokenReuseIfValid must be a boolean value'; - } - if (passwordPolicy.resetTokenReuseIfValid && !passwordPolicy.resetTokenValidityDuration) { - throw 'You cannot use resetTokenReuseIfValid without resetTokenValidityDuration'; - } - - if ( - passwordPolicy.resetPasswordSuccessOnInvalidEmail !== undefined && - typeof passwordPolicy.resetPasswordSuccessOnInvalidEmail !== 'boolean' - ) { - throw 'resetPasswordSuccessOnInvalidEmail must be a boolean value'; - } - - } - } - - // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern static setupPasswordValidator(passwordPolicy) { if (passwordPolicy && passwordPolicy.validatorPattern) { + if (typeof passwordPolicy.validatorPattern === 'string') { + passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern); + } passwordPolicy.patternValidator = value => { return passwordPolicy.validatorPattern.test(value); }; } } - static validatePublicServerURL({ publicServerURL, required = false }) { - if (!publicServerURL) { - if (!required) { - return; - } - throw 'The option publicServerURL is required.'; - } - - const type = typeof publicServerURL; - - if (type === 'string') { - if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) { - throw 'The option publicServerURL must be a valid URL starting with http:// or https://.'; - } - return; - } - - if (type === 'function') { - return; - } - - throw `The option publicServerURL must be a string or function, but got ${type}.`; - } - - static validateEmailConfiguration({ - emailAdapter, - appName, - publicServerURL, - emailVerifyTokenValidityDuration, - emailVerifyTokenReuseIfValid, - emailVerifySuccessOnInvalidEmail, - }) { - if (!emailAdapter) { - throw 'An emailAdapter is required for e-mail verification and password resets.'; - } - if (typeof appName !== 'string') { - throw 'An app name is required for e-mail verification and password resets.'; - } - this.validatePublicServerURL({ publicServerURL, required: true }); - if (emailVerifyTokenValidityDuration) { - if (isNaN(emailVerifyTokenValidityDuration)) { - throw 'Email verify token validity duration must be a valid number.'; - } else if (emailVerifyTokenValidityDuration <= 0) { - throw 'Email verify token validity duration must be a value greater than 0.'; - } - } - if (emailVerifyTokenReuseIfValid && typeof emailVerifyTokenReuseIfValid !== 'boolean') { - throw 'emailVerifyTokenReuseIfValid must be a boolean value'; - } - if (emailVerifyTokenReuseIfValid && !emailVerifyTokenValidityDuration) { - throw 'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration'; - } - if (emailVerifySuccessOnInvalidEmail !== undefined && typeof emailVerifySuccessOnInvalidEmail !== 'boolean') { - throw 'emailVerifySuccessOnInvalidEmail must be a boolean value'; - } - } - - static validateFileUploadOptions(fileUpload) { - try { - if (fileUpload == null || typeof fileUpload !== 'object' || Array.isArray(fileUpload)) { - throw 'fileUpload must be an object value.'; - } - } catch (e) { - if (e instanceof ReferenceError) { - return; - } - throw e; - } - if (fileUpload.enableForAnonymousUser === undefined) { - fileUpload.enableForAnonymousUser = FileUploadOptions.enableForAnonymousUser.default; - } else if (typeof fileUpload.enableForAnonymousUser !== 'boolean') { - throw 'fileUpload.enableForAnonymousUser must be a boolean value.'; - } - if (fileUpload.enableForPublic === undefined) { - fileUpload.enableForPublic = FileUploadOptions.enableForPublic.default; - } else if (typeof fileUpload.enableForPublic !== 'boolean') { - throw 'fileUpload.enableForPublic must be a boolean value.'; - } - if (fileUpload.enableForAuthenticatedUser === undefined) { - fileUpload.enableForAuthenticatedUser = FileUploadOptions.enableForAuthenticatedUser.default; - } else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') { - throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.'; - } - if (fileUpload.fileExtensions === undefined) { - fileUpload.fileExtensions = FileUploadOptions.fileExtensions.default; - } else if (!Array.isArray(fileUpload.fileExtensions)) { - throw 'fileUpload.fileExtensions must be an array.'; - } - if (fileUpload.allowedFileUrlDomains === undefined) { - fileUpload.allowedFileUrlDomains = FileUploadOptions.allowedFileUrlDomains.default; - } else if (!Array.isArray(fileUpload.allowedFileUrlDomains)) { - throw 'fileUpload.allowedFileUrlDomains must be an array.'; - } else { - for (const domain of fileUpload.allowedFileUrlDomains) { - if (typeof domain !== 'string' || domain === '') { - throw 'fileUpload.allowedFileUrlDomains must contain only non-empty strings.'; - } - } - } - } - - static validateIps(field, masterKeyIps) { - for (let ip of masterKeyIps) { - if (ip.includes('/')) { - ip = ip.split('/')[0]; - } - if (!net.isIP(ip)) { - throw `The Parse Server option "${field}" contains an invalid IP address "${ip}".`; - } - } - } - - static validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters) { - if (enableInsecureAuthAdapters && typeof enableInsecureAuthAdapters !== 'boolean') { - throw 'Parse Server option enableInsecureAuthAdapters must be a boolean.'; - } - if (enableInsecureAuthAdapters) { - Deprecator.logRuntimeDeprecation({ usage: 'insecure adapter' }); - } - } - get mount() { var mount = this._mount; if (this.publicServerURL) { @@ -611,178 +113,6 @@ export class Config { this._mount = newValue; } - static validateSessionConfiguration(sessionLength, expireInactiveSessions) { - if (expireInactiveSessions) { - if (isNaN(sessionLength)) { - throw 'Session length must be a valid number.'; - } else if (sessionLength <= 0) { - throw 'Session length must be a value greater than 0.'; - } - } - } - - static validateDefaultLimit(defaultLimit) { - if (defaultLimit == null) { - defaultLimit = ParseServerOptions.defaultLimit.default; - } - if (typeof defaultLimit !== 'number') { - throw 'Default limit must be a number.'; - } - if (defaultLimit <= 0) { - throw 'Default limit must be a value greater than 0.'; - } - } - - static validateMaxLimit(maxLimit) { - if (maxLimit <= 0) { - throw 'Max limit must be a value greater than 0.'; - } - } - - static validateRequestComplexity(requestComplexity) { - if (requestComplexity == null) { - return; - } - if (typeof requestComplexity !== 'object' || Array.isArray(requestComplexity)) { - throw new Error('requestComplexity must be an object.'); - } - const validKeys = Object.keys(RequestComplexityOptions); - for (const key of Object.keys(requestComplexity)) { - if (!validKeys.includes(key)) { - throw new Error(`requestComplexity contains unknown property '${key}'.`); - } - } - for (const key of validKeys) { - if (requestComplexity[key] !== undefined) { - const value = requestComplexity[key]; - if (!Number.isInteger(value) || (value < 1 && value !== -1)) { - throw new Error(`requestComplexity.${key} must be a positive integer or -1 to disable.`); - } - } else { - requestComplexity[key] = RequestComplexityOptions[key].default; - } - } - } - - static validateAllowHeaders(allowHeaders) { - if (![null, undefined].includes(allowHeaders)) { - if (Array.isArray(allowHeaders)) { - allowHeaders.forEach(header => { - if (typeof header !== 'string') { - throw 'Allow headers must only contain strings'; - } else if (!header.trim().length) { - throw 'Allow headers must not contain empty strings'; - } - }); - } else { - throw 'Allow headers must be an array'; - } - } - } - - static validateLogLevels(logLevels) { - for (const key of Object.keys(LogLevels)) { - if (logLevels[key]) { - if (validLogLevels.indexOf(logLevels[key]) === -1) { - throw `'${key}' must be one of ${JSON.stringify(validLogLevels)}`; - } - } else { - logLevels[key] = LogLevels[key].default; - } - } - } - - static validateDatabaseOptions(databaseOptions) { - if (databaseOptions == undefined) { - return; - } - if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') { - throw `databaseOptions must be an object`; - } - - if (databaseOptions.enableSchemaHooks === undefined) { - databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default; - } else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') { - throw `databaseOptions.enableSchemaHooks must be a boolean`; - } - if (databaseOptions.schemaCacheTtl === undefined) { - databaseOptions.schemaCacheTtl = DatabaseOptions.schemaCacheTtl.default; - } else if (typeof databaseOptions.schemaCacheTtl !== 'number') { - throw `databaseOptions.schemaCacheTtl must be a number`; - } - if (databaseOptions.allowPublicExplain === undefined) { - databaseOptions.allowPublicExplain = DatabaseOptions.allowPublicExplain.default; - } else if (typeof databaseOptions.allowPublicExplain !== 'boolean') { - throw `Parse Server option 'databaseOptions.allowPublicExplain' must be a boolean.`; - } - } - - static validateLiveQueryOptions(liveQuery) { - if (liveQuery == undefined) { - return; - } - if (liveQuery.regexTimeout === undefined) { - liveQuery.regexTimeout = LiveQueryOptions.regexTimeout.default; - } else if (typeof liveQuery.regexTimeout !== 'number') { - throw `liveQuery.regexTimeout must be a number`; - } - } - - static validateRateLimit(rateLimit) { - if (!rateLimit) { - return; - } - if ( - Object.prototype.toString.call(rateLimit) !== '[object Object]' && - !Array.isArray(rateLimit) - ) { - throw `rateLimit must be an array or object`; - } - const options = Array.isArray(rateLimit) ? rateLimit : [rateLimit]; - for (const option of options) { - if (Object.prototype.toString.call(option) !== '[object Object]') { - throw `rateLimit must be an array of objects`; - } - if (option.requestPath == null) { - throw `rateLimit.requestPath must be defined`; - } - if (typeof option.requestPath !== 'string') { - throw `rateLimit.requestPath must be a string`; - } - - // Validate that the path is valid path-to-regexp syntax - try { - pathToRegexp(option.requestPath); - } catch (error) { - throw `rateLimit.requestPath "${option.requestPath}" is not valid: ${error.message}`; - } - - if (option.requestTimeWindow == null) { - throw `rateLimit.requestTimeWindow must be defined`; - } - if (typeof option.requestTimeWindow !== 'number') { - throw `rateLimit.requestTimeWindow must be a number`; - } - if (option.includeInternalRequests && typeof option.includeInternalRequests !== 'boolean') { - throw `rateLimit.includeInternalRequests must be a boolean`; - } - if (option.requestCount == null) { - throw `rateLimit.requestCount must be defined`; - } - if (typeof option.requestCount !== 'number') { - throw `rateLimit.requestCount must be a number`; - } - if (option.errorResponseMessage && typeof option.errorResponseMessage !== 'string') { - throw `rateLimit.errorResponseMessage must be a string`; - } - const options = Object.keys(ParseServer.RateLimitZone); - if (option.zone && !options.includes(option.zone)) { - const formatter = new Intl.ListFormat('en', { style: 'short', type: 'disjunction' }); - throw `rateLimit.zone must be one of ${formatter.format(options)}`; - } - } - } - generateEmailVerifyTokenExpiresAt() { if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { return undefined; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js deleted file mode 100644 index e9f8227317..0000000000 --- a/src/Options/Definitions.js +++ /dev/null @@ -1,1482 +0,0 @@ -/* -**** GENERATED CODE **** -This code has been generated by resources/buildConfigDefinitions.js -Do not edit manually, but update Options/index.js -*/ -var parsers = require('./parsers'); -module.exports.SchemaOptions = { - afterMigration: { - env: 'PARSE_SERVER_SCHEMA_AFTER_MIGRATION', - help: 'Execute a callback after running schema migrations.', - }, - beforeMigration: { - env: 'PARSE_SERVER_SCHEMA_BEFORE_MIGRATION', - help: 'Execute a callback before running schema migrations.', - }, - definitions: { - env: 'PARSE_SERVER_SCHEMA_DEFINITIONS', - help: 'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema', - required: true, - action: parsers.objectParser, - default: [], - }, - deleteExtraFields: { - env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS', - help: 'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.', - action: parsers.booleanParser, - default: false, - }, - keepUnknownIndexes: { - env: 'PARSE_SERVER_SCHEMA_KEEP_UNKNOWN_INDEXES', - help: "(Optional) Keep indexes that are present in the database but not defined in the schema. Set this to `true` if you are adding indexes manually, so that they won't be removed when running schema migration. Default is `false`.", - action: parsers.booleanParser, - default: false, - }, - lockSchemas: { - env: 'PARSE_SERVER_SCHEMA_LOCK_SCHEMAS', - help: 'Is true if Parse Server will reject any attempts to modify the schema while the server is running.', - action: parsers.booleanParser, - default: false, - }, - recreateModifiedFields: { - env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS', - help: 'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.', - action: parsers.booleanParser, - default: false, - }, - strict: { - env: 'PARSE_SERVER_SCHEMA_STRICT', - help: 'Is true if Parse Server should exit if schema update fail.', - action: parsers.booleanParser, - default: false, - }, -}; -module.exports.ParseServerOptions = { - accountLockout: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', - help: "The account lockout policy for failed login attempts.

Note: Setting a user's ACL to an empty object `{}` via master key is a separate mechanism that only prevents new logins; it does not invalidate existing session tokens. To immediately revoke a user's access, destroy their sessions via master key in addition to setting the ACL.", - action: parsers.objectParser, - type: 'AccountLockoutOptions', - }, - allowClientClassCreation: { - env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', - help: 'Enable (or disable) client class creation, defaults to false', - action: parsers.booleanParser, - default: false, - }, - allowCustomObjectId: { - env: 'PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID', - help: 'Enable (or disable) custom objectId', - action: parsers.booleanParser, - default: false, - }, - allowExpiredAuthDataToken: { - env: 'PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN', - help: 'Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.', - action: parsers.booleanParser, - default: false, - }, - allowHeaders: { - env: 'PARSE_SERVER_ALLOW_HEADERS', - help: 'Add headers to Access-Control-Allow-Headers', - action: parsers.arrayParser, - }, - allowOrigin: { - env: 'PARSE_SERVER_ALLOW_ORIGIN', - help: 'Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins.', - action: parsers.arrayParser, - }, - analyticsAdapter: { - env: 'PARSE_SERVER_ANALYTICS_ADAPTER', - help: 'Adapter module for the analytics', - action: parsers.moduleOrObjectParser, - }, - appId: { - env: 'PARSE_SERVER_APPLICATION_ID', - help: 'Your Parse Application ID', - required: true, - }, - appName: { - env: 'PARSE_SERVER_APP_NAME', - help: 'Sets the app name', - }, - auth: { - env: 'PARSE_SERVER_AUTH_PROVIDERS', - help: "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication

Provider names must start with a letter and contain only letters, digits, and underscores (`/^[A-Za-z][A-Za-z0-9_]*$/`). This is because each provider name is used to construct a database field (`_auth_data_`), which must comply with Parse Server's field naming rules.", - action: parsers.objectParser, - }, - cacheAdapter: { - env: 'PARSE_SERVER_CACHE_ADAPTER', - help: 'Adapter module for the cache', - action: parsers.moduleOrObjectParser, - }, - cacheMaxSize: { - env: 'PARSE_SERVER_CACHE_MAX_SIZE', - help: 'Sets the maximum size for the in memory cache, defaults to 10000', - action: parsers.numberParser('cacheMaxSize'), - default: 10000, - }, - cacheTTL: { - env: 'PARSE_SERVER_CACHE_TTL', - help: 'Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)', - action: parsers.numberParser('cacheTTL'), - default: 5000, - }, - clientKey: { - env: 'PARSE_SERVER_CLIENT_KEY', - help: 'Key for iOS, MacOS, tvOS clients', - }, - cloud: { - env: 'PARSE_SERVER_CLOUD', - help: 'Full path to your cloud code main.js', - }, - cluster: { - env: 'PARSE_SERVER_CLUSTER', - help: 'Run with cluster, optionally set the number of processes default to os.cpus().length', - action: parsers.numberOrBooleanParser, - }, - collectionPrefix: { - env: 'PARSE_SERVER_COLLECTION_PREFIX', - help: 'A collection prefix for the classes', - default: '', - }, - convertEmailToLowercase: { - env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE', - help: 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', - action: parsers.booleanParser, - default: false, - }, - convertUsernameToLowercase: { - env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE', - help: 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', - action: parsers.booleanParser, - default: false, - }, - customPages: { - env: 'PARSE_SERVER_CUSTOM_PAGES', - help: 'custom pages for password validation and reset', - action: parsers.objectParser, - type: 'CustomPagesOptions', - default: {}, - }, - databaseAdapter: { - env: 'PARSE_SERVER_DATABASE_ADAPTER', - help: 'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.', - action: parsers.moduleOrObjectParser, - }, - databaseOptions: { - env: 'PARSE_SERVER_DATABASE_OPTIONS', - help: 'Options to pass to the database client', - action: parsers.objectParser, - type: 'DatabaseOptions', - }, - databaseURI: { - env: 'PARSE_SERVER_DATABASE_URI', - help: 'The full URI to your database. Supported databases are mongodb or postgres.', - required: true, - default: 'mongodb://localhost:27017/parse', - }, - defaultLimit: { - env: 'PARSE_SERVER_DEFAULT_LIMIT', - help: 'Default value for limit option on queries, defaults to `100`.', - action: parsers.numberParser('defaultLimit'), - default: 100, - }, - directAccess: { - env: 'PARSE_SERVER_DIRECT_ACCESS', - help: 'Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

\u26A0\uFE0F In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.', - action: parsers.booleanParser, - default: true, - }, - dotNetKey: { - env: 'PARSE_SERVER_DOT_NET_KEY', - help: 'Key for Unity and .Net SDK', - }, - emailAdapter: { - env: 'PARSE_SERVER_EMAIL_ADAPTER', - help: 'Adapter module for email sending', - action: parsers.moduleOrObjectParser, - }, - emailVerifySuccessOnInvalidEmail: { - env: 'PARSE_SERVER_EMAIL_VERIFY_SUCCESS_ON_INVALID_EMAIL', - help: 'Set to `true` if a request to verify the email should return a success response even if the provided email address does not belong to a verifiable account, for example because it is unknown or already verified, or `false` if the request should return an error response in those cases.

Default is `true`.
Requires option `verifyUserEmails: true`.', - action: parsers.booleanParser, - default: true, - }, - emailVerifyTokenReuseIfValid: { - env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', - help: 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`.', - action: parsers.booleanParser, - default: false, - }, - emailVerifyTokenValidityDuration: { - env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', - help: 'Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`.', - action: parsers.numberParser('emailVerifyTokenValidityDuration'), - }, - enableAnonymousUsers: { - env: 'PARSE_SERVER_ENABLE_ANON_USERS', - help: 'Enable (or disable) anonymous users, defaults to true', - action: parsers.booleanParser, - default: true, - }, - enableCollationCaseComparison: { - env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON', - help: 'Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.', - action: parsers.booleanParser, - default: false, - }, - enableExpressErrorHandler: { - env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', - help: 'Enables the default express error handler for all errors', - action: parsers.booleanParser, - default: false, - }, - enableInsecureAuthAdapters: { - env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS', - help: 'Optional. Enables insecure authentication adapters. Insecure auth adapters are deprecated and will be removed in a future version. Defaults to `false`.', - action: parsers.booleanParser, - default: false, - }, - enableProductPurchaseLegacyApi: { - env: 'PARSE_SERVER_ENABLE_PRODUCT_PURCHASE_LEGACY_API', - help: 'Deprecated. Enables the legacy product purchase API including the `_Product` class and the `/validate_purchase` endpoint. This is an undocumented, unmaintained legacy feature inherited from the original Parse platform that may not function as expected. We strongly advise against using it. It will be removed in a future major version.', - action: parsers.booleanParser, - default: true, - }, - enableSanitizedErrorResponse: { - env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE', - help: 'If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.', - action: parsers.booleanParser, - default: true, - }, - encryptionKey: { - env: 'PARSE_SERVER_ENCRYPTION_KEY', - help: 'Key for encrypting your files', - }, - enforcePrivateUsers: { - env: 'PARSE_SERVER_ENFORCE_PRIVATE_USERS', - help: 'Set to true if new users should be created without public read and write access.', - action: parsers.booleanParser, - default: true, - }, - expireInactiveSessions: { - env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', - help: 'Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.', - action: parsers.booleanParser, - default: true, - }, - extendSessionOnUse: { - env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE', - help: "Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.", - action: parsers.booleanParser, - default: false, - }, - fileKey: { - env: 'PARSE_SERVER_FILE_KEY', - help: 'Key for your files', - }, - filesAdapter: { - env: 'PARSE_SERVER_FILES_ADAPTER', - help: 'Adapter module for the files sub-system', - action: parsers.moduleOrObjectParser, - }, - fileUpload: { - env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', - help: 'Options for file uploads', - action: parsers.objectParser, - type: 'FileUploadOptions', - default: {}, - }, - graphQLPath: { - env: 'PARSE_SERVER_GRAPHQL_PATH', - help: 'The mount path for the GraphQL endpoint

\u26A0\uFE0F File upload inside the GraphQL mutation system requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Defaults is `/graphql`.', - default: '/graphql', - }, - graphQLPublicIntrospection: { - env: 'PARSE_SERVER_GRAPHQL_PUBLIC_INTROSPECTION', - help: 'Enable public introspection for the GraphQL endpoint, defaults to false', - action: parsers.booleanParser, - default: false, - }, - graphQLSchema: { - env: 'PARSE_SERVER_GRAPH_QLSCHEMA', - help: 'Full path to your GraphQL custom schema.graphql file', - }, - host: { - env: 'PARSE_SERVER_HOST', - help: 'The host to serve ParseServer on, defaults to 0.0.0.0', - default: '0.0.0.0', - }, - idempotencyOptions: { - env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS', - help: 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', - action: parsers.objectParser, - type: 'IdempotencyOptions', - default: {}, - }, - javascriptKey: { - env: 'PARSE_SERVER_JAVASCRIPT_KEY', - help: 'Key for the Javascript SDK', - }, - jsonLogs: { - env: 'JSON_LOGS', - help: 'Log as structured JSON objects', - action: parsers.booleanParser, - }, - liveQuery: { - env: 'PARSE_SERVER_LIVE_QUERY', - help: "parse-server's LiveQuery configuration object", - action: parsers.objectParser, - type: 'LiveQueryOptions', - }, - liveQueryServerOptions: { - env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', - help: 'Live query server configuration options (will start the liveQuery server)', - action: parsers.objectParser, - type: 'LiveQueryServerOptions', - }, - loggerAdapter: { - env: 'PARSE_SERVER_LOGGER_ADAPTER', - help: 'Adapter module for the logging sub-system', - action: parsers.moduleOrObjectParser, - }, - logLevel: { - env: 'PARSE_SERVER_LOG_LEVEL', - help: 'Sets the level for logs', - }, - logLevels: { - env: 'PARSE_SERVER_LOG_LEVELS', - help: '(Optional) Overrides the log levels used internally by Parse Server to log events.', - action: parsers.objectParser, - type: 'LogLevels', - default: {}, - }, - logsFolder: { - env: 'PARSE_SERVER_LOGS_FOLDER', - help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", - default: './logs', - }, - maintenanceKey: { - env: 'PARSE_SERVER_MAINTENANCE_KEY', - help: '(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', - required: true, - }, - maintenanceKeyIps: { - env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', - help: "(Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.", - action: parsers.arrayParser, - default: ['127.0.0.1', '::1'], - }, - masterKey: { - env: 'PARSE_SERVER_MASTER_KEY', - help: 'Your Parse Master Key', - required: true, - }, - masterKeyIps: { - env: 'PARSE_SERVER_MASTER_KEY_IPS', - help: "(Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key.", - action: parsers.arrayParser, - default: ['127.0.0.1', '::1'], - }, - masterKeyTtl: { - env: 'PARSE_SERVER_MASTER_KEY_TTL', - help: '(Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.', - action: parsers.numberParser('masterKeyTtl'), - }, - maxLimit: { - env: 'PARSE_SERVER_MAX_LIMIT', - help: 'Max value for limit option on queries, defaults to unlimited', - action: parsers.numberParser('maxLimit'), - }, - maxLogFiles: { - env: 'PARSE_SERVER_MAX_LOG_FILES', - help: "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", - action: parsers.numberOrStringParser('maxLogFiles'), - }, - maxUploadSize: { - env: 'PARSE_SERVER_MAX_UPLOAD_SIZE', - help: 'Max file size for uploads, defaults to 20mb', - default: '20mb', - }, - middleware: { - env: 'PARSE_SERVER_MIDDLEWARE', - help: 'middleware for express server, can be string or function', - }, - mountGraphQL: { - env: 'PARSE_SERVER_MOUNT_GRAPHQL', - help: 'Mounts the GraphQL endpoint', - action: parsers.booleanParser, - default: false, - }, - mountPath: { - env: 'PARSE_SERVER_MOUNT_PATH', - help: 'Mount path for the server, defaults to /parse', - default: '/parse', - }, - mountPlayground: { - env: 'PARSE_SERVER_MOUNT_PLAYGROUND', - help: 'Deprecated. Mounts the GraphQL Playground which is deprecated and will be removed in a future version. The playground exposes the master key in the browser. Use Parse Dashboard as GraphQL IDE or configure a third-party GraphQL client with custom request headers.', - action: parsers.booleanParser, - default: false, - }, - objectIdSize: { - env: 'PARSE_SERVER_OBJECT_ID_SIZE', - help: "Sets the number of characters in generated object id's, default 10", - action: parsers.numberParser('objectIdSize'), - default: 10, - }, - pages: { - env: 'PARSE_SERVER_PAGES', - help: 'The options for pages such as password reset and email verification.', - action: parsers.objectParser, - type: 'PagesOptions', - default: {}, - }, - passwordPolicy: { - env: 'PARSE_SERVER_PASSWORD_POLICY', - help: 'The password policy for enforcing password related rules.', - action: parsers.objectParser, - type: 'PasswordPolicyOptions', - }, - playgroundPath: { - env: 'PARSE_SERVER_PLAYGROUND_PATH', - help: 'Deprecated. Mount path for the GraphQL Playground. The playground is deprecated and will be removed in a future version.', - default: '/playground', - }, - port: { - env: 'PORT', - help: 'The port to run the ParseServer, defaults to 1337.', - action: parsers.numberParser('port'), - default: 1337, - }, - preserveFileName: { - env: 'PARSE_SERVER_PRESERVE_FILE_NAME', - help: 'Enable (or disable) the addition of a unique hash to the file names', - action: parsers.booleanParser, - default: false, - }, - preventLoginWithUnverifiedEmail: { - env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', - help: "Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:Default is `false`.
Requires option `verifyUserEmails: true`.", - action: parsers.booleanOrFunctionParser, - default: false, - }, - preventSignupWithUnverifiedEmail: { - env: 'PARSE_SERVER_PREVENT_SIGNUP_WITH_UNVERIFIED_EMAIL', - help: "If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`.", - action: parsers.booleanParser, - default: false, - }, - protectedFields: { - env: 'PARSE_SERVER_PROTECTED_FIELDS', - help: 'Protected fields that should be treated with extra security when fetching details.', - action: parsers.objectParser, - default: { - _User: { - '*': ['email'], - }, - }, - }, - publicServerURL: { - env: 'PARSE_PUBLIC_SERVER_URL', - help: 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.', - }, - push: { - env: 'PARSE_SERVER_PUSH', - help: 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', - action: parsers.objectParser, - }, - rateLimit: { - env: 'PARSE_SERVER_RATE_LIMIT', - help: "Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

\u2139\uFE0F Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.", - action: parsers.arrayParser, - type: 'RateLimitOptions[]', - default: [], - }, - readOnlyMasterKey: { - env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', - help: 'Read-only key, which has the same capabilities as MasterKey without writes', - }, - readOnlyMasterKeyIps: { - env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY_IPS', - help: "(Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the read-only master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the read-only master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['0.0.0.0/0', '::0']` which means that any IP address is allowed to use the read-only master key. It is recommended to set this option to `['127.0.0.1', '::1']` to restrict access to `localhost`.", - action: parsers.arrayParser, - default: ['0.0.0.0/0', '::0'], - }, - requestComplexity: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY', - help: 'Options to limit the complexity of requests to prevent denial-of-service attacks. Limits are enforced for all requests except those using the master or maintenance key. Each property can be set to `-1` to disable that specific limit.', - action: parsers.objectParser, - type: 'RequestComplexityOptions', - default: {}, - }, - requestContextMiddleware: { - env: 'PARSE_SERVER_REQUEST_CONTEXT_MIDDLEWARE', - help: 'Options to customize the request context using inversion of control/dependency injection.', - }, - requestKeywordDenylist: { - env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST', - help: 'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.', - action: parsers.arrayParser, - default: [ - { - key: '_bsontype', - value: 'Code', - }, - { - key: 'constructor', - }, - { - key: '__proto__', - }, - ], - }, - restAPIKey: { - env: 'PARSE_SERVER_REST_API_KEY', - help: 'Key for REST calls', - }, - revokeSessionOnPasswordReset: { - env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', - help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", - action: parsers.booleanParser, - default: true, - }, - scheduledPush: { - env: 'PARSE_SERVER_SCHEDULED_PUSH', - help: 'Configuration for push scheduling, defaults to false.', - action: parsers.booleanParser, - default: false, - }, - schema: { - env: 'PARSE_SERVER_SCHEMA', - help: 'Defined schema', - action: parsers.objectParser, - type: 'SchemaOptions', - }, - security: { - env: 'PARSE_SERVER_SECURITY', - help: 'The security options to identify and report weak security settings.', - action: parsers.objectParser, - type: 'SecurityOptions', - default: {}, - }, - sendUserEmailVerification: { - env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', - help: 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', - action: parsers.booleanOrFunctionParser, - default: true, - }, - serverCloseComplete: { - env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE', - help: 'Callback when server has closed', - }, - serverURL: { - env: 'PARSE_SERVER_URL', - help: 'The URL to Parse Server.

\u26A0\uFE0F Certain server features or adapters may require Parse Server to be able to call itself by making requests to the URL set in `serverURL`. If a feature requires this, it is mentioned in the documentation. In that case ensure that the URL is accessible from the server itself.', - required: true, - }, - sessionLength: { - env: 'PARSE_SERVER_SESSION_LENGTH', - help: 'Session duration, in seconds, defaults to 1 year', - action: parsers.numberParser('sessionLength'), - default: 31536000, - }, - silent: { - env: 'SILENT', - help: 'Disables console output', - action: parsers.booleanParser, - }, - startLiveQueryServer: { - env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', - help: 'Starts the liveQuery server', - action: parsers.booleanParser, - }, - trustProxy: { - env: 'PARSE_SERVER_TRUST_PROXY', - help: 'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`.', - action: parsers.objectParser, - default: [], - }, - userSensitiveFields: { - env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', - help: 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', - action: parsers.arrayParser, - }, - verbose: { - env: 'VERBOSE', - help: 'Set the logging to verbose', - action: parsers.booleanParser, - }, - verifyServerUrl: { - env: 'PARSE_SERVER_VERIFY_SERVER_URL', - help: 'Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.

\u26A0\uFE0F Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Default is `true`.', - action: parsers.booleanParser, - default: true, - }, - verifyUserEmails: { - env: 'PARSE_SERVER_VERIFY_USER_EMAILS', - help: "Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:Default is `false`.", - action: parsers.booleanOrFunctionParser, - default: false, - }, - webhookKey: { - env: 'PARSE_SERVER_WEBHOOK_KEY', - help: 'Key sent with outgoing webhook calls', - }, -}; -module.exports.RateLimitOptions = { - errorResponseMessage: { - env: 'PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE', - help: 'The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.', - default: 'Too many requests.', - }, - includeInternalRequests: { - env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS', - help: 'Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.', - action: parsers.booleanParser, - default: false, - }, - includeMasterKey: { - env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_MASTER_KEY', - help: 'Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.', - action: parsers.booleanParser, - default: false, - }, - redisUrl: { - env: 'PARSE_SERVER_RATE_LIMIT_REDIS_URL', - help: 'Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.', - }, - requestCount: { - env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT', - help: 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. For batch requests, this also limits the number of sub-requests in a single batch that target this path; however, requests already consumed in the current time window are not counted against the batch, so the effective limit may be higher when combining individual and batch requests. Note that this is a basic server-level rate limit; for comprehensive protection, use a reverse proxy or WAF for rate limiting.', - action: parsers.numberParser('requestCount'), - }, - requestMethods: { - env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS', - help: 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.', - action: parsers.arrayParser, - }, - requestPath: { - env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_PATH', - help: 'The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or string patterns following path-to-regexp v8 syntax.', - required: true, - }, - requestTimeWindow: { - env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW', - help: 'The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.', - action: parsers.numberParser('requestTimeWindow'), - }, - zone: { - env: 'PARSE_SERVER_RATE_LIMIT_ZONE', - help: 'The type of rate limit to apply. The following types are supported:Default is `ip`.', - default: 'ip', - }, -}; -module.exports.RequestComplexityOptions = { - graphQLDepth: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_DEPTH', - help: 'Maximum depth of GraphQL field selections. Set to `-1` to disable. Default is `-1`.', - action: parsers.numberParser('graphQLDepth'), - default: -1, - }, - graphQLFields: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_FIELDS', - help: 'Maximum number of field selections in a GraphQL query. Set to `-1` to disable. Default is `-1`.', - action: parsers.numberParser('graphQLFields'), - default: -1, - }, - includeCount: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY_INCLUDE_COUNT', - help: 'Maximum number of include paths in a single query. Set to `-1` to disable. Default is `-1`.', - action: parsers.numberParser('includeCount'), - default: -1, - }, - includeDepth: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY_INCLUDE_DEPTH', - help: 'Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `-1`.', - action: parsers.numberParser('includeDepth'), - default: -1, - }, - queryDepth: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY_QUERY_DEPTH', - help: 'Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`.', - action: parsers.numberParser('queryDepth'), - default: -1, - }, - subqueryDepth: { - env: 'PARSE_SERVER_REQUEST_COMPLEXITY_SUBQUERY_DEPTH', - help: 'Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `-1`.', - action: parsers.numberParser('subqueryDepth'), - default: -1, - }, -}; -module.exports.SecurityOptions = { - checkGroups: { - env: 'PARSE_SERVER_SECURITY_CHECK_GROUPS', - help: 'The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.', - action: parsers.arrayParser, - }, - enableCheck: { - env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK', - help: 'Is true if Parse Server should check for weak security settings.', - action: parsers.booleanParser, - default: false, - }, - enableCheckLog: { - env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK_LOG', - help: 'Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.', - action: parsers.booleanParser, - default: false, - }, -}; -module.exports.PagesOptions = { - customRoutes: { - env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES', - help: 'The custom routes.', - action: parsers.arrayParser, - type: 'PagesRoute[]', - default: [], - }, - customUrls: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URLS', - help: 'The URLs to the custom pages.', - action: parsers.objectParser, - type: 'PagesCustomUrlsOptions', - default: {}, - }, - enableLocalization: { - env: 'PARSE_SERVER_PAGES_ENABLE_LOCALIZATION', - help: 'Is true if pages should be localized; this has no effect on custom page redirects.', - action: parsers.booleanParser, - default: false, - }, - encodePageParamHeaders: { - env: 'PARSE_SERVER_PAGES_ENCODE_PAGE_PARAM_HEADERS', - help: 'Is `true` if the page parameter headers should be URI-encoded. This is required if any page parameter value contains non-ASCII characters, such as the app name.', - action: parsers.booleanParser, - default: false, - }, - forceRedirect: { - env: 'PARSE_SERVER_PAGES_FORCE_REDIRECT', - help: 'Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).', - action: parsers.booleanParser, - default: false, - }, - localizationFallbackLocale: { - env: 'PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE', - help: 'The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file.', - default: 'en', - }, - localizationJsonPath: { - env: 'PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH', - help: 'The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale.', - }, - pagesEndpoint: { - env: 'PARSE_SERVER_PAGES_PAGES_ENDPOINT', - help: "The API endpoint for the pages. Default is 'apps'.", - default: 'apps', - }, - pagesPath: { - env: 'PARSE_SERVER_PAGES_PAGES_PATH', - help: "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module.", - }, - placeholders: { - env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', - help: 'The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function.', - action: parsers.objectParser, - default: {}, - }, -}; -module.exports.PagesRoute = { - handler: { - env: 'PARSE_SERVER_PAGES_ROUTE_HANDLER', - help: 'The route handler that is an async function.', - required: true, - }, - method: { - env: 'PARSE_SERVER_PAGES_ROUTE_METHOD', - help: "The route method, e.g. 'GET' or 'POST'.", - required: true, - }, - path: { - env: 'PARSE_SERVER_PAGES_ROUTE_PATH', - help: 'The route path.', - required: true, - }, -}; -module.exports.PagesCustomUrlsOptions = { - emailVerificationLinkExpired: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED', - help: 'The URL to the custom page for email verification -> link expired.', - }, - emailVerificationLinkInvalid: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID', - help: 'The URL to the custom page for email verification -> link invalid.', - }, - emailVerificationSendFail: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL', - help: 'The URL to the custom page for email verification -> link send fail.', - }, - emailVerificationSendSuccess: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS', - help: 'The URL to the custom page for email verification -> resend link -> success.', - }, - emailVerificationSuccess: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS', - help: 'The URL to the custom page for email verification -> success.', - }, - passwordReset: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET', - help: 'The URL to the custom page for password reset.', - }, - passwordResetLinkInvalid: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID', - help: 'The URL to the custom page for password reset -> link invalid.', - }, - passwordResetSuccess: { - env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS', - help: 'The URL to the custom page for password reset -> success.', - }, -}; -module.exports.CustomPagesOptions = { - choosePassword: { - env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', - help: 'choose password page path', - }, - expiredVerificationLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_EXPIRED_VERIFICATION_LINK', - help: 'expired verification link page path', - }, - invalidLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', - help: 'invalid link page path', - }, - invalidPasswordResetLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_PASSWORD_RESET_LINK', - help: 'invalid password reset link page path', - }, - invalidVerificationLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK', - help: 'invalid verification link page path', - }, - linkSendFail: { - env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL', - help: 'verification link send fail page path', - }, - linkSendSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS', - help: 'verification link send success page path', - }, - parseFrameURL: { - env: 'PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL', - help: 'for masking user-facing pages', - }, - passwordResetSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', - help: 'password reset success page path', - }, - verifyEmailSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', - help: 'verify email success page path', - }, -}; -module.exports.LiveQueryOptions = { - classNames: { - env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', - help: "parse-server's LiveQuery classNames", - action: parsers.arrayParser, - }, - pubSubAdapter: { - env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', - help: 'LiveQuery pubsub adapter', - action: parsers.moduleOrObjectParser, - }, - redisOptions: { - env: 'PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS', - help: "parse-server's LiveQuery redisOptions", - action: parsers.objectParser, - }, - redisURL: { - env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', - help: "parse-server's LiveQuery redisURL", - }, - regexTimeout: { - env: 'PARSE_SERVER_LIVEQUERY_REGEX_TIMEOUT', - help: 'Sets the maximum execution time in milliseconds for regular expression pattern matching in LiveQuery. This protects against Regular Expression Denial of Service (ReDoS) attacks where a malicious regex pattern could block the event loop. A regex that exceeds the timeout will be treated as non-matching.

The protection runs each regex evaluation in an isolated VM context with a timeout. This adds approximately 50 microseconds of overhead per regex evaluation. For most applications this is negligible, but it can add up if you have a very large number of LiveQuery subscriptions that use `$regex` on the same class. For example, 10,000 concurrent regex subscriptions would add approximately 500ms of processing time per object save event on that class.

Set to `0` to disable the timeout and use native regex evaluation without protection. Defaults to `100`.', - action: parsers.numberParser('regexTimeout'), - default: 100, - }, - wssAdapter: { - env: 'PARSE_SERVER_LIVEQUERY_WSS_ADAPTER', - help: 'Adapter module for the WebSocketServer', - action: parsers.moduleOrObjectParser, - }, -}; -module.exports.LiveQueryServerOptions = { - appId: { - env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', - help: 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', - }, - cacheTimeout: { - env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', - help: "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", - action: parsers.numberParser('cacheTimeout'), - }, - keyPairs: { - env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', - help: 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', - action: parsers.objectParser, - }, - logLevel: { - env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', - help: 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', - }, - masterKey: { - env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', - help: 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', - }, - port: { - env: 'PARSE_LIVE_QUERY_SERVER_PORT', - help: 'The port to run the LiveQuery server, defaults to 1337.', - action: parsers.numberParser('port'), - default: 1337, - }, - pubSubAdapter: { - env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', - help: 'LiveQuery pubsub adapter', - action: parsers.moduleOrObjectParser, - }, - redisOptions: { - env: 'PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS', - help: "parse-server's LiveQuery redisOptions", - action: parsers.objectParser, - }, - redisURL: { - env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', - help: "parse-server's LiveQuery redisURL", - }, - serverURL: { - env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', - help: 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', - }, - websocketTimeout: { - env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', - help: 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', - action: parsers.numberParser('websocketTimeout'), - }, - wssAdapter: { - env: 'PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER', - help: 'Adapter module for the WebSocketServer', - action: parsers.moduleOrObjectParser, - }, -}; -module.exports.IdempotencyOptions = { - paths: { - env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', - help: 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', - action: parsers.arrayParser, - default: [], - }, - ttl: { - env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', - help: 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', - action: parsers.numberParser('ttl'), - default: 300, - }, -}; -module.exports.AccountLockoutOptions = { - duration: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', - help: 'Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked.

Valid values are greater than `0` and less than `100000`.', - action: parsers.numberParser('duration'), - }, - threshold: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', - help: 'Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made.

Valid values are greater than `0` and less than `1000`.', - action: parsers.numberParser('threshold'), - }, - unlockOnPasswordReset: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET', - help: 'Set to `true` if the account should be unlocked after a successful password reset.

Default is `false`.
Requires options `duration` and `threshold` to be set.', - action: parsers.booleanParser, - default: false, - }, -}; -module.exports.PasswordPolicyOptions = { - doNotAllowUsername: { - env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', - help: 'Set to `true` to disallow the username as part of the password.

Default is `false`.', - action: parsers.booleanParser, - default: false, - }, - maxPasswordAge: { - env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', - help: 'Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration.', - action: parsers.numberParser('maxPasswordAge'), - }, - maxPasswordHistory: { - env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', - help: 'Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.

Valid values are >= `0` and <= `20`.
Default is `0`.', - action: parsers.numberParser('maxPasswordHistory'), - }, - resetPasswordSuccessOnInvalidEmail: { - env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_PASSWORD_SUCCESS_ON_INVALID_EMAIL', - help: 'Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.', - action: parsers.booleanParser, - default: true, - }, - resetTokenReuseIfValid: { - env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', - help: 'Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.', - action: parsers.booleanParser, - default: false, - }, - resetTokenValidityDuration: { - env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', - help: 'Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.', - action: parsers.numberParser('resetTokenValidityDuration'), - }, - validationError: { - env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR', - help: 'Set the error message to be sent.

Default is `Password does not meet the Password Policy requirements.`', - }, - validatorCallback: { - env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', - help: 'Set a callback function to validate a password to be accepted.

If used in combination with `validatorPattern`, the password must pass both to be accepted.', - }, - validatorPattern: { - env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', - help: 'Set the regular expression validation pattern a password must match to be accepted.

If used in combination with `validatorCallback`, the password must pass both to be accepted.', - }, -}; -module.exports.FileUploadOptions = { - allowedFileUrlDomains: { - env: 'PARSE_SERVER_FILE_UPLOAD_ALLOWED_FILE_URL_DOMAINS', - help: "Sets the allowed hostnames for file URLs referenced in Parse objects. When a File object includes a URL, its hostname must match one of these entries to be accepted. Supports exact hostnames (e.g., `'cdn.example.com'`) and wildcard subdomains (e.g., `'*.example.com'`). Use `['*']` to allow any domain. Use `[]` to block all file URLs (only name-based files allowed).", - action: parsers.arrayParser, - default: ['*'], - }, - enableForAnonymousUser: { - env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER', - help: 'Is true if file upload should be allowed for anonymous users.', - action: parsers.booleanParser, - default: false, - }, - enableForAuthenticatedUser: { - env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER', - help: 'Is true if file upload should be allowed for authenticated users.', - action: parsers.booleanParser, - default: true, - }, - enableForPublic: { - env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC', - help: 'Is true if file upload should be allowed for anyone, regardless of user authentication.', - action: parsers.booleanParser, - default: false, - }, - fileExtensions: { - env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS', - help: 'Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.

The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser\'s local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.

Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\\\+[xX][mM][lL])?)$)"]`.', - action: parsers.arrayParser, - default: [ - '^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)', - ], - }, -}; -/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ -module.exports.LogLevel = { - debug: { - env: 'PARSE_SERVER_LOG_LEVEL_DEBUG', - help: 'Debug level', - required: true, - }, - error: { - env: 'PARSE_SERVER_LOG_LEVEL_ERROR', - help: 'Error level - highest priority', - required: true, - }, - info: { - env: 'PARSE_SERVER_LOG_LEVEL_INFO', - help: 'Info level - default', - required: true, - }, - silly: { - env: 'PARSE_SERVER_LOG_LEVEL_SILLY', - help: 'Silly level - lowest priority', - required: true, - }, - verbose: { - env: 'PARSE_SERVER_LOG_LEVEL_VERBOSE', - help: 'Verbose level', - required: true, - }, - warn: { - env: 'PARSE_SERVER_LOG_LEVEL_WARN', - help: 'Warning level', - required: true, - }, -}; -module.exports.LogClientEvent = { - keys: { - env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_KEYS', - help: 'Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.', - action: parsers.arrayParser, - }, - logLevel: { - env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_LOG_LEVEL', - help: "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.", - default: 'info', - }, - name: { - env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_NAME', - help: 'The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.', - required: true, - }, -}; -module.exports.DatabaseOptions = { - allowPublicExplain: { - env: 'PARSE_SERVER_DATABASE_ALLOW_PUBLIC_EXPLAIN', - help: 'Set to `true` to allow `Parse.Query.explain` without master key.

\u26A0\uFE0F Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.', - action: parsers.booleanParser, - default: false, - }, - appName: { - env: 'PARSE_SERVER_DATABASE_APP_NAME', - help: 'The MongoDB driver option to specify the name of the application that created this MongoClient instance.', - }, - authMechanism: { - env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM', - help: 'The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection.', - }, - authMechanismProperties: { - env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM_PROPERTIES', - help: 'The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs.', - action: parsers.objectParser, - }, - authSource: { - env: 'PARSE_SERVER_DATABASE_AUTH_SOURCE', - help: "The MongoDB driver option to specify the database name associated with the user's credentials.", - }, - autoSelectFamily: { - env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY', - help: 'The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address.', - action: parsers.booleanParser, - }, - autoSelectFamilyAttemptTimeout: { - env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT', - help: 'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.', - action: parsers.numberParser('autoSelectFamilyAttemptTimeout'), - }, - batchSize: { - env: 'PARSE_SERVER_DATABASE_BATCH_SIZE', - help: 'The number of documents per batch for MongoDB cursor `getMore` operations. A lower value reduces memory usage per batch; a higher value reduces the number of network round-trips.', - action: parsers.numberParser('batchSize'), - default: 1000, - }, - clientMetadata: { - env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA', - help: "Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead.", - action: parsers.objectParser, - type: 'DatabaseOptionsClientMetadata', - }, - compressors: { - env: 'PARSE_SERVER_DATABASE_COMPRESSORS', - help: 'The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance.', - }, - connectTimeoutMS: { - env: 'PARSE_SERVER_DATABASE_CONNECT_TIMEOUT_MS', - help: 'The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.', - action: parsers.numberParser('connectTimeoutMS'), - }, - createIndexAuthDataUniqueness: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_AUTH_DATA_UNIQUENESS', - help: 'Set to `true` to automatically create unique indexes on the authData fields of the _User collection for each configured auth provider on server start, including `anonymous` when anonymous users are enabled. These indexes prevent race conditions during concurrent signups with the same authData. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the indexes, keep in mind that the otherwise automatically created indexes may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexRoleName: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME', - help: 'Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexUserEmail: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL', - help: 'Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexUserEmailCaseInsensitive: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_CASE_INSENSITIVE', - help: 'Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexUserEmailVerifyToken: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_VERIFY_TOKEN', - help: 'Set to `true` to automatically create an index on the _email_verify_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexUserPasswordResetToken: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_PASSWORD_RESET_TOKEN', - help: 'Set to `true` to automatically create an index on the _perishable_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexUserUsername: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME', - help: 'Set to `true` to automatically create indexes on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - createIndexUserUsernameCaseInsensitive: { - env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME_CASE_INSENSITIVE', - help: 'Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.', - action: parsers.booleanParser, - default: true, - }, - directConnection: { - env: 'PARSE_SERVER_DATABASE_DIRECT_CONNECTION', - help: 'The MongoDB driver option to force a Single topology type with a connection string containing one host.', - action: parsers.booleanParser, - }, - disableIndexFieldValidation: { - env: 'PARSE_SERVER_DATABASE_DISABLE_INDEX_FIELD_VALIDATION', - help: 'Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later.', - action: parsers.booleanParser, - }, - enableSchemaHooks: { - env: 'PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS', - help: 'Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.', - action: parsers.booleanParser, - default: false, - }, - forceServerObjectId: { - env: 'PARSE_SERVER_DATABASE_FORCE_SERVER_OBJECT_ID', - help: 'The MongoDB driver option to force server to assign _id values instead of driver.', - action: parsers.booleanParser, - }, - heartbeatFrequencyMS: { - env: 'PARSE_SERVER_DATABASE_HEARTBEAT_FREQUENCY_MS', - help: 'The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.', - action: parsers.numberParser('heartbeatFrequencyMS'), - }, - loadBalanced: { - env: 'PARSE_SERVER_DATABASE_LOAD_BALANCED', - help: 'The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service.', - action: parsers.booleanParser, - }, - localThresholdMS: { - env: 'PARSE_SERVER_DATABASE_LOCAL_THRESHOLD_MS', - help: 'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.', - action: parsers.numberParser('localThresholdMS'), - }, - logClientEvents: { - env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS', - help: 'An array of MongoDB client event configurations to enable logging of specific events.', - action: parsers.arrayParser, - type: 'LogClientEvent[]', - }, - maxConnecting: { - env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', - help: 'The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool.', - action: parsers.numberParser('maxConnecting'), - }, - maxIdleTimeMS: { - env: 'PARSE_SERVER_DATABASE_MAX_IDLE_TIME_MS', - help: 'The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed.', - action: parsers.numberParser('maxIdleTimeMS'), - }, - maxPoolSize: { - env: 'PARSE_SERVER_DATABASE_MAX_POOL_SIZE', - help: 'The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.', - action: parsers.numberParser('maxPoolSize'), - }, - maxStalenessSeconds: { - env: 'PARSE_SERVER_DATABASE_MAX_STALENESS_SECONDS', - help: 'The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.', - action: parsers.numberParser('maxStalenessSeconds'), - }, - maxTimeMS: { - env: 'PARSE_SERVER_DATABASE_MAX_TIME_MS', - help: 'The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor.', - action: parsers.numberParser('maxTimeMS'), - }, - minPoolSize: { - env: 'PARSE_SERVER_DATABASE_MIN_POOL_SIZE', - help: 'The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.', - action: parsers.numberParser('minPoolSize'), - }, - proxyHost: { - env: 'PARSE_SERVER_DATABASE_PROXY_HOST', - help: 'The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections.', - }, - proxyPassword: { - env: 'PARSE_SERVER_DATABASE_PROXY_PASSWORD', - help: 'The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication.', - }, - proxyPort: { - env: 'PARSE_SERVER_DATABASE_PROXY_PORT', - help: 'The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections.', - action: parsers.numberParser('proxyPort'), - }, - proxyUsername: { - env: 'PARSE_SERVER_DATABASE_PROXY_USERNAME', - help: 'The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication.', - }, - readConcernLevel: { - env: 'PARSE_SERVER_DATABASE_READ_CONCERN_LEVEL', - help: 'The MongoDB driver option to specify the level of isolation.', - }, - readPreference: { - env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE', - help: 'The MongoDB driver option to specify the read preferences for this connection.', - }, - readPreferenceTags: { - env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE_TAGS', - help: 'The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs.', - action: parsers.arrayParser, - }, - replicaSet: { - env: 'PARSE_SERVER_DATABASE_REPLICA_SET', - help: 'The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set.', - }, - retryReads: { - env: 'PARSE_SERVER_DATABASE_RETRY_READS', - help: 'The MongoDB driver option to enable retryable reads.', - action: parsers.booleanParser, - }, - retryWrites: { - env: 'PARSE_SERVER_DATABASE_RETRY_WRITES', - help: 'The MongoDB driver option to set whether to retry failed writes.', - action: parsers.booleanParser, - }, - schemaCacheTtl: { - env: 'PARSE_SERVER_DATABASE_SCHEMA_CACHE_TTL', - help: 'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.', - action: parsers.numberParser('schemaCacheTtl'), - }, - serverMonitoringMode: { - env: 'PARSE_SERVER_DATABASE_SERVER_MONITORING_MODE', - help: 'The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode.', - }, - serverSelectionTimeoutMS: { - env: 'PARSE_SERVER_DATABASE_SERVER_SELECTION_TIMEOUT_MS', - help: 'The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection.', - action: parsers.numberParser('serverSelectionTimeoutMS'), - }, - socketTimeoutMS: { - env: 'PARSE_SERVER_DATABASE_SOCKET_TIMEOUT_MS', - help: 'The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout.', - action: parsers.numberParser('socketTimeoutMS'), - }, - srvMaxHosts: { - env: 'PARSE_SERVER_DATABASE_SRV_MAX_HOSTS', - help: 'The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts.', - action: parsers.numberParser('srvMaxHosts'), - }, - srvServiceName: { - env: 'PARSE_SERVER_DATABASE_SRV_SERVICE_NAME', - help: 'The MongoDB driver option to modify the srv URI service name.', - }, - ssl: { - env: 'PARSE_SERVER_DATABASE_SSL', - help: 'The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option).', - action: parsers.booleanParser, - }, - tls: { - env: 'PARSE_SERVER_DATABASE_TLS', - help: 'The MongoDB driver option to enable or disable TLS/SSL for the connection.', - action: parsers.booleanParser, - }, - tlsAllowInvalidCertificates: { - env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_CERTIFICATES', - help: 'The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance.', - action: parsers.booleanParser, - }, - tlsAllowInvalidHostnames: { - env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_HOSTNAMES', - help: 'The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance.', - action: parsers.booleanParser, - }, - tlsCAFile: { - env: 'PARSE_SERVER_DATABASE_TLS_CAFILE', - help: 'The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority.', - }, - tlsCertificateKeyFile: { - env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE', - help: "The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key.", - }, - tlsCertificateKeyFilePassword: { - env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE_PASSWORD', - help: 'The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile.', - }, - tlsInsecure: { - env: 'PARSE_SERVER_DATABASE_TLS_INSECURE', - help: 'The MongoDB driver option to disable various certificate validations.', - action: parsers.booleanParser, - }, - waitQueueTimeoutMS: { - env: 'PARSE_SERVER_DATABASE_WAIT_QUEUE_TIMEOUT_MS', - help: 'The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available.', - action: parsers.numberParser('waitQueueTimeoutMS'), - }, - zlibCompressionLevel: { - env: 'PARSE_SERVER_DATABASE_ZLIB_COMPRESSION_LEVEL', - help: 'The MongoDB driver option to specify the compression level if using zlib for network compression (0-9).', - action: parsers.numberParser('zlibCompressionLevel'), - }, -}; -module.exports.DatabaseOptionsClientMetadata = { - name: { - env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_NAME', - help: "The name to identify your application in database logs (e.g., 'MyApp').", - required: true, - }, - version: { - env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_VERSION', - help: "The version of your application (e.g., '1.0.0').", - required: true, - }, -}; -module.exports.AuthAdapter = { - enabled: { - help: 'Is `true` if the auth adapter is enabled, `false` otherwise.', - action: parsers.booleanParser, - default: false, - }, -}; -module.exports.LogLevels = { - cloudFunctionError: { - env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR', - help: 'Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.', - default: 'error', - }, - cloudFunctionSuccess: { - env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS', - help: 'Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', - default: 'info', - }, - signupUsernameTaken: { - env: 'PARSE_SERVER_LOG_LEVELS_SIGNUP_USERNAME_TAKEN', - help: 'Log level used when a sign-up fails because the username already exists. Default is `info`. See [LogLevel](LogLevel.html) for available values.', - default: 'info', - }, - triggerAfter: { - env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', - help: 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.', - default: 'info', - }, - triggerBeforeError: { - env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', - help: 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.', - default: 'error', - }, - triggerBeforeSuccess: { - env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', - help: 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', - default: 'info', - }, -}; diff --git a/src/Options/docs.js b/src/Options/docs.js deleted file mode 100644 index 5845629022..0000000000 --- a/src/Options/docs.js +++ /dev/null @@ -1,356 +0,0 @@ -/** - * @interface SchemaOptions - * @property {Function} afterMigration Execute a callback after running schema migrations. - * @property {Function} beforeMigration Execute a callback before running schema migrations. - * @property {Any} definitions Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema - * @property {Boolean} deleteExtraFields Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development. - * @property {Boolean} keepUnknownIndexes (Optional) Keep indexes that are present in the database but not defined in the schema. Set this to `true` if you are adding indexes manually, so that they won't be removed when running schema migration. Default is `false`. - * @property {Boolean} lockSchemas Is true if Parse Server will reject any attempts to modify the schema while the server is running. - * @property {Boolean} recreateModifiedFields Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development. - * @property {Boolean} strict Is true if Parse Server should exit if schema update fail. - */ - -/** - * @interface ParseServerOptions - * @property {AccountLockoutOptions} accountLockout The account lockout policy for failed login attempts.

Note: Setting a user's ACL to an empty object `{}` via master key is a separate mechanism that only prevents new logins; it does not invalidate existing session tokens. To immediately revoke a user's access, destroy their sessions via master key in addition to setting the ACL. - * @property {Boolean} allowClientClassCreation Enable (or disable) client class creation, defaults to false - * @property {Boolean} allowCustomObjectId Enable (or disable) custom objectId - * @property {Boolean} allowExpiredAuthDataToken Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`. - * @property {String[]} allowHeaders Add headers to Access-Control-Allow-Headers - * @property {String|String[]} allowOrigin Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins. - * @property {Adapter} analyticsAdapter Adapter module for the analytics - * @property {String} appId Your Parse Application ID - * @property {String} appName Sets the app name - * @property {Object} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication

Provider names must start with a letter and contain only letters, digits, and underscores (`/^[A-Za-z][A-Za-z0-9_]*$/`). This is because each provider name is used to construct a database field (`_auth_data_`), which must comply with Parse Server's field naming rules. - * @property {Adapter} cacheAdapter Adapter module for the cache - * @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000 - * @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) - * @property {String} clientKey Key for iOS, MacOS, tvOS clients - * @property {String} cloud Full path to your cloud code main.js - * @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length - * @property {String} collectionPrefix A collection prefix for the classes - * @property {Boolean} convertEmailToLowercase Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`. - * @property {Boolean} convertUsernameToLowercase Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`. - * @property {CustomPagesOptions} customPages custom pages for password validation and reset - * @property {Adapter} databaseAdapter Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. - * @property {DatabaseOptions} databaseOptions Options to pass to the database client - * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. - * @property {Number} defaultLimit Default value for limit option on queries, defaults to `100`. - * @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. - * @property {String} dotNetKey Key for Unity and .Net SDK - * @property {Adapter} emailAdapter Adapter module for email sending - * @property {Boolean} emailVerifySuccessOnInvalidEmail Set to `true` if a request to verify the email should return a success response even if the provided email address does not belong to a verifiable account, for example because it is unknown or already verified, or `false` if the request should return an error response in those cases.

Default is `true`.
Requires option `verifyUserEmails: true`. - * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. - * @property {Number} emailVerifyTokenValidityDuration Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`. - * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true - * @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. - * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors - * @property {Boolean} enableInsecureAuthAdapters Optional. Enables insecure authentication adapters. Insecure auth adapters are deprecated and will be removed in a future version. Defaults to `false`. - * @property {Boolean} enableProductPurchaseLegacyApi Deprecated. Enables the legacy product purchase API including the `_Product` class and the `/validate_purchase` endpoint. This is an undocumented, unmaintained legacy feature inherited from the original Parse platform that may not function as expected. We strongly advise against using it. It will be removed in a future major version. - * @property {Boolean} enableSanitizedErrorResponse If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`. - * @property {String} encryptionKey Key for encrypting your files - * @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access. - * @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date. - * @property {Boolean} extendSessionOnUse Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed. - * @property {String} fileKey Key for your files - * @property {Adapter} filesAdapter Adapter module for the files sub-system - * @property {FileUploadOptions} fileUpload Options for file uploads - * @property {String} graphQLPath The mount path for the GraphQL endpoint

⚠️ File upload inside the GraphQL mutation system requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Defaults is `/graphql`. - * @property {Boolean} graphQLPublicIntrospection Enable public introspection for the GraphQL endpoint, defaults to false - * @property {String} graphQLSchema Full path to your GraphQL custom schema.graphql file - * @property {String} host The host to serve ParseServer on, defaults to 0.0.0.0 - * @property {IdempotencyOptions} idempotencyOptions Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production. - * @property {String} javascriptKey Key for the Javascript SDK - * @property {Boolean} jsonLogs Log as structured JSON objects - * @property {LiveQueryOptions} liveQuery parse-server's LiveQuery configuration object - * @property {LiveQueryServerOptions} liveQueryServerOptions Live query server configuration options (will start the liveQuery server) - * @property {Adapter} loggerAdapter Adapter module for the logging sub-system - * @property {String} logLevel Sets the level for logs - * @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events. - * @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging - * @property {String} maintenanceKey (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. - * @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key. - * @property {Union} masterKey Your Parse Master Key - * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key. - * @property {Number} masterKeyTtl (Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server. - * @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited - * @property {Number|String} maxLogFiles Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) - * @property {String} maxUploadSize Max file size for uploads, defaults to 20mb - * @property {Union} middleware middleware for express server, can be string or function - * @property {Boolean} mountGraphQL Mounts the GraphQL endpoint - * @property {String} mountPath Mount path for the server, defaults to /parse - * @property {Boolean} mountPlayground Deprecated. Mounts the GraphQL Playground which is deprecated and will be removed in a future version. The playground exposes the master key in the browser. Use Parse Dashboard as GraphQL IDE or configure a third-party GraphQL client with custom request headers. - * @property {Number} objectIdSize Sets the number of characters in generated object id's, default 10 - * @property {PagesOptions} pages The options for pages such as password reset and email verification. - * @property {PasswordPolicyOptions} passwordPolicy The password policy for enforcing password related rules. - * @property {String} playgroundPath Deprecated. Mount path for the GraphQL Playground. The playground is deprecated and will be removed in a future version. - * @property {Number} port The port to run the ParseServer, defaults to 1337. - * @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names - * @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
Default is `false`.
Requires option `verifyUserEmails: true`. - * @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`. - * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. - * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. - * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications - * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. - * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes - * @property {String[]} readOnlyMasterKeyIps (Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the read-only master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the read-only master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['0.0.0.0/0', '::0']` which means that any IP address is allowed to use the read-only master key. It is recommended to set this option to `['127.0.0.1', '::1']` to restrict access to `localhost`. - * @property {RequestComplexityOptions} requestComplexity Options to limit the complexity of requests to prevent denial-of-service attacks. Limits are enforced for all requests except those using the master or maintenance key. Each property can be set to `-1` to disable that specific limit. - * @property {Function} requestContextMiddleware Options to customize the request context using inversion of control/dependency injection. - * @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns. - * @property {String} restAPIKey Key for REST calls - * @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. - * @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false. - * @property {SchemaOptions} schema Defined schema - * @property {SecurityOptions} security The security options to identify and report weak security settings. - * @property {Boolean} sendUserEmailVerification Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
- * @property {Function} serverCloseComplete Callback when server has closed - * @property {String} serverURL The URL to Parse Server.

⚠️ Certain server features or adapters may require Parse Server to be able to call itself by making requests to the URL set in `serverURL`. If a feature requires this, it is mentioned in the documentation. In that case ensure that the URL is accessible from the server itself. - * @property {Number} sessionLength Session duration, in seconds, defaults to 1 year - * @property {Boolean} silent Disables console output - * @property {Boolean} startLiveQueryServer Starts the liveQuery server - * @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`. - * @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields - * @property {Boolean} verbose Set the logging to verbose - * @property {Boolean} verifyServerUrl Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.

⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Default is `true`. - * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.

The `createdWith` values per scenario:
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
  • Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those
Default is `false`. - * @property {String} webhookKey Key sent with outgoing webhook calls - */ - -/** - * @interface RateLimitOptions - * @property {String} errorResponseMessage The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`. - * @property {Boolean} includeInternalRequests Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks. - * @property {Boolean} includeMasterKey Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks. - * @property {String} redisUrl Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests. - * @property {Number} requestCount The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. For batch requests, this also limits the number of sub-requests in a single batch that target this path; however, requests already consumed in the current time window are not counted against the batch, so the effective limit may be higher when combining individual and batch requests. Note that this is a basic server-level rate limit; for comprehensive protection, use a reverse proxy or WAF for rate limiting. - * @property {String[]} requestMethods Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. - * @property {String} requestPath The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or string patterns following path-to-regexp v8 syntax. - * @property {Number} requestTimeWindow The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. - * @property {String} zone The type of rate limit to apply. The following types are supported:
  • `global`: rate limit based on the number of requests made by all users
  • `ip`: rate limit based on the IP address of the request
  • `user`: rate limit based on the user ID of the request
  • `session`: rate limit based on the session token of the request
Default is `ip`. - */ - -/** - * @interface RequestComplexityOptions - * @property {Number} graphQLDepth Maximum depth of GraphQL field selections. Set to `-1` to disable. Default is `-1`. - * @property {Number} graphQLFields Maximum number of field selections in a GraphQL query. Set to `-1` to disable. Default is `-1`. - * @property {Number} includeCount Maximum number of include paths in a single query. Set to `-1` to disable. Default is `-1`. - * @property {Number} includeDepth Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `-1`. - * @property {Number} queryDepth Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`. - * @property {Number} subqueryDepth Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `-1`. - */ - -/** - * @interface SecurityOptions - * @property {CheckGroup[]} checkGroups The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`. - * @property {Boolean} enableCheck Is true if Parse Server should check for weak security settings. - * @property {Boolean} enableCheckLog Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs. - */ - -/** - * @interface PagesOptions - * @property {PagesRoute[]} customRoutes The custom routes. - * @property {PagesCustomUrlsOptions} customUrls The URLs to the custom pages. - * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. - * @property {Boolean} encodePageParamHeaders Is `true` if the page parameter headers should be URI-encoded. This is required if any page parameter value contains non-ASCII characters, such as the app name. - * @property {Boolean} forceRedirect Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). - * @property {String} localizationFallbackLocale The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. - * @property {String} localizationJsonPath The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. - * @property {String} pagesEndpoint The API endpoint for the pages. Default is 'apps'. - * @property {String} pagesPath The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module. - * @property {Object} placeholders The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. - */ - -/** - * @interface PagesRoute - * @property {Function} handler The route handler that is an async function. - * @property {String} method The route method, e.g. 'GET' or 'POST'. - * @property {String} path The route path. - */ - -/** - * @interface PagesCustomUrlsOptions - * @property {String} emailVerificationLinkExpired The URL to the custom page for email verification -> link expired. - * @property {String} emailVerificationLinkInvalid The URL to the custom page for email verification -> link invalid. - * @property {String} emailVerificationSendFail The URL to the custom page for email verification -> link send fail. - * @property {String} emailVerificationSendSuccess The URL to the custom page for email verification -> resend link -> success. - * @property {String} emailVerificationSuccess The URL to the custom page for email verification -> success. - * @property {String} passwordReset The URL to the custom page for password reset. - * @property {String} passwordResetLinkInvalid The URL to the custom page for password reset -> link invalid. - * @property {String} passwordResetSuccess The URL to the custom page for password reset -> success. - */ - -/** - * @interface CustomPagesOptions - * @property {String} choosePassword choose password page path - * @property {String} expiredVerificationLink expired verification link page path - * @property {String} invalidLink invalid link page path - * @property {String} invalidPasswordResetLink invalid password reset link page path - * @property {String} invalidVerificationLink invalid verification link page path - * @property {String} linkSendFail verification link send fail page path - * @property {String} linkSendSuccess verification link send success page path - * @property {String} parseFrameURL for masking user-facing pages - * @property {String} passwordResetSuccess password reset success page path - * @property {String} verifyEmailSuccess verify email success page path - */ - -/** - * @interface LiveQueryOptions - * @property {String[]} classNames parse-server's LiveQuery classNames - * @property {Adapter} pubSubAdapter LiveQuery pubsub adapter - * @property {Any} redisOptions parse-server's LiveQuery redisOptions - * @property {String} redisURL parse-server's LiveQuery redisURL - * @property {Number} regexTimeout Sets the maximum execution time in milliseconds for regular expression pattern matching in LiveQuery. This protects against Regular Expression Denial of Service (ReDoS) attacks where a malicious regex pattern could block the event loop. A regex that exceeds the timeout will be treated as non-matching.

The protection runs each regex evaluation in an isolated VM context with a timeout. This adds approximately 50 microseconds of overhead per regex evaluation. For most applications this is negligible, but it can add up if you have a very large number of LiveQuery subscriptions that use `$regex` on the same class. For example, 10,000 concurrent regex subscriptions would add approximately 500ms of processing time per object save event on that class.

Set to `0` to disable the timeout and use native regex evaluation without protection. Defaults to `100`. - * @property {Adapter} wssAdapter Adapter module for the WebSocketServer - */ - -/** - * @interface LiveQueryServerOptions - * @property {String} appId This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId. - * @property {Number} cacheTimeout Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds). - * @property {Any} keyPairs A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details. - * @property {String} logLevel This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO. - * @property {String} masterKey This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey. - * @property {Number} port The port to run the LiveQuery server, defaults to 1337. - * @property {Adapter} pubSubAdapter LiveQuery pubsub adapter - * @property {Any} redisOptions parse-server's LiveQuery redisOptions - * @property {String} redisURL parse-server's LiveQuery redisURL - * @property {String} serverURL This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL. - * @property {Number} websocketTimeout Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s). - * @property {Adapter} wssAdapter Adapter module for the WebSocketServer - */ - -/** - * @interface IdempotencyOptions - * @property {String[]} paths An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths. - * @property {Number} ttl The duration in seconds after which a request record is discarded from the database, defaults to 300s. - */ - -/** - * @interface AccountLockoutOptions - * @property {Number} duration Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked.

Valid values are greater than `0` and less than `100000`. - * @property {Number} threshold Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made.

Valid values are greater than `0` and less than `1000`. - * @property {Boolean} unlockOnPasswordReset Set to `true` if the account should be unlocked after a successful password reset.

Default is `false`.
Requires options `duration` and `threshold` to be set. - */ - -/** - * @interface PasswordPolicyOptions - * @property {Boolean} doNotAllowUsername Set to `true` to disallow the username as part of the password.

Default is `false`. - * @property {Number} maxPasswordAge Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration. - * @property {Number} maxPasswordHistory Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.

Valid values are >= `0` and <= `20`.
Default is `0`. - * @property {Boolean} resetPasswordSuccessOnInvalidEmail Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`. - * @property {Boolean} resetTokenReuseIfValid Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`. - * @property {Number} resetTokenValidityDuration Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`. - * @property {String} validationError Set the error message to be sent.

Default is `Password does not meet the Password Policy requirements.` - * @property {Function} validatorCallback Set a callback function to validate a password to be accepted.

If used in combination with `validatorPattern`, the password must pass both to be accepted. - * @property {String} validatorPattern Set the regular expression validation pattern a password must match to be accepted.

If used in combination with `validatorCallback`, the password must pass both to be accepted. - */ - -/** - * @interface FileUploadOptions - * @property {String[]} allowedFileUrlDomains Sets the allowed hostnames for file URLs referenced in Parse objects. When a File object includes a URL, its hostname must match one of these entries to be accepted. Supports exact hostnames (e.g., `'cdn.example.com'`) and wildcard subdomains (e.g., `'*.example.com'`). Use `['*']` to allow any domain. Use `[]` to block all file URLs (only name-based files allowed). - * @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users. - * @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users. - * @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication. - * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.

The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser's local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.

Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)"]`. - */ - -/** - * @interface LogLevel - * @property {StringLiteral} debug Debug level - * @property {StringLiteral} error Error level - highest priority - * @property {StringLiteral} info Info level - default - * @property {StringLiteral} silly Silly level - lowest priority - * @property {StringLiteral} verbose Verbose level - * @property {StringLiteral} warn Warning level - */ - -/** - * @interface LogClientEvent - * @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. - * @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`. - * @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. - */ - -/** - * @interface DatabaseOptions - * @property {Boolean} allowPublicExplain Set to `true` to allow `Parse.Query.explain` without master key.

⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes. - * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance. - * @property {String} authMechanism The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection. - * @property {Any} authMechanismProperties The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. - * @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials. - * @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. - * @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. - * @property {Number} batchSize The number of documents per batch for MongoDB cursor `getMore` operations. A lower value reduces memory usage per batch; a higher value reduces the number of network round-trips. - * @property {DatabaseOptionsClientMetadata} clientMetadata Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead. - * @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. - * @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. - * @property {Boolean} createIndexAuthDataUniqueness Set to `true` to automatically create unique indexes on the authData fields of the _User collection for each configured auth provider on server start, including `anonymous` when anonymous users are enabled. These indexes prevent race conditions during concurrent signups with the same authData. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the indexes, keep in mind that the otherwise automatically created indexes may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexUserEmail Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexUserEmailCaseInsensitive Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexUserEmailVerifyToken Set to `true` to automatically create an index on the _email_verify_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexUserPasswordResetToken Set to `true` to automatically create an index on the _perishable_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexUserUsername Set to `true` to automatically create indexes on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} createIndexUserUsernameCaseInsensitive Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - * @property {Boolean} directConnection The MongoDB driver option to force a Single topology type with a connection string containing one host. - * @property {Boolean} disableIndexFieldValidation Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. - * @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. - * @property {Boolean} forceServerObjectId The MongoDB driver option to force server to assign _id values instead of driver. - * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. - * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. - * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. - * @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events. - * @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. - * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. - * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. - * @property {Number} maxStalenessSeconds The MongoDB driver option to set the maximum replication lag for reads from secondary nodes. - * @property {Number} maxTimeMS The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor. - * @property {Number} minPoolSize The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver. - * @property {String} proxyHost The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections. - * @property {String} proxyPassword The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication. - * @property {Number} proxyPort The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections. - * @property {String} proxyUsername The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication. - * @property {String} readConcernLevel The MongoDB driver option to specify the level of isolation. - * @property {String} readPreference The MongoDB driver option to specify the read preferences for this connection. - * @property {Any[]} readPreferenceTags The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs. - * @property {String} replicaSet The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set. - * @property {Boolean} retryReads The MongoDB driver option to enable retryable reads. - * @property {Boolean} retryWrites The MongoDB driver option to set whether to retry failed writes. - * @property {Number} schemaCacheTtl The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires. - * @property {String} serverMonitoringMode The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode. - * @property {Number} serverSelectionTimeoutMS The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection. - * @property {Number} socketTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout. - * @property {Number} srvMaxHosts The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts. - * @property {String} srvServiceName The MongoDB driver option to modify the srv URI service name. - * @property {Boolean} ssl The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option). - * @property {Boolean} tls The MongoDB driver option to enable or disable TLS/SSL for the connection. - * @property {Boolean} tlsAllowInvalidCertificates The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance. - * @property {Boolean} tlsAllowInvalidHostnames The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance. - * @property {String} tlsCAFile The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority. - * @property {String} tlsCertificateKeyFile The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key. - * @property {String} tlsCertificateKeyFilePassword The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile. - * @property {Boolean} tlsInsecure The MongoDB driver option to disable various certificate validations. - * @property {Number} waitQueueTimeoutMS The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available. - * @property {Number} zlibCompressionLevel The MongoDB driver option to specify the compression level if using zlib for network compression (0-9). - */ - -/** - * @interface DatabaseOptionsClientMetadata - * @property {String} name The name to identify your application in database logs (e.g., 'MyApp'). - * @property {String} version The version of your application (e.g., '1.0.0'). - */ - -/** - * @interface AuthAdapter - * @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise. - */ - -/** - * @interface LogLevels - * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. - * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. - * @property {String} signupUsernameTaken Log level used when a sign-up fails because the username already exists. Default is `info`. See [LogLevel](LogLevel.html) for available values. - * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. - * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. - * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. - */ diff --git a/src/Options/index.js b/src/Options/index.js deleted file mode 100644 index d9f447cf19..0000000000 --- a/src/Options/index.js +++ /dev/null @@ -1,882 +0,0 @@ -// @flow -import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; -import { CacheAdapter } from '../Adapters/Cache/CacheAdapter'; -import { MailAdapter } from '../Adapters/Email/MailAdapter'; -import { FilesAdapter } from '../Adapters/Files/FilesAdapter'; -import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter'; -import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter'; -import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; -import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter'; -import { CheckGroup } from '../Security/CheckGroup'; - -export interface SchemaOptions { - /* Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema - :DEFAULT: [] */ - definitions: any; - /* Is true if Parse Server should exit if schema update fail. - :DEFAULT: false */ - strict: ?boolean; - /* Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development. - :DEFAULT: false */ - deleteExtraFields: ?boolean; - /* Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development. - :DEFAULT: false */ - recreateModifiedFields: ?boolean; - /* Is true if Parse Server will reject any attempts to modify the schema while the server is running. - :DEFAULT: false */ - lockSchemas: ?boolean; - /* (Optional) Keep indexes that are present in the database but not defined in the schema. Set this to `true` if you are adding indexes manually, so that they won't be removed when running schema migration. Default is `false`. - :DEFAULT: false */ - keepUnknownIndexes: ?boolean; - /* Execute a callback before running schema migrations. */ - beforeMigration: ?() => void | Promise; - /* Execute a callback after running schema migrations. */ - afterMigration: ?() => void | Promise; -} - -type Adapter = string | any | T; -type NumberOrBoolean = number | boolean; -type NumberOrString = number | string; -type ProtectedFields = any; -type StringOrStringArray = string | string[]; -type RequestKeywordDenylist = { - key: string | any, - value: any, -}; -type EmailVerificationRequest = { - original?: any, - object: any, - master?: boolean, - ip?: string, - installationId?: string, - createdWith?: { - action: 'login' | 'signup', - authProvider: string, - }, - resendRequest?: boolean, -}; -type SendEmailVerificationRequest = { - user: any, - master?: boolean, -}; - -export interface ParseServerOptions { - /* Your Parse Application ID - :ENV: PARSE_SERVER_APPLICATION_ID */ - appId: string; - /* Your Parse Master Key */ - masterKey: (() => void) | string; - /* (Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server. */ - masterKeyTtl: ?number; - /* (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */ - maintenanceKey: string; - /* The URL to Parse Server.

⚠️ Certain server features or adapters may require Parse Server to be able to call itself by making requests to the URL set in `serverURL`. If a feature requires this, it is mentioned in the documentation. In that case ensure that the URL is accessible from the server itself. - :ENV: PARSE_SERVER_URL */ - serverURL: string; - /* Parse Server makes a HTTP request to the URL set in `serverURL` at the end of its launch routine to verify that the launch succeeded. If this option is set to `false`, the verification will be skipped. This can be useful in environments where the server URL is not accessible from the server itself, such as when running behind a firewall or in certain containerized environments.

⚠️ Server URL verification requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Default is `true`. - :DEFAULT: true */ - verifyServerUrl: ?boolean; - /* (Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key. - :DEFAULT: ["127.0.0.1","::1"] */ - masterKeyIps: ?(string[]); - /* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key. - :DEFAULT: ["127.0.0.1","::1"] */ - maintenanceKeyIps: ?(string[]); - /* (Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the read-only master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the read-only master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['0.0.0.0/0', '::0']` which means that any IP address is allowed to use the read-only master key. It is recommended to set this option to `['127.0.0.1', '::1']` to restrict access to `localhost`. - :DEFAULT: ["0.0.0.0/0","::0"] */ - readOnlyMasterKeyIps: ?(string[]); - /* Sets the app name */ - appName: ?string; - /* Add headers to Access-Control-Allow-Headers */ - allowHeaders: ?(string[]); - /* Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins. */ - allowOrigin: ?StringOrStringArray; - /* Adapter module for the analytics */ - analyticsAdapter: ?Adapter; - /* Adapter module for the files sub-system */ - filesAdapter: ?Adapter; - /* Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications */ - push: ?any; - /* Configuration for push scheduling, defaults to false. - :DEFAULT: false */ - scheduledPush: ?boolean; - /* Adapter module for the logging sub-system */ - loggerAdapter: ?Adapter; - /* Log as structured JSON objects - :ENV: JSON_LOGS */ - jsonLogs: ?boolean; - /* Folder for the logs (defaults to './logs'); set to null to disable file based logging - :ENV: PARSE_SERVER_LOGS_FOLDER - :DEFAULT: ./logs */ - logsFolder: ?string; - /* Set the logging to verbose - :ENV: VERBOSE */ - verbose: ?boolean; - /* Sets the level for logs */ - logLevel: ?string; - /* (Optional) Overrides the log levels used internally by Parse Server to log events. - :DEFAULT: {} */ - logLevels: ?LogLevels; - /* Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) */ - maxLogFiles: ?NumberOrString; - /* Disables console output - :ENV: SILENT */ - silent: ?boolean; - /* The full URI to your database. Supported databases are mongodb or postgres. - :DEFAULT: mongodb://localhost:27017/parse */ - databaseURI: string; - /* Options to pass to the database client - :ENV: PARSE_SERVER_DATABASE_OPTIONS */ - databaseOptions: ?DatabaseOptions; - /* Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. */ - databaseAdapter: ?Adapter; - /* Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. - :DEFAULT: false */ - enableCollationCaseComparison: ?boolean; - /* Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`. - :DEFAULT: false */ - convertEmailToLowercase: ?boolean; - /* Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`. - :DEFAULT: false */ - convertUsernameToLowercase: ?boolean; - /* Full path to your cloud code main.js */ - cloud: ?string; - /* A collection prefix for the classes - :DEFAULT: '' */ - collectionPrefix: ?string; - /* Key for iOS, MacOS, tvOS clients */ - clientKey: ?string; - /* Key for the Javascript SDK */ - javascriptKey: ?string; - /* Key for Unity and .Net SDK */ - dotNetKey: ?string; - /* Key for encrypting your files - :ENV: PARSE_SERVER_ENCRYPTION_KEY */ - encryptionKey: ?string; - /* Key for REST calls - :ENV: PARSE_SERVER_REST_API_KEY */ - restAPIKey: ?string; - /* Read-only key, which has the same capabilities as MasterKey without writes */ - readOnlyMasterKey: ?string; - /* Key sent with outgoing webhook calls */ - webhookKey: ?string; - /* Key for your files */ - fileKey: ?string; - /* Enable (or disable) the addition of a unique hash to the file names - :ENV: PARSE_SERVER_PRESERVE_FILE_NAME - :DEFAULT: false */ - preserveFileName: ?boolean; - /* Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields */ - userSensitiveFields: ?(string[]); - /* Protected fields that should be treated with extra security when fetching details. - :DEFAULT: {"_User": {"*": ["email"]}} */ - protectedFields: ?ProtectedFields; - /* Enable (or disable) anonymous users, defaults to true - :ENV: PARSE_SERVER_ENABLE_ANON_USERS - :DEFAULT: true */ - enableAnonymousUsers: ?boolean; - /* Enable (or disable) client class creation, defaults to false - :ENV: PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION - :DEFAULT: false */ - allowClientClassCreation: ?boolean; - /* Enable (or disable) custom objectId - :ENV: PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID - :DEFAULT: false */ - allowCustomObjectId: ?boolean; - /* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication

Provider names must start with a letter and contain only letters, digits, and underscores (`/^[A-Za-z][A-Za-z0-9_]*$/`). This is because each provider name is used to construct a database field (`_auth_data_`), which must comply with Parse Server's field naming rules. - :ENV: PARSE_SERVER_AUTH_PROVIDERS */ - auth: ?{ [string]: AuthAdapter }; - /* Optional. Enables insecure authentication adapters. Insecure auth adapters are deprecated and will be removed in a future version. Defaults to `false`. - :ENV: PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS - :DEFAULT: false */ - enableInsecureAuthAdapters: ?boolean; - /* Max file size for uploads, defaults to 20mb - :DEFAULT: 20mb */ - maxUploadSize: ?string; - /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider. -

- The `createdWith` values per scenario: -
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
  • Resend verification email: `createdWith` is `undefined`; use the `resendRequest` property to identify those
- Default is `false`. - :DEFAULT: false */ - verifyUserEmails: ?(boolean | (EmailVerificationRequest => boolean | Promise)); - /* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider. -

- The `createdWith` values per scenario: -
  • Password signup: `{ action: 'signup', authProvider: 'password' }`
  • Auth provider signup: `{ action: 'signup', authProvider: '' }`
  • Password login: `{ action: 'login', authProvider: 'password' }`
  • Auth provider login: function not invoked; auth provider login bypasses email verification
- Default is `false`. -
- Requires option `verifyUserEmails: true`. - :DEFAULT: false */ - preventLoginWithUnverifiedEmail: ?( - | boolean - | (EmailVerificationRequest => boolean | Promise) - ); - /* If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified. -

- Default is `false`. -
- Requires option `verifyUserEmails: true`. - :DEFAULT: false */ - preventSignupWithUnverifiedEmail: ?boolean; - /* Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires. -

- For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours). -

- Default is `undefined`. -
- Requires option `verifyUserEmails: true`. - */ - emailVerifyTokenValidityDuration: ?number; - /* Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token. -

- Default is `false`. -
- Requires option `verifyUserEmails: true`. - :DEFAULT: false */ - emailVerifyTokenReuseIfValid: ?boolean; - /* Set to `true` if a request to verify the email should return a success response even if the provided email address does not belong to a verifiable account, for example because it is unknown or already verified, or `false` if the request should return an error response in those cases. -

- Default is `true`. -
- Requires option `verifyUserEmails: true`. - :DEFAULT: true */ - emailVerifySuccessOnInvalidEmail: ?boolean; - /* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending. -

- Default is `true`. -
- :DEFAULT: true */ - sendUserEmailVerification: ?( - | boolean - | (SendEmailVerificationRequest => boolean | Promise) - ); - /* The account lockout policy for failed login attempts. -

- Note: Setting a user's ACL to an empty object `{}` via master key is a separate mechanism that only prevents new logins; it does not invalidate existing session tokens. To immediately revoke a user's access, destroy their sessions via master key in addition to setting the ACL. */ - accountLockout: ?AccountLockoutOptions; - /* The password policy for enforcing password related rules. */ - passwordPolicy: ?PasswordPolicyOptions; - /* Adapter module for the cache */ - cacheAdapter: ?Adapter; - /* Adapter module for email sending */ - emailAdapter: ?Adapter; - /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. - :ENV: PARSE_PUBLIC_SERVER_URL */ - publicServerURL: ?(string | (() => string) | (() => Promise)); - /* The options for pages such as password reset and email verification. - :DEFAULT: {} */ - pages: ?PagesOptions; - /* custom pages for password validation and reset - :DEFAULT: {} */ - customPages: ?CustomPagesOptions; - /* parse-server's LiveQuery configuration object */ - liveQuery: ?LiveQueryOptions; - /* Session duration, in seconds, defaults to 1 year - :DEFAULT: 31536000 */ - sessionLength: ?number; - /* Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed. - :DEFAULT: false */ - extendSessionOnUse: ?boolean; - /* Default value for limit option on queries, defaults to `100`. - :DEFAULT: 100 */ - defaultLimit: ?number; - /* Max value for limit option on queries, defaults to unlimited */ - maxLimit: ?number; - /* Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date. - :DEFAULT: true */ - expireInactiveSessions: ?boolean; - /* When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. - :DEFAULT: true */ - revokeSessionOnPasswordReset: ?boolean; - /* Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) - :DEFAULT: 5000 */ - cacheTTL: ?number; - /* Sets the maximum size for the in memory cache, defaults to 10000 - :DEFAULT: 10000 */ - cacheMaxSize: ?number; - /* Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`. -

- If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports. -

- ⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. - :DEFAULT: true */ - directAccess: ?boolean; - /* Enables the default express error handler for all errors - :DEFAULT: false */ - enableExpressErrorHandler: ?boolean; - /* Deprecated. Enables the legacy product purchase API including the `_Product` class and the `/validate_purchase` endpoint. This is an undocumented, unmaintained legacy feature inherited from the original Parse platform that may not function as expected. We strongly advise against using it. It will be removed in a future major version. - :ENV: PARSE_SERVER_ENABLE_PRODUCT_PURCHASE_LEGACY_API - :DEFAULT: true */ - enableProductPurchaseLegacyApi: ?boolean; - /* Sets the number of characters in generated object id's, default 10 - :DEFAULT: 10 */ - objectIdSize: ?number; - /* The port to run the ParseServer, defaults to 1337. - :ENV: PORT - :DEFAULT: 1337 */ - port: ?number; - /* The host to serve ParseServer on, defaults to 0.0.0.0 - :DEFAULT: 0.0.0.0 */ - host: ?string; - /* Mount path for the server, defaults to /parse - :DEFAULT: /parse */ - mountPath: ?string; - /* Run with cluster, optionally set the number of processes default to os.cpus().length */ - cluster: ?NumberOrBoolean; - /* middleware for express server, can be string or function */ - middleware: ?((() => void) | string); - /* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`. - :DEFAULT: false */ - trustProxy: ?any; - /* Starts the liveQuery server */ - startLiveQueryServer: ?boolean; - /* Live query server configuration options (will start the liveQuery server) */ - liveQueryServerOptions: ?LiveQueryServerOptions; - /* Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production. - :ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS - :DEFAULT: false */ - idempotencyOptions: ?IdempotencyOptions; - /* Options for file uploads - :ENV: PARSE_SERVER_FILE_UPLOAD_OPTIONS - :DEFAULT: {} */ - fileUpload: ?FileUploadOptions; - /* Full path to your GraphQL custom schema.graphql file */ - graphQLSchema: ?string; - /* Mounts the GraphQL endpoint - :ENV: PARSE_SERVER_MOUNT_GRAPHQL - :DEFAULT: false */ - mountGraphQL: ?boolean; - /* The mount path for the GraphQL endpoint

⚠️ File upload inside the GraphQL mutation system requires Parse Server to be able to call itself by making requests to the URL set in `serverURL`.

Defaults is `/graphql`. - :ENV: PARSE_SERVER_GRAPHQL_PATH - :DEFAULT: /graphql */ - graphQLPath: ?string; - /* Enable public introspection for the GraphQL endpoint, defaults to false - :ENV: PARSE_SERVER_GRAPHQL_PUBLIC_INTROSPECTION - :DEFAULT: false */ - graphQLPublicIntrospection: ?boolean; - /* Deprecated. Mounts the GraphQL Playground which is deprecated and will be removed in a future version. The playground exposes the master key in the browser. Use Parse Dashboard as GraphQL IDE or configure a third-party GraphQL client with custom request headers. - :ENV: PARSE_SERVER_MOUNT_PLAYGROUND - :DEFAULT: false */ - mountPlayground: ?boolean; - /* Deprecated. Mount path for the GraphQL Playground. The playground is deprecated and will be removed in a future version. - :ENV: PARSE_SERVER_PLAYGROUND_PATH - :DEFAULT: /playground */ - playgroundPath: ?string; - /* Defined schema - :ENV: PARSE_SERVER_SCHEMA - */ - schema: ?SchemaOptions; - /* Callback when server has closed */ - serverCloseComplete: ?() => void; - /* Options to limit the complexity of requests to prevent denial-of-service attacks. Limits are enforced for all requests except those using the master or maintenance key. Each property can be set to `-1` to disable that specific limit. - :ENV: PARSE_SERVER_REQUEST_COMPLEXITY - :DEFAULT: {} */ - requestComplexity: ?RequestComplexityOptions; - /* The security options to identify and report weak security settings. - :DEFAULT: {} */ - security: ?SecurityOptions; - /* Set to true if new users should be created without public read and write access. - :DEFAULT: true */ - enforcePrivateUsers: ?boolean; - /* Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`. - :DEFAULT: false */ - allowExpiredAuthDataToken: ?boolean; - /* An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns. - :DEFAULT: [{"key":"_bsontype","value":"Code"},{"key":"constructor"},{"key":"__proto__"}] */ - requestKeywordDenylist: ?(RequestKeywordDenylist[]); - /* Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. - :DEFAULT: [] */ - rateLimit: ?(RateLimitOptions[]); - /* Options to customize the request context using inversion of control/dependency injection.*/ - requestContextMiddleware: ?(req: any, res: any, next: any) => void; - /* If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`. - :DEFAULT: true */ - enableSanitizedErrorResponse: ?boolean; -} - -export interface RateLimitOptions { - /* The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or string patterns following path-to-regexp v8 syntax. */ - requestPath: string; - /* The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. */ - requestTimeWindow: ?number; - /* The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. For batch requests, this also limits the number of sub-requests in a single batch that target this path; however, requests already consumed in the current time window are not counted against the batch, so the effective limit may be higher when combining individual and batch requests. Note that this is a basic server-level rate limit; for comprehensive protection, use a reverse proxy or WAF for rate limiting. */ - requestCount: ?number; - /* The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`. - :DEFAULT: Too many requests. */ - errorResponseMessage: ?string; - /* Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. */ - requestMethods: ?(string[]); - /* Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks. - :DEFAULT: false */ - includeMasterKey: ?boolean; - /* Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks. - :DEFAULT: false */ - includeInternalRequests: ?boolean; - /* Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests. - */ - redisUrl: ?string; - /* The type of rate limit to apply. The following types are supported: -
    -
  • `global`: rate limit based on the number of requests made by all users
  • -
  • `ip`: rate limit based on the IP address of the request
  • -
  • `user`: rate limit based on the user ID of the request
  • -
  • `session`: rate limit based on the session token of the request
  • -
- Default is `ip`. - :DEFAULT: ip */ - zone: ?string; -} - -export interface RequestComplexityOptions { - /* Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `-1`. - :DEFAULT: -1 */ - includeDepth: ?number; - /* Maximum number of include paths in a single query. Set to `-1` to disable. Default is `-1`. - :DEFAULT: -1 */ - includeCount: ?number; - /* Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `-1`. - :DEFAULT: -1 */ - subqueryDepth: ?number; - /* Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`. - :DEFAULT: -1 */ - queryDepth: ?number; - /* Maximum depth of GraphQL field selections. Set to `-1` to disable. Default is `-1`. - :ENV: PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_DEPTH - :DEFAULT: -1 */ - graphQLDepth: ?number; - /* Maximum number of field selections in a GraphQL query. Set to `-1` to disable. Default is `-1`. - :ENV: PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_FIELDS - :DEFAULT: -1 */ - graphQLFields: ?number; -} - -export interface SecurityOptions { - /* Is true if Parse Server should check for weak security settings. - :DEFAULT: false */ - enableCheck: ?boolean; - /* Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs. - :DEFAULT: false */ - enableCheckLog: ?boolean; - /* The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`. */ - checkGroups: ?(CheckGroup[]); -} - -export interface PagesOptions { - /* Is true if pages should be localized; this has no effect on custom page redirects. - :DEFAULT: false */ - enableLocalization: ?boolean; - /* The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. */ - localizationJsonPath: ?string; - /* The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. - :DEFAULT: en */ - localizationFallbackLocale: ?string; - /* The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. - :DEFAULT: {} */ - placeholders: ?Object; - /* Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response). - :DEFAULT: false */ - forceRedirect: ?boolean; - /* The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory of the parse-server module. */ - pagesPath: ?string; - /* The API endpoint for the pages. Default is 'apps'. - :DEFAULT: apps */ - pagesEndpoint: ?string; - /* The URLs to the custom pages. - :DEFAULT: {} */ - customUrls: ?PagesCustomUrlsOptions; - /* The custom routes. - :DEFAULT: [] */ - customRoutes: ?(PagesRoute[]); - /* Is `true` if the page parameter headers should be URI-encoded. This is required if any page parameter value contains non-ASCII characters, such as the app name. - :DEFAULT: false */ - encodePageParamHeaders: ?boolean; -} - -export interface PagesRoute { - /* The route path. */ - path: string; - /* The route method, e.g. 'GET' or 'POST'. */ - method: string; - /* The route handler that is an async function. */ - handler: () => void; -} - -export interface PagesCustomUrlsOptions { - /* The URL to the custom page for password reset. */ - passwordReset: ?string; - /* The URL to the custom page for password reset -> link invalid. */ - passwordResetLinkInvalid: ?string; - /* The URL to the custom page for password reset -> success. */ - passwordResetSuccess: ?string; - /* The URL to the custom page for email verification -> success. */ - emailVerificationSuccess: ?string; - /* The URL to the custom page for email verification -> link send fail. */ - emailVerificationSendFail: ?string; - /* The URL to the custom page for email verification -> resend link -> success. */ - emailVerificationSendSuccess: ?string; - /* The URL to the custom page for email verification -> link invalid. */ - emailVerificationLinkInvalid: ?string; - /* The URL to the custom page for email verification -> link expired. */ - emailVerificationLinkExpired: ?string; -} - -export interface CustomPagesOptions { - /* invalid link page path */ - invalidLink: ?string; - /* verification link send fail page path */ - linkSendFail: ?string; - /* choose password page path */ - choosePassword: ?string; - /* verification link send success page path */ - linkSendSuccess: ?string; - /* verify email success page path */ - verifyEmailSuccess: ?string; - /* password reset success page path */ - passwordResetSuccess: ?string; - /* invalid verification link page path */ - invalidVerificationLink: ?string; - /* expired verification link page path */ - expiredVerificationLink: ?string; - /* invalid password reset link page path */ - invalidPasswordResetLink: ?string; - /* for masking user-facing pages */ - parseFrameURL: ?string; -} - -export interface LiveQueryOptions { - /* parse-server's LiveQuery classNames - :ENV: PARSE_SERVER_LIVEQUERY_CLASSNAMES */ - classNames: ?(string[]); - /* parse-server's LiveQuery redisOptions */ - redisOptions: ?any; - /* parse-server's LiveQuery redisURL */ - redisURL: ?string; - /* LiveQuery pubsub adapter */ - pubSubAdapter: ?Adapter; - /* Sets the maximum execution time in milliseconds for regular expression pattern matching in LiveQuery. This protects against Regular Expression Denial of Service (ReDoS) attacks where a malicious regex pattern could block the event loop. A regex that exceeds the timeout will be treated as non-matching.

The protection runs each regex evaluation in an isolated VM context with a timeout. This adds approximately 50 microseconds of overhead per regex evaluation. For most applications this is negligible, but it can add up if you have a very large number of LiveQuery subscriptions that use `$regex` on the same class. For example, 10,000 concurrent regex subscriptions would add approximately 500ms of processing time per object save event on that class.

Set to `0` to disable the timeout and use native regex evaluation without protection. Defaults to `100`. - :DEFAULT: 100 */ - regexTimeout: ?number; - /* Adapter module for the WebSocketServer */ - wssAdapter: ?Adapter; -} - -export interface LiveQueryServerOptions { - /* This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.*/ - appId: ?string; - /* This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.*/ - masterKey: ?string; - /* This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.*/ - serverURL: ?string; - /* A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.*/ - keyPairs: ?any; - /* Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).*/ - websocketTimeout: ?number; - /* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).*/ - cacheTimeout: ?number; - /* This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.*/ - logLevel: ?string; - /* The port to run the LiveQuery server, defaults to 1337. - :DEFAULT: 1337 */ - port: ?number; - /* parse-server's LiveQuery redisOptions */ - redisOptions: ?any; - /* parse-server's LiveQuery redisURL */ - redisURL: ?string; - /* LiveQuery pubsub adapter */ - pubSubAdapter: ?Adapter; - /* Adapter module for the WebSocketServer */ - wssAdapter: ?Adapter; -} - -export interface IdempotencyOptions { - /* An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths. - :DEFAULT: [] */ - paths: ?(string[]); - /* The duration in seconds after which a request record is discarded from the database, defaults to 300s. - :DEFAULT: 300 */ - ttl: ?number; -} - -export interface AccountLockoutOptions { - /* Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked. -

- Valid values are greater than `0` and less than `100000`. */ - duration: ?number; - /* Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made. -

- Valid values are greater than `0` and less than `1000`. */ - threshold: ?number; - /* Set to `true` if the account should be unlocked after a successful password reset. -

- Default is `false`. -
- Requires options `duration` and `threshold` to be set. - :DEFAULT: false */ - unlockOnPasswordReset: ?boolean; -} - -export interface PasswordPolicyOptions { - /* Set the regular expression validation pattern a password must match to be accepted. -

- If used in combination with `validatorCallback`, the password must pass both to be accepted. */ - validatorPattern: ?string; - /* */ - /* Set a callback function to validate a password to be accepted. -

- If used in combination with `validatorPattern`, the password must pass both to be accepted. */ - validatorCallback: ?() => void; - /* Set the error message to be sent. -

- Default is `Password does not meet the Password Policy requirements.` */ - validationError: ?string; - /* Set to `true` to disallow the username as part of the password. -

- Default is `false`. - :DEFAULT: false */ - doNotAllowUsername: ?boolean; - /* Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration. */ - maxPasswordAge: ?number; - /* Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered. -

- Valid values are >= `0` and <= `20`. -
- Default is `0`. - */ - maxPasswordHistory: ?number; - /* Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires. -

- For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours). -

- Default is `undefined`. - */ - resetTokenValidityDuration: ?number; - /* Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token. -

- Default is `false`. - :DEFAULT: false */ - resetTokenReuseIfValid: ?boolean; - /* Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid. -

- Default is `true`. - :DEFAULT: true */ - resetPasswordSuccessOnInvalidEmail: ?boolean; -} - -export interface FileUploadOptions { - /* Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.

The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser's local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.

Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)"]`. - :DEFAULT: ["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)"] */ - fileExtensions: ?(string[]); - /* Is true if file upload should be allowed for anonymous users. - :DEFAULT: false */ - enableForAnonymousUser: ?boolean; - /* Is true if file upload should be allowed for authenticated users. - :DEFAULT: true */ - enableForAuthenticatedUser: ?boolean; - /* Is true if file upload should be allowed for anyone, regardless of user authentication. - :DEFAULT: false */ - enableForPublic: ?boolean; - /* Sets the allowed hostnames for file URLs referenced in Parse objects. When a File object includes a URL, its hostname must match one of these entries to be accepted. Supports exact hostnames (e.g., `'cdn.example.com'`) and wildcard subdomains (e.g., `'*.example.com'`). Use `['*']` to allow any domain. Use `[]` to block all file URLs (only name-based files allowed). - :DEFAULT: ["*"] */ - allowedFileUrlDomains: ?(string[]); -} - -/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ -export interface LogLevel { - /* Error level - highest priority */ - error: 'error'; - /* Warning level */ - warn: 'warn'; - /* Info level - default */ - info: 'info'; - /* Verbose level */ - verbose: 'verbose'; - /* Debug level */ - debug: 'debug'; - /* Silly level - lowest priority */ - silly: 'silly'; -} - -export interface LogClientEvent { - /* The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */ - name: string; - /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ - keys: ?(string[]); - /* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`. - :DEFAULT: info */ - logLevel: ?string; -} - -export interface DatabaseOptions { - /* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. - :DEFAULT: false */ - enableSchemaHooks: ?boolean; - /* The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires. */ - schemaCacheTtl: ?number; - /* The MongoDB driver option to set whether to retry failed writes. */ - retryWrites: ?boolean; - /* The number of documents per batch for MongoDB cursor `getMore` operations. A lower value reduces memory usage per batch; a higher value reduces the number of network round-trips. - :DEFAULT: 1000 */ - batchSize: ?number; - /* The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor. */ - maxTimeMS: ?number; - /* The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.*/ - maxStalenessSeconds: ?number; - /* The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver. */ - minPoolSize: ?number; - /* The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. */ - maxPoolSize: ?number; - /* The MongoDB driver option to specify the amount of time in milliseconds for a server to be considered suitable for selection. */ - serverSelectionTimeoutMS: ?number; - /* The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. */ - maxIdleTimeMS: ?number; - /* The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. */ - heartbeatFrequencyMS: ?number; - /* The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. */ - connectTimeoutMS: ?number; - /* The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout. */ - socketTimeoutMS: ?number; - /* The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. */ - autoSelectFamily: ?boolean; - /* The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. */ - autoSelectFamilyAttemptTimeout: ?number; - /* The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. */ - maxConnecting: ?number; - /* The MongoDB driver option to specify the maximum time in milliseconds that a thread can wait for a connection to become available. */ - waitQueueTimeoutMS: ?number; - /* The MongoDB driver option to specify the name of the replica set, if the mongod is a member of a replica set. */ - replicaSet: ?string; - /* The MongoDB driver option to force a Single topology type with a connection string containing one host. */ - directConnection: ?boolean; - /* The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. */ - loadBalanced: ?boolean; - /* The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. */ - localThresholdMS: ?number; - /* The MongoDB driver option to specify the maximum number of hosts to connect to when using an srv connection string, a setting of 0 means unlimited hosts. */ - srvMaxHosts: ?number; - /* The MongoDB driver option to modify the srv URI service name. */ - srvServiceName: ?string; - /* The MongoDB driver option to enable or disable TLS/SSL for the connection. */ - tls: ?boolean; - /* The MongoDB driver option to enable or disable TLS/SSL for the connection (equivalent to tls option). */ - ssl: ?boolean; - /* The MongoDB driver option to specify the location of a local .pem file that contains the client's TLS/SSL certificate and key. */ - tlsCertificateKeyFile: ?string; - /* The MongoDB driver option to specify the password to decrypt the tlsCertificateKeyFile. */ - tlsCertificateKeyFilePassword: ?string; - /* The MongoDB driver option to specify the location of a local .pem file that contains the root certificate chain from the Certificate Authority. */ - tlsCAFile: ?string; - /* The MongoDB driver option to bypass validation of the certificates presented by the mongod/mongos instance. */ - tlsAllowInvalidCertificates: ?boolean; - /* The MongoDB driver option to disable hostname validation of the certificate presented by the mongod/mongos instance. */ - tlsAllowInvalidHostnames: ?boolean; - /* The MongoDB driver option to disable various certificate validations. */ - tlsInsecure: ?boolean; - /* The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. */ - compressors: ?(string[] | string); - /* The MongoDB driver option to specify the compression level if using zlib for network compression (0-9). */ - zlibCompressionLevel: ?number; - /* The MongoDB driver option to specify the read preferences for this connection. */ - readPreference: ?string; - /* The MongoDB driver option to specify the tags document as a comma-separated list of colon-separated key-value pairs. */ - readPreferenceTags: ?(any[]); - /* The MongoDB driver option to specify the level of isolation. */ - readConcernLevel: ?string; - /* The MongoDB driver option to specify the database name associated with the user's credentials. */ - authSource: ?string; - /* The MongoDB driver option to specify the authentication mechanism that MongoDB will use to authenticate the connection. */ - authMechanism: ?string; - /* The MongoDB driver option to specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. */ - authMechanismProperties: ?any; - /* The MongoDB driver option to specify the name of the application that created this MongoClient instance. */ - appName: ?string; - /* The MongoDB driver option to enable retryable reads. */ - retryReads: ?boolean; - /* The MongoDB driver option to force server to assign _id values instead of driver. */ - forceServerObjectId: ?boolean; - /* The MongoDB driver option to instruct the driver monitors to use a specific monitoring mode. */ - serverMonitoringMode: ?string; - /* The MongoDB driver option to configure a Socks5 proxy host used for creating TCP connections. */ - proxyHost: ?string; - /* The MongoDB driver option to configure a Socks5 proxy port used for creating TCP connections. */ - proxyPort: ?number; - /* The MongoDB driver option to configure a Socks5 proxy username when the proxy requires username/password authentication. */ - proxyUsername: ?string; - /* The MongoDB driver option to configure a Socks5 proxy password when the proxy requires username/password authentication. */ - proxyPassword: ?string; - /* Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexUserEmail: ?boolean; - /* Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexUserEmailCaseInsensitive: ?boolean; - /* Set to `true` to automatically create an index on the _email_verify_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexUserEmailVerifyToken: ?boolean; - /* Set to `true` to automatically create an index on the _perishable_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexUserPasswordResetToken: ?boolean; - /* Set to `true` to automatically create indexes on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexUserUsername: ?boolean; - /* Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexUserUsernameCaseInsensitive: ?boolean; - /* Set to `true` to automatically create unique indexes on the authData fields of the _User collection for each configured auth provider on server start, including `anonymous` when anonymous users are enabled. These indexes prevent race conditions during concurrent signups with the same authData. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the indexes, keep in mind that the otherwise automatically created indexes may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexAuthDataUniqueness: ?boolean; - /* Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. - :DEFAULT: true */ - createIndexRoleName: ?boolean; - /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ - disableIndexFieldValidation: ?boolean; - /* Set to `true` to allow `Parse.Query.explain` without master key.

⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes. - :DEFAULT: false */ - allowPublicExplain: ?boolean; - /* An array of MongoDB client event configurations to enable logging of specific events. */ - logClientEvents: ?(LogClientEvent[]); - /* Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead. */ - clientMetadata: ?DatabaseOptionsClientMetadata; -} - -export interface DatabaseOptionsClientMetadata { - /* The name to identify your application in database logs (e.g., 'MyApp'). */ - name: string; - /* The version of your application (e.g., '1.0.0'). */ - version: string; -} - -export interface AuthAdapter { - /* Is `true` if the auth adapter is enabled, `false` otherwise. - :DEFAULT: false - :ENV: - */ - enabled: ?boolean; -} - -export interface LogLevels { - /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. - :DEFAULT: info - */ - triggerAfter: ?string; - /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. - :DEFAULT: info - */ - triggerBeforeSuccess: ?string; - /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. - :DEFAULT: error - */ - triggerBeforeError: ?string; - /* Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. - :DEFAULT: info - */ - cloudFunctionSuccess: ?string; - /* Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. - :DEFAULT: error - */ - cloudFunctionError: ?string; - /* Log level used when a sign-up fails because the username already exists. Default is `info`. See [LogLevel](LogLevel.html) for available values. - :DEFAULT: info - */ - signupUsernameTaken: ?string; -} diff --git a/src/Options/index.ts b/src/Options/index.ts new file mode 100644 index 0000000000..ec0e27a5e2 --- /dev/null +++ b/src/Options/index.ts @@ -0,0 +1,29 @@ +/** + * Parse Server Options - Type definitions + * + * All types are inferred from Zod schemas. This file re-exports them + * for backwards compatibility with existing imports. + */ +export type { ParseServerOptions } from './schemas/ParseServerOptions'; +export type { SchemaOptions } from './schemas/SchemaOptions'; +export type { AccountLockoutOptions } from './schemas/AccountLockoutOptions'; +export type { PasswordPolicyOptions } from './schemas/PasswordPolicyOptions'; +export type { FileUploadOptions } from './schemas/FileUploadOptions'; +export type { IdempotencyOptions } from './schemas/IdempotencyOptions'; +export type { SecurityOptions } from './schemas/SecurityOptions'; +export type { RequestComplexityOptions } from './schemas/RequestComplexityOptions'; +export type { + PagesOptions, + PagesRoute, + PagesCustomUrlsOptions, + CustomPagesOptions, +} from './schemas/PagesOptions'; +export type { LiveQueryOptions, LiveQueryServerOptions } from './schemas/LiveQueryOptions'; +export type { RateLimitOptions } from './schemas/RateLimitOptions'; +export type { LogLevels } from './schemas/LogLevels'; +export type { + DatabaseOptions, + DatabaseOptionsClientMetadata, + LogClientEvent, + LogLevel, +} from './schemas/DatabaseOptions'; diff --git a/src/Options/loaders/cliLoader.ts b/src/Options/loaders/cliLoader.ts new file mode 100644 index 0000000000..e2a2c095c8 --- /dev/null +++ b/src/Options/loaders/cliLoader.ts @@ -0,0 +1,63 @@ +import { Command } from 'commander'; +import { z } from 'zod'; +import { getAllOptionMeta, coerceValue } from '../schemaUtils'; + +/** + * Registers Commander options from a Zod object schema's metadata. + * + * For each field in the schema that has option metadata, creates a + * corresponding Commander option with the appropriate flag format, + * help text, and type coercion. + */ +export function registerSchemaOptions( + program: Command, + schema: z.ZodObject +): void { + const metaMap = getAllOptionMeta(schema); + const shape = schema.shape; + + for (const [key, meta] of metaMap) { + const fieldSchema = shape[key] as z.ZodTypeAny; + const isRequired = !isOptionalOrDefaulted(fieldSchema); + + const flag = isRequired ? `--${key} <${key}>` : `--${key} [${key}]`; + + program.option(flag, meta.help, (value: string) => { + return coerceValue(value, fieldSchema); + }); + } +} + +/** + * Extracts option values from a parsed Commander program. + * Only returns values that were explicitly set via CLI args. + */ +export function extractCliOptions( + program: Command, + schema: z.ZodObject +): Record { + const opts = program.opts(); + const shape = schema.shape; + const result: Record = {}; + + for (const key of Object.keys(shape)) { + if (opts[key] !== undefined) { + result[key] = opts[key]; + } + } + + return result; +} + +/** + * Checks if a Zod schema field is optional or has a default value. + */ +function isOptionalOrDefaulted(schema: z.ZodTypeAny): boolean { + if (schema instanceof z.ZodOptional) { + return true; + } + if (schema instanceof z.ZodDefault) { + return true; + } + return false; +} diff --git a/src/Options/loaders/envLoader.ts b/src/Options/loaders/envLoader.ts new file mode 100644 index 0000000000..f4e4fb4bbc --- /dev/null +++ b/src/Options/loaders/envLoader.ts @@ -0,0 +1,55 @@ +import { z } from 'zod'; +import { buildEnvMap, coerceValue } from '../schemaUtils'; + +/** + * Loads configuration from environment variables based on Zod schema metadata. + * + * For each field in the schema that has an `env` metadata key, checks if that + * environment variable is set. If so, coerces the string value to the + * appropriate type and places it at the correct path in the result object. + * + * Supports nested schemas: if a field is a ZodObject, its fields are also + * checked for env var mappings. + */ +export function loadFromEnv( + schema: z.ZodObject, + env: Record = process.env +): Record { + const envMap = buildEnvMap(schema); + const result: Record = {}; + + for (const [envKey, { path, fieldSchema }] of envMap) { + const rawValue = env[envKey]; + if (rawValue === undefined || rawValue === '') { + continue; + } + + const coerced = coerceValue(rawValue, fieldSchema); + setNestedValue(result, path, coerced); + } + + return result; +} + +/** + * Sets a value at a nested path in an object. + * Creates intermediate objects as needed. + * + * Example: setNestedValue(obj, ['schema', 'strict'], true) + * Result: { schema: { strict: true } } + */ +function setNestedValue(obj: Record, path: string[], value: unknown): void { + let current: Record = obj; + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]; + if (current[key] === undefined) { + current[key] = {}; + } else if (current[key] === null || typeof current[key] !== 'object' || Array.isArray(current[key])) { + throw new Error( + `Environment variable path collision at "${path.slice(0, i + 1).join('.')}": expected an object but found ${Array.isArray(current[key]) ? 'an array' : typeof current[key]}` + ); + } + current = current[key] as Record; + } + current[path[path.length - 1]] = value; +} diff --git a/src/Options/loaders/fileLoader.ts b/src/Options/loaders/fileLoader.ts new file mode 100644 index 0000000000..b1aa8db65c --- /dev/null +++ b/src/Options/loaders/fileLoader.ts @@ -0,0 +1,36 @@ +import path from 'path'; + +/** + * Loads configuration from a JSON or JS config file. + * + * Supports two formats: + * 1. Direct config object: `{ appId: "...", masterKey: "..." }` + * 2. PM2/multi-app format: `{ apps: [{ appId: "...", masterKey: "..." }] }` + * (only single app is supported) + * + * @param filePath - Path to the JSON or JS config file + * @returns The parsed configuration object + */ +export function loadFromFile(filePath: string): Record { + const resolvedPath = path.resolve(filePath); + const jsonConfig = require(resolvedPath); + + let options: Record; + + if (jsonConfig.apps) { + if (!Array.isArray(jsonConfig.apps)) { + throw new Error('The "apps" property must be an array'); + } + if (jsonConfig.apps.length === 0) { + throw new Error('The "apps" array must contain at least one configuration'); + } + if (jsonConfig.apps.length > 1) { + throw new Error('Multiple apps are not supported'); + } + options = jsonConfig.apps[0]; + } else { + options = jsonConfig; + } + + return options; +} diff --git a/src/Options/loaders/mergeConfig.ts b/src/Options/loaders/mergeConfig.ts new file mode 100644 index 0000000000..03d278e55c --- /dev/null +++ b/src/Options/loaders/mergeConfig.ts @@ -0,0 +1,70 @@ +/** + * Merges multiple configuration sources with defined priority. + * Later sources override earlier sources. Nested objects are deep merged. + * + * Standard priority order (lowest to highest): + * 1. Config file + * 2. Environment variables + * 3. CLI arguments + * + * Zod schema defaults are applied separately during parse(). + */ +export function mergeConfigs( + ...sources: Array> +): Record { + const result: Record = {}; + + for (const source of sources) { + deepMerge(result, source); + } + + return result; +} + +/** + * Deep merges source into target, mutating target in place. + * - Plain objects are recursively merged. + * - Arrays and primitives from source overwrite target values. + * - Undefined values in source are skipped. + */ +function deepMerge( + target: Record, + source: Record +): void { + for (const key of Object.keys(source)) { + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue; + } + if (!Object.prototype.hasOwnProperty.call(source, key)) { + continue; + } + + const sourceVal = source[key]; + const targetVal = target[key]; + + if (sourceVal === undefined) { + continue; + } + + if (isPlainObject(sourceVal) && isPlainObject(targetVal)) { + deepMerge( + targetVal as Record, + sourceVal as Record + ); + } else if (isPlainObject(sourceVal)) { + const clone: Record = {}; + deepMerge(clone, sourceVal as Record); + target[key] = clone; + } else { + target[key] = sourceVal; + } + } +} + +function isPlainObject(value: unknown): value is Record { + if (value === null || typeof value !== 'object') { + return false; + } + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} diff --git a/src/Options/parsers.js b/src/Options/parsers.js deleted file mode 100644 index d2f0b7b6fb..0000000000 --- a/src/Options/parsers.js +++ /dev/null @@ -1,95 +0,0 @@ -function numberParser(key) { - return function (opt) { - const intOpt = parseInt(opt); - if (!Number.isInteger(intOpt)) { - throw new Error(`Key ${key} has invalid value ${opt}`); - } - return intOpt; - }; -} - -function numberOrBoolParser(key) { - return function (opt) { - if (typeof opt === 'boolean') { - return opt; - } - if (opt === 'true') { - return true; - } - if (opt === 'false') { - return false; - } - return numberParser(key)(opt); - }; -} - -function numberOrStringParser(key) { - return function (opt) { - if (typeof opt === 'string') { - return opt; - } - return numberParser(key)(opt); - }; -} - -function objectParser(opt) { - if (typeof opt == 'object') { - return opt; - } - return JSON.parse(opt); -} - -function arrayParser(opt) { - if (Array.isArray(opt)) { - return opt; - } else if (typeof opt === 'string') { - return opt.split(','); - } else { - throw new Error(`${opt} should be a comma separated string or an array`); - } -} - -function moduleOrObjectParser(opt) { - if (typeof opt == 'object') { - return opt; - } - try { - return JSON.parse(opt); - } catch { - /* */ - } - return opt; -} - -function booleanParser(opt) { - if (opt == true || opt == 'true' || opt == '1') { - return true; - } - return false; -} - -function booleanOrFunctionParser(opt) { - if (typeof opt === 'function') { - return opt; - } - return booleanParser(opt); -} - -function nullParser(opt) { - if (opt == 'null') { - return null; - } - return opt; -} - -module.exports = { - numberParser, - numberOrBoolParser, - numberOrStringParser, - nullParser, - booleanParser, - booleanOrFunctionParser, - moduleOrObjectParser, - arrayParser, - objectParser, -}; diff --git a/src/Options/schemaUtils.ts b/src/Options/schemaUtils.ts new file mode 100644 index 0000000000..50c52c7294 --- /dev/null +++ b/src/Options/schemaUtils.ts @@ -0,0 +1,498 @@ +import { z } from 'zod'; + +/** + * Metadata attached to each Zod schema field describing its config behavior. + */ +export interface OptionMeta { + /** Environment variable name (e.g. 'PARSE_SERVER_APPLICATION_ID'). Null means not settable via env. */ + env?: string | null; + /** Help text for CLI and documentation. */ + help: string; + /** Deprecation info if this option is deprecated. */ + deprecated?: DeprecationInfo; + /** Which startup methods this option applies to. */ + applicableTo?: Array<'cli' | 'api'>; + /** Whether this option accepts a dynamic (function) value with TTL caching. */ + dynamic?: boolean; + /** Override the JSDoc type string for documentation (e.g. 'Adapter'). */ + docType?: string; + /** Whether this option contains sensitive data that should be redacted in logs. */ + sensitive?: boolean; +} + +export interface DeprecationInfo { + /** The option that replaces this one, if any. */ + replacement?: string; + /** Message to display when the deprecated option is used. */ + message: string; +} + +/** + * Symbol used to tag schemas with a unique metadata ID. + * This avoids key collisions when the same schema instance is reused + * across multiple fields (e.g. a shared adapterSchema). + */ +const META_ID = Symbol('optionMetaId'); +let nextMetaId = 0; +const metaRegistry = new Map(); + +/** + * Wraps a Zod schema with option metadata (env var name, help text, etc.). + * Each call creates a lightweight wrapper via `.describe()` so shared schema + * instances get distinct metadata per field. + * + * Usage: + * ```ts + * const schema = z.object({ + * appId: option(z.string(), { + * env: 'PARSE_SERVER_APPLICATION_ID', + * help: 'Your Parse Application ID', + * }), + * }); + * ``` + */ +export function option(schema: T, meta: OptionMeta): T { + const tagged = schema.describe(meta.help) as T; + const id = nextMetaId++; + (tagged as any)[META_ID] = id; + metaRegistry.set(id, meta); + return tagged; +} + +/** + * Retrieves the option metadata for a Zod schema field. + * Returns undefined if no metadata was attached. + */ +export function getOptionMeta(schema: z.ZodTypeAny): OptionMeta | undefined { + const id = (schema as any)[META_ID]; + if (id === undefined) return undefined; + return metaRegistry.get(id); +} + +/** + * Extracts all option metadata from a Zod object schema. + * Returns a map of field name -> OptionMeta. + */ +export function getAllOptionMeta( + schema: z.ZodObject +): Map { + const result = new Map(); + const shape = schema.shape; + for (const [key, fieldSchema] of Object.entries(shape)) { + const meta = getOptionMeta(fieldSchema as z.ZodTypeAny); + if (meta) { + result.set(key, meta); + } + } + return result; +} + +/** + * Builds a reverse map from environment variable names to option paths. + * Supports nested schemas by recursing into fields whose Zod type is a ZodObject. + */ +export function buildEnvMap( + schema: z.ZodObject, + parentPath: string[] = [] +): Map { + const envMap = new Map(); + const shape = schema.shape; + + for (const [key, fieldSchema] of Object.entries(shape)) { + const zodField = fieldSchema as z.ZodTypeAny; + const meta = getOptionMeta(zodField); + const currentPath = [...parentPath, key]; + + if (meta?.env) { + const existing = envMap.get(meta.env); + if (existing) { + throw new Error( + `Duplicate environment variable key "${meta.env}" found: ` + + `"${existing.path.join('.')}" and "${currentPath.join('.')}"` + ); + } + envMap.set(meta.env, { path: currentPath, fieldSchema: zodField }); + } + + // Recurse into nested ZodObject schemas + const innerSchema = unwrapToObject(zodField); + if (innerSchema) { + const nestedMap = buildEnvMap(innerSchema, currentPath); + for (const [envKey, value] of nestedMap) { + const existing = envMap.get(envKey); + if (existing) { + throw new Error( + `Duplicate environment variable key "${envKey}" found: ` + + `"${existing.path.join('.')}" and "${value.path.join('.')}"` + ); + } + envMap.set(envKey, value); + } + } + } + + return envMap; +} + +/** + * Unwraps optional/default/nullable wrappers to find an inner ZodObject, if any. + */ +function unwrapToObject(schema: z.ZodTypeAny): z.ZodObject | null { + if (schema instanceof z.ZodObject) { + return schema; + } + if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) { + return unwrapToObject(schema.unwrap() as z.ZodTypeAny); + } + if (schema instanceof z.ZodDefault) { + return unwrapToObject(schema.removeDefault() as z.ZodTypeAny); + } + return null; +} + +/** + * Coerces a string value (from env var or CLI) to the appropriate type + * based on the Zod schema field type. + */ +export function coerceValue(value: string, fieldSchema: z.ZodTypeAny): unknown { + const innerType = unwrapType(fieldSchema); + + if (innerType instanceof z.ZodNumber) { + const num = Number(value); + if (isNaN(num)) { + throw new Error(`Expected a number, got "${value}"`); + } + return num; + } + + if (innerType instanceof z.ZodBoolean) { + if (value === 'true' || value === '1') return true; + if (value === 'false' || value === '0') return false; + throw new Error(`Expected a boolean ('true', 'false', '1', '0'), got "${value}"`); + } + + if (innerType instanceof z.ZodArray) { + if (typeof value !== 'string') { + return value; + } + // Try JSON array first + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return parsed; + } + } catch { + // Not valid JSON — fall through to CSV + } + // Fall back to comma-separated values + return value.split(','); + } + + if (innerType instanceof z.ZodObject || innerType instanceof z.ZodRecord) { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + throw new Error(`Expected valid JSON for object value, got "${value}"`); + } + } + return value; + } + + if (innerType instanceof z.ZodUnion) { + // For union types, try each branch + const unionDef = innerType as z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>; + const options = (unionDef as any)._zod?.def?.options ?? (unionDef as any)._def.options; + for (const opt of options) { + const inner = unwrapType(opt); + // Skip function types for string coercion + if (inner instanceof z.ZodFunction) continue; + try { + return coerceValue(value, opt); + } catch { + continue; + } + } + // If nothing else matched, return as string + return value; + } + + // Default: return as string + return value; +} + +/** + * Unwraps optional/default/nullable wrappers to find the inner type. + */ +function unwrapType(schema: z.ZodTypeAny): z.ZodTypeAny { + if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) { + return unwrapType(schema.unwrap() as z.ZodTypeAny); + } + if (schema instanceof z.ZodDefault) { + return unwrapType(schema.removeDefault() as z.ZodTypeAny); + } + return schema; +} + +// --- Default Extraction --- + +/** + * Gets the default value from a Zod schema field, if any. + * Uses instanceof checks consistent with unwrapType/unwrapToObject. + * Unwraps optional/nullable wrappers. + */ +export function getSchemaDefault(schema: z.ZodTypeAny): unknown { + if (!schema) return undefined; + if (schema instanceof z.ZodDefault) { + const val = (schema as any).def.defaultValue; + return typeof val === 'function' ? val() : val; + } + if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) { + return getSchemaDefault(schema.unwrap() as z.ZodTypeAny); + } + return undefined; +} + +/** + * Extracts all default values from a Zod object schema. + * Returns a plain object with only the keys that have defaults. + */ +export function extractSchemaDefaults( + schema: z.ZodObject +): Record { + const result: Record = {}; + for (const [key, fieldSchema] of Object.entries(schema.shape)) { + const def = getSchemaDefault(fieldSchema as z.ZodTypeAny); + if (def !== undefined) { + result[key] = def; + } + } + return result; +} + +/** + * Converts a Zod object schema into a Definitions-compatible format. + * Each key maps to `{ default: value }` if the field has a default. + * Used by Config.js and middlewares.js for backwards compatibility. + */ +export function schemaToLegacyDefinitions( + schema: z.ZodObject +): Record { + const result: Record = {}; + for (const [key, fieldSchema] of Object.entries(schema.shape)) { + const entry: { default?: unknown } = {}; + const def = getSchemaDefault(fieldSchema as z.ZodTypeAny); + if (def !== undefined) { + entry.default = def; + } + result[key] = entry; + } + return result; +} + +// --- Phase 4: Advanced Features --- + +/** + * Returns a list of field names marked as dynamic in schema metadata. + * Dynamic fields accept function values that are resolved at runtime with TTL caching. + */ +export function getDynamicKeys(schema: z.ZodObject): string[] { + const result: string[] = []; + const allMeta = getAllOptionMeta(schema); + for (const [key, meta] of allMeta) { + if (meta.dynamic) { + result.push(key); + } + } + return result; +} + +/** + * Returns a list of field names marked as sensitive in schema metadata. + * Sensitive fields should be redacted when logging configuration values. + */ +export function getSensitiveOptionKeys(schema: z.ZodObject): string[] { + const result: string[] = []; + const allMeta = getAllOptionMeta(schema); + for (const [key, meta] of allMeta) { + if (meta.sensitive) { + result.push(key); + } + } + return result; +} + +/** + * Option group definition for organizing options in documentation and CLI help. + */ +export interface OptionGroup { + /** Display name for the group. */ + name: string; + /** Description of the group. */ + description: string; + /** Keys of options belonging to this group. */ + keys: string[]; +} + +/** + * Returns the logical option groups for ParseServerOptions. + * Groups organize the flat option namespace into categories for + * documentation, CLI --help output, and option discovery (#7069). + */ +export function getOptionGroups(): OptionGroup[] { + return [ + { + name: 'Core', + description: 'Essential server configuration', + keys: [ + 'appId', 'masterKey', 'masterKeyTtl', 'maintenanceKey', 'serverURL', + 'publicServerURL', 'port', 'host', 'mountPath', 'databaseURI', + 'cloud', 'verbose', 'silent', 'logLevel', 'logLevels', 'logsFolder', + 'jsonLogs', 'maxLogFiles', + ], + }, + { + name: 'Keys', + description: 'API keys for client and server access', + keys: [ + 'clientKey', 'javascriptKey', 'restAPIKey', 'dotNetKey', 'webhookKey', + 'fileKey', 'encryptionKey', 'readOnlyMasterKey', + ], + }, + { + name: 'Security', + description: 'Security and access control', + keys: [ + 'masterKeyIps', 'maintenanceKeyIps', 'readOnlyMasterKeyIps', + 'enforcePrivateUsers', 'security', 'requestKeywordDenylist', + 'enableInsecureAuthAdapters', 'allowExpiredAuthDataToken', + 'protectedFields', 'userSensitiveFields', 'trustProxy', + ], + }, + { + name: 'Users & Auth', + description: 'User authentication and email verification', + keys: [ + 'auth', 'enableAnonymousUsers', 'verifyUserEmails', 'sendUserEmailVerification', + 'preventLoginWithUnverifiedEmail', 'preventSignupWithUnverifiedEmail', + 'emailVerifyTokenValidityDuration', 'emailVerifyTokenReuseIfValid', + 'emailVerifySuccessOnInvalidEmail', 'accountLockout', 'passwordPolicy', + 'convertEmailToLowercase', 'convertUsernameToLowercase', + ], + }, + { + name: 'Sessions', + description: 'Session management', + keys: [ + 'sessionLength', 'expireInactiveSessions', 'extendSessionOnUse', + 'revokeSessionOnPasswordReset', + ], + }, + { + name: 'Database', + description: 'Database connection and options', + keys: [ + 'databaseAdapter', 'databaseOptions', 'collectionPrefix', + 'enableCollationCaseComparison', 'objectIdSize', 'allowCustomObjectId', + ], + }, + { + name: 'Files', + description: 'File storage and upload', + keys: [ + 'filesAdapter', 'fileUpload', 'maxUploadSize', 'preserveFileName', + ], + }, + { + name: 'API Behavior', + description: 'API features and limits', + keys: [ + 'defaultLimit', 'maxLimit', 'allowClientClassCreation', 'allowHeaders', + 'allowOrigin', 'directAccess', 'idempotencyOptions', 'rateLimit', + 'requestComplexity', 'enableSanitizedErrorResponse', + 'enableExpressErrorHandler', 'middleware', 'requestContextMiddleware', + ], + }, + { + name: 'GraphQL', + description: 'GraphQL configuration', + keys: [ + 'mountGraphQL', 'graphQLPath', 'graphQLSchema', + 'graphQLPublicIntrospection', 'mountPlayground', 'playgroundPath', + ], + }, + { + name: 'LiveQuery', + description: 'Real-time query subscriptions', + keys: [ + 'liveQuery', 'liveQueryServerOptions', 'startLiveQueryServer', + ], + }, + { + name: 'Push & Email', + description: 'Push notifications and email', + keys: [ + 'push', 'scheduledPush', 'emailAdapter', + ], + }, + { + name: 'Pages', + description: 'Custom pages for password reset and email verification', + keys: [ + 'pages', 'customPages', + ], + }, + { + name: 'Adapters', + description: 'Pluggable adapter modules', + keys: [ + 'analyticsAdapter', 'cacheAdapter', 'loggerAdapter', + 'cacheMaxSize', 'cacheTTL', + ], + }, + { + name: 'Schema', + description: 'Schema migration and management', + keys: ['schema'], + }, + { + name: 'Server Lifecycle', + description: 'Server startup and clustering', + keys: [ + 'cluster', 'serverCloseComplete', 'verifyServerUrl', 'appName', + 'enableProductPurchaseLegacyApi', + ], + }, + ]; +} + +/** + * Validates that options marked with `applicableTo` are used in the + * correct startup context. Logs a warning for options used outside + * their applicable context. + * + * @param options - The parsed config object + * @param context - The current startup context ('cli' or 'api') + * @param schema - The Zod schema with option metadata + * @param logger - Logger function for warnings (defaults to console.warn) + */ +export function warnInapplicableOptions( + options: Record, + context: 'cli' | 'api', + schema: z.ZodObject, + logger: (msg: string) => void = console.warn +): void { + const allMeta = getAllOptionMeta(schema); + for (const [key, meta] of allMeta) { + if ( + meta.applicableTo && + !meta.applicableTo.includes(context) && + options[key] !== undefined + ) { + logger( + `Warning: The option '${key}' is only applicable when using Parse Server via ` + + `${meta.applicableTo.join(' or ')}. It has no effect in the current '${context}' context.` + ); + } + } +} diff --git a/src/Options/schemas/AccountLockoutOptions.ts b/src/Options/schemas/AccountLockoutOptions.ts new file mode 100644 index 0000000000..be45716bb8 --- /dev/null +++ b/src/Options/schemas/AccountLockoutOptions.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const AccountLockoutOptionsSchema = z + .object({ + duration: option( + z.number() + .gt(0, 'Account lockout duration should be greater than 0') + .max(99999, 'Account lockout duration should be less than 100000') + .optional(), + { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', + help: 'Number of minutes the account remains locked after reaching the failed login threshold.', + } + ), + threshold: option( + z.number() + .int('Account lockout threshold should be an integer') + .min(1, 'Account lockout threshold should be greater than 0') + .max(999, 'Account lockout threshold should be less than 1000') + .optional(), + { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', + help: 'Number of failed login attempts before the account is temporarily locked.', + } + ), + unlockOnPasswordReset: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET', + help: 'Automatically unlock a locked account when the user successfully resets their password.', + }), + }) + .loose(); + +export type AccountLockoutOptions = z.infer; diff --git a/src/Options/schemas/DatabaseOptions.ts b/src/Options/schemas/DatabaseOptions.ts new file mode 100644 index 0000000000..a3a53d2702 --- /dev/null +++ b/src/Options/schemas/DatabaseOptions.ts @@ -0,0 +1,310 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const DatabaseOptionsClientMetadataSchema = z + .object({ + name: option(z.string(), { + env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_NAME', + help: 'Application name sent to the database server, visible in database connection logs.', + }), + version: option(z.string(), { + env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA_VERSION', + help: 'Application version sent to the database server for connection metadata.', + }), + }) + .loose(); + +export type DatabaseOptionsClientMetadata = z.infer; + +export const LogClientEventSchema = z + .object({ + keys: option(z.array(z.string()).optional(), { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_KEYS', + help: 'Dot-notation paths to extract from the MongoDB driver event for logging.', + }), + logLevel: option(z.string().default('info'), { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_LOG_LEVEL', + help: 'Log level to use when logging this database client event.', + }), + name: option(z.string(), { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_NAME', + help: "The MongoDB driver event name to subscribe to (e.g. 'commandStarted', 'connectionReady').", + }), + }) + .loose(); + +export type LogClientEvent = z.infer; + +export const DatabaseOptionsSchema = z + .object({ + allowPublicExplain: option(z.boolean().default(false), { + env: 'PARSE_SERVER_DATABASE_ALLOW_PUBLIC_EXPLAIN', + help: 'Allow Parse.Query.explain() without the master key, useful for debugging query performance.', + }), + appName: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_APP_NAME', + help: 'Application name sent to MongoDB in the connection handshake, visible in db.currentOp() and logs.', + }), + authMechanism: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM', + help: "MongoDB authentication mechanism (e.g. 'SCRAM-SHA-256', 'MONGODB-X509').", + }), + authMechanismProperties: option(z.record(z.string(), z.any()).optional(), { + env: 'PARSE_SERVER_DATABASE_AUTH_MECHANISM_PROPERTIES', + help: 'Additional properties for the selected MongoDB authentication mechanism.', + }), + authSource: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_AUTH_SOURCE', + help: 'The database used to authenticate the MongoDB connection credentials.', + }), + autoSelectFamily: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY', + help: 'Automatically select between IPv4 and IPv6 when connecting to the database.', + }), + autoSelectFamilyAttemptTimeout: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT', + help: 'Timeout in milliseconds for the IPv4/IPv6 auto-selection attempt.', + }), + batchSize: option(z.number().default(1000), { + env: 'PARSE_SERVER_DATABASE_BATCH_SIZE', + help: 'Number of documents returned per batch in MongoDB cursor getMore operations.', + }), + clientMetadata: option(DatabaseOptionsClientMetadataSchema.optional(), { + env: 'PARSE_SERVER_DATABASE_CLIENT_METADATA', + help: 'Custom application metadata sent to MongoDB in the connection handshake.', + }), + compressors: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_COMPRESSORS', + help: "Comma-separated list of compression algorithms for network traffic (e.g. 'snappy,zstd,zlib').", + }), + connectTimeoutMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_CONNECT_TIMEOUT_MS', + help: 'Timeout in milliseconds for establishing a new TCP connection to MongoDB.', + }), + createIndexAuthDataUniqueness: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_AUTH_DATA_UNIQUENESS', + help: 'Automatically create unique indexes on auth data fields to enforce provider uniqueness.', + }), + createIndexRoleName: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME', + help: "Automatically create a unique index on the Role class 'name' field.", + }), + createIndexUserEmail: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL', + help: "Automatically create an index on the User class 'email' field.", + }), + createIndexUserEmailCaseInsensitive: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_CASE_INSENSITIVE', + help: "Automatically create a case-insensitive index on the User class 'email' field.", + }), + createIndexUserEmailVerifyToken: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_VERIFY_TOKEN', + help: 'Automatically create an index on the User class email verification token field.', + }), + createIndexUserPasswordResetToken: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_PASSWORD_RESET_TOKEN', + help: 'Automatically create an index on the User class password reset token field.', + }), + createIndexUserUsername: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME', + help: "Automatically create a unique index on the User class 'username' field.", + }), + createIndexUserUsernameCaseInsensitive: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME_CASE_INSENSITIVE', + help: "Automatically create a case-insensitive index on the User class 'username' field.", + }), + directConnection: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_DIRECT_CONNECTION', + help: 'Connect directly to a single MongoDB server, bypassing replica set topology discovery.', + }), + disableIndexFieldValidation: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_DISABLE_INDEX_FIELD_VALIDATION', + help: 'Skip validation of index field names against the schema, allowing indexes on any field.', + }), + enableSchemaHooks: option(z.boolean().default(false), { + env: 'PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS', + help: 'Enable real-time schema change notifications across server instances via database polling.', + }), + forceServerObjectId: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_FORCE_SERVER_OBJECT_ID', + help: 'Force MongoDB to generate _id values instead of the driver.', + }), + heartbeatFrequencyMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_HEARTBEAT_FREQUENCY_MS', + help: 'Interval in milliseconds between server monitoring heartbeat checks.', + }), + loadBalanced: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_LOAD_BALANCED', + help: 'Enable load-balanced connection mode for MongoDB Atlas or similar services.', + }), + localThresholdMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_LOCAL_THRESHOLD_MS', + help: 'Latency window in milliseconds for selecting among multiple suitable read replicas.', + }), + logClientEvents: option(z.array(LogClientEventSchema).optional(), { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS', + help: 'Array of MongoDB driver events to log, with configurable log level and field extraction.', + }), + maxConnecting: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', + help: 'Maximum number of connections that can be established concurrently to the database.', + }), + maxIdleTimeMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_MAX_IDLE_TIME_MS', + help: 'Maximum time in milliseconds a connection can remain idle before being closed.', + }), + maxPoolSize: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_MAX_POOL_SIZE', + help: 'Maximum number of connections in the MongoDB connection pool.', + }), + maxStalenessSeconds: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_MAX_STALENESS_SECONDS', + help: 'Maximum acceptable replication lag in seconds when reading from secondaries.', + }), + maxTimeMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_MAX_TIME_MS', + help: 'Maximum cumulative time in milliseconds allowed for cursor operations on the server.', + }), + minPoolSize: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_MIN_POOL_SIZE', + help: 'Minimum number of connections maintained in the MongoDB connection pool.', + }), + proxyHost: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_PROXY_HOST', + help: 'SOCKS5 proxy hostname for routing database connections.', + }), + proxyPassword: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_PROXY_PASSWORD', + help: 'Password for SOCKS5 proxy authentication.', + }), + proxyPort: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_PROXY_PORT', + help: 'SOCKS5 proxy port number.', + }), + proxyUsername: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_PROXY_USERNAME', + help: 'Username for SOCKS5 proxy authentication.', + }), + readConcernLevel: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_READ_CONCERN_LEVEL', + help: "MongoDB read concern level (e.g. 'local', 'majority', 'linearizable').", + }), + readPreference: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE', + help: "MongoDB read preference for distributing reads (e.g. 'primary', 'secondary', 'nearest').", + }), + readPreferenceTags: option(z.array(z.any()).optional(), { + env: 'PARSE_SERVER_DATABASE_READ_PREFERENCE_TAGS', + help: 'Tag sets for filtering which replica set members are eligible for reads.', + }), + replicaSet: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_REPLICA_SET', + help: 'Name of the MongoDB replica set to connect to.', + }), + retryReads: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_RETRY_READS', + help: 'Automatically retry failed read operations once on transient errors.', + }), + retryWrites: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_RETRY_WRITES', + help: 'Automatically retry failed write operations once on transient errors.', + }), + schemaCacheTtl: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_SCHEMA_CACHE_TTL', + help: 'Duration in seconds to cache the database schema before refreshing.', + }), + serverMonitoringMode: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_SERVER_MONITORING_MODE', + help: "MongoDB server monitoring mode ('auto', 'poll', or 'stream').", + }), + serverSelectionTimeoutMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_SERVER_SELECTION_TIMEOUT_MS', + help: 'Timeout in milliseconds for selecting a suitable server from the topology.', + }), + socketTimeoutMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_SOCKET_TIMEOUT_MS', + help: 'Timeout in milliseconds for send/receive operations on an established socket.', + }), + srvMaxHosts: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_SRV_MAX_HOSTS', + help: 'Maximum number of hosts to connect to when using a mongodb+srv:// connection string.', + }), + srvServiceName: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_SRV_SERVICE_NAME', + help: 'Custom SRV service name when using a mongodb+srv:// connection string.', + }), + ssl: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_SSL', + help: 'Enable SSL/TLS encryption for the database connection.', + }), + tls: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS', + help: 'Enable TLS encryption for the database connection (alias for ssl).', + }), + tlsAllowInvalidCertificates: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_CERTIFICATES', + help: 'Accept server certificates that fail validation, such as self-signed certificates.', + }), + tlsAllowInvalidHostnames: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS_ALLOW_INVALID_HOSTNAMES', + help: 'Accept server certificates where the hostname does not match the certificate.', + }), + tlsCAFile: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS_CAFILE', + help: "Path to the Certificate Authority file for verifying the server's TLS certificate.", + }), + tlsCertificateKeyFile: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE', + help: 'Path to the client certificate and private key file for mutual TLS authentication.', + }), + tlsCertificateKeyFilePassword: option(z.string().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS_CERTIFICATE_KEY_FILE_PASSWORD', + help: 'Password to decrypt the client certificate private key file.', + }), + tlsInsecure: option(z.boolean().optional(), { + env: 'PARSE_SERVER_DATABASE_TLS_INSECURE', + help: 'Disable all TLS certificate validation. Not recommended for production.', + }), + waitQueueTimeoutMS: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_WAIT_QUEUE_TIMEOUT_MS', + help: 'Timeout in milliseconds for a request waiting for an available connection from the pool.', + }), + zlibCompressionLevel: option(z.number().optional(), { + env: 'PARSE_SERVER_DATABASE_ZLIB_COMPRESSION_LEVEL', + help: 'Zlib compression level from 0 (no compression) to 9 (maximum compression).', + }), + }) + .loose(); + +export type DatabaseOptions = z.infer; + +export const LogLevelSchema = z + .object({ + debug: option(z.string(), { + env: 'PARSE_SERVER_LOG_LEVEL_DEBUG', + help: 'The log output target for debug level messages.', + }), + error: option(z.string(), { + env: 'PARSE_SERVER_LOG_LEVEL_ERROR', + help: 'The log output target for error level messages.', + }), + info: option(z.string(), { + env: 'PARSE_SERVER_LOG_LEVEL_INFO', + help: 'The log output target for info level messages.', + }), + silly: option(z.string(), { + env: 'PARSE_SERVER_LOG_LEVEL_SILLY', + help: 'The log output target for silly (most verbose) level messages.', + }), + verbose: option(z.string(), { + env: 'PARSE_SERVER_LOG_LEVEL_VERBOSE', + help: 'The log output target for verbose level messages.', + }), + warn: option(z.string(), { + env: 'PARSE_SERVER_LOG_LEVEL_WARN', + help: 'The log output target for warning level messages.', + }), + }) + .loose(); + +export type LogLevel = z.infer; diff --git a/src/Options/schemas/FileUploadOptions.ts b/src/Options/schemas/FileUploadOptions.ts new file mode 100644 index 0000000000..831d921bb6 --- /dev/null +++ b/src/Options/schemas/FileUploadOptions.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const FileUploadOptionsSchema = z + .object({ + allowedFileUrlDomains: option(z.array(z.string()).default(['*']), { + env: 'PARSE_SERVER_FILE_UPLOAD_ALLOWED_FILE_URL_DOMAINS', + help: "Domains from which file URLs are accepted for upload. Use ['*'] to allow all domains.", + }), + enableForAnonymousUser: option(z.boolean().default(false), { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER', + help: 'Allow anonymous users to upload files.', + }), + enableForAuthenticatedUser: option(z.boolean().default(true), { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER', + help: 'Allow authenticated users to upload files.', + }), + enableForPublic: option(z.boolean().default(false), { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC', + help: 'Allow unauthenticated public requests to upload files.', + }), + fileExtensions: option( + z.array(z.string()).default([ + '^(?!([xXsS]?[hH][tT][mM][lL]?(\\\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\\\+[xX][mM][lL])?)$)', + ]), + { + env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS', + help: 'Regex patterns for allowed file extensions. Files with extensions matching any pattern are accepted.', + } + ), + }) + .loose(); + +export type FileUploadOptions = z.infer; diff --git a/src/Options/schemas/IdempotencyOptions.ts b/src/Options/schemas/IdempotencyOptions.ts new file mode 100644 index 0000000000..6e12d83ee2 --- /dev/null +++ b/src/Options/schemas/IdempotencyOptions.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const IdempotencyOptionsSchema = z + .object({ + paths: option(z.array(z.string()).default([]), { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', + help: "API request paths for which idempotency is enforced (e.g. 'functions/.*', 'classes/.*').", + }), + ttl: option(z.number().min(1, 'idempotency TTL value must be greater than 0 seconds').default(300), { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', + help: 'Duration in seconds that an idempotency key is cached to detect duplicate requests.', + }), + }) + .loose(); + +export type IdempotencyOptions = z.infer; diff --git a/src/Options/schemas/LiveQueryOptions.ts b/src/Options/schemas/LiveQueryOptions.ts new file mode 100644 index 0000000000..e5a02b005e --- /dev/null +++ b/src/Options/schemas/LiveQueryOptions.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const LiveQueryOptionsSchema = z + .object({ + classNames: option(z.array(z.string()).optional(), { + env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', + help: 'Parse class names for which the LiveQuery server will publish events. Only listed classes will emit LiveQuery events server-side.', + }), + pubSubAdapter: option(z.any().optional(), { + env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', + help: 'Adapter module for pub/sub messaging between the API server and LiveQuery server.', + docType: 'Adapter', + }), + redisOptions: option(z.record(z.string(), z.any()).optional(), { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS', + help: 'Redis client configuration options for the LiveQuery pub/sub connection.', + }), + redisURL: option(z.string().optional(), { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', + help: 'Redis connection URL used for LiveQuery pub/sub messaging.', + }), + regexTimeout: option(z.number().default(100), { + env: 'PARSE_SERVER_LIVEQUERY_REGEX_TIMEOUT', + help: 'Maximum time in milliseconds allowed for regex evaluation in LiveQuery subscription matching.', + }), + wssAdapter: option(z.any().optional(), { + env: 'PARSE_SERVER_LIVEQUERY_WSS_ADAPTER', + help: 'Custom WebSocket server adapter for the LiveQuery server.', + docType: 'Adapter', + }), + }) + .loose(); + +export type LiveQueryOptions = z.infer; + +export const LiveQueryServerOptionsSchema = z + .object({ + appId: option(z.string().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', + help: 'The Parse application ID the LiveQuery server authenticates against.', + }), + cacheTimeout: option(z.number().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', + help: 'Duration in milliseconds before cached subscription data expires.', + }), + keyPairs: option(z.record(z.string(), z.any()).optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', + help: 'Key-value pairs for authenticating client connections to the LiveQuery server.', + }), + logLevel: option(z.string().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', + help: 'Log verbosity level for the LiveQuery server.', + }), + masterKey: option(z.string().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', + help: 'The master key for the Parse app this LiveQuery server connects to.', + }), + port: option(z.number().default(1337), { + env: 'PARSE_LIVE_QUERY_SERVER_PORT', + help: 'The port number the LiveQuery WebSocket server listens on.', + }), + pubSubAdapter: option(z.any().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', + help: 'Adapter module for pub/sub messaging in the standalone LiveQuery server.', + docType: 'Adapter', + }), + redisOptions: option(z.record(z.string(), z.any()).optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS', + help: 'Redis client configuration options for the standalone LiveQuery server.', + }), + redisURL: option(z.string().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', + help: 'Redis connection URL for the standalone LiveQuery server.', + }), + serverURL: option(z.string().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', + help: 'The Parse Server URL that the standalone LiveQuery server connects to for query matching.', + }), + websocketTimeout: option(z.number().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', + help: 'Duration in milliseconds before an idle WebSocket connection is closed.', + }), + wssAdapter: option(z.any().optional(), { + env: 'PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER', + help: 'Custom WebSocket server adapter for the standalone LiveQuery server.', + docType: 'Adapter', + }), + }) + .loose(); + +export type LiveQueryServerOptions = z.infer; diff --git a/src/Options/schemas/LogLevels.ts b/src/Options/schemas/LogLevels.ts new file mode 100644 index 0000000000..82dc41fba2 --- /dev/null +++ b/src/Options/schemas/LogLevels.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const LogLevelsSchema = z + .object({ + cloudFunctionError: option(z.string().default('error'), { + env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR', + help: 'Log level used when a cloud function throws an error.', + }), + cloudFunctionSuccess: option(z.string().default('info'), { + env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS', + help: 'Log level used when a cloud function completes successfully.', + }), + signupUsernameTaken: option(z.string().default('info'), { + env: 'PARSE_SERVER_LOG_LEVELS_SIGNUP_USERNAME_TAKEN', + help: 'Log level used when a signup attempt fails because the username is already taken.', + }), + triggerAfter: option(z.string().default('info'), { + env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', + help: "Log level used for afterSave, afterDelete, and other 'after' trigger executions.", + }), + triggerBeforeError: option(z.string().default('error'), { + env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', + help: 'Log level used when a beforeSave or beforeDelete trigger throws an error.', + }), + triggerBeforeSuccess: option(z.string().default('info'), { + env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', + help: 'Log level used when a beforeSave or beforeDelete trigger completes successfully.', + }), + }) + .loose(); + +export type LogLevels = z.infer; diff --git a/src/Options/schemas/PagesOptions.ts b/src/Options/schemas/PagesOptions.ts new file mode 100644 index 0000000000..fd32998d41 --- /dev/null +++ b/src/Options/schemas/PagesOptions.ts @@ -0,0 +1,153 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const PagesRouteSchema = z + .object({ + handler: option(z.function(), { + help: 'The Express route handler function for this custom page route.', + }), + method: option(z.string(), { + env: 'PARSE_SERVER_PAGES_ROUTE_METHOD', + help: "The HTTP method for this custom page route (e.g. 'GET', 'POST').", + }), + path: option(z.string(), { + env: 'PARSE_SERVER_PAGES_ROUTE_PATH', + help: 'The URL path for this custom page route.', + }), + }) + .loose(); + +export type PagesRoute = z.infer; + +export const PagesCustomUrlsOptionsSchema = z + .object({ + emailVerificationLinkExpired: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED', + help: 'Redirect URL shown when a user clicks an expired email verification link.', + }), + emailVerificationLinkInvalid: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID', + help: 'Redirect URL shown when a user clicks an invalid email verification link.', + }), + emailVerificationSendFail: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL', + help: 'Redirect URL shown when sending the email verification email fails.', + }), + emailVerificationSendSuccess: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS', + help: 'Redirect URL shown when a re-send verification email request succeeds.', + }), + emailVerificationSuccess: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS', + help: 'Redirect URL shown after successful email verification.', + }), + passwordReset: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET', + help: 'URL of the custom password reset form page.', + }), + passwordResetLinkInvalid: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID', + help: 'Redirect URL shown when a user clicks an invalid password reset link.', + }), + passwordResetSuccess: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS', + help: 'Redirect URL shown after a successful password reset.', + }), + }) + .loose(); + +export type PagesCustomUrlsOptions = z.infer; + +export const PagesOptionsSchema = z + .object({ + customRoutes: option(z.array(PagesRouteSchema).default([]), { + env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES', + help: 'Array of custom Express routes to add to the pages router.', + }), + customUrls: option(PagesCustomUrlsOptionsSchema.default({}), { + env: 'PARSE_SERVER_PAGES_CUSTOM_URLS', + help: 'Custom redirect URLs for email verification and password reset page flows.', + }), + enableLocalization: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PAGES_ENABLE_LOCALIZATION', + help: "Enable localized page templates based on the user's locale.", + }), + encodePageParamHeaders: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PAGES_ENCODE_PAGE_PARAM_HEADERS', + help: 'Encode page parameters in HTTP headers for custom page routing.', + }), + forceRedirect: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PAGES_FORCE_REDIRECT', + help: 'Always redirect to custom URLs instead of rendering built-in pages.', + }), + localizationFallbackLocale: option(z.string().default('en'), { + env: 'PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE', + help: "Default locale used when the user's locale is not available.", + }), + localizationJsonPath: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH', + help: 'Path to the directory containing localization JSON files.', + }), + pagesEndpoint: option(z.string().default('apps'), { + env: 'PARSE_SERVER_PAGES_PAGES_ENDPOINT', + help: 'The URL path prefix for all server-rendered pages.', + }), + pagesPath: option(z.string().optional(), { + env: 'PARSE_SERVER_PAGES_PAGES_PATH', + help: 'Path to the directory containing custom page templates.', + }), + placeholders: option(z.record(z.string(), z.any()).default({}), { + env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', + help: 'Key-value pairs for template variable substitution in page templates.', + }), + }) + .loose(); + +export type PagesOptions = z.infer; + +export const CustomPagesOptionsSchema = z + .object({ + choosePassword: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', + help: 'URL of the custom page where users choose a new password.', + }), + expiredVerificationLink: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_EXPIRED_VERIFICATION_LINK', + help: 'URL of the custom page shown when an email verification link has expired.', + }), + invalidLink: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', + help: 'URL of the custom page shown for any invalid link.', + }), + invalidPasswordResetLink: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_PASSWORD_RESET_LINK', + help: 'URL of the custom page shown when a password reset link is invalid.', + }), + invalidVerificationLink: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK', + help: 'URL of the custom page shown when an email verification link is invalid.', + }), + linkSendFail: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL', + help: 'URL of the custom page shown when sending a verification or reset email fails.', + }), + linkSendSuccess: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS', + help: 'URL of the custom page shown when sending a verification or reset email succeeds.', + }), + parseFrameURL: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL', + help: 'URL loaded in an iframe on built-in pages, used for Parse Dashboard integration.', + }), + passwordResetSuccess: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', + help: 'URL of the custom page shown after a successful password reset.', + }), + verifyEmailSuccess: option(z.string().optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', + help: 'URL of the custom page shown after successful email verification.', + }), + }) + .loose(); + +export type CustomPagesOptions = z.infer; diff --git a/src/Options/schemas/ParseServerOptions.ts b/src/Options/schemas/ParseServerOptions.ts new file mode 100644 index 0000000000..b3606738be --- /dev/null +++ b/src/Options/schemas/ParseServerOptions.ts @@ -0,0 +1,502 @@ +import { z } from 'zod'; +import { isIP } from 'net'; +import { option } from '../schemaUtils'; +import { SchemaOptionsSchema } from './SchemaOptions'; +import { AccountLockoutOptionsSchema } from './AccountLockoutOptions'; +import { PasswordPolicyOptionsSchema } from './PasswordPolicyOptions'; +import { FileUploadOptionsSchema } from './FileUploadOptions'; +import { IdempotencyOptionsSchema } from './IdempotencyOptions'; +import { SecurityOptionsSchema } from './SecurityOptions'; +import { RequestComplexityOptionsSchema } from './RequestComplexityOptions'; +import { PagesOptionsSchema, CustomPagesOptionsSchema } from './PagesOptions'; +import { LiveQueryOptionsSchema, LiveQueryServerOptionsSchema } from './LiveQueryOptions'; +import { RateLimitOptionsSchema } from './RateLimitOptions'; +import { LogLevelsSchema } from './LogLevels'; +import { DatabaseOptionsSchema } from './DatabaseOptions'; + +/** Schema for adapter fields that accept a module path string, config object, or class instance. */ +const adapterSchema = z.union([ + z.string(), + z.record(z.string(), z.any()), + // Deliberately permissive: adapters may be runtime class instances or other + // non-serializable values that stricter Zod validators would reject. + z.custom(() => true), +]).optional(); + +/** Validates an array of IP addresses, supporting CIDR notation. */ +const ipArraySchema = (fieldName: string) => + z.array(z.string()).refine( + ips => { + return ips.every(ip => { + const bare = ip.includes('/') ? ip.split('/')[0] : ip; + return isIP(bare); + }); + }, + { + message: `The option "${fieldName}" contains an invalid IP address. All entries must be valid IPv4 or IPv6 addresses, optionally with CIDR notation.`, + } + ); + +export const ParseServerOptionsSchema = z.object({ + accountLockout: option(AccountLockoutOptionsSchema.optional(), { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', + help: 'Configures account lockout policy to temporarily disable login after repeated failed attempts.', + }), + allowClientClassCreation: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', + help: 'Allow clients to create new classes on the server. Disable in production to prevent schema pollution.', + }), + allowCustomObjectId: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID', + help: 'Allow clients to provide a custom objectId on create, instead of auto-generating one.', + }), + allowExpiredAuthDataToken: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN', + help: 'Allow a user to log in even if the 3rd party auth token has expired.', + }), + allowHeaders: option(z.array(z.string().min(1, 'Allow headers must not contain empty strings')).optional(), { + env: 'PARSE_SERVER_ALLOW_HEADERS', + help: 'Additional headers to allow in CORS requests, beyond the default set.', + }), + allowOrigin: option(z.array(z.string()).optional(), { + env: 'PARSE_SERVER_ALLOW_ORIGIN', + help: 'Sets the allowed origins for CORS requests to the server.', + docType: 'String|String[]', + }), + analyticsAdapter: option(adapterSchema, { + env: 'PARSE_SERVER_ANALYTICS_ADAPTER', + help: 'Adapter module for handling analytics events from clients.', + docType: 'Adapter', + }), + appId: option(z.string(), { + env: 'PARSE_SERVER_APPLICATION_ID', + help: 'A unique identifier for your Parse application.', + }), + appName: option(z.string().optional(), { + env: 'PARSE_SERVER_APP_NAME', + help: 'The display name of your app, used in email templates and verification links.', + }), + auth: option(z.record(z.string(), z.any()).optional(), { + env: 'PARSE_SERVER_AUTH_PROVIDERS', + help: 'Configuration for 3rd party authentication providers such as Google, Facebook, or Apple.', + }), + cacheAdapter: option(adapterSchema, { + env: 'PARSE_SERVER_CACHE_ADAPTER', + help: 'Adapter module for caching query results and schema data.', + docType: 'Adapter', + }), + cacheMaxSize: option(z.number().default(10000), { + env: 'PARSE_SERVER_CACHE_MAX_SIZE', + help: 'Maximum number of entries to store in the in-memory cache.', + }), + cacheTTL: option(z.number().default(5000), { + env: 'PARSE_SERVER_CACHE_TTL', + help: 'Duration in milliseconds that cached values remain valid before being refreshed.', + }), + clientKey: option(z.string().optional(), { + env: 'PARSE_SERVER_CLIENT_KEY', + help: 'Key used to authenticate requests from iOS, macOS, and tvOS clients.', + sensitive: true, + }), + cloud: option(z.string().optional(), { + env: 'PARSE_SERVER_CLOUD', + help: 'Path to the cloud code file (e.g. ./cloud/main.js) containing triggers and functions.', + }), + cluster: option(z.union([z.number(), z.boolean()]).optional(), { + env: 'PARSE_SERVER_CLUSTER', + help: 'Run Parse Server in cluster mode across multiple processes. Set to true for auto-detect or a number for specific worker count.', + applicableTo: ['cli'], + }), + collectionPrefix: option(z.string().default(''), { + env: 'PARSE_SERVER_COLLECTION_PREFIX', + help: 'A prefix added to all database collection names, useful for shared databases.', + }), + convertEmailToLowercase: option(z.boolean().default(false), { + env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE', + help: 'Automatically convert user email addresses to lowercase on signup and login.', + }), + convertUsernameToLowercase: option(z.boolean().default(false), { + env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE', + help: 'Automatically convert usernames to lowercase on signup and login.', + }), + customPages: option(CustomPagesOptionsSchema.optional(), { + env: 'PARSE_SERVER_CUSTOM_PAGES', + help: 'Custom page URLs for password reset and email verification user-facing pages.', + }), + databaseAdapter: option(adapterSchema, { + env: 'PARSE_SERVER_DATABASE_ADAPTER', + help: 'Adapter module for the database connection, overrides databaseURI.', + docType: 'Adapter', + }), + databaseOptions: option(DatabaseOptionsSchema.optional(), { + env: 'PARSE_SERVER_DATABASE_OPTIONS', + help: 'Options passed to the MongoDB driver, such as connection pool size and TLS settings.', + }), + databaseURI: option(z.string().default('mongodb://localhost:27017/parse'), { + env: 'PARSE_SERVER_DATABASE_URI', + help: 'The full MongoDB connection URI, including host, port, and database name.', + }), + defaultLimit: option(z.number().min(1, 'Default limit must be a value greater than 0.').default(100), { + env: 'PARSE_SERVER_DEFAULT_LIMIT', + help: 'Default number of results returned per query when no limit is specified by the client.', + }), + directAccess: option(z.boolean().default(true), { + env: 'PARSE_SERVER_DIRECT_ACCESS', + help: 'Route internal Parse requests directly to the server handler, bypassing the HTTP layer for better performance.', + }), + dotNetKey: option(z.string().optional(), { + env: 'PARSE_SERVER_DOT_NET_KEY', + help: 'Key used to authenticate requests from Unity and .NET SDK clients.', + sensitive: true, + }), + emailAdapter: option(adapterSchema, { + env: 'PARSE_SERVER_EMAIL_ADAPTER', + help: 'Adapter module for sending emails, required for password reset and email verification.', + docType: 'Adapter', + }), + emailVerifySuccessOnInvalidEmail: option(z.boolean().default(true), { + env: 'PARSE_SERVER_EMAIL_VERIFY_SUCCESS_ON_INVALID_EMAIL', + help: 'Return a success response for email verification requests even if the email address is not found.', + }), + emailVerifyTokenReuseIfValid: option(z.boolean().default(false), { + env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', + help: 'Reuse an existing email verification token if it is still valid, instead of generating a new one.', + }), + emailVerifyTokenValidityDuration: option(z.number().optional(), { + env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', + help: 'Duration in seconds that an email verification token remains valid.', + }), + enableAnonymousUsers: option(z.boolean().default(true), { + env: 'PARSE_SERVER_ENABLE_ANON_USERS', + help: 'Allow clients to create and use anonymous user accounts.', + }), + enableCollationCaseComparison: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON', + help: 'Use database collation for case-insensitive string comparisons in queries.', + }), + enableExpressErrorHandler: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', + help: 'Enable the default Express error handler for unhandled errors in middleware.', + }), + enableInsecureAuthAdapters: option(z.boolean().default(false), { + env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS', + help: 'Allow legacy auth adapters that transmit sensitive data insecurely.', + }), + enableProductPurchaseLegacyApi: option(z.boolean().default(true), { + env: 'PARSE_SERVER_ENABLE_PRODUCT_PURCHASE_LEGACY_API', + help: 'Enable the deprecated in-app purchase validation API endpoints.', + }), + enableSanitizedErrorResponse: option(z.boolean().default(true), { + env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE', + help: 'Strip internal error details from API responses to avoid leaking server information.', + }), + encryptionKey: option(z.string().optional(), { + env: 'PARSE_SERVER_ENCRYPTION_KEY', + help: 'A key used to encrypt files stored via the files adapter.', + }), + enforcePrivateUsers: option(z.boolean().default(true), { + env: 'PARSE_SERVER_ENFORCE_PRIVATE_USERS', + help: 'Set new user ACLs to private by default, preventing public read access to user records.', + }), + expireInactiveSessions: option(z.boolean().default(true), { + env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', + help: 'Automatically expire session tokens that have not been used within the session length.', + }), + extendSessionOnUse: option(z.boolean().default(false), { + env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE', + help: 'Extend the session expiration each time the session token is used in a request.', + }), + fileKey: option(z.string().optional(), { + env: 'PARSE_SERVER_FILE_KEY', + help: 'Key used by the files adapter for file access control.', + sensitive: true, + }), + filesAdapter: option(adapterSchema, { + env: 'PARSE_SERVER_FILES_ADAPTER', + help: 'Adapter module for file storage, such as S3 or GridFS.', + docType: 'Adapter', + }), + fileUpload: option(FileUploadOptionsSchema.optional(), { + env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', + help: 'Options for controlling file upload permissions and allowed file types.', + }), + graphQLPath: option(z.string().default('/graphql'), { + env: 'PARSE_SERVER_GRAPHQL_PATH', + help: 'The URL path where the GraphQL API endpoint is mounted.', + }), + graphQLPublicIntrospection: option(z.boolean().default(false), { + env: 'PARSE_SERVER_GRAPHQL_PUBLIC_INTROSPECTION', + help: 'Allow GraphQL introspection queries without requiring authentication.', + }), + graphQLSchema: option(z.string().optional(), { + env: 'PARSE_SERVER_GRAPH_QLSCHEMA', + help: 'Path to a custom GraphQL schema definition file to extend the auto-generated schema.', + }), + host: option(z.string().default('0.0.0.0'), { + env: 'PARSE_SERVER_HOST', + help: 'The hostname or IP address the server binds to.', + }), + idempotencyOptions: option(IdempotencyOptionsSchema.optional(), { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS', + help: 'Options for request idempotency to prevent duplicate operations from retried requests.', + }), + javascriptKey: option(z.string().optional(), { + env: 'PARSE_SERVER_JAVASCRIPT_KEY', + help: 'Key used to authenticate requests from the JavaScript SDK.', + sensitive: true, + }), + jsonLogs: option(z.boolean().optional(), { + env: 'JSON_LOGS', + help: 'Output logs as structured JSON objects instead of plain text, useful for log aggregation.', + }), + liveQuery: option(LiveQueryOptionsSchema.optional(), { + env: 'PARSE_SERVER_LIVE_QUERY', + help: 'Configuration for LiveQuery, including class subscriptions and pub/sub adapter.', + }), + liveQueryServerOptions: option(LiveQueryServerOptionsSchema.optional(), { + env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', + help: 'Options for the standalone LiveQuery server, such as port and WebSocket settings.', + }), + loggerAdapter: option(adapterSchema, { + env: 'PARSE_SERVER_LOGGER_ADAPTER', + help: 'Adapter module for custom log transport, replacing the default Winston file logger.', + docType: 'Adapter', + }), + logLevel: option(z.string().optional(), { + env: 'PARSE_SERVER_LOG_LEVEL', + help: 'Sets the minimum log level for output (e.g. error, warn, info, verbose, debug, silly).', + }), + logLevels: option(LogLevelsSchema.optional(), { + env: 'PARSE_SERVER_LOG_LEVELS', + help: 'Override log levels for specific internal events like cloud function results and triggers.', + }), + logsFolder: option(z.string().default('./logs'), { + env: 'PARSE_SERVER_LOGS_FOLDER', + help: 'Directory path where log files are stored.', + }), + maintenanceKey: option(z.string().optional(), { + env: 'PARSE_SERVER_MAINTENANCE_KEY', + help: 'A key for maintenance operations like clearing caches. Must differ from masterKey.', + }), + maintenanceKeyIps: option(ipArraySchema('maintenanceKeyIps').default(['127.0.0.1', '::1']), { + env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', + help: 'IP addresses allowed to use the maintenance key. Defaults to localhost only.', + }), + masterKey: option(z.union([z.string(), z.custom(v => typeof v === 'function')]), { + env: 'PARSE_SERVER_MASTER_KEY', + help: 'The master key grants unrestricted access to all data and operations. Keep it secret. Can be a function for rotation.', + dynamic: true, + sensitive: true, + }), + masterKeyIps: option(ipArraySchema('masterKeyIps').default(['127.0.0.1', '::1']), { + env: 'PARSE_SERVER_MASTER_KEY_IPS', + help: 'IP addresses allowed to use the master key. Defaults to localhost only.', + }), + masterKeyTtl: option(z.number().optional(), { + env: 'PARSE_SERVER_MASTER_KEY_TTL', + help: 'Cache duration in seconds when masterKey is provided as a function.', + }), + maxLimit: option(z.number().min(1, 'Max limit must be a value greater than 0.').optional(), { + env: 'PARSE_SERVER_MAX_LIMIT', + help: 'Maximum value a client can set for query limit. Prevents excessively large result sets.', + }), + maxLogFiles: option(z.union([z.number(), z.string()]).optional(), { + env: 'PARSE_SERVER_MAX_LOG_FILES', + help: 'Maximum number of log files to retain before rotating old files.', + }), + maxUploadSize: option(z.string().default('20mb'), { + env: 'PARSE_SERVER_MAX_UPLOAD_SIZE', + help: "Maximum file upload size (e.g. '20mb', '1gb').", + }), + middleware: option(z.any().optional(), { + env: 'PARSE_SERVER_MIDDLEWARE', + help: 'Custom Express middleware function applied to all Parse Server routes.', + }), + mountGraphQL: option(z.boolean().default(false), { + env: 'PARSE_SERVER_MOUNT_GRAPHQL', + help: 'Enable the GraphQL API endpoint alongside the REST API.', + }), + mountPath: option(z.string().default('/parse'), { + env: 'PARSE_SERVER_MOUNT_PATH', + help: "The URL path where the Parse REST API is mounted (e.g. '/parse').", + }), + mountPlayground: option(z.boolean().default(false), { + env: 'PARSE_SERVER_MOUNT_PLAYGROUND', + help: 'Deprecated. Enable the GraphQL Playground IDE at the playground path.', + }), + objectIdSize: option(z.number().default(10), { + env: 'PARSE_SERVER_OBJECT_ID_SIZE', + help: 'Number of characters in auto-generated object IDs. Default is 10.', + }), + pages: option(PagesOptionsSchema.optional(), { + env: 'PARSE_SERVER_PAGES', + help: 'Configuration for server-rendered pages like password reset and email verification forms.', + }), + passwordPolicy: option(PasswordPolicyOptionsSchema.optional(), { + env: 'PARSE_SERVER_PASSWORD_POLICY', + help: 'Password policy rules such as minimum strength, history, and expiration.', + }), + playgroundPath: option(z.string().default('/playground'), { + env: 'PARSE_SERVER_PLAYGROUND_PATH', + help: 'Deprecated. URL path for the GraphQL Playground IDE.', + }), + port: option(z.number().default(1337), { + env: 'PORT', + help: 'The port number the server listens on.', + }), + preserveFileName: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PRESERVE_FILE_NAME', + help: 'Keep original file names when uploading, instead of generating random names.', + }), + preventLoginWithUnverifiedEmail: option(z.union([z.boolean(), z.custom(v => typeof v === 'function')]).default(false), { + env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', + help: 'Reject login attempts from users whose email has not been verified. Can be a function for custom logic.', + }), + preventSignupWithUnverifiedEmail: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PREVENT_SIGNUP_WITH_UNVERIFIED_EMAIL', + help: 'Reject signup attempts if the email cannot be verified.', + }), + protectedFields: option(z.record(z.string(), z.any()).default({ _User: { '*': ['email'] } }), { + env: 'PARSE_SERVER_PROTECTED_FIELDS', + help: 'Fields hidden from API responses for non-authorized users, keyed by class name and access level.', + }), + publicServerURL: option(z.union([ + z.string().refine(v => v.startsWith('http://') || v.startsWith('https://'), { + message: 'publicServerURL must start with http:// or https://', + }), + z.custom(v => typeof v === 'function'), + ]).optional(), { + env: 'PARSE_PUBLIC_SERVER_URL', + help: 'The public-facing URL of the server, used in email links and verification URLs. Must start with http:// or https://.', + dynamic: true, + }), + push: option(z.record(z.string(), z.any()).optional(), { + env: 'PARSE_SERVER_PUSH', + help: 'Configuration for push notification providers such as APNs and FCM.', + }), + rateLimit: option(z.array(RateLimitOptionsSchema).default([]), { + env: 'PARSE_SERVER_RATE_LIMIT', + help: 'Rate limiting rules to throttle requests by IP, user, or session.', + }), + readOnlyMasterKey: option(z.string().optional(), { + env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', + help: 'A master key that only allows read operations, useful for dashboards and monitoring.', + }), + readOnlyMasterKeyIps: option(ipArraySchema('readOnlyMasterKeyIps').default(['0.0.0.0/0', '::0']), { + env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY_IPS', + help: 'IP addresses allowed to use the read-only master key.', + }), + requestComplexity: option(RequestComplexityOptionsSchema.optional(), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY', + help: 'Limits on query complexity to prevent expensive operations, such as include depth and subquery nesting.', + }), + requestContextMiddleware: option(z.any().optional(), { + env: 'PARSE_SERVER_REQUEST_CONTEXT_MIDDLEWARE', + help: 'Middleware to inject custom context into every Parse request before processing.', + }), + requestKeywordDenylist: option( + z.array(z.any()).default([ + { key: '_bsontype', value: 'Code' }, + { key: 'constructor' }, + { key: '__proto__' }, + ]), + { + env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST', + help: 'Blocked keywords in request payloads to prevent injection attacks like prototype pollution.', + } + ), + restAPIKey: option(z.string().optional(), { + env: 'PARSE_SERVER_REST_API_KEY', + help: 'Key used to authenticate REST API requests.', + sensitive: true, + }), + revokeSessionOnPasswordReset: option(z.boolean().default(true), { + env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', + help: 'Invalidate all active sessions when a user resets their password.', + }), + scheduledPush: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEDULED_PUSH', + help: 'Allow push notifications to be scheduled for future delivery.', + }), + schema: option(SchemaOptionsSchema.optional(), { + env: 'PARSE_SERVER_SCHEMA', + help: 'Schema migration options, including class definitions and migration callbacks.', + }), + security: option(SecurityOptionsSchema.optional(), { + env: 'PARSE_SERVER_SECURITY', + help: 'Security check options to audit the server configuration for common vulnerabilities.', + }), + sendUserEmailVerification: option(z.union([z.boolean(), z.custom(v => typeof v === 'function')]).default(true), { + env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', + help: 'Send a verification email when a user signs up or changes their email. Can be a function for custom logic.', + }), + serverCloseComplete: option(z.any().optional(), { + env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE', + help: 'Callback function called after the server has fully shut down.', + }), + serverURL: option(z.string(), { + env: 'PARSE_SERVER_URL', + help: 'The URL where Parse Server is accessible, used for internal requests.', + }), + sessionLength: option(z.number().min(1, 'Session length must be a value greater than 0.').default(31536000), { + env: 'PARSE_SERVER_SESSION_LENGTH', + help: 'Duration in seconds before a session token expires. Default is 1 year.', + }), + silent: option(z.boolean().optional(), { + env: 'SILENT', + help: 'Suppress all console output from the server.', + }), + startLiveQueryServer: option(z.boolean().optional(), { + env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', + help: 'Automatically start a LiveQuery WebSocket server alongside the HTTP server.', + applicableTo: ['cli'], + }), + trustProxy: option(z.any().default([]), { + env: 'PARSE_SERVER_TRUST_PROXY', + help: 'Express trust proxy setting for running behind a reverse proxy or load balancer.', + }), + userSensitiveFields: option(z.array(z.string()).optional(), { + env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', + help: 'Deprecated. Use protectedFields instead. List of user fields excluded from API responses.', + }), + verbose: option(z.boolean().optional(), { + env: 'VERBOSE', + help: 'Enable verbose logging, printing all configuration options on startup.', + }), + verifyServerUrl: option(z.boolean().default(true), { + env: 'PARSE_SERVER_VERIFY_SERVER_URL', + help: 'Verify that the serverURL is reachable when the server launches.', + }), + verifyUserEmails: option(z.union([z.boolean(), z.custom(v => typeof v === 'function')]).default(false), { + env: 'PARSE_SERVER_VERIFY_USER_EMAILS', + help: 'Require users to verify their email address before they can log in. Can be a function for custom logic.', + }), + webhookKey: option(z.string().optional(), { + env: 'PARSE_SERVER_WEBHOOK_KEY', + help: 'Key sent with outgoing webhook requests for authentication.', + }), +}).superRefine((data, ctx) => { + if (data.masterKey && data.readOnlyMasterKey && data.masterKey === data.readOnlyMasterKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'masterKey and readOnlyMasterKey should be different', + path: ['readOnlyMasterKey'], + }); + } + if (data.masterKey && data.maintenanceKey && data.masterKey === data.maintenanceKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'masterKey and maintenanceKey should be different', + path: ['maintenanceKey'], + }); + } + if (data.maintenanceKey && data.readOnlyMasterKey && data.maintenanceKey === data.readOnlyMasterKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'maintenanceKey and readOnlyMasterKey should be different', + path: ['maintenanceKey'], + }); + } +}).loose(); + +export type ParseServerOptions = z.infer; diff --git a/src/Options/schemas/PasswordPolicyOptions.ts b/src/Options/schemas/PasswordPolicyOptions.ts new file mode 100644 index 0000000000..4e606c3287 --- /dev/null +++ b/src/Options/schemas/PasswordPolicyOptions.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const PasswordPolicyOptionsSchema = z + .object({ + doNotAllowUsername: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', + help: 'Reject passwords that contain the username as a substring.', + }), + maxPasswordAge: option( + z.number().min(0, 'passwordPolicy.maxPasswordAge must be a non-negative number').optional(), + { + env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', + help: 'Maximum number of days a password remains valid before the user must change it.', + } + ), + maxPasswordHistory: option( + z.number() + .int() + .min(0, 'passwordPolicy.maxPasswordHistory must be >= 0') + .max(20, 'passwordPolicy.maxPasswordHistory must be <= 20') + .optional(), + { + env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', + help: 'Number of previous passwords to remember; prevents reuse of recent passwords.', + } + ), + resetPasswordSuccessOnInvalidEmail: option(z.boolean().default(true), { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_PASSWORD_SUCCESS_ON_INVALID_EMAIL', + help: 'Return a success response for password reset requests even if the email is not found, to prevent user enumeration.', + }), + resetTokenReuseIfValid: option(z.boolean().default(false), { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', + help: 'Reuse an existing password reset token if it is still valid, instead of generating a new one.', + }), + resetTokenValidityDuration: option( + z.number().min(1, 'passwordPolicy.resetTokenValidityDuration must be a positive number').optional(), + { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', + help: 'Duration in seconds that a password reset token remains valid.', + } + ), + validationError: option(z.string().optional(), { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR', + help: 'Custom error message shown to users when their password fails validation.', + }), + validatorCallback: option(z.custom(v => typeof v === 'function').optional(), { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', + help: 'Custom function to validate passwords programmatically, receives the password as input.', + }), + validatorPattern: option(z.union([z.string(), z.instanceof(RegExp)]).optional(), { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', + help: 'Regular expression pattern that passwords must match to be accepted.', + }), + }) + .refine( + data => !data.resetTokenReuseIfValid || data.resetTokenValidityDuration, + { message: 'You cannot use resetTokenReuseIfValid without resetTokenValidityDuration' } + ) + .loose(); + +export type PasswordPolicyOptions = z.infer; diff --git a/src/Options/schemas/RateLimitOptions.ts b/src/Options/schemas/RateLimitOptions.ts new file mode 100644 index 0000000000..8744681163 --- /dev/null +++ b/src/Options/schemas/RateLimitOptions.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const RateLimitOptionsSchema = z + .object({ + errorResponseMessage: option(z.string().default('Too many requests.'), { + env: 'PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE', + help: 'Custom error message returned to the client when the rate limit is exceeded.', + }), + includeInternalRequests: option(z.boolean().default(false), { + env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS', + help: 'Apply rate limiting to internal server-to-server requests in addition to external client requests.', + }), + includeMasterKey: option(z.boolean().default(false), { + env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_MASTER_KEY', + help: 'Apply rate limiting to requests authenticated with the master key.', + }), + redisUrl: option(z.string().optional(), { + env: 'PARSE_SERVER_RATE_LIMIT_REDIS_URL', + help: 'Redis connection URL for a shared rate limit store across multiple server instances.', + }), + requestCount: option(z.number().int().min(1).optional(), { + env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT', + help: 'Maximum number of requests allowed within the time window before rate limiting kicks in.', + }), + requestMethods: option(z.union([z.array(z.string()), z.string()]).optional(), { + env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS', + help: "HTTP methods to apply rate limiting to (e.g. ['GET', 'POST']). Defaults to all methods.", + }), + requestPath: option(z.string(), { + env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_PATH', + help: "The API path pattern to apply rate limiting to (e.g. 'users', 'functions/.*').", + }), + requestTimeWindow: option(z.number().int().min(1).optional(), { + env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW', + help: 'Duration in milliseconds of the sliding time window for counting requests.', + }), + zone: option(z.enum(['ip', 'user', 'session', 'global']).default('ip'), { + env: 'PARSE_SERVER_RATE_LIMIT_ZONE', + help: "Rate limit zone identifier for grouping requests (e.g. 'ip', 'user', 'session', or 'global').", + }), + }) + .loose(); + +export type RateLimitOptions = z.infer; diff --git a/src/Options/schemas/RequestComplexityOptions.ts b/src/Options/schemas/RequestComplexityOptions.ts new file mode 100644 index 0000000000..db5a9909e8 --- /dev/null +++ b/src/Options/schemas/RequestComplexityOptions.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +/** A positive integer or -1 to disable. */ +const complexityLimit = z.number().int().refine( + v => v === -1 || v >= 1, + { message: 'Must be a positive integer or -1 to disable.' } +); + +export const RequestComplexityOptionsSchema = z + .object({ + graphQLDepth: option(complexityLimit.default(-1), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_DEPTH', + help: 'Maximum allowed nesting depth for GraphQL queries. Set to -1 to disable.', + }), + graphQLFields: option(complexityLimit.default(-1), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_FIELDS', + help: 'Maximum number of fields allowed in a single GraphQL query. Set to -1 to disable.', + }), + includeCount: option(complexityLimit.default(-1), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_INCLUDE_COUNT', + help: 'Maximum number of include pointers allowed in a single query. Set to -1 to disable.', + }), + includeDepth: option(complexityLimit.default(-1), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_INCLUDE_DEPTH', + help: 'Maximum nesting depth of include pointers in a query. Set to -1 to disable.', + }), + queryDepth: option(complexityLimit.default(-1), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_QUERY_DEPTH', + help: 'Maximum depth of nested query constraints. Set to -1 to disable.', + }), + subqueryDepth: option(complexityLimit.default(-1), { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_SUBQUERY_DEPTH', + help: 'Maximum depth of nested subqueries (e.g. $inQuery). Set to -1 to disable.', + }), + }) + .loose(); + +export type RequestComplexityOptions = z.infer; diff --git a/src/Options/schemas/SchemaOptions.ts b/src/Options/schemas/SchemaOptions.ts new file mode 100644 index 0000000000..a4b270180d --- /dev/null +++ b/src/Options/schemas/SchemaOptions.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const SchemaOptionsSchema = z + .object({ + afterMigration: option(z.custom(v => typeof v === 'function').nullable().optional(), { + env: 'PARSE_SERVER_SCHEMA_AFTER_MIGRATION', + help: 'Callback function executed after schema migrations have completed.', + }), + beforeMigration: option(z.custom(v => typeof v === 'function').nullable().optional(), { + env: 'PARSE_SERVER_SCHEMA_BEFORE_MIGRATION', + help: 'Callback function executed before schema migrations begin.', + }), + definitions: option(z.array(z.any()).default([]), { + env: 'PARSE_SERVER_SCHEMA_DEFINITIONS', + help: 'Array of schema definitions in REST format, used to configure classes, fields, indexes, and CLPs.', + docType: 'Any', + }), + deleteExtraFields: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS', + help: 'Delete fields from existing classes that are not defined in the schema definitions.', + }), + keepUnknownIndexes: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_KEEP_UNKNOWN_INDEXES', + help: 'Preserve database indexes that are not defined in the schema definitions, instead of dropping them.', + }), + lockSchemas: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_LOCK_SCHEMAS', + help: 'Prevent any schema modifications at runtime; all changes must be made through schema definitions.', + }), + recreateModifiedFields: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS', + help: 'Drop and recreate fields whose type has changed in the schema definitions.', + }), + strict: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SCHEMA_STRICT', + help: 'Exit the server process if a schema migration fails, instead of continuing with errors.', + }), + }) + .loose(); + +export type SchemaOptions = z.infer; diff --git a/src/Options/schemas/SecurityOptions.ts b/src/Options/schemas/SecurityOptions.ts new file mode 100644 index 0000000000..8f8a7bb755 --- /dev/null +++ b/src/Options/schemas/SecurityOptions.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { option } from '../schemaUtils'; + +export const SecurityOptionsSchema = z + .object({ + checkGroups: option(z.array(z.any()).optional(), { + env: 'PARSE_SERVER_SECURITY_CHECK_GROUPS', + help: 'Array of security check group classes to run during a security audit.', + }), + enableCheck: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK', + help: 'Enable the Parse Server security check that audits configuration for vulnerabilities.', + }), + enableCheckLog: option(z.boolean().default(false), { + env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK_LOG', + help: 'Log the results of security checks to the server console on startup.', + }), + }) + .loose(); + +export type SecurityOptions = z.infer; diff --git a/src/Options/schemas/index.ts b/src/Options/schemas/index.ts new file mode 100644 index 0000000000..4e944936f5 --- /dev/null +++ b/src/Options/schemas/index.ts @@ -0,0 +1,58 @@ +export { SchemaOptionsSchema } from './SchemaOptions'; +export type { SchemaOptions } from './SchemaOptions'; + +export { AccountLockoutOptionsSchema } from './AccountLockoutOptions'; +export type { AccountLockoutOptions } from './AccountLockoutOptions'; + +export { PasswordPolicyOptionsSchema } from './PasswordPolicyOptions'; +export type { PasswordPolicyOptions } from './PasswordPolicyOptions'; + +export { FileUploadOptionsSchema } from './FileUploadOptions'; +export type { FileUploadOptions } from './FileUploadOptions'; + +export { IdempotencyOptionsSchema } from './IdempotencyOptions'; +export type { IdempotencyOptions } from './IdempotencyOptions'; + +export { SecurityOptionsSchema } from './SecurityOptions'; +export type { SecurityOptions } from './SecurityOptions'; + +export { RequestComplexityOptionsSchema } from './RequestComplexityOptions'; +export type { RequestComplexityOptions } from './RequestComplexityOptions'; + +export { + PagesOptionsSchema, + PagesRouteSchema, + PagesCustomUrlsOptionsSchema, + CustomPagesOptionsSchema, +} from './PagesOptions'; +export type { + PagesOptions, + PagesRoute, + PagesCustomUrlsOptions, + CustomPagesOptions, +} from './PagesOptions'; + +export { LiveQueryOptionsSchema, LiveQueryServerOptionsSchema } from './LiveQueryOptions'; +export type { LiveQueryOptions, LiveQueryServerOptions } from './LiveQueryOptions'; + +export { RateLimitOptionsSchema } from './RateLimitOptions'; +export type { RateLimitOptions } from './RateLimitOptions'; + +export { LogLevelsSchema } from './LogLevels'; +export type { LogLevels } from './LogLevels'; + +export { + DatabaseOptionsSchema, + DatabaseOptionsClientMetadataSchema, + LogClientEventSchema, + LogLevelSchema, +} from './DatabaseOptions'; +export type { + DatabaseOptions, + DatabaseOptionsClientMetadata, + LogClientEvent, + LogLevel, +} from './DatabaseOptions'; + +export { ParseServerOptionsSchema } from './ParseServerOptions'; +export type { ParseServerOptions } from './ParseServerOptions'; diff --git a/src/Options/validateConfig.ts b/src/Options/validateConfig.ts new file mode 100644 index 0000000000..76ef7180d0 --- /dev/null +++ b/src/Options/validateConfig.ts @@ -0,0 +1,25 @@ +import { ParseServerOptionsSchema } from './schemas/ParseServerOptions'; +import { SchemaValidator } from './validators/SchemaValidator'; + +/** + * The schema validator instance for Parse Server configuration. + * All validation rules (types, defaults, constraints, cross-field checks) + * are encoded in the Zod schema itself. + */ +const schemaValidator = new SchemaValidator(ParseServerOptionsSchema); + +/** + * Validates and applies defaults to a Parse Server configuration object. + * + * @param options - Raw configuration object + * @returns The validated and defaulted configuration object + * @throws Error with descriptive message if validation fails + */ +export function validateConfig(options: Record): Record { + if (options == null || typeof options !== 'object') { + throw new Error('Parse Server configuration must be a non-null object.'); + } + const config = { ...options }; + schemaValidator.validate(config); + return config; +} diff --git a/src/Options/validators/ConfigValidationPipeline.ts b/src/Options/validators/ConfigValidationPipeline.ts new file mode 100644 index 0000000000..468e8e34c6 --- /dev/null +++ b/src/Options/validators/ConfigValidationPipeline.ts @@ -0,0 +1,28 @@ +import type { ConfigValidator } from './ConfigValidator'; + +/** + * Composes multiple ConfigValidators into a sequential pipeline. + * Each validator runs in order; if one fails, subsequent validators are skipped. + * + * Usage: + * ```ts + * const pipeline = new ConfigValidationPipeline([ + * new SchemaValidator(ParseServerOptionsSchema), + * new BusinessRuleValidator(), + * ]); + * pipeline.validate(config); + * ``` + */ +export class ConfigValidationPipeline implements ConfigValidator { + private validators: ConfigValidator[]; + + constructor(validators: ConfigValidator[]) { + this.validators = validators; + } + + validate(config: Record): void { + for (const validator of this.validators) { + validator.validate(config); + } + } +} diff --git a/src/Options/validators/ConfigValidator.ts b/src/Options/validators/ConfigValidator.ts new file mode 100644 index 0000000000..6c885047e3 --- /dev/null +++ b/src/Options/validators/ConfigValidator.ts @@ -0,0 +1,13 @@ +/** + * Interface for config validators. + * Each validator is responsible for a single concern (SRP). + * Validators can be composed into a pipeline (OCP). + */ +export interface ConfigValidator { + /** + * Validates the config object. May mutate config to apply defaults or transformations. + * @param config - The configuration object to validate + * @throws Error if validation fails + */ + validate(config: Record): void; +} diff --git a/src/Options/validators/ControllerValidator.ts b/src/Options/validators/ControllerValidator.ts new file mode 100644 index 0000000000..8738c92aad --- /dev/null +++ b/src/Options/validators/ControllerValidator.ts @@ -0,0 +1,40 @@ +import type { ConfigValidator } from './ConfigValidator'; + +/** + * Validates controller-dependent configuration. + * Runs after controllers are assembled — checks that runtime dependencies + * required by certain config options are present. + * + * Type-level checks (booleans, numbers, formats) are handled by Zod schemas. + * This validator only checks cross-field runtime constraints that depend + * on assembled controller state. + */ +export class ControllerValidator implements ConfigValidator { + validate(config: Record): void { + if (config.verifyUserEmails) { + this.validateEmailVerificationDependencies(config); + } + } + + /** + * When email verification is enabled, an email adapter, app name, + * and publicServerURL must all be configured. + */ + private validateEmailVerificationDependencies(config: Record): void { + const userController = config.userController as Record | undefined; + const emailAdapter = userController?.adapter; + if (!emailAdapter) { + throw new Error('An emailAdapter is required for e-mail verification and password resets.'); + } + if (typeof config.appName !== 'string') { + throw new Error('An app name is required for e-mail verification and password resets.'); + } + const publicServerURL = config.publicServerURL || config._publicServerURL; + if (!publicServerURL) { + throw new Error('The option publicServerURL is required when verifyUserEmails is enabled.'); + } + if (config.emailVerifyTokenReuseIfValid && !config.emailVerifyTokenValidityDuration) { + throw new Error('You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration'); + } + } +} diff --git a/src/Options/validators/SchemaValidator.ts b/src/Options/validators/SchemaValidator.ts new file mode 100644 index 0000000000..86b4f6f95c --- /dev/null +++ b/src/Options/validators/SchemaValidator.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import type { ConfigValidator } from './ConfigValidator'; + +/** + * Validates config against a Zod schema, applying type coercion and defaults. + * This is the first validator in the pipeline — it ensures the config object + * has the correct shape and types before business rules are checked. + */ +export class SchemaValidator implements ConfigValidator { + private schema: z.ZodObject; + + constructor(schema: z.ZodObject) { + this.schema = schema; + } + + validate(config: Record): void { + const result = this.schema.safeParse(config); + if (!result.success) { + const messages = result.error.issues.map(issue => { + const path = issue.path.join('.'); + return path ? `${path}: ${issue.message}` : issue.message; + }); + throw new Error(`Parse Server configuration error:\n${messages.join('\n')}`); + } + + // Replace config contents with only schema-approved keys + const validated = result.data; + for (const key of Object.keys(config)) { + delete config[key]; + } + Object.assign(config, validated); + } +} diff --git a/src/Options/validators/index.ts b/src/Options/validators/index.ts new file mode 100644 index 0000000000..7c7e03dc15 --- /dev/null +++ b/src/Options/validators/index.ts @@ -0,0 +1,4 @@ +export type { ConfigValidator } from './ConfigValidator'; +export { SchemaValidator } from './SchemaValidator'; +export { ControllerValidator } from './ControllerValidator'; +export { ConfigValidationPipeline } from './ConfigValidationPipeline'; diff --git a/src/ParseServer.ts b/src/ParseServer.ts index b1f03c5863..396ed04796 100644 --- a/src/ParseServer.ts +++ b/src/ParseServer.ts @@ -44,7 +44,9 @@ import { SecurityRouter } from './Routers/SecurityRouter'; import CheckRunner from './Security/CheckRunner'; import Deprecator from './Deprecator/Deprecator'; import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas'; -import OptionsDefinitions from './Options/Definitions'; +import { validateConfig } from './Options/validateConfig'; +import { warnInapplicableOptions } from './Options/schemaUtils'; +import { ParseServerOptionsSchema } from './Options/schemas/ParseServerOptions'; import { resolvingPromise, Connections } from './TestUtils'; // Mutate the Parse object to add the Cloud Code handlers @@ -63,65 +65,46 @@ class ParseServer { liveQueryServer: any; /** * @constructor - * @param {ParseServerOptions} options the parse server initialization options + * @param {ParseServerOptions} options the parse server initialization options. + * + * **Note:** The `options` object is modified in-place. {@link validateConfig} is + * called to validate and apply schema defaults, and the resulting values are + * written back onto `options` to preserve the original reference for consumers + * (e.g. middleware, controllers) that hold a pointer to this object. + * + * Callers should pass a shallow clone if they need to retain the original + * unmodified options object. */ constructor(options: ParseServerOptions) { // Scan for deprecated Parse Server options Deprecator.scanParseServerOptions(options); - const interfaces = JSON.parse(JSON.stringify(OptionsDefinitions)); - - function getValidObject(root) { - const result = {}; - for (const key in root) { - if (Object.prototype.hasOwnProperty.call(root[key], 'type')) { - if (root[key].type.endsWith('[]')) { - result[key] = [getValidObject(interfaces[root[key].type.slice(0, -2)])]; - } else { - result[key] = getValidObject(interfaces[root[key].type]); - } - } else { - result[key] = ''; - } - } - return result; - } + // Validate and apply defaults via Zod schema + const validated = validateConfig(options); - const optionsBlueprint = getValidObject(interfaces['ParseServerOptions']); + // Copy validated values back to options object (preserving reference for consumers). + // First remove all existing own properties so stale keys (e.g. unknown keys + // stripped by Zod) don't linger on the object. + Object.keys(options).forEach(key => { + delete options[key]; + }); + Object.keys(validated).forEach(key => { + options[key] = validated[key]; + }); - function validateKeyNames(original, ref, name = '') { - let result = []; - const prefix = name + (name !== '' ? '.' : ''); - for (const key in original) { - if (!Object.prototype.hasOwnProperty.call(ref, key)) { - result.push(prefix + key); - } else { - if (ref[key] === '') { continue; } - let res = []; - if (Array.isArray(original[key]) && Array.isArray(ref[key])) { - const type = ref[key][0]; - original[key].forEach((item, idx) => { - if (typeof item === 'object' && item !== null) { - res = res.concat(validateKeyNames(item, type, prefix + key + `[${idx}]`)); - } - }); - } else if (typeof original[key] === 'object' && typeof ref[key] === 'object') { - res = validateKeyNames(original[key], ref[key], prefix + key); - } - result = result.concat(res); - } - } - return result; - } + // Apply special defaults and backwards compatibility that go beyond Zod schema defaults + injectSpecialDefaults(options); - const diff = validateKeyNames(options, optionsBlueprint); - if (diff.length > 0) { + // Warn about options that only apply to CLI context (#8432/#8300) + warnInapplicableOptions(options, 'api', ParseServerOptionsSchema, (msg) => { const logger = (logging as any).logger; - logger.error(`Invalid key(s) found in Parse Server configuration: ${diff.join(', ')}`); - } + if (logger) { + logger.warn(msg); + } else { + console.warn(msg); + } + }); - // Set option defaults - injectDefaults(options); const { appId = requiredParameter('You must provide an appId!'), masterKey = requiredParameter('You must provide a masterKey!'), @@ -131,7 +114,6 @@ class ParseServer { // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; - Config.validateOptions(options); const allControllers = controllers.getControllers(options); (options as any).state = 'initialized'; @@ -595,19 +577,21 @@ function addParseCloud() { global.Parse = Parse; } -function injectDefaults(options: ParseServerOptions) { - Object.keys(defaults).forEach(key => { - if (!Object.prototype.hasOwnProperty.call(options, key)) { - options[key] = defaults[key]; - } - }); - - // Inject defaults for database options; only when no explicit database adapter is set, - // because an explicit adapter manages its own options and passing databaseOptions alongside - // it would cause a conflict error in getDatabaseController. +/** + * Applies special defaults and backwards compatibility logic that goes beyond + * what Zod schema defaults can handle. Zod handles all simple field defaults; + * this function handles: + * - Database option defaults (conditional on databaseAdapter) + * - serverURL derivation from port + mountPath + * - appId special character warning + * - userSensitiveFields backwards compatibility + * - protectedFields deep merge with defaults + */ +function injectSpecialDefaults(options: ParseServerOptions) { + // Inject defaults for database options; only when no explicit database adapter is set if (!options.databaseAdapter) { if (options.databaseOptions == null) { - options.databaseOptions = {}; + (options as any).databaseOptions = {}; } if (typeof options.databaseOptions === 'object' && !Array.isArray(options.databaseOptions)) { Object.keys(DatabaseOptionDefaults).forEach(key => { @@ -646,13 +630,7 @@ function injectDefaults(options: ParseServerOptions) { new Set([...(defaults.userSensitiveFields || []), ...(options.userSensitiveFields || [])]) ); - // If the options.protectedFields is unset, - // it'll be assigned the default above. - // Here, protect against the case where protectedFields - // is set, but doesn't have _User. - if (!('_User' in options.protectedFields)) { - options.protectedFields = Object.assign({ _User: [] }, options.protectedFields); - } + options.protectedFields['_User'] = options.protectedFields['_User'] || {}; options.protectedFields['_User']['*'] = Array.from( new Set([...(options.protectedFields['_User']['*'] || []), ...userSensitiveFields]) diff --git a/src/cli/definitions/parse-live-query-server.js b/src/cli/definitions/parse-live-query-server.js deleted file mode 100644 index 0fd2fca6c9..0000000000 --- a/src/cli/definitions/parse-live-query-server.js +++ /dev/null @@ -1,2 +0,0 @@ -const LiveQueryServerOptions = require('../../Options/Definitions').LiveQueryServerOptions; -export default LiveQueryServerOptions; diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js deleted file mode 100644 index d19dcc5d8a..0000000000 --- a/src/cli/definitions/parse-server.js +++ /dev/null @@ -1,2 +0,0 @@ -const ParseServerDefinitions = require('../../Options/Definitions').ParseServerOptions; -export default ParseServerDefinitions; diff --git a/src/cli/parse-live-query-server.js b/src/cli/parse-live-query-server.js index 525a202a26..034e9f3d92 100644 --- a/src/cli/parse-live-query-server.js +++ b/src/cli/parse-live-query-server.js @@ -1,9 +1,9 @@ -import definitions from './definitions/parse-live-query-server'; -import runner from './utils/runner'; +import runnerZod from './utils/runner-zod'; import { ParseServer } from '../index'; +import { LiveQueryServerOptionsSchema } from '../Options/schemas/LiveQueryOptions'; -runner({ - definitions, +runnerZod({ + schema: LiveQueryServerOptionsSchema, start: function (program, options, logOptions) { logOptions(); ParseServer.createLiveQueryServer(undefined, options); diff --git a/src/cli/parse-server.js b/src/cli/parse-server.js index 7c7639b497..0351274ab1 100755 --- a/src/cli/parse-server.js +++ b/src/cli/parse-server.js @@ -1,9 +1,9 @@ /* eslint-disable no-console */ import ParseServer from '../index'; -import definitions from './definitions/parse-server'; import cluster from 'cluster'; import os from 'os'; -import runner from './utils/runner'; +import runnerZod from './utils/runner-zod'; +import { ParseServerOptionsSchema } from '../Options/schemas/ParseServerOptions'; const help = function () { console.log(' Get Started guide:'); @@ -27,8 +27,8 @@ const help = function () { console.log(''); }; -runner({ - definitions, +runnerZod({ + schema: ParseServerOptionsSchema, help, usage: '[options] ', start: function (program, options, logOptions) { diff --git a/src/cli/utils/commander.js b/src/cli/utils/commander.js deleted file mode 100644 index e2e06e0550..0000000000 --- a/src/cli/utils/commander.js +++ /dev/null @@ -1,143 +0,0 @@ -/* eslint-disable no-console */ -import { Command } from 'commander'; -import path from 'path'; -import Deprecator from '../../Deprecator/Deprecator'; - -let _definitions; -let _reverseDefinitions; -let _defaults; - -Command.prototype.loadDefinitions = function (definitions) { - _definitions = definitions; - - Object.keys(definitions).reduce((program, opt) => { - if (typeof definitions[opt] == 'object') { - const additionalOptions = definitions[opt]; - if (additionalOptions.required === true) { - return program.option( - `--${opt} <${opt}>`, - additionalOptions.help, - additionalOptions.action - ); - } else { - return program.option( - `--${opt} [${opt}]`, - additionalOptions.help, - additionalOptions.action - ); - } - } - return program.option(`--${opt} [${opt}]`); - }, this); - - _reverseDefinitions = Object.keys(definitions).reduce((object, key) => { - let value = definitions[key]; - if (typeof value == 'object') { - value = value.env; - } - if (value) { - object[value] = key; - } - return object; - }, {}); - - _defaults = Object.keys(definitions).reduce((defs, opt) => { - if (_definitions[opt].default !== undefined) { - defs[opt] = _definitions[opt].default; - } - return defs; - }, {}); - - /* istanbul ignore next */ - this.on('--help', function () { - console.log(' Configure From Environment:'); - console.log(''); - Object.keys(_reverseDefinitions).forEach(key => { - console.log(` $ ${key}='${_reverseDefinitions[key]}'`); - }); - console.log(''); - }); -}; - -function parseEnvironment(env = {}) { - return Object.keys(_reverseDefinitions).reduce((options, key) => { - if (env[key]) { - const originalKey = _reverseDefinitions[key]; - let action = option => option; - if (typeof _definitions[originalKey] === 'object') { - action = _definitions[originalKey].action || action; - } - options[_reverseDefinitions[key]] = action(env[key]); - } - return options; - }, {}); -} - -function parseConfigFile(program) { - let options = {}; - if (program.args.length > 0) { - let jsonPath = program.args[0]; - jsonPath = path.resolve(jsonPath); - const jsonConfig = require(jsonPath); - if (jsonConfig.apps) { - if (jsonConfig.apps.length > 1) { - throw 'Multiple apps are not supported'; - } - options = jsonConfig.apps[0]; - } else { - options = jsonConfig; - } - Object.keys(options).forEach(key => { - const value = options[key]; - if (!_definitions[key]) { - throw `error: unknown option ${key}`; - } - const action = _definitions[key].action; - if (action) { - options[key] = action(value); - } - }); - console.log(`Configuration loaded from ${jsonPath}`); - } - return options; -} - -Command.prototype.setValuesIfNeeded = function (options) { - Object.keys(options).forEach(key => { - if (!Object.prototype.hasOwnProperty.call(this, key)) { - this[key] = options[key]; - } - }); -}; - -Command.prototype._parse = Command.prototype.parse; - -Command.prototype.parse = function (args, env) { - this._parse(args); - // Parse the environment first - const envOptions = parseEnvironment(env); - const fromFile = parseConfigFile(this); - // Load the env if not passed from command line - this.setValuesIfNeeded(envOptions); - // Load from file to override - this.setValuesIfNeeded(fromFile); - // Scan for deprecated Parse Server options - Deprecator.scanParseServerOptions(this); - // Last set the defaults - this.setValuesIfNeeded(_defaults); -}; - -Command.prototype.getOptions = function () { - return Object.keys(_definitions).reduce((options, key) => { - if (typeof this[key] !== 'undefined') { - options[key] = this[key]; - } - return options; - }, {}); -}; - -const commander = new Command() -commander.storeOptionsAsProperties(); -commander.allowExcessArguments(); -export default commander; -/* eslint-enable no-console */ diff --git a/src/cli/utils/runner-zod.ts b/src/cli/utils/runner-zod.ts new file mode 100644 index 0000000000..32aeca44e6 --- /dev/null +++ b/src/cli/utils/runner-zod.ts @@ -0,0 +1,139 @@ +/* eslint-disable no-console */ +import fs from 'fs'; +import path from 'path'; +import { Command } from 'commander'; +import { z } from 'zod'; +import { loadFromEnv } from '../../Options/loaders/envLoader'; +import { loadFromFile } from '../../Options/loaders/fileLoader'; +import { registerSchemaOptions, extractCliOptions } from '../../Options/loaders/cliLoader'; +import { mergeConfigs } from '../../Options/loaders/mergeConfig'; +import Deprecator from '../../Deprecator/Deprecator'; +import { getAllOptionMeta, getSensitiveOptionKeys } from '../../Options/schemaUtils'; + +const FALLBACK_KEYS_TO_REDACT = ['databaseAdapter', 'databaseURI', 'masterKey', 'maintenanceKey', 'push']; + +function logStartupOptions(options: Record, schema?: z.ZodObject) { + if (!options.verbose) { + return; + } + const sensitiveKeys = schema ? getSensitiveOptionKeys(schema) : []; + const keysToRedact = sensitiveKeys.length > 0 ? sensitiveKeys : FALLBACK_KEYS_TO_REDACT; + for (const key in options) { + let value = options[key]; + if (keysToRedact.includes(key)) { + value = ''; + } + if (typeof value === 'object') { + try { + value = JSON.stringify(value); + } catch { + if (value && value.constructor && value.constructor.name) { + value = value.constructor.name; + } + } + } + console.log(`${key}: ${value}`); + } +} + +interface RunnerZodOptions { + schema: z.ZodObject; + help?: () => void; + usage?: string; + start: ( + program: Command, + options: Record, + logOptions: () => void + ) => void; +} + +/** + * Zod-based CLI runner that replaces the old definitions-based runner. + * + * Uses Zod schemas for option registration, env var loading, config file + * loading, and merge with proper priority order. + * + * Priority (lowest to highest): + * 1. Schema defaults (applied by Zod during parse) + * 2. Config file + * 3. Environment variables + * 4. CLI arguments + */ +export default function runnerZod({ schema, help, usage, start }: RunnerZodOptions) { + const program = new Command(); + program.allowExcessArguments(); + + // Register options from Zod schema metadata + registerSchemaOptions(program, schema); + + if (usage) { + program.usage(usage); + } + if (help) { + program.on('--help', help); + } + + // Add environment variable help + const allMeta = getAllOptionMeta(schema); + program.on('--help', () => { + console.log(' Configure From Environment:'); + console.log(''); + for (const [key, meta] of allMeta) { + if (meta.env) { + console.log(` $ ${meta.env}='${key}'`); + } + } + console.log(''); + }); + + // Parse CLI args + program.parse(process.argv); + + // Extract only explicitly-set CLI options (not defaults from Commander) + const cliOptions = extractCliOptions(program, schema); + + // Load from config file (first positional arg) + let fileOptions: Record = {}; + if (program.args.length > 0) { + const configFilePath = path.resolve(program.args[0]); + if (!fs.existsSync(configFilePath)) { + console.error(`Config file not found: ${configFilePath}`); + process.exit(1); + } + try { + fileOptions = loadFromFile(program.args[0]); + console.log(`Configuration loaded from ${configFilePath}`); + } catch (e: any) { + console.error(`Error parsing config file ${configFilePath}: ${e.message}`); + process.exit(1); + } + } + + // Load from environment variables + const envOptions = loadFromEnv(schema, process.env); + + // Merge: file < env < CLI (CLI wins) + const merged = mergeConfigs(fileOptions, envOptions, cliOptions); + + // Scan for deprecated options + Deprecator.scanParseServerOptions(merged); + + // Parse through Zod to apply defaults and validate types + const result = schema.safeParse(merged); + if (!result.success) { + const messages = result.error.issues.map(issue => { + const path = issue.path.join('.'); + return path ? ` ${path}: ${issue.message}` : ` ${issue.message}`; + }); + console.error('Configuration errors:'); + console.error(messages.join('\n')); + process.exit(1); + } + + const options = result.data as Record; + + start(program, options, function () { + logStartupOptions(options, schema); + }); +} +/* eslint-enable no-console */ diff --git a/src/cli/utils/runner.js b/src/cli/utils/runner.js deleted file mode 100644 index 6b18012af5..0000000000 --- a/src/cli/utils/runner.js +++ /dev/null @@ -1,49 +0,0 @@ -import program from './commander'; - -function logStartupOptions(options) { - if (!options.verbose) { - return; - } - // Keys that may include sensitive information that will be redacted in logs - const keysToRedact = [ - 'databaseAdapter', - 'databaseURI', - 'masterKey', - 'maintenanceKey', - 'push', - ]; - for (const key in options) { - let value = options[key]; - if (keysToRedact.includes(key)) { - value = ''; - } - if (typeof value === 'object') { - try { - value = JSON.stringify(value); - } catch { - if (value && value.constructor && value.constructor.name) { - value = value.constructor.name; - } - } - } - /* eslint-disable no-console */ - console.log(`${key}: ${value}`); - /* eslint-enable no-console */ - } -} - -export default function ({ definitions, help, usage, start }) { - program.loadDefinitions(definitions); - if (usage) { - program.usage(usage); - } - if (help) { - program.on('--help', help); - } - program.parse(process.argv, process.env); - - const options = program.getOptions(); - start(program, options, function () { - logStartupOptions(options); - }); -} diff --git a/src/defaults.js b/src/defaults.js index b7d05f1550..9e2d454d15 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,12 +1,14 @@ -import { nullParser } from './Options/parsers'; -const { ParseServerOptions, DatabaseOptions } = require('./Options/Definitions'); +import { extractSchemaDefaults } from './Options/schemaUtils'; +import { ParseServerOptionsSchema } from './Options/schemas/ParseServerOptions'; +import { DatabaseOptionsSchema } from './Options/schemas/DatabaseOptions'; + const logsFolder = (() => { let folder = './logs/'; if (typeof process !== 'undefined' && process.env.TESTING === '1') { folder = './test_logs/'; } if (process.env.PARSE_SERVER_LOGS_FOLDER) { - folder = nullParser(process.env.PARSE_SERVER_LOGS_FOLDER); + folder = process.env.PARSE_SERVER_LOGS_FOLDER === 'null' ? null : process.env.PARSE_SERVER_LOGS_FOLDER; } return folder; })(); @@ -16,13 +18,7 @@ const { verbose, level } = (() => { return { verbose, level: verbose ? 'verbose' : undefined }; })(); -const DefinitionDefaults = Object.keys(ParseServerOptions).reduce((memo, key) => { - const def = ParseServerOptions[key]; - if (Object.prototype.hasOwnProperty.call(def, 'default')) { - memo[key] = def.default; - } - return memo; -}, {}); +const DefinitionDefaults = extractSchemaDefaults(ParseServerOptionsSchema); const computedDefaults = { jsonLogs: process.env.JSON_LOGS || false, @@ -34,13 +30,7 @@ const computedDefaults = { export default Object.assign({}, DefinitionDefaults, computedDefaults); export const DefaultMongoURI = DefinitionDefaults.databaseURI; -export const DatabaseOptionDefaults = Object.keys(DatabaseOptions).reduce((memo, key) => { - const def = DatabaseOptions[key]; - if (Object.prototype.hasOwnProperty.call(def, 'default')) { - memo[key] = def.default; - } - return memo; -}, {}); +export const DatabaseOptionDefaults = extractSchemaDefaults(DatabaseOptionsSchema); // Parse Server-specific database options that should be filtered out // before passing to MongoDB client diff --git a/src/middlewares.js b/src/middlewares.js index f12fdd0ed1..61eb91cd35 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -9,7 +9,10 @@ import rest from './rest'; import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter'; import rateLimit from 'express-rate-limit'; -import { RateLimitOptions } from './Options/Definitions'; +import { schemaToLegacyDefinitions } from './Options/schemaUtils'; +import { RateLimitOptionsSchema } from './Options/schemas/RateLimitOptions'; + +const RateLimitOptions = schemaToLegacyDefinitions(RateLimitOptionsSchema); import { pathToRegexp } from 'path-to-regexp'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis';