import { fileURLToPath, pathToFileURL } from 'node:url'; import pg from 'pg'; const DEFAULT_TENANT_ID = '00000000-0000-0000-0000-000000000001'; function envString(env, key, fallback = '') { return typeof env[key] === 'string' && env[key].trim() ? env[key].trim() : fallback; } function envNumberString(env, key, fallback) { const value = envString(env, key, fallback); return /^\d+$/.test(value) ? value : fallback; } function envJsonObject(env, key, fallback) { const raw = envString(env, key); if (!raw) return fallback; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error(`${key} must be a JSON object`); } return parsed; } function buildConfig(env = process.env) { const databaseUrl = envString(env, 'DATABASE_URL'); const tenantId = envString(env, 'PNVS_TENANT_ID', envString(env, 'TENANT_ID', DEFAULT_TENANT_ID)); const accessKeyId = envString(env, 'ALIYUN_ACCESS_KEY_ID'); const accessKeySecret = envString(env, 'ALIYUN_ACCESS_KEY_SECRET'); const signName = envString(env, 'ALIYUN_PNVS_SIGN_NAME', envString(env, 'ALIYUN_SIGN_NAME')); const templateCode = envString(env, 'ALIYUN_PNVS_TEMPLATE_CODE', envString(env, 'ALIYUN_TEMPLATE_CODE')); const missing = []; if (!databaseUrl) missing.push('DATABASE_URL'); if (!tenantId) missing.push('PNVS_TENANT_ID or TENANT_ID'); if (!accessKeyId) missing.push('ALIYUN_ACCESS_KEY_ID'); if (!accessKeySecret) missing.push('ALIYUN_ACCESS_KEY_SECRET'); if (!signName) missing.push('ALIYUN_PNVS_SIGN_NAME or ALIYUN_SIGN_NAME'); if (!templateCode) missing.push('ALIYUN_PNVS_TEMPLATE_CODE or ALIYUN_TEMPLATE_CODE'); if (missing.length > 0) throw new Error(`Missing required PNVS config env: ${missing.join(', ')}`); const validTime = envNumberString(env, 'ALIYUN_PNVS_VALID_TIME', '300'); const interval = envNumberString(env, 'ALIYUN_PNVS_INTERVAL', '60'); const templateParam = envJsonObject(env, 'ALIYUN_PNVS_TEMPLATE_PARAM', { code: '##code##', min: String(Math.max(1, Math.ceil(Number(validTime) / 60))), }); if (!Object.values(templateParam).some(value => String(value) === '##code##')) { templateParam.code = '##code##'; } return { databaseUrl, tenantId, accessKeyId, accessKeySecret, signName, templateCode, displayName: envString(env, 'ALIYUN_PNVS_DISPLAY_NAME', '阿里云短信认证'), endpoint: envString(env, 'ALIYUN_PNVS_ENDPOINT', 'https://dypnsapi.aliyuncs.com'), regionId: envString(env, 'ALIYUN_PNVS_REGION_ID', 'cn-hangzhou'), countryCode: envString(env, 'ALIYUN_PNVS_COUNTRY_CODE', '86'), codeType: envNumberString(env, 'ALIYUN_PNVS_CODE_TYPE', '1'), codeLength: envNumberString(env, 'ALIYUN_PNVS_CODE_LENGTH', '6'), validTime, interval, duplicatePolicy: envNumberString(env, 'ALIYUN_PNVS_DUPLICATE_POLICY', '1'), schemeName: envString(env, 'ALIYUN_PNVS_SCHEME_NAME'), templateParam, }; } function publicConfig(config) { return { signName: config.signName, templateCode: config.templateCode, endpoint: config.endpoint, regionId: config.regionId, countryCode: config.countryCode, templateParam: config.templateParam, codeType: config.codeType, codeLength: config.codeLength, validTime: config.validTime, interval: config.interval, duplicatePolicy: config.duplicatePolicy, ...(config.schemeName ? { schemeName: config.schemeName } : {}), secretRef: 'app_private.tenant_secrets:sms:aliyun-pnvs', }; } async function configureAliyunPnvsProvider(inputConfig, options = {}) { const config = inputConfig?.databaseUrl ? inputConfig : buildConfig(options.env || process.env); const configPublic = publicConfig(config); if (options.dryRun) { return { tenantId: config.tenantId, provider: 'aliyun-pnvs', configPublic, secretScope: 'sms', secretKey: 'aliyun-pnvs', }; } const pool = new pg.Pool({ connectionString: config.databaseUrl, max: 1 }); try { await pool.query('begin'); await pool.query( ` insert into app_private.tenant_secrets ( tenant_id, secret_scope, secret_key, provider, secret_json, last_rotated_at ) values ($1::uuid, 'sms', 'aliyun-pnvs', 'aliyun-pnvs', $2::jsonb, now()) on conflict (tenant_id, secret_scope, secret_key) do update set provider = excluded.provider, secret_json = excluded.secret_json, last_rotated_at = now(), updated_at = now() `, [ config.tenantId, JSON.stringify({ accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret, }), ], ); const providerResult = await pool.query( ` insert into public.tenant_auth_providers ( tenant_id, provider, status, display_name, config_public ) values ($1::uuid, 'aliyun-pnvs', 'active', $2, $3::jsonb) on conflict (tenant_id, provider) do update set status = excluded.status, display_name = excluded.display_name, config_public = excluded.config_public, updated_at = now() returning tenant_id as "tenantId", provider, status, config_public as "configPublic" `, [config.tenantId, config.displayName, JSON.stringify(configPublic)], ); await pool.query('commit'); return providerResult.rows[0]; } catch (error) { await pool.query('rollback').catch(() => undefined); throw error; } finally { await pool.end(); } } async function main() { try { const dryRun = process.argv.includes('--dry-run'); const result = await configureAliyunPnvsProvider(null, { dryRun }); console.log(JSON.stringify({ ok: true, dryRun, tenantId: result.tenantId, provider: result.provider, status: result.status || 'active', secretRef: result.configPublic?.secretRef || 'app_private.tenant_secrets:sms:aliyun-pnvs', }, null, 2)); } catch (error) { console.error(error.message); console.error(` Required example: DATABASE_URL=postgresql://tiku_app:***@127.0.0.1:54322/postgres PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 ALIYUN_ACCESS_KEY_ID= ALIYUN_ACCESS_KEY_SECRET= ALIYUN_PNVS_SIGN_NAME= ALIYUN_PNVS_TEMPLATE_CODE= Optional: ALIYUN_PNVS_TEMPLATE_PARAM='{"code":"##code##","min":"5"}' ALIYUN_PNVS_VALID_TIME=300 ALIYUN_PNVS_INTERVAL=60 `); process.exitCode = 1; } } const currentFile = fileURLToPath(import.meta.url); if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) { await main(); } export { buildConfig, configureAliyunPnvsProvider, publicConfig };