Files
gongxue-base/scripts/diagnose-aliyun-pnvs-provider.js
2026-07-03 23:57:01 +08:00

276 lines
10 KiB
JavaScript

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,
};