85 lines
4.1 KiB
JavaScript
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.`);
|