完善接单平台上传与物品规则
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import path from 'node:path'
|
||||
|
||||
import type { RuntimeConfig } from "../types/runtime-config.js";
|
||||
import type { RuntimeConfig } from '../types/runtime-config.js'
|
||||
|
||||
export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
||||
return {
|
||||
@@ -9,99 +9,107 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
||||
},
|
||||
|
||||
data: {
|
||||
root: path.resolve(projectRoot, "data"),
|
||||
root: path.resolve(projectRoot, 'data'),
|
||||
},
|
||||
|
||||
logging: {
|
||||
level: "info",
|
||||
level: 'info',
|
||||
// 对接日志默认只保留 warn/error,避免 integration-*.log 被 info 刷爆
|
||||
integrationLevel: "warn",
|
||||
integrationLevel: 'warn',
|
||||
retentionDays: 30,
|
||||
},
|
||||
|
||||
database: {
|
||||
url: "postgres://postgres:postgres@127.0.0.1:5432/order_site",
|
||||
url: 'postgres://postgres:postgres@127.0.0.1:5432/order_site',
|
||||
ssl: false,
|
||||
maxConnections: 10,
|
||||
},
|
||||
|
||||
storage: {
|
||||
endpoint: 'http://127.0.0.1:9000',
|
||||
bucket: 'order-site',
|
||||
accessKeyId: 'minioadmin',
|
||||
secretAccessKey: 'minioadmin',
|
||||
region: 'us-east-1',
|
||||
maxUploadSizeMb: 10,
|
||||
},
|
||||
|
||||
orders: {
|
||||
claimBaseUrl: "http://127.0.0.1:5173/#/claim",
|
||||
claimBaseUrl: 'http://127.0.0.1:5173/#/claim',
|
||||
tokenTtlHours: 24,
|
||||
},
|
||||
|
||||
admin: {
|
||||
sessionSecret: "",
|
||||
sessionSecret: '',
|
||||
sessionTtlHours: 12,
|
||||
defaultUsers: [],
|
||||
},
|
||||
|
||||
platforms: {
|
||||
ninetyone: {
|
||||
userId: "",
|
||||
secret: "",
|
||||
version: "1.0",
|
||||
shopId: "91kaquan",
|
||||
shopName: "91卡券",
|
||||
userId: '',
|
||||
secret: '',
|
||||
version: '1.0',
|
||||
shopId: '91kaquan',
|
||||
shopName: '91卡券',
|
||||
timestampToleranceSeconds: 600,
|
||||
cardsEncoding: "aes-256-ecb-base64",
|
||||
cardsEncoding: 'aes-256-ecb-base64',
|
||||
},
|
||||
kuaishouIndustry: {
|
||||
appKey: "",
|
||||
appSecret: "",
|
||||
signSecret: "",
|
||||
messageSecret: "",
|
||||
provider: "kuaishou-industry",
|
||||
platform: "kuaishou",
|
||||
shopId: "kuaishou-industry",
|
||||
shopName: "快手行业电子凭证",
|
||||
version: "1",
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
signSecret: '',
|
||||
messageSecret: '',
|
||||
provider: 'kuaishou-industry',
|
||||
platform: 'kuaishou',
|
||||
shopId: 'kuaishou-industry',
|
||||
shopName: '快手行业电子凭证',
|
||||
version: '1',
|
||||
},
|
||||
kuaishouFeifei: {
|
||||
enabled: true,
|
||||
baseUrl: "http://skin-exchange.yiquyou.icu",
|
||||
appKey: "",
|
||||
appSecret: "",
|
||||
baseUrl: 'http://skin-exchange.yiquyou.icu',
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
timeoutMs: 10000,
|
||||
notifyUrl: "",
|
||||
notifyUrl: '',
|
||||
productRules: [],
|
||||
},
|
||||
cloudtentacles: {
|
||||
baseUrl: "https://123.207.217.176",
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
timeoutMs: 5000,
|
||||
sendSmsPath: "/public/verif_code",
|
||||
loginPath: "/public/login",
|
||||
userInfoPath: "/user/info",
|
||||
assetPath: "/user/get_asset",
|
||||
permissionPath: "/user/get_permission",
|
||||
categoriesPath: "/categories/get",
|
||||
skuListPath: "/sku/list",
|
||||
skuBuyPath: "/sku/buy",
|
||||
skuUsePath: "/sku/use",
|
||||
knapsackPath: "/user/get_knapsack",
|
||||
vnListPath: "/vn/list",
|
||||
vnAppointPath: "/vn/appoint",
|
||||
vnGenerateLoginCodePath: "/vn/generate_login_code",
|
||||
vnVerifCodePath: "/public/vn_verif_code",
|
||||
vnVerifyLoginCodePath: "/vn/verify_login_code",
|
||||
vnBindUrlPath: "/vn/bind_url",
|
||||
vnBindInfoPath: "/vn/bind_info",
|
||||
vnBackPath: "/vn/back",
|
||||
sendSmsPath: '/public/verif_code',
|
||||
loginPath: '/public/login',
|
||||
userInfoPath: '/user/info',
|
||||
assetPath: '/user/get_asset',
|
||||
permissionPath: '/user/get_permission',
|
||||
categoriesPath: '/categories/get',
|
||||
skuListPath: '/sku/list',
|
||||
skuBuyPath: '/sku/buy',
|
||||
skuUsePath: '/sku/use',
|
||||
knapsackPath: '/user/get_knapsack',
|
||||
vnListPath: '/vn/list',
|
||||
vnAppointPath: '/vn/appoint',
|
||||
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
||||
vnVerifCodePath: '/public/vn_verif_code',
|
||||
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||
vnBindUrlPath: '/vn/bind_url',
|
||||
vnBindInfoPath: '/vn/bind_info',
|
||||
vnBackPath: '/vn/back',
|
||||
bindUrlTtlSeconds: 600,
|
||||
bindUrlProbeIntervalSeconds: 30,
|
||||
bindUrlProbeTimeoutMs: 5000,
|
||||
bindUrlProbeUserAgent:
|
||||
"Mozilla/5.0 (Linux; Android 12; M2012K11AC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/98.0.4758.102 MQQBrowser/6.2 TBS/046141 Mobile Safari/537.36 V1_AND_SQ_8.8.85_2674_YYB_D QQ/8.8.85.7685 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/108 SimpleUISwitch/0 QQTheme/1000",
|
||||
bindUrlProbeEndpoint: "https://comm.ams.game.qq.com/ide/",
|
||||
bindUrlProbeChartId: "323794",
|
||||
bindUrlProbeSubChartId: "323794",
|
||||
bindUrlProbeIdeToken: "z90Syo",
|
||||
bindUrlProbeActivityUrl:
|
||||
"http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F",
|
||||
bindUrlProbeReferer: "https://gp.qq.com/",
|
||||
bindUrlProbeExtraCookie: "",
|
||||
clientSource: "ct-client",
|
||||
deviceId: "08bc9d8c-fd15-48ea-bc00-8d754076cafc",
|
||||
'Mozilla/5.0 (Linux; Android 12; M2012K11AC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/98.0.4758.102 MQQBrowser/6.2 TBS/046141 Mobile Safari/537.36 V1_AND_SQ_8.8.85_2674_YYB_D QQ/8.8.85.7685 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/108 SimpleUISwitch/0 QQTheme/1000',
|
||||
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
||||
bindUrlProbeChartId: '323794',
|
||||
bindUrlProbeSubChartId: '323794',
|
||||
bindUrlProbeIdeToken: 'z90Syo',
|
||||
bindUrlProbeActivityUrl: 'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F',
|
||||
bindUrlProbeReferer: 'https://gp.qq.com/',
|
||||
bindUrlProbeExtraCookie: '',
|
||||
clientSource: 'ct-client',
|
||||
deviceId: '08bc9d8c-fd15-48ea-bc00-8d754076cafc',
|
||||
deviceType: 1,
|
||||
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsSzjk+9st2f8IpYqYaM5
|
||||
@@ -116,7 +124,7 @@ PfQTPNA++KsXtwRX9M6Re3vkTDRsutIIWKtj8jqhUbbYS3vzS8GJnAWavUFVkR15
|
||||
},
|
||||
|
||||
cors: {
|
||||
allowedOrigins: ["*"],
|
||||
allowedOrigins: ['*'],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const BACKEND_ROOT = path.resolve(CURRENT_DIR, '../..')
|
||||
const WORKSPACE_ROOT = path.resolve(BACKEND_ROOT, '../..')
|
||||
|
||||
const SPECIAL_RUNTIME_ENV_NAMES = [
|
||||
'APP_BASE_URL',
|
||||
'CLAIM_BASE_URL',
|
||||
]
|
||||
const SPECIAL_RUNTIME_ENV_NAMES = ['APP_BASE_URL', 'CLAIM_BASE_URL']
|
||||
|
||||
const DEPLOYMENT_ONLY_ENV_NAMES = [
|
||||
'APP_DOMAIN',
|
||||
@@ -21,6 +18,8 @@ const DEPLOYMENT_ONLY_ENV_NAMES = [
|
||||
'CADDY_SITE_ADDR',
|
||||
'CHOKIDAR_USEPOLLING',
|
||||
'KUASHOU_INDUSTRY_RATE_LIMIT_MAX',
|
||||
'MINIO_ROOT_PASSWORD',
|
||||
'MINIO_ROOT_USER',
|
||||
'NPM_CONFIG_REGISTRY',
|
||||
'POSTGRES_DB',
|
||||
'POSTGRES_PASSWORD',
|
||||
|
||||
@@ -1,352 +1,215 @@
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
import type {
|
||||
AdminDefaultUser,
|
||||
KuaishouFeifeiProductRule,
|
||||
RuntimeConfig,
|
||||
} from "../types/runtime-config.js";
|
||||
import {
|
||||
parseBoolean,
|
||||
parseInteger,
|
||||
parseJsonArray,
|
||||
} from "./runtime-env.js";
|
||||
import { deepMerge } from "./runtime-merge.js";
|
||||
} from '../types/runtime-config.js'
|
||||
import { parseBoolean, parseInteger, parseJsonArray } from './runtime-env.js'
|
||||
import { deepMerge } from './runtime-merge.js'
|
||||
|
||||
type RuntimeConfigPath = readonly [string, ...string[]];
|
||||
type RuntimeConfigPath = readonly [string, ...string[]]
|
||||
type RuntimeConfigValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| AdminDefaultUser[]
|
||||
| KuaishouFeifeiProductRule[]
|
||||
| string[];
|
||||
type RuntimeEnv = Record<string, string | undefined>;
|
||||
| string[]
|
||||
type RuntimeEnv = Record<string, string | undefined>
|
||||
|
||||
type EnvOverride = {
|
||||
env: string;
|
||||
path: RuntimeConfigPath;
|
||||
read: (rawValue: unknown) => RuntimeConfigValue | null;
|
||||
};
|
||||
env: string
|
||||
path: RuntimeConfigPath
|
||||
read: (rawValue: unknown) => RuntimeConfigValue | null
|
||||
}
|
||||
|
||||
export const ENV_OVERRIDES: readonly EnvOverride[] = [
|
||||
integerEnv("PORT", ["server", "port"]),
|
||||
stringEnv("DATA_ROOT", ["data", "root"], path.resolve),
|
||||
stringEnv("LOG_LEVEL", ["logging", "level"]),
|
||||
stringEnv("LOG_INTEGRATION_LEVEL", ["logging", "integrationLevel"]),
|
||||
integerEnv("LOG_RETENTION_DAYS", ["logging", "retentionDays"]),
|
||||
stringEnv("DATABASE_URL", ["database", "url"]),
|
||||
booleanEnv("DATABASE_SSL", ["database", "ssl"]),
|
||||
integerEnv("DATABASE_MAX_CONNECTIONS", ["database", "maxConnections"]),
|
||||
integerEnv("CLAIM_TOKEN_TTL_HOURS", ["orders", "tokenTtlHours"]),
|
||||
stringEnv("ADMIN_SESSION_SECRET", ["admin", "sessionSecret"]),
|
||||
integerEnv("ADMIN_SESSION_TTL_HOURS", ["admin", "sessionTtlHours"]),
|
||||
adminUsersJsonEnv("ADMIN_DEFAULT_USERS_JSON", ["admin", "defaultUsers"]),
|
||||
stringEnv("KAQUAN91_USER_ID", ["platforms", "ninetyone", "userId"]),
|
||||
stringEnv("KAQUAN91_SECRET", ["platforms", "ninetyone", "secret"]),
|
||||
stringEnv("KAQUAN91_VERSION", ["platforms", "ninetyone", "version"]),
|
||||
stringEnv("KAQUAN91_SHOP_ID", ["platforms", "ninetyone", "shopId"]),
|
||||
stringEnv("KAQUAN91_SHOP_NAME", ["platforms", "ninetyone", "shopName"]),
|
||||
integerEnv("KAQUAN91_TIMESTAMP_TOLERANCE_SECONDS", [
|
||||
"platforms",
|
||||
"ninetyone",
|
||||
"timestampToleranceSeconds",
|
||||
integerEnv('PORT', ['server', 'port']),
|
||||
stringEnv('DATA_ROOT', ['data', 'root'], path.resolve),
|
||||
stringEnv('LOG_LEVEL', ['logging', 'level']),
|
||||
stringEnv('LOG_INTEGRATION_LEVEL', ['logging', 'integrationLevel']),
|
||||
integerEnv('LOG_RETENTION_DAYS', ['logging', 'retentionDays']),
|
||||
stringEnv('DATABASE_URL', ['database', 'url']),
|
||||
booleanEnv('DATABASE_SSL', ['database', 'ssl']),
|
||||
integerEnv('DATABASE_MAX_CONNECTIONS', ['database', 'maxConnections']),
|
||||
stringEnv('STORAGE_ENDPOINT', ['storage', 'endpoint']),
|
||||
stringEnv('STORAGE_BUCKET', ['storage', 'bucket']),
|
||||
stringEnv('STORAGE_ACCESS_KEY_ID', ['storage', 'accessKeyId']),
|
||||
stringEnv('STORAGE_SECRET_ACCESS_KEY', ['storage', 'secretAccessKey']),
|
||||
stringEnv('STORAGE_REGION', ['storage', 'region']),
|
||||
integerEnv('STORAGE_MAX_UPLOAD_SIZE_MB', ['storage', 'maxUploadSizeMb']),
|
||||
integerEnv('CLAIM_TOKEN_TTL_HOURS', ['orders', 'tokenTtlHours']),
|
||||
stringEnv('ADMIN_SESSION_SECRET', ['admin', 'sessionSecret']),
|
||||
integerEnv('ADMIN_SESSION_TTL_HOURS', ['admin', 'sessionTtlHours']),
|
||||
adminUsersJsonEnv('ADMIN_DEFAULT_USERS_JSON', ['admin', 'defaultUsers']),
|
||||
stringEnv('KAQUAN91_USER_ID', ['platforms', 'ninetyone', 'userId']),
|
||||
stringEnv('KAQUAN91_SECRET', ['platforms', 'ninetyone', 'secret']),
|
||||
stringEnv('KAQUAN91_VERSION', ['platforms', 'ninetyone', 'version']),
|
||||
stringEnv('KAQUAN91_SHOP_ID', ['platforms', 'ninetyone', 'shopId']),
|
||||
stringEnv('KAQUAN91_SHOP_NAME', ['platforms', 'ninetyone', 'shopName']),
|
||||
integerEnv('KAQUAN91_TIMESTAMP_TOLERANCE_SECONDS', [
|
||||
'platforms',
|
||||
'ninetyone',
|
||||
'timestampToleranceSeconds',
|
||||
]),
|
||||
stringEnv("KAQUAN91_CARDS_ENCODING", [
|
||||
"platforms",
|
||||
"ninetyone",
|
||||
"cardsEncoding",
|
||||
stringEnv('KAQUAN91_CARDS_ENCODING', ['platforms', 'ninetyone', 'cardsEncoding']),
|
||||
stringEnv('CLOUDTENTACLES_BASE_URL', ['platforms', 'cloudtentacles', 'baseUrl']),
|
||||
integerEnv('CLOUDTENTACLES_TIMEOUT_MS', ['platforms', 'cloudtentacles', 'timeoutMs']),
|
||||
stringEnv('CLOUDTENTACLES_SEND_SMS_PATH', ['platforms', 'cloudtentacles', 'sendSmsPath']),
|
||||
stringEnv('CLOUDTENTACLES_LOGIN_PATH', ['platforms', 'cloudtentacles', 'loginPath']),
|
||||
stringEnv('CLOUDTENTACLES_USER_INFO_PATH', ['platforms', 'cloudtentacles', 'userInfoPath']),
|
||||
stringEnv('CLOUDTENTACLES_ASSET_PATH', ['platforms', 'cloudtentacles', 'assetPath']),
|
||||
stringEnv('CLOUDTENTACLES_PERMISSION_PATH', ['platforms', 'cloudtentacles', 'permissionPath']),
|
||||
stringEnv('CLOUDTENTACLES_CATEGORIES_PATH', ['platforms', 'cloudtentacles', 'categoriesPath']),
|
||||
stringEnv('CLOUDTENTACLES_SKU_LIST_PATH', ['platforms', 'cloudtentacles', 'skuListPath']),
|
||||
stringEnv('CLOUDTENTACLES_SKU_BUY_PATH', ['platforms', 'cloudtentacles', 'skuBuyPath']),
|
||||
stringEnv('CLOUDTENTACLES_KNAPSACK_PATH', ['platforms', 'cloudtentacles', 'knapsackPath']),
|
||||
stringEnv('CLOUDTENTACLES_VN_LIST_PATH', ['platforms', 'cloudtentacles', 'vnListPath']),
|
||||
stringEnv('CLOUDTENTACLES_VN_APPOINT_PATH', ['platforms', 'cloudtentacles', 'vnAppointPath']),
|
||||
stringEnv('CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'vnGenerateLoginCodePath',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BASE_URL", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"baseUrl",
|
||||
stringEnv('CLOUDTENTACLES_VN_VERIF_CODE_PATH', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'vnVerifCodePath',
|
||||
]),
|
||||
integerEnv("CLOUDTENTACLES_TIMEOUT_MS", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"timeoutMs",
|
||||
stringEnv('CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'vnVerifyLoginCodePath',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_SEND_SMS_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"sendSmsPath",
|
||||
stringEnv('CLOUDTENTACLES_VN_BIND_URL_PATH', ['platforms', 'cloudtentacles', 'vnBindUrlPath']),
|
||||
stringEnv('CLOUDTENTACLES_VN_BIND_INFO_PATH', ['platforms', 'cloudtentacles', 'vnBindInfoPath']),
|
||||
stringEnv('CLOUDTENTACLES_VN_BACK_PATH', ['platforms', 'cloudtentacles', 'vnBackPath']),
|
||||
integerEnv('CLOUDTENTACLES_BIND_URL_TTL_SECONDS', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlTtlSeconds',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_LOGIN_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"loginPath",
|
||||
integerEnv('CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeIntervalSeconds',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_USER_INFO_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"userInfoPath",
|
||||
integerEnv('CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeTimeoutMs',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_ASSET_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"assetPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_USER_AGENT', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeUserAgent',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_PERMISSION_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"permissionPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_ENDPOINT', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeEndpoint',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_CATEGORIES_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"categoriesPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_CHART_ID', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeChartId',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_SKU_LIST_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"skuListPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_SUB_CHART_ID', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeSubChartId',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_SKU_BUY_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"skuBuyPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_IDE_TOKEN', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeIdeToken',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_KNAPSACK_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"knapsackPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeActivityUrl',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_LIST_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnListPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_REFERER', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeReferer',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_APPOINT_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnAppointPath",
|
||||
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_EXTRA_COOKIE', [
|
||||
'platforms',
|
||||
'cloudtentacles',
|
||||
'bindUrlProbeExtraCookie',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnGenerateLoginCodePath",
|
||||
stringEnv('CLOUDTENTACLES_PUBLIC_KEY_PEM', ['platforms', 'cloudtentacles', 'publicKeyPem']),
|
||||
stringEnv('CLOUDTENTACLES_CLIENT_SOURCE', ['platforms', 'cloudtentacles', 'clientSource']),
|
||||
stringEnv('CLOUDTENTACLES_DEVICE_ID', ['platforms', 'cloudtentacles', 'deviceId']),
|
||||
integerEnv('CLOUDTENTACLES_DEVICE_TYPE', ['platforms', 'cloudtentacles', 'deviceType']),
|
||||
stringEnv('KUASHOU_INDUSTRY_APP_KEY', ['platforms', 'kuaishouIndustry', 'appKey']),
|
||||
stringEnv('KUASHOU_INDUSTRY_APP_SECRET', ['platforms', 'kuaishouIndustry', 'appSecret']),
|
||||
stringEnv('KUASHOU_INDUSTRY_SIGN_SECRET', ['platforms', 'kuaishouIndustry', 'signSecret']),
|
||||
stringEnv('KUASHOU_INDUSTRY_MESSAGE_SECRET', ['platforms', 'kuaishouIndustry', 'messageSecret']),
|
||||
stringEnv('KUASHOU_INDUSTRY_PROVIDER', ['platforms', 'kuaishouIndustry', 'provider']),
|
||||
stringEnv('KUASHOU_INDUSTRY_SHOP_ID', ['platforms', 'kuaishouIndustry', 'shopId']),
|
||||
stringEnv('KUASHOU_INDUSTRY_SHOP_NAME', ['platforms', 'kuaishouIndustry', 'shopName']),
|
||||
stringEnv('KUASHOU_INDUSTRY_VERSION', ['platforms', 'kuaishouIndustry', 'version']),
|
||||
stringEnv('KUAISHOU_FEIFEI_BASE_URL', ['platforms', 'kuaishouFeifei', 'baseUrl']),
|
||||
stringEnv('KUAISHOU_FEIFEI_APP_KEY', ['platforms', 'kuaishouFeifei', 'appKey']),
|
||||
stringEnv('KUAISHOU_FEIFEI_APP_SECRET', ['platforms', 'kuaishouFeifei', 'appSecret']),
|
||||
integerEnv('KUAISHOU_FEIFEI_TIMEOUT_MS', ['platforms', 'kuaishouFeifei', 'timeoutMs']),
|
||||
stringEnv('KUAISHOU_FEIFEI_NOTIFY_URL', ['platforms', 'kuaishouFeifei', 'notifyUrl']),
|
||||
kuaishouFeifeiProductRulesEnv('KUAISHOU_FEIFEI_PRODUCT_RULES_JSON', [
|
||||
'platforms',
|
||||
'kuaishouFeifei',
|
||||
'productRules',
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_VERIF_CODE_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnVerifCodePath",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnVerifyLoginCodePath",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_BIND_URL_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnBindUrlPath",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_BIND_INFO_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnBindInfoPath",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_VN_BACK_PATH", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"vnBackPath",
|
||||
]),
|
||||
integerEnv("CLOUDTENTACLES_BIND_URL_TTL_SECONDS", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlTtlSeconds",
|
||||
]),
|
||||
integerEnv("CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeIntervalSeconds",
|
||||
]),
|
||||
integerEnv("CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeTimeoutMs",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_USER_AGENT", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeUserAgent",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_ENDPOINT", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeEndpoint",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_CHART_ID", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeChartId",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_SUB_CHART_ID", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeSubChartId",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_IDE_TOKEN", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeIdeToken",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeActivityUrl",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_REFERER", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeReferer",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_BIND_URL_PROBE_EXTRA_COOKIE", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"bindUrlProbeExtraCookie",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_PUBLIC_KEY_PEM", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"publicKeyPem",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_CLIENT_SOURCE", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"clientSource",
|
||||
]),
|
||||
stringEnv("CLOUDTENTACLES_DEVICE_ID", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"deviceId",
|
||||
]),
|
||||
integerEnv("CLOUDTENTACLES_DEVICE_TYPE", [
|
||||
"platforms",
|
||||
"cloudtentacles",
|
||||
"deviceType",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_APP_KEY", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"appKey",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_APP_SECRET", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"appSecret",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_SIGN_SECRET", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"signSecret",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_MESSAGE_SECRET", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"messageSecret",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_PROVIDER", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"provider",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_SHOP_ID", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"shopId",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_SHOP_NAME", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"shopName",
|
||||
]),
|
||||
stringEnv("KUASHOU_INDUSTRY_VERSION", [
|
||||
"platforms",
|
||||
"kuaishouIndustry",
|
||||
"version",
|
||||
]),
|
||||
stringEnv("KUAISHOU_FEIFEI_BASE_URL", [
|
||||
"platforms",
|
||||
"kuaishouFeifei",
|
||||
"baseUrl",
|
||||
]),
|
||||
stringEnv("KUAISHOU_FEIFEI_APP_KEY", [
|
||||
"platforms",
|
||||
"kuaishouFeifei",
|
||||
"appKey",
|
||||
]),
|
||||
stringEnv("KUAISHOU_FEIFEI_APP_SECRET", [
|
||||
"platforms",
|
||||
"kuaishouFeifei",
|
||||
"appSecret",
|
||||
]),
|
||||
integerEnv("KUAISHOU_FEIFEI_TIMEOUT_MS", [
|
||||
"platforms",
|
||||
"kuaishouFeifei",
|
||||
"timeoutMs",
|
||||
]),
|
||||
stringEnv("KUAISHOU_FEIFEI_NOTIFY_URL", [
|
||||
"platforms",
|
||||
"kuaishouFeifei",
|
||||
"notifyUrl",
|
||||
]),
|
||||
kuaishouFeifeiProductRulesEnv("KUAISHOU_FEIFEI_PRODUCT_RULES_JSON", [
|
||||
"platforms",
|
||||
"kuaishouFeifei",
|
||||
"productRules",
|
||||
]),
|
||||
corsOriginsEnv("CORS_ALLOWED_ORIGINS", ["cors", "allowedOrigins"]),
|
||||
];
|
||||
corsOriginsEnv('CORS_ALLOWED_ORIGINS', ['cors', 'allowedOrigins']),
|
||||
]
|
||||
|
||||
export function applyEnvOverrides(
|
||||
baseConfig: RuntimeConfig,
|
||||
env: RuntimeEnv = process.env
|
||||
env: RuntimeEnv = process.env,
|
||||
): RuntimeConfig {
|
||||
const nextConfig = deepMerge(baseConfig, {});
|
||||
const nextConfig = deepMerge(baseConfig, {})
|
||||
|
||||
applyClaimBaseUrlOverride(nextConfig, env);
|
||||
applyClaimBaseUrlOverride(nextConfig, env)
|
||||
|
||||
for (const override of ENV_OVERRIDES) {
|
||||
const value = override.read(env[override.env]);
|
||||
const value = override.read(env[override.env])
|
||||
if (value !== null) {
|
||||
setConfigValue(nextConfig, override.path, value);
|
||||
setConfigValue(nextConfig, override.path, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
return nextConfig
|
||||
}
|
||||
|
||||
function applyClaimBaseUrlOverride(
|
||||
config: RuntimeConfig,
|
||||
env: RuntimeEnv
|
||||
): void {
|
||||
const claimBaseUrl = parseString(env.CLAIM_BASE_URL);
|
||||
function applyClaimBaseUrlOverride(config: RuntimeConfig, env: RuntimeEnv): void {
|
||||
const claimBaseUrl = parseString(env.CLAIM_BASE_URL)
|
||||
if (claimBaseUrl !== null) {
|
||||
config.orders.claimBaseUrl = claimBaseUrl;
|
||||
return;
|
||||
config.orders.claimBaseUrl = claimBaseUrl
|
||||
return
|
||||
}
|
||||
|
||||
const appBaseUrl = parseString(env.APP_BASE_URL);
|
||||
const appBaseUrl = parseString(env.APP_BASE_URL)
|
||||
if (appBaseUrl !== null) {
|
||||
config.orders.claimBaseUrl = `${appBaseUrl.replace(/\/+$/, "")}/#/claim`;
|
||||
config.orders.claimBaseUrl = `${appBaseUrl.replace(/\/+$/, '')}/#/claim`
|
||||
}
|
||||
}
|
||||
|
||||
function stringEnv(
|
||||
env: string,
|
||||
configPath: RuntimeConfigPath,
|
||||
transform: (value: string) => string = (value) => value
|
||||
transform: (value: string) => string = (value) => value,
|
||||
): EnvOverride {
|
||||
return {
|
||||
env,
|
||||
path: configPath,
|
||||
read(rawValue) {
|
||||
const value = parseString(rawValue);
|
||||
return value === null ? null : transform(value);
|
||||
const value = parseString(rawValue)
|
||||
return value === null ? null : transform(value)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function integerEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
@@ -354,7 +217,7 @@ function integerEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
env,
|
||||
path: configPath,
|
||||
read: parseInteger,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function booleanEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
@@ -362,90 +225,74 @@ function booleanEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
env,
|
||||
path: configPath,
|
||||
read: parseBoolean,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function adminUsersJsonEnv(
|
||||
env: string,
|
||||
configPath: RuntimeConfigPath
|
||||
): EnvOverride {
|
||||
function adminUsersJsonEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
return {
|
||||
env,
|
||||
path: configPath,
|
||||
read(rawValue) {
|
||||
return parseJsonArray<AdminDefaultUser>(rawValue);
|
||||
return parseJsonArray<AdminDefaultUser>(rawValue)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function kuaishouFeifeiProductRulesEnv(
|
||||
env: string,
|
||||
configPath: RuntimeConfigPath
|
||||
): EnvOverride {
|
||||
function kuaishouFeifeiProductRulesEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
return {
|
||||
env,
|
||||
path: configPath,
|
||||
read(rawValue) {
|
||||
const parsed = parseJsonArray<KuaishouFeifeiProductRule>(rawValue);
|
||||
return parsed === null ? null : parsed;
|
||||
const parsed = parseJsonArray<KuaishouFeifeiProductRule>(rawValue)
|
||||
return parsed === null ? null : parsed
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function corsOriginsEnv(
|
||||
env: string,
|
||||
configPath: RuntimeConfigPath
|
||||
): EnvOverride {
|
||||
function corsOriginsEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
||||
return {
|
||||
env,
|
||||
path: configPath,
|
||||
read(rawValue) {
|
||||
const value = parseString(rawValue);
|
||||
const value = parseString(rawValue)
|
||||
if (value === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (value.trim() === "*") {
|
||||
return ["*"];
|
||||
if (value.trim() === '*') {
|
||||
return ['*']
|
||||
}
|
||||
return value
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
.filter(Boolean)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseString(rawValue: unknown): string | null {
|
||||
const value = String(rawValue || "").trim();
|
||||
return value ? value : null;
|
||||
const value = String(rawValue || '').trim()
|
||||
return value ? value : null
|
||||
}
|
||||
|
||||
function setConfigValue(
|
||||
config: RuntimeConfig,
|
||||
configPath: RuntimeConfigPath,
|
||||
value: RuntimeConfigValue
|
||||
value: RuntimeConfigValue,
|
||||
): void {
|
||||
let current: Record<string, unknown> = config as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
let current: Record<string, unknown> = config as unknown as Record<string, unknown>
|
||||
|
||||
for (const key of configPath.slice(0, -1)) {
|
||||
const nextValue = current[key];
|
||||
if (
|
||||
!nextValue ||
|
||||
typeof nextValue !== "object" ||
|
||||
Array.isArray(nextValue)
|
||||
) {
|
||||
current[key] = {};
|
||||
const nextValue = current[key]
|
||||
if (!nextValue || typeof nextValue !== 'object' || Array.isArray(nextValue)) {
|
||||
current[key] = {}
|
||||
}
|
||||
current = current[key] as Record<string, unknown>;
|
||||
current = current[key] as Record<string, unknown>
|
||||
}
|
||||
|
||||
const lastKey = configPath[configPath.length - 1];
|
||||
const lastKey = configPath[configPath.length - 1]
|
||||
if (!lastKey) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
current[lastKey] = value;
|
||||
current[lastKey] = value
|
||||
}
|
||||
|
||||
@@ -10,54 +10,63 @@ import { deepMerge } from './runtime-merge.js'
|
||||
import type { RuntimeConfig } from '../types/runtime-config.js'
|
||||
|
||||
test('validateRuntimeConfig allows missing admin credentials outside production', () => {
|
||||
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||
admin: {
|
||||
sessionSecret: '',
|
||||
defaultUsers: [],
|
||||
const issues = validateRuntimeConfig(
|
||||
createRuntimeConfig({
|
||||
admin: {
|
||||
sessionSecret: '',
|
||||
defaultUsers: [],
|
||||
},
|
||||
}),
|
||||
{
|
||||
env: { NODE_ENV: 'development' },
|
||||
},
|
||||
}), {
|
||||
env: { NODE_ENV: 'development' },
|
||||
})
|
||||
)
|
||||
|
||||
assert.deepEqual(issues, [])
|
||||
})
|
||||
|
||||
test('validateRuntimeConfig requires admin credentials in production', () => {
|
||||
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||
admin: {
|
||||
sessionSecret: '',
|
||||
defaultUsers: [],
|
||||
const issues = validateRuntimeConfig(
|
||||
createRuntimeConfig({
|
||||
admin: {
|
||||
sessionSecret: '',
|
||||
defaultUsers: [],
|
||||
},
|
||||
}),
|
||||
{
|
||||
env: { NODE_ENV: 'production' },
|
||||
},
|
||||
}), {
|
||||
env: { NODE_ENV: 'production' },
|
||||
})
|
||||
)
|
||||
|
||||
assert.deepEqual(issues.map((issue) => issue.path), [
|
||||
'admin.sessionSecret',
|
||||
'admin.defaultUsers',
|
||||
])
|
||||
assert.deepEqual(
|
||||
issues.map((issue) => issue.path),
|
||||
['admin.sessionSecret', 'admin.defaultUsers'],
|
||||
)
|
||||
})
|
||||
|
||||
test('validateRuntimeConfig rejects unsafe production placeholders', () => {
|
||||
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||
admin: {
|
||||
sessionSecret: 'change-me-long-random-session-secret',
|
||||
defaultUsers: [
|
||||
{
|
||||
username: 'admin',
|
||||
password: 'change-me-admin-password',
|
||||
role: 'admin',
|
||||
},
|
||||
],
|
||||
const issues = validateRuntimeConfig(
|
||||
createRuntimeConfig({
|
||||
admin: {
|
||||
sessionSecret: 'change-me-long-random-session-secret',
|
||||
defaultUsers: [
|
||||
{
|
||||
username: 'admin',
|
||||
password: 'change-me-admin-password',
|
||||
role: 'admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{
|
||||
env: { NODE_ENV: 'production' },
|
||||
},
|
||||
}), {
|
||||
env: { NODE_ENV: 'production' },
|
||||
})
|
||||
)
|
||||
|
||||
assert.deepEqual(issues.map((issue) => issue.path), [
|
||||
'admin.sessionSecret',
|
||||
'admin.defaultUsers[0].password',
|
||||
])
|
||||
assert.deepEqual(
|
||||
issues.map((issue) => issue.path),
|
||||
['admin.sessionSecret', 'admin.defaultUsers[0].password'],
|
||||
)
|
||||
})
|
||||
|
||||
test('validateRuntimeConfig accepts complete production config', () => {
|
||||
@@ -69,123 +78,140 @@ test('validateRuntimeConfig accepts complete production config', () => {
|
||||
})
|
||||
|
||||
test('validateRuntimeConfig catches invalid numeric and url values', () => {
|
||||
const issues = validateRuntimeConfig(createRuntimeConfig({
|
||||
server: {
|
||||
port: 70000,
|
||||
const issues = validateRuntimeConfig(
|
||||
createRuntimeConfig({
|
||||
server: {
|
||||
port: 70000,
|
||||
},
|
||||
database: {
|
||||
maxConnections: 0,
|
||||
},
|
||||
orders: {
|
||||
claimBaseUrl: 'not-a-url',
|
||||
tokenTtlHours: 0,
|
||||
},
|
||||
}),
|
||||
{
|
||||
env: { NODE_ENV: 'development' },
|
||||
},
|
||||
database: {
|
||||
maxConnections: 0,
|
||||
},
|
||||
orders: {
|
||||
claimBaseUrl: 'not-a-url',
|
||||
tokenTtlHours: 0,
|
||||
},
|
||||
}), {
|
||||
env: { NODE_ENV: 'development' },
|
||||
})
|
||||
)
|
||||
|
||||
assert.deepEqual(issues.map((issue) => issue.path), [
|
||||
'server.port',
|
||||
'database.maxConnections',
|
||||
'orders.tokenTtlHours',
|
||||
'orders.claimBaseUrl',
|
||||
])
|
||||
assert.deepEqual(
|
||||
issues.map((issue) => issue.path),
|
||||
['server.port', 'database.maxConnections', 'orders.tokenTtlHours', 'orders.claimBaseUrl'],
|
||||
)
|
||||
})
|
||||
|
||||
test('assertRuntimeConfigValid throws a readable validation error', () => {
|
||||
assert.throws(() => {
|
||||
assertRuntimeConfigValid(createRuntimeConfig({
|
||||
server: {
|
||||
port: 0,
|
||||
},
|
||||
}))
|
||||
}, (error) => {
|
||||
assert.equal(error instanceof RuntimeConfigValidationError, true)
|
||||
assert.match((error as Error).message, /运行时配置校验失败/)
|
||||
assert.match((error as Error).message, /server\.port/)
|
||||
return true
|
||||
})
|
||||
assert.throws(
|
||||
() => {
|
||||
assertRuntimeConfigValid(
|
||||
createRuntimeConfig({
|
||||
server: {
|
||||
port: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
(error) => {
|
||||
assert.equal(error instanceof RuntimeConfigValidationError, true)
|
||||
assert.match((error as Error).message, /运行时配置校验失败/)
|
||||
assert.match((error as Error).message, /server\.port/)
|
||||
return true
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
function createRuntimeConfig(overrides: Record<string, unknown> = {}): RuntimeConfig {
|
||||
return deepMerge({
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
data: {
|
||||
root: '/tmp/order-site',
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
integrationLevel: 'warn',
|
||||
},
|
||||
database: {
|
||||
url: 'postgres://postgres:postgres@postgres:5432/order_site',
|
||||
ssl: false,
|
||||
maxConnections: 10,
|
||||
},
|
||||
orders: {
|
||||
claimBaseUrl: 'https://order.example.test/#/claim',
|
||||
tokenTtlHours: 24,
|
||||
},
|
||||
admin: {
|
||||
sessionSecret: '0123456789abcdef0123456789abcdef',
|
||||
sessionTtlHours: 12,
|
||||
defaultUsers: [
|
||||
{
|
||||
username: 'admin',
|
||||
password: 'Qp7xT2mN9vR4',
|
||||
role: 'admin',
|
||||
return deepMerge(
|
||||
{
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
data: {
|
||||
root: '/tmp/order-site',
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
integrationLevel: 'warn',
|
||||
},
|
||||
database: {
|
||||
url: 'postgres://postgres:postgres@postgres:5432/order_site',
|
||||
ssl: false,
|
||||
maxConnections: 10,
|
||||
},
|
||||
storage: {
|
||||
endpoint: 'http://minio:9000',
|
||||
bucket: 'order-site',
|
||||
accessKeyId: 'minioadmin',
|
||||
secretAccessKey: 'minioadmin',
|
||||
region: 'us-east-1',
|
||||
maxUploadSizeMb: 10,
|
||||
},
|
||||
orders: {
|
||||
claimBaseUrl: 'https://order.example.test/#/claim',
|
||||
tokenTtlHours: 24,
|
||||
},
|
||||
admin: {
|
||||
sessionSecret: '0123456789abcdef0123456789abcdef',
|
||||
sessionTtlHours: 12,
|
||||
defaultUsers: [
|
||||
{
|
||||
username: 'admin',
|
||||
password: 'Qp7xT2mN9vR4',
|
||||
role: 'admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
platforms: {
|
||||
ninetyone: {
|
||||
userId: '',
|
||||
secret: '',
|
||||
version: '1.0',
|
||||
shopId: '91kaquan',
|
||||
shopName: '91卡券',
|
||||
timestampToleranceSeconds: 600,
|
||||
cardsEncoding: 'aes-256-ecb-base64',
|
||||
},
|
||||
cloudtentacles: {
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
timeoutMs: 5000,
|
||||
sendSmsPath: '/public/verif_code',
|
||||
loginPath: '/public/login',
|
||||
userInfoPath: '/user/info',
|
||||
assetPath: '/user/get_asset',
|
||||
permissionPath: '/user/get_permission',
|
||||
categoriesPath: '/categories/get',
|
||||
skuListPath: '/sku/list',
|
||||
skuBuyPath: '/sku/buy',
|
||||
skuUsePath: '/sku/use',
|
||||
knapsackPath: '/user/get_knapsack',
|
||||
vnListPath: '/vn/list',
|
||||
vnAppointPath: '/vn/appoint',
|
||||
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
||||
vnVerifCodePath: '/public/vn_verif_code',
|
||||
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||
vnBindUrlPath: '/vn/bind_url',
|
||||
vnBindInfoPath: '/vn/bind_info',
|
||||
vnBackPath: '/vn/back',
|
||||
bindUrlTtlSeconds: 600,
|
||||
bindUrlProbeIntervalSeconds: 30,
|
||||
bindUrlProbeTimeoutMs: 5000,
|
||||
bindUrlProbeUserAgent: '',
|
||||
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
||||
bindUrlProbeChartId: '323794',
|
||||
bindUrlProbeSubChartId: '323794',
|
||||
bindUrlProbeIdeToken: 'z90Syo',
|
||||
bindUrlProbeActivityUrl: '',
|
||||
bindUrlProbeReferer: 'https://gp.qq.com/',
|
||||
bindUrlProbeExtraCookie: '',
|
||||
publicKeyPem: '',
|
||||
clientSource: 'ct-client',
|
||||
deviceId: '08bc9d8c-fd15-48ea-bc00-8d754076cafc',
|
||||
deviceType: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
platforms: {
|
||||
ninetyone: {
|
||||
userId: '',
|
||||
secret: '',
|
||||
version: '1.0',
|
||||
shopId: '91kaquan',
|
||||
shopName: '91卡券',
|
||||
timestampToleranceSeconds: 600,
|
||||
cardsEncoding: 'aes-256-ecb-base64',
|
||||
},
|
||||
cloudtentacles: {
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
timeoutMs: 5000,
|
||||
sendSmsPath: '/public/verif_code',
|
||||
loginPath: '/public/login',
|
||||
userInfoPath: '/user/info',
|
||||
assetPath: '/user/get_asset',
|
||||
permissionPath: '/user/get_permission',
|
||||
categoriesPath: '/categories/get',
|
||||
skuListPath: '/sku/list',
|
||||
skuBuyPath: '/sku/buy',
|
||||
skuUsePath: '/sku/use',
|
||||
knapsackPath: '/user/get_knapsack',
|
||||
vnListPath: '/vn/list',
|
||||
vnAppointPath: '/vn/appoint',
|
||||
vnGenerateLoginCodePath: '/vn/generate_login_code',
|
||||
vnVerifCodePath: '/public/vn_verif_code',
|
||||
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||
vnBindUrlPath: '/vn/bind_url',
|
||||
vnBindInfoPath: '/vn/bind_info',
|
||||
vnBackPath: '/vn/back',
|
||||
bindUrlTtlSeconds: 600,
|
||||
bindUrlProbeIntervalSeconds: 30,
|
||||
bindUrlProbeTimeoutMs: 5000,
|
||||
bindUrlProbeUserAgent: '',
|
||||
bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/',
|
||||
bindUrlProbeChartId: '323794',
|
||||
bindUrlProbeSubChartId: '323794',
|
||||
bindUrlProbeIdeToken: 'z90Syo',
|
||||
bindUrlProbeActivityUrl: '',
|
||||
bindUrlProbeReferer: 'https://gp.qq.com/',
|
||||
bindUrlProbeExtraCookie: '',
|
||||
publicKeyPem: '',
|
||||
clientSource: 'ct-client',
|
||||
deviceId: '08bc9d8c-fd15-48ea-bc00-8d754076cafc',
|
||||
deviceType: 1,
|
||||
},
|
||||
},
|
||||
}, overrides) as RuntimeConfig
|
||||
overrides,
|
||||
) as RuntimeConfig
|
||||
}
|
||||
|
||||
@@ -45,13 +45,32 @@ export function validateRuntimeConfig(
|
||||
|
||||
requireInteger(issues, 'server.port', config.server?.port, { min: 1, max: 65535 })
|
||||
requireInteger(issues, 'database.maxConnections', config.database?.maxConnections, { min: 1 })
|
||||
requireInteger(issues, 'storage.maxUploadSizeMb', config.storage?.maxUploadSizeMb, {
|
||||
min: 1,
|
||||
max: 100,
|
||||
})
|
||||
requireInteger(issues, 'orders.tokenTtlHours', config.orders?.tokenTtlHours, { min: 1 })
|
||||
requireInteger(issues, 'admin.sessionTtlHours', config.admin?.sessionTtlHours, { min: 1 })
|
||||
requireInteger(issues, 'platforms.cloudtentacles.timeoutMs', config.platforms?.cloudtentacles?.timeoutMs, { min: 1 })
|
||||
requireInteger(
|
||||
issues,
|
||||
'platforms.cloudtentacles.timeoutMs',
|
||||
config.platforms?.cloudtentacles?.timeoutMs,
|
||||
{ min: 1 },
|
||||
)
|
||||
if (config.platforms?.kuaishouFeifei) {
|
||||
requireInteger(issues, 'platforms.kuaishouFeifei.timeoutMs', config.platforms.kuaishouFeifei.timeoutMs, { min: 1 })
|
||||
requireInteger(
|
||||
issues,
|
||||
'platforms.kuaishouFeifei.timeoutMs',
|
||||
config.platforms.kuaishouFeifei.timeoutMs,
|
||||
{ min: 1 },
|
||||
)
|
||||
}
|
||||
requireInteger(issues, 'platforms.cloudtentacles.bindUrlTtlSeconds', config.platforms?.cloudtentacles?.bindUrlTtlSeconds, { min: 1 })
|
||||
requireInteger(
|
||||
issues,
|
||||
'platforms.cloudtentacles.bindUrlTtlSeconds',
|
||||
config.platforms?.cloudtentacles?.bindUrlTtlSeconds,
|
||||
{ min: 1 },
|
||||
)
|
||||
requireInteger(
|
||||
issues,
|
||||
'platforms.cloudtentacles.bindUrlProbeIntervalSeconds',
|
||||
@@ -66,10 +85,18 @@ export function validateRuntimeConfig(
|
||||
)
|
||||
|
||||
validateOptionalHttpUrl(issues, 'orders.claimBaseUrl', config.orders?.claimBaseUrl)
|
||||
validateOptionalStorageEndpoint(issues, 'storage.endpoint', config.storage?.endpoint)
|
||||
validateRequiredString(issues, 'data.root', config.data?.root)
|
||||
|
||||
if (productionLike) {
|
||||
validateRequiredString(issues, 'database.url', config.database?.url)
|
||||
validateRequiredString(issues, 'storage.endpoint', config.storage?.endpoint)
|
||||
validateOptionalStorageEndpoint(issues, 'storage.endpoint', config.storage?.endpoint)
|
||||
validateRequiredString(issues, 'storage.bucket', config.storage?.bucket)
|
||||
validateRequiredString(issues, 'storage.accessKeyId', config.storage?.accessKeyId)
|
||||
validateSecret(issues, 'storage.secretAccessKey', config.storage?.secretAccessKey, {
|
||||
minLength: 8,
|
||||
})
|
||||
validateRequiredHttpUrl(issues, 'orders.claimBaseUrl', config.orders?.claimBaseUrl)
|
||||
validateSecret(issues, 'admin.sessionSecret', config.admin?.sessionSecret, { minLength: 32 })
|
||||
validateAdminDefaultUsers(issues, config.admin?.defaultUsers)
|
||||
@@ -96,7 +123,9 @@ function validateAdminDefaultUsers(
|
||||
|
||||
users.forEach((user, index) => {
|
||||
validateRequiredString(issues, `admin.defaultUsers[${index}].username`, user?.username)
|
||||
validateSecret(issues, `admin.defaultUsers[${index}].password`, user?.password, { minLength: 8 })
|
||||
validateSecret(issues, `admin.defaultUsers[${index}].password`, user?.password, {
|
||||
minLength: 8,
|
||||
})
|
||||
validateRequiredString(issues, `admin.defaultUsers[${index}].role`, user?.role)
|
||||
})
|
||||
}
|
||||
@@ -171,11 +200,34 @@ function validateRequiredHttpUrl(
|
||||
validateOptionalHttpUrl(issues, configPath, value)
|
||||
}
|
||||
|
||||
function validateOptionalStorageEndpoint(
|
||||
issues: RuntimeConfigValidationIssue[],
|
||||
configPath: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(text)) {
|
||||
validateOptionalHttpUrl(issues, configPath, text)
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9.-]+(?::\d{1,5})?$/i.test(text)) {
|
||||
issues.push({
|
||||
path: configPath,
|
||||
message: '必须是 http/https URL 或 host:port',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function requireInteger(
|
||||
issues: RuntimeConfigValidationIssue[],
|
||||
configPath: string,
|
||||
value: unknown,
|
||||
{ min, max }: { min?: number, max?: number } = {},
|
||||
{ min, max }: { min?: number; max?: number } = {},
|
||||
): void {
|
||||
const numberValue = Number(value)
|
||||
if (!Number.isInteger(numberValue)) {
|
||||
|
||||
Reference in New Issue
Block a user