import { fileURLToPath, pathToFileURL } from 'node:url'; import pg from 'pg'; const DEFAULT_TENANT_ID = '00000000-0000-0000-0000-000000000001'; const PNVS_ALIASES = new Set(['aliyun-pnvs', 'aliyun_pnvs', 'aliyun-pnvs-sms', 'aliyun_sms_auth', 'aliyun-sms-auth']); function envString(env, key, fallback = '') { return typeof env[key] === 'string' && env[key].trim() ? env[key].trim() : fallback; } function normalizeProvider(value) { return String(value || '').trim().toLowerCase().replace(/_/g, '-'); } function isPnvsProvider(value) { return PNVS_ALIASES.has(String(value || '').trim().toLowerCase()) || PNVS_ALIASES.has(normalizeProvider(value)); } function parseSecretRef(value) { const raw = String(value || '').trim(); const parts = raw.split(':'); if (parts.length !== 3 || parts[0] !== 'app_private.tenant_secrets') { return { raw, scope: 'sms', key: 'aliyun-pnvs', valid: !raw }; } return { raw, scope: parts[1] || 'sms', key: parts[2] || 'aliyun-pnvs', valid: true }; } function templateParamHasCodePlaceholder(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; return Object.values(value).some(item => String(item) === '##code##'); } function mask(value) { const text = String(value || ''); if (!text) return ''; if (text.length <= 8) return `${text.slice(0, 2)}...${text.slice(-2)}`; return `${text.slice(0, 4)}...${text.slice(-4)}`; } function summarizePublicConfig(configPublic = {}) { const secretRef = parseSecretRef(configPublic.secretRef); return { signName: typeof configPublic.signName === 'string' ? configPublic.signName : '', templateCode: typeof configPublic.templateCode === 'string' ? configPublic.templateCode : '', endpoint: typeof configPublic.endpoint === 'string' ? configPublic.endpoint : '', regionId: typeof configPublic.regionId === 'string' ? configPublic.regionId : '', countryCode: typeof configPublic.countryCode === 'string' ? configPublic.countryCode : '', codeType: typeof configPublic.codeType === 'string' ? configPublic.codeType : '', codeLength: typeof configPublic.codeLength === 'string' ? configPublic.codeLength : '', validTime: typeof configPublic.validTime === 'string' ? configPublic.validTime : '', interval: typeof configPublic.interval === 'string' ? configPublic.interval : '', templateParamHasCodePlaceholder: templateParamHasCodePlaceholder(configPublic.templateParam), secretRef: secretRef.raw || 'app_private.tenant_secrets:sms:aliyun-pnvs', secretRefValid: secretRef.valid, secretScope: secretRef.scope, secretKey: secretRef.key, }; } function summarizeSecret(row) { if (!row) return { found: false }; const accessKeyId = String(row.accessKeyId || ''); const accessKeySecret = String(row.accessKeySecret || ''); return { found: true, provider: row.provider || '', accessKeyIdLength: accessKeyId.length, accessKeyIdMasked: mask(accessKeyId), accessKeySecretLength: accessKeySecret.length, accessKeyIdHasEdgeWhitespace: /^\s|\s$/.test(accessKeyId), accessKeySecretHasEdgeWhitespace: /^\s|\s$/.test(accessKeySecret), lastRotatedAt: row.lastRotatedAt || null, }; } 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 authSmsProvider = envString(env, 'AUTH_SMS_PROVIDER'); if (!databaseUrl) throw new Error('Missing required env: DATABASE_URL'); if (!tenantId) throw new Error('Missing required env: PNVS_TENANT_ID or TENANT_ID'); return { databaseUrl, tenantId, authSmsProvider }; } async function queryProviderRows(query, tenantId) { const result = await query( ` select tenant_id as "tenantId", provider, status, display_name as "displayName", config_public as "configPublic", updated_at as "updatedAt" from public.tenant_auth_providers where tenant_id = $1::uuid and lower(replace(provider, '_', '-')) in ('aliyun-pnvs', 'aliyun-pnvs-sms', 'aliyun-sms-auth') order by case status when 'active' then 0 when 'testing' then 1 else 2 end, updated_at desc nulls last `, [tenantId], ); return result.rows || []; } async function querySecretRow(query, tenantId, secretScope, secretKey) { const result = await query( ` select provider, secret_json->>'accessKeyId' as "accessKeyId", secret_json->>'accessKeySecret' as "accessKeySecret", last_rotated_at as "lastRotatedAt" from app_private.tenant_secrets where tenant_id = $1::uuid and secret_scope = $2 and secret_key = $3 limit 1 `, [tenantId, secretScope, secretKey], ); return result.rows?.[0] || null; } function buildChecks({ config, providerRow, publicConfig, secret }) { const checks = []; const add = (status, id, message, details = {}) => checks.push({ status, id, message, details }); if (isPnvsProvider(config.authSmsProvider)) { add('pass', 'env.auth_sms_provider', 'AUTH_SMS_PROVIDER is set to aliyun-pnvs-compatible provider', { provider: config.authSmsProvider, }); } else { add('blocker', 'env.auth_sms_provider', 'AUTH_SMS_PROVIDER should be aliyun-pnvs for PNVS SMS authentication', { provider: config.authSmsProvider || '(unset)', }); } if (!providerRow) { add('blocker', 'db.auth_provider', 'No aliyun-pnvs tenant_auth_providers row exists for tenant'); return checks; } if (providerRow.status === 'active' || providerRow.status === 'testing') { add('pass', 'db.auth_provider.status', 'PNVS tenant auth provider is active/testing', { provider: providerRow.provider, status: providerRow.status, }); } else { add('blocker', 'db.auth_provider.status', 'PNVS tenant auth provider should be active or testing', { provider: providerRow.provider, status: providerRow.status, }); } for (const key of ['signName', 'templateCode']) { if (publicConfig[key]) { add('pass', `db.auth_provider.${key}`, `PNVS public config has ${key}`); } else { add('blocker', `db.auth_provider.${key}`, `PNVS public config is missing ${key}`); } } if (publicConfig.endpoint && !/^https:\/\/[^/]*aliyuncs\.com\b/i.test(publicConfig.endpoint)) { add('blocker', 'db.auth_provider.endpoint', 'PNVS endpoint should be an aliyuncs.com HTTPS endpoint', { endpoint: publicConfig.endpoint, }); } else { add('pass', 'db.auth_provider.endpoint', 'PNVS endpoint is aliyuncs.com HTTPS or default-compatible', { endpoint: publicConfig.endpoint || '(default)', }); } if (publicConfig.templateParamHasCodePlaceholder) { add('pass', 'db.auth_provider.template_param', 'PNVS templateParam preserves ##code## placeholder'); } else { add('warn', 'db.auth_provider.template_param', 'PNVS templateParam lacks ##code##; backend can add it, but configure script should preserve it'); } if (publicConfig.secretRefValid) { add('pass', 'db.auth_provider.secret_ref', 'PNVS secretRef is valid', { secretRef: publicConfig.secretRef, }); } else { add('blocker', 'db.auth_provider.secret_ref', 'PNVS secretRef is invalid', { secretRef: publicConfig.secretRef, }); } if (!secret.found) { add('blocker', 'db.tenant_secret', 'PNVS tenant secret row is missing', { secretScope: publicConfig.secretScope, secretKey: publicConfig.secretKey, }); } else { add('pass', 'db.tenant_secret', 'PNVS tenant secret row exists', { secretScope: publicConfig.secretScope, secretKey: publicConfig.secretKey, }); if (secret.accessKeyIdLength <= 0) add('blocker', 'db.tenant_secret.access_key_id', 'PNVS AccessKeyId is empty'); if (secret.accessKeySecretLength <= 0) add('blocker', 'db.tenant_secret.access_key_secret', 'PNVS AccessKeySecret is empty'); if (secret.accessKeyIdHasEdgeWhitespace || secret.accessKeySecretHasEdgeWhitespace) { add('blocker', 'db.tenant_secret.whitespace', 'PNVS AccessKey fields contain leading/trailing whitespace'); } } return checks; } async function diagnoseAliyunPnvsProvider(inputConfig, options = {}) { const config = inputConfig?.databaseUrl ? inputConfig : buildConfig(options.env || process.env); const query = options.query; if (!query) throw new Error('diagnoseAliyunPnvsProvider requires a query function'); const rows = await queryProviderRows(query, config.tenantId); const providerRow = rows.find(row => row.status === 'active') || rows.find(row => row.status === 'testing') || rows[0] || null; const publicConfig = providerRow ? summarizePublicConfig(providerRow.configPublic || {}) : summarizePublicConfig(); const secret = providerRow ? summarizeSecret(await querySecretRow(query, config.tenantId, publicConfig.secretScope, publicConfig.secretKey)) : { found: false }; const checks = buildChecks({ config, providerRow, publicConfig, secret }); return { ok: checks.every(item => item.status !== 'blocker'), tenantId: config.tenantId, env: { authSmsProvider: config.authSmsProvider || '', providerMatchesPnvs: isPnvsProvider(config.authSmsProvider), }, provider: providerRow ? { found: true, provider: providerRow.provider, status: providerRow.status, displayName: providerRow.displayName || '', updatedAt: providerRow.updatedAt || null, publicConfig, } : { found: false, publicConfig }, secret, checks, }; } async function diagnoseWithPool(config) { const pool = new pg.Pool({ connectionString: config.databaseUrl, max: 1 }); try { return await diagnoseAliyunPnvsProvider(config, { query: (text, params) => pool.query(text, params) }); } finally { await pool.end(); } } async function main() { try { const config = buildConfig(); const result = await diagnoseWithPool(config); console.log(JSON.stringify(result, null, 2)); if (!result.ok) process.exitCode = 1; } catch (error) { console.error(error.message); console.error(` Usage: set -a source /etc/tiku-saas/api.env set +a PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run diagnose:aliyun-pnvs `); process.exitCode = 1; } } const currentFile = fileURLToPath(import.meta.url); if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) { await main(); } export { buildChecks, buildConfig, diagnoseAliyunPnvsProvider, isPnvsProvider, parseSecretRef, summarizePublicConfig, summarizeSecret, };