feat: establish production SaaS foundation

This commit is contained in:
Codex
2026-07-12 19:26:57 +08:00
parent 1c2ce38cea
commit 39f7332f33
219 changed files with 20647 additions and 2628 deletions

View File

@@ -0,0 +1,148 @@
export const DESTRUCTIVE_TEST_CONFIRMATION = 'SMOKE_SEED_LOCAL_OR_CI_ONLY';
const ALLOWED_ENVIRONMENTS = new Set(['local', 'test', 'ci']);
const KNOWN_PRODUCTION_DATABASE_USERS = new Set(['tiku_api', 'tiku_worker']);
function argumentValue(argv, name) {
const directIndex = argv.indexOf(name);
if (directIndex >= 0) return String(argv[directIndex + 1] || '').trim();
const prefix = `${name}=`;
return String(argv.find(value => value.startsWith(prefix)) || '').slice(prefix.length).trim();
}
export function resolveDestructiveTestConfirmation(
env = process.env,
argv = process.argv.slice(2),
) {
return argumentValue(argv, '--confirm') || String(env.SMOKE_SEED_CONFIRM || '').trim();
}
export function describeDatabaseTarget(databaseUrl) {
if (!databaseUrl || typeof databaseUrl !== 'string') {
throw new Error('DATABASE_URL is required');
}
let parsed;
try {
parsed = new URL(databaseUrl);
} catch {
throw new Error('DATABASE_URL must be a valid PostgreSQL URL');
}
if (!['postgres:', 'postgresql:'].includes(parsed.protocol)) {
throw new Error('DATABASE_URL must use the postgres or postgresql protocol');
}
if (!parsed.hostname || !parsed.pathname || parsed.pathname === '/') {
throw new Error('DATABASE_URL must include a host and database name');
}
return {
host: parsed.hostname,
port: parsed.port || '5432',
database: decodeURIComponent(parsed.pathname.slice(1)),
user: decodeURIComponent(parsed.username || ''),
};
}
function targetText(target, environment = 'unavailable') {
const safe = value => String(value || '[missing]')
.replace(/[\u0000-\u001f\u007f\s]+/g, '_')
.slice(0, 160);
return [
`host=${safe(target.host)}`,
`port=${safe(target.port)}`,
`database=${safe(target.database)}`,
`user=${safe(target.user)}`,
`databaseEnvironment=${safe(environment)}`,
].join(' ');
}
function refusal(operation, reason, target, environment) {
return new Error(
`Refusing ${operation}: ${reason}. Target: ${targetText(target, environment)}`,
);
}
function knownProductionReason(target) {
const user = target.user.toLowerCase();
const host = target.host.toLowerCase();
if (KNOWN_PRODUCTION_DATABASE_USERS.has(user)) {
return 'database user is reserved for production runtime';
}
if (host === 'tjszsb.com' || host.endsWith('.tjszsb.com')) {
return 'database host belongs to the production domain';
}
return '';
}
export async function assertDestructiveTestDatabase({
client,
databaseUrl,
confirmation = resolveDestructiveTestConfirmation(),
operation = 'destructive database test',
} = {}) {
let target;
try {
target = describeDatabaseTarget(databaseUrl);
} catch (error) {
throw new Error(`Refusing ${operation}: ${error.message}`);
}
if (confirmation !== DESTRUCTIVE_TEST_CONFIRMATION) {
throw refusal(
operation,
`explicit confirmation ${DESTRUCTIVE_TEST_CONFIRMATION} is required`,
target,
);
}
const productionReason = knownProductionReason(target);
if (productionReason) {
throw refusal(operation, productionReason, target);
}
if (!client || typeof client.query !== 'function') {
throw refusal(operation, 'a connected PostgreSQL client is required', target);
}
let result;
try {
result = await client.query(
`
select environment,
allow_destructive_tests as "allowDestructiveTests"
from app_private.environment_safety
where id = true
limit 1
`,
);
} catch {
throw refusal(
operation,
'database safety marker is unavailable or unreadable',
target,
);
}
const marker = result?.rows?.[0];
const environment = String(marker?.environment || 'missing').toLowerCase();
if (!marker) {
throw refusal(operation, 'database safety marker row is missing', target, environment);
}
if (!ALLOWED_ENVIRONMENTS.has(environment)) {
throw refusal(
operation,
`database environment ${environment} is not approved for destructive tests`,
target,
environment,
);
}
if (marker.allowDestructiveTests !== true) {
throw refusal(
operation,
'database marker does not allow destructive tests',
target,
environment,
);
}
return { target, environment, allowDestructiveTests: true };
}

View File

@@ -0,0 +1,249 @@
import crypto from 'node:crypto';
export const TENANT_FOREIGN_KEY_AUDIT_KIND = 'tenant-foreign-key-audit';
export const EXPECTED_TENANT_FOREIGN_KEY_RELATION_COUNT = 189;
export const EXPECTED_TENANT_FOREIGN_KEY_SCHEMA_SHA256 = '884a5a59c101299551c27bde83f82b9738074a8729da9284775f75537f615868';
const TENANT_FOREIGN_KEY_EXCEPTIONS = new Map([
[
'platform_audit_alerts.platform_audit_alerts_rule_id_fkey',
{
mode: 'global-or-same-tenant-parent',
reason: 'Platform audit rules may be global (tenant_id is null) or scoped to the alert tenant.',
childColumn: 'rule_id',
parentTable: 'platform_audit_alert_rules',
parentColumn: 'id',
},
],
[
'tenant_question_bank_adoptions.tenant_question_bank_adoptions_source_question_bank_id_fkey',
{
mode: 'platform-source-or-same-tenant-parent',
reason: 'A tenant adoption may reference a platform-owned public question bank.',
childColumn: 'source_question_bank_id',
parentTable: 'question_banks',
parentColumn: 'id',
},
],
[
'tenant_content_notifications.tenant_content_notifications_source_question_bank_id_fkey',
{
mode: 'platform-source-or-same-tenant-parent',
reason: 'A tenant notification may identify the platform-owned public question bank that triggered it.',
childColumn: 'source_question_bank_id',
parentTable: 'question_banks',
parentColumn: 'id',
},
],
]);
const RELATION_QUERY = `
with tenant_tables as (
select cls.oid, cls.relname
from pg_class cls
join pg_namespace ns on ns.oid = cls.relnamespace
where ns.nspname = 'public'
and cls.relkind in ('r', 'p')
and exists (
select 1
from pg_attribute attribute
where attribute.attrelid = cls.oid
and attribute.attname = 'tenant_id'
and not attribute.attisdropped
)
)
select child.relname as "childTable",
constraint_row.conname as "constraintName",
parent.relname as "parentTable",
array(
select attribute.attname
from unnest(constraint_row.conkey) with ordinality key_column(attnum, ordinal)
join pg_attribute attribute
on attribute.attrelid = constraint_row.conrelid
and attribute.attnum = key_column.attnum
order by key_column.ordinal
) as "childColumns",
array(
select attribute.attname
from unnest(constraint_row.confkey) with ordinality key_column(attnum, ordinal)
join pg_attribute attribute
on attribute.attrelid = constraint_row.confrelid
and attribute.attnum = key_column.attnum
order by key_column.ordinal
) as "parentColumns",
constraint_row.convalidated as validated,
constraint_row.confupdtype as "updateAction",
constraint_row.confdeltype as "deleteAction"
from pg_constraint constraint_row
join tenant_tables child on child.oid = constraint_row.conrelid
join tenant_tables parent on parent.oid = constraint_row.confrelid
where constraint_row.contype = 'f'
and not exists (
select 1
from unnest(constraint_row.conkey) key_column(attnum)
join pg_attribute attribute
on attribute.attrelid = constraint_row.conrelid
and attribute.attnum = key_column.attnum
where attribute.attname = 'tenant_id'
)
order by child.relname, constraint_row.conname
`;
function textArray(value) {
if (Array.isArray(value)) return value.map(item => String(item));
if (typeof value !== 'string' || value.length < 2) return [];
return value.slice(1, -1).split(',').filter(Boolean).map(item => item.replace(/^"|"$/g, ''));
}
export function normalizeTenantForeignKeyRelation(row) {
return {
childTable: String(row.childTable || row.child_table || ''),
constraintName: String(row.constraintName || row.constraint_name || ''),
childColumns: textArray(row.childColumns || row.child_columns),
parentTable: String(row.parentTable || row.parent_table || ''),
parentColumns: textArray(row.parentColumns || row.parent_columns),
validated: row.validated === true,
updateAction: String(row.updateAction || row.update_action || ''),
deleteAction: String(row.deleteAction || row.delete_action || ''),
};
}
export function tenantForeignKeyRelationKey(relation) {
return `${relation.childTable}.${relation.constraintName}`;
}
export function tenantForeignKeyRelationCanonical(relation) {
return [
relation.childTable,
relation.constraintName,
relation.childColumns.join(','),
relation.parentTable,
relation.parentColumns.join(','),
relation.validated ? 'validated' : 'not-valid',
`update:${relation.updateAction}`,
`delete:${relation.deleteAction}`,
].join('|');
}
export function tenantForeignKeySchemaSha256(relations) {
const canonical = relations
.map(normalizeTenantForeignKeyRelation)
.sort((left, right) => tenantForeignKeyRelationKey(left).localeCompare(tenantForeignKeyRelationKey(right)))
.map(tenantForeignKeyRelationCanonical)
.join('\n');
return crypto.createHash('sha256').update(canonical).digest('hex');
}
function exceptionFor(relation) {
const key = tenantForeignKeyRelationKey(relation);
const exception = TENANT_FOREIGN_KEY_EXCEPTIONS.get(key);
if (!exception) return null;
if (
relation.childColumns.length !== 1
|| relation.parentColumns.length !== 1
|| relation.childColumns[0] !== exception.childColumn
|| relation.parentTable !== exception.parentTable
|| relation.parentColumns[0] !== exception.parentColumn
) {
throw new Error(`Tenant foreign key exception definition no longer matches ${key}`);
}
return exception;
}
export function summarizeTenantForeignKeySchema(inputRelations) {
const relations = inputRelations.map(normalizeTenantForeignKeyRelation);
const keys = new Set(relations.map(tenantForeignKeyRelationKey));
const missingExceptions = [...TENANT_FOREIGN_KEY_EXCEPTIONS.keys()].filter(key => !keys.has(key));
const exceptionRelations = relations.filter(relation => exceptionFor(relation));
const unvalidatedRelations = relations
.filter(relation => !relation.validated)
.map(tenantForeignKeyRelationKey);
const schemaSha256 = tenantForeignKeySchemaSha256(relations);
const schemaMatches = relations.length === EXPECTED_TENANT_FOREIGN_KEY_RELATION_COUNT
&& schemaSha256 === EXPECTED_TENANT_FOREIGN_KEY_SCHEMA_SHA256
&& missingExceptions.length === 0
&& unvalidatedRelations.length === 0;
return {
relationCount: relations.length,
expectedRelationCount: EXPECTED_TENANT_FOREIGN_KEY_RELATION_COUNT,
schemaSha256,
expectedSchemaSha256: EXPECTED_TENANT_FOREIGN_KEY_SCHEMA_SHA256,
schemaMatches,
exceptionCount: exceptionRelations.length,
expectedExceptionCount: TENANT_FOREIGN_KEY_EXCEPTIONS.size,
missingExceptions,
unvalidatedRelations,
};
}
export async function loadTenantForeignKeyRelations(queryable) {
const result = await queryable.query(RELATION_QUERY);
return result.rows.map(normalizeTenantForeignKeyRelation);
}
function quoteIdentifier(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
function quoteLiteral(value) {
return `'${String(value).replaceAll("'", "''")}'`;
}
function violationPredicate(relation) {
const exception = exceptionFor(relation);
if (!exception) return 'child.tenant_id is distinct from parent.tenant_id';
if (exception.mode === 'global-or-same-tenant-parent') {
return 'parent.tenant_id is not null and child.tenant_id is distinct from parent.tenant_id';
}
if (exception.mode === 'platform-source-or-same-tenant-parent') {
return "child.tenant_id is distinct from parent.tenant_id and parent.source_scope is distinct from 'platform'";
}
throw new Error(`Unsupported tenant foreign key exception mode: ${exception.mode}`);
}
export function buildTenantForeignKeyViolationQuery(inputRelations) {
const relations = inputRelations.map(normalizeTenantForeignKeyRelation);
if (relations.length === 0) {
return `select null::text as "relationKey", null::text as "childTenantId", null::text as "parentTenantId" where false`;
}
return relations.map(relation => {
if (relation.childColumns.length !== relation.parentColumns.length || relation.childColumns.length === 0) {
throw new Error(`Invalid tenant foreign key shape: ${tenantForeignKeyRelationKey(relation)}`);
}
const join = relation.childColumns.map((childColumn, index) => (
`parent.${quoteIdentifier(relation.parentColumns[index])} = child.${quoteIdentifier(childColumn)}`
)).join(' and ');
return `(
select ${quoteLiteral(tenantForeignKeyRelationKey(relation))}::text as "relationKey",
child.tenant_id::text as "childTenantId",
parent.tenant_id::text as "parentTenantId"
from public.${quoteIdentifier(relation.childTable)} child
join public.${quoteIdentifier(relation.parentTable)} parent on ${join}
where ${violationPredicate(relation)}
limit 1
)`;
}).join('\nunion all\n');
}
export async function auditTenantForeignKeyData(client, inputRelations, statementTimeoutMs = 120_000) {
const relations = inputRelations.map(normalizeTenantForeignKeyRelation);
const timeout = Math.max(1_000, Math.min(900_000, Number(statementTimeoutMs) || 120_000));
await client.query('begin read only');
try {
await client.query(`set local statement_timeout = '${timeout}ms'`);
await client.query(`set local lock_timeout = '5s'`);
const result = await client.query(buildTenantForeignKeyViolationQuery(relations));
await client.query('commit');
return result.rows;
} catch (error) {
await client.query('rollback').catch(() => undefined);
throw error;
}
}
export function tenantForeignKeyExceptions() {
return [...TENANT_FOREIGN_KEY_EXCEPTIONS.entries()].map(([relationKey, definition]) => ({
relationKey,
...definition,
}));
}