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.`);