forked from wangziqi/gongxue-base
chore: add PNVS provider diagnostics
This commit is contained in:
@@ -106,6 +106,11 @@ npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-pe
|
|||||||
PNVS 短信登录上线前要用真实手机号跑一次远程 smoke。脚本不会读取或输出密钥;它只调用公网 API,发送验证码后在终端输入收到的短信验证码,再确认 `/api/auth/me` 可用:
|
PNVS 短信登录上线前要用真实手机号跑一次远程 smoke。脚本不会读取或输出密钥;它只调用公网 API,发送验证码后在终端输入收到的短信验证码,再确认 `/api/auth/me` 可用:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
set -a
|
||||||
|
source /etc/tiku-saas/api.env
|
||||||
|
set +a
|
||||||
|
PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run diagnose:aliyun-pnvs
|
||||||
|
|
||||||
SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com \
|
SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com \
|
||||||
SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 \
|
SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 \
|
||||||
SMS_SMOKE_PHONE=13800138000 \
|
SMS_SMOKE_PHONE=13800138000 \
|
||||||
|
|||||||
@@ -58,9 +58,10 @@
|
|||||||
"test:worker:public-banks": "npm run db:smoke-seed && npm run build:worker && node scripts/public-bank-worker-integration-test.js",
|
"test:worker:public-banks": "npm run db:smoke-seed && npm run build:worker && node scripts/public-bank-worker-integration-test.js",
|
||||||
"test:worker:student-supervision": "npm run db:smoke-seed && npm run build:worker && node scripts/student-supervision-worker-integration-test.js",
|
"test:worker:student-supervision": "npm run db:smoke-seed && npm run build:worker && node scripts/student-supervision-worker-integration-test.js",
|
||||||
"test:rls": "npm run db:smoke-seed && node scripts/rls-tenant-isolation-test.js",
|
"test:rls": "npm run db:smoke-seed && node scripts/rls-tenant-isolation-test.js",
|
||||||
"test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node scripts/aliyun-pnvs-provider-contract-test.js && node scripts/configure-aliyun-pnvs-provider-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/product-scope-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node scripts/taro-h5-release-manifest-test.js && node scripts/taro-visual-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/docker-benchmark-resource-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/remote-sms-login-smoke-test.js && node scripts/launch-persona-smoke-test.js && node scripts/production-launch-gate-test.js",
|
"test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node scripts/aliyun-pnvs-provider-contract-test.js && node scripts/configure-aliyun-pnvs-provider-test.js && node scripts/diagnose-aliyun-pnvs-provider-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/product-scope-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node scripts/taro-h5-release-manifest-test.js && node scripts/taro-visual-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/docker-benchmark-resource-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/remote-sms-login-smoke-test.js && node scripts/launch-persona-smoke-test.js && node scripts/production-launch-gate-test.js",
|
||||||
"test:auth:remote-smoke": "node scripts/remote-auth-jwt-smoke-test.js",
|
"test:auth:remote-smoke": "node scripts/remote-auth-jwt-smoke-test.js",
|
||||||
"configure:aliyun-pnvs": "node scripts/configure-aliyun-pnvs-provider.js",
|
"configure:aliyun-pnvs": "node scripts/configure-aliyun-pnvs-provider.js",
|
||||||
|
"diagnose:aliyun-pnvs": "node scripts/diagnose-aliyun-pnvs-provider.js",
|
||||||
"smoke:sms-login:remote": "node scripts/remote-sms-login-smoke.js",
|
"smoke:sms-login:remote": "node scripts/remote-sms-login-smoke.js",
|
||||||
"test:launch-gate": "node scripts/production-launch-gate-test.js",
|
"test:launch-gate": "node scripts/production-launch-gate-test.js",
|
||||||
"smoke:launch-persona": "npm run build:api && node scripts/launch-persona-smoke.js",
|
"smoke:launch-persona": "npm run build:api && node scripts/launch-persona-smoke.js",
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ Taro H5 构建存在 webpack asset size warning,这是前端包体优化事项
|
|||||||
|
|
||||||
2026-07-03 最新接管状态:
|
2026-07-03 最新接管状态:
|
||||||
|
|
||||||
- Gitea `main` 已包含 PNVS 短信认证、后台登录修复、PNVS provider 配置脚本和旧题库视觉对齐版本,最新提交应至少是 `f8f8316`。
|
- Gitea `main` 已包含 PNVS 短信认证、后台登录修复、PNVS provider 配置/诊断脚本和旧题库视觉对齐版本,最新提交应至少是 `4fb4125`。
|
||||||
- 服务器仓库仍在 `/opt/tiku-saas/repo`,归属用户应为 `deploy:deploy`。
|
- 服务器仓库仍在 `/opt/tiku-saas/repo`,归属用户应为 `deploy:deploy`。
|
||||||
- 生产 API 已能启动,`https://api.tjszsb.com/api/tenant/resolve?host=app.tjszsb.com` 已返回 `master` 租户。
|
- 生产 API 已能启动,`https://api.tjszsb.com/api/tenant/resolve?host=app.tjszsb.com` 已返回 `master` 租户。
|
||||||
- Supabase self-hosted 运行在 `/opt/tiku-saas/supabase-project`,Kong 通过 Nginx 暴露到 `https://supabase.tjszsb.com`。
|
- Supabase self-hosted 运行在 `/opt/tiku-saas/supabase-project`,Kong 通过 Nginx 暴露到 `https://supabase.tjszsb.com`。
|
||||||
@@ -178,6 +178,16 @@ unset ALIYUN_ACCESS_KEY_ID ALIYUN_ACCESS_KEY_SECRET ALIYUN_PNVS_SIGN_NAME ALIYUN
|
|||||||
set -o history
|
set -o history
|
||||||
```
|
```
|
||||||
|
|
||||||
|
配置后先跑只读诊断,确认 env、`tenant_auth_providers` 和 `tenant_secrets` 对齐;输出只包含 AccessKey 长度和脱敏前后缀,不会打印密钥明文:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/tiku-saas/repo
|
||||||
|
set -a
|
||||||
|
source /etc/tiku-saas/api.env
|
||||||
|
set +a
|
||||||
|
PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run diagnose:aliyun-pnvs
|
||||||
|
```
|
||||||
|
|
||||||
接管服务器时先做只读检查:
|
接管服务器时先做只读检查:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
101
scripts/diagnose-aliyun-pnvs-provider-test.js
Normal file
101
scripts/diagnose-aliyun-pnvs-provider-test.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
diagnoseAliyunPnvsProvider,
|
||||||
|
parseSecretRef,
|
||||||
|
summarizeSecret,
|
||||||
|
} from './diagnose-aliyun-pnvs-provider.js';
|
||||||
|
|
||||||
|
const tenantId = '00000000-0000-0000-0000-000000000001';
|
||||||
|
|
||||||
|
function queryFixture({ providerRows = [], secretRows = [] }) {
|
||||||
|
return async (sql, params) => {
|
||||||
|
if (sql.includes('from public.tenant_auth_providers')) {
|
||||||
|
assert.equal(params[0], tenantId);
|
||||||
|
return { rows: providerRows };
|
||||||
|
}
|
||||||
|
if (sql.includes('from app_private.tenant_secrets')) {
|
||||||
|
assert.equal(params[0], tenantId);
|
||||||
|
return {
|
||||||
|
rows: secretRows.filter(row => row.scope === params[1] && row.key === params[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected SQL: ${sql}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const goodProviderRows = [
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
provider: 'aliyun-pnvs',
|
||||||
|
status: 'active',
|
||||||
|
displayName: '阿里云短信认证',
|
||||||
|
updatedAt: '2026-07-03T00:00:00.000Z',
|
||||||
|
configPublic: {
|
||||||
|
signName: '升本刷题通',
|
||||||
|
templateCode: 'SMS_123456789',
|
||||||
|
endpoint: 'https://dypnsapi.aliyuncs.com',
|
||||||
|
regionId: 'cn-hangzhou',
|
||||||
|
countryCode: '86',
|
||||||
|
codeType: '1',
|
||||||
|
codeLength: '6',
|
||||||
|
validTime: '300',
|
||||||
|
interval: '60',
|
||||||
|
templateParam: { code: '##code##', min: '5' },
|
||||||
|
secretRef: 'app_private.tenant_secrets:sms:aliyun-pnvs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const goodSecretRows = [
|
||||||
|
{
|
||||||
|
scope: 'sms',
|
||||||
|
key: 'aliyun-pnvs',
|
||||||
|
provider: 'aliyun-pnvs',
|
||||||
|
accessKeyId: 'LTAI5tExampleId',
|
||||||
|
accessKeySecret: 'very-long-secret-value-that-must-not-leak',
|
||||||
|
lastRotatedAt: '2026-07-03T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ok = await diagnoseAliyunPnvsProvider(
|
||||||
|
{ databaseUrl: 'postgresql://example', tenantId, authSmsProvider: 'aliyun-pnvs' },
|
||||||
|
{ query: queryFixture({ providerRows: goodProviderRows, secretRows: goodSecretRows }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(ok.ok, true);
|
||||||
|
assert.equal(ok.provider.found, true);
|
||||||
|
assert.equal(ok.provider.publicConfig.templateParamHasCodePlaceholder, true);
|
||||||
|
assert.equal(ok.secret.found, true);
|
||||||
|
assert.equal(ok.secret.accessKeySecretLength, goodSecretRows[0].accessKeySecret.length);
|
||||||
|
assert.ok(!JSON.stringify(ok).includes(goodSecretRows[0].accessKeySecret), 'diagnostic output must not include AccessKeySecret plaintext');
|
||||||
|
assert.ok(!JSON.stringify(ok).includes(goodSecretRows[0].accessKeyId), 'diagnostic output should mask AccessKeyId plaintext');
|
||||||
|
assert.match(ok.secret.accessKeyIdMasked, /^LTAI\.\.\./);
|
||||||
|
|
||||||
|
const missingSecret = await diagnoseAliyunPnvsProvider(
|
||||||
|
{ databaseUrl: 'postgresql://example', tenantId, authSmsProvider: 'aliyun-pnvs' },
|
||||||
|
{ query: queryFixture({ providerRows: goodProviderRows, secretRows: [] }) },
|
||||||
|
);
|
||||||
|
assert.equal(missingSecret.ok, false);
|
||||||
|
assert.ok(missingSecret.checks.some(item => item.id === 'db.tenant_secret' && item.status === 'blocker'));
|
||||||
|
|
||||||
|
const wrongEnvProvider = await diagnoseAliyunPnvsProvider(
|
||||||
|
{ databaseUrl: 'postgresql://example', tenantId, authSmsProvider: 'aliyun' },
|
||||||
|
{ query: queryFixture({ providerRows: goodProviderRows, secretRows: goodSecretRows }) },
|
||||||
|
);
|
||||||
|
assert.equal(wrongEnvProvider.ok, false);
|
||||||
|
assert.ok(wrongEnvProvider.checks.some(item => item.id === 'env.auth_sms_provider' && item.status === 'blocker'));
|
||||||
|
|
||||||
|
const invalidSecretRef = parseSecretRef('plain-secret');
|
||||||
|
assert.equal(invalidSecretRef.valid, false);
|
||||||
|
assert.equal(invalidSecretRef.scope, 'sms');
|
||||||
|
assert.equal(invalidSecretRef.key, 'aliyun-pnvs');
|
||||||
|
|
||||||
|
const secretSummary = summarizeSecret({
|
||||||
|
provider: 'aliyun-pnvs',
|
||||||
|
accessKeyId: ' LTAI5tExampleId',
|
||||||
|
accessKeySecret: 'secret-value ',
|
||||||
|
});
|
||||||
|
assert.equal(secretSummary.accessKeyIdHasEdgeWhitespace, true);
|
||||||
|
assert.equal(secretSummary.accessKeySecretHasEdgeWhitespace, true);
|
||||||
|
assert.ok(!JSON.stringify(secretSummary).includes('secret-value '));
|
||||||
|
|
||||||
|
console.log('[PASS] diagnose Aliyun PNVS provider script');
|
||||||
275
scripts/diagnose-aliyun-pnvs-provider.js
Normal file
275
scripts/diagnose-aliyun-pnvs-provider.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user