Files
lislgosms/tools/quality/verify-admin-api-r1.mjs
T

85 lines
4.1 KiB
JavaScript

import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import ts from 'typescript';
const root = process.cwd();
const contract = JSON.parse(readFileSync(resolve(root, 'docs/contracts/admin-api-r1-methods.json'), 'utf8'));
function objectProperties(relativePath, variableNames) {
const path = resolve(root, relativePath);
const source = readFileSync(path, 'utf8');
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const result = [];
for (const statement of sourceFile.statements) {
if (!ts.isVariableStatement(statement)) continue;
for (const declaration of statement.declarationList.declarations) {
if (!variableNames.includes(declaration.name.getText(sourceFile)) || !ts.isObjectLiteralExpression(declaration.initializer)) continue;
for (const property of declaration.initializer.properties) {
result.push({
name: property.name?.getText(sourceFile),
implementationSha256: createHash('sha256').update(property.getText(sourceFile)).digest('hex'),
});
}
}
}
return result;
}
function functionBodies(relativePath) {
const path = resolve(root, relativePath);
const source = readFileSync(path, 'utf8');
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const result = new Map();
for (const statement of sourceFile.statements) {
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
result.set(
statement.name.text,
createHash('sha256').update(statement.body.getText(sourceFile)).digest('hex'),
);
}
}
return result;
}
function assertExactContract(label, expected, actual) {
const expectedByName = new Map(expected.map((item) => [item.name, item.implementationSha256]));
const actualByName = new Map(actual.map((item) => [item.name, item.implementationSha256]));
const missing = [...expectedByName.keys()].filter((name) => !actualByName.has(name));
const unexpected = [...actualByName.keys()].filter((name) => !expectedByName.has(name));
const changed = [...expectedByName.entries()]
.filter(([name, hash]) => actualByName.get(name) !== undefined && actualByName.get(name) !== hash)
.map(([name]) => name);
if (missing.length || unexpected.length || changed.length) {
throw new Error(`${label} contract changed: missing=[${missing}], unexpected=[${unexpected}], implementationChanged=[${changed}]`);
}
}
const adminMethods = [
...objectProperties('src/api/admin/identity.api.ts', ['adminIdentityApi']),
...objectProperties('src/api/admin/channels-reports.api.ts', ['adminChannelsReportsApi']),
...objectProperties('src/api/admin/operations.api.ts', ['adminOperationsApi']),
...objectProperties('src/api/admin/governance.api.ts', ['adminGovernanceApi']),
...objectProperties('src/api/admin/files.api.ts', ['adminFilesApi']),
];
const clientMethods = objectProperties('src/api/client/client.api.ts', ['clientApi']);
const sessionMethods = objectProperties('src/api/admin/session.api.ts', ['portalSessionApi']);
assertExactContract('adminApi', contract.objects.adminApi, adminMethods);
assertExactContract('clientApi', contract.objects.clientApi, clientMethods);
assertExactContract('portalSessionApi', contract.objects.portalSessionApi, sessionMethods);
const coreFunctions = functionBodies('src/api/core/httpClient.ts');
for (const [name, expectedHash] of Object.entries(contract.coreFunctions)) {
if (coreFunctions.get(name) !== expectedHash) {
throw new Error(`Shared HTTP behavior changed: ${name}`);
}
}
const facade = readFileSync(resolve(root, 'src/api/adminApi.ts'), 'utf8');
for (const stableExport of ['adminApi', 'clientApi', 'portalSessionApi', 'fileDownloadUrl']) {
if (!facade.includes(stableExport)) throw new Error(`Stable facade export is missing: ${stableExport}`);
}
console.log(`R1 API facade verified: ${adminMethods.length} admin methods, ${clientMethods.length} client methods, ${sessionMethods.length} session methods and ${coreFunctions.size} HTTP helpers; all implementations unchanged.`);