hivecast-sdk/scripts/prove-external-consumer.mjs
Ubuntu 25d8a62140 Revert "chore: rename SDK facade to hivecast-sdk"
This reverts commit de5b25ae232977c693b2a1a457265df4844c3cfd.
2026-06-08 00:21:26 +00:00

258 lines
8.2 KiB
JavaScript

#!/usr/bin/env node
import { mkdtemp, rm, mkdir, writeFile, readdir, readFile, readlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const workspaceDir = path.resolve(scriptDir, '..');
const packages = [
'@open-matrix/contracts',
'@open-matrix/federation',
'@open-matrix/omega-core',
'@open-matrix/core',
'@open-matrix/browser-host',
'@open-matrix/browser-kit',
'@open-matrix/sdk',
'@open-matrix/cli',
];
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd ?? workspaceDir,
stdio: options.stdio ?? 'inherit',
env: { ...process.env, ...options.env },
encoding: 'utf8',
});
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}`);
}
return result;
}
async function findTarball(tarballDir, packageName) {
const packageDir = path.join(workspaceDir, 'packages', packageName.split('/').at(-1) === 'omega-core'
? 'omega-core'
: packageName.split('/').at(-1));
const packageJson = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
const expectedPrefix = packageJson.name.replace('@', '').replace('/', '-');
const entries = await readdir(tarballDir);
const tarball = entries.find((entry) => entry.startsWith(expectedPrefix) && entry.endsWith('.tgz'));
if (!tarball) {
throw new Error(`Packed tarball missing for ${packageName} (${expectedPrefix}-*.tgz)`);
}
return path.join(tarballDir, tarball);
}
async function assertNoSymlinkToHarness(dir) {
const pending = [dir];
while (pending.length > 0) {
const current = pending.pop();
const entries = await readdir(current, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(current, entry.name);
if (entry.isSymbolicLink()) {
const target = await readlink(full);
if (target.includes('matrix-work-harness')) {
throw new Error(`Consumer node_modules symlink points at harness: ${full}`);
}
continue;
}
if (entry.isDirectory()) {
if (entry.name === '.pnpm' || entry.name === '.bin') continue;
pending.push(full);
}
}
}
}
async function main() {
const proofRoot = await mkdtemp(path.join(tmpdir(), 'matrix-sdk-consumer-proof.'));
const tarballDir = path.join(proofRoot, 'tarballs');
const consumerDir = path.join(proofRoot, 'consumer');
let keep = process.env.MATRIX_SDK_KEEP_CONSUMER_PROOF === '1';
try {
await mkdir(tarballDir, { recursive: true });
await mkdir(path.join(consumerDir, 'src'), { recursive: true });
run('pnpm', ['run', 'clean']);
run('pnpm', ['run', 'build']);
for (const packageName of packages) {
run('pnpm', ['--filter', packageName, 'pack', '--pack-destination', tarballDir]);
}
const tarballs = Object.fromEntries(
await Promise.all(packages.map(async (packageName) => [packageName, await findTarball(tarballDir, packageName)]))
);
const tarballDependencies = Object.fromEntries(
Object.entries(tarballs).map(([packageName, tarballPath]) => [packageName, `file:${tarballPath}`])
);
await writeFile(path.join(consumerDir, 'package.json'), JSON.stringify({
name: 'matrix-sdk-external-consumer-proof',
version: '0.0.0',
private: true,
type: 'module',
scripts: {
build: 'tsc -p tsconfig.json',
run: 'node dist/index.js',
},
dependencies: tarballDependencies,
devDependencies: {
'@types/node': '^25.0.10',
typescript: '^5.7.0',
},
pnpm: {
overrides: tarballDependencies,
},
}, null, 2));
await writeFile(path.join(consumerDir, 'tsconfig.json'), JSON.stringify({
compilerOptions: {
target: 'ES2022',
module: 'NodeNext',
moduleResolution: 'NodeNext',
strict: true,
skipLibCheck: true,
outDir: 'dist',
},
include: ['src/**/*.ts'],
}, null, 2));
await writeFile(path.join(consumerDir, 'src', 'browser-actor.ts'), `import { MatrixActorHtmlElement } from '@open-matrix/core';
import type { MatrixAppReadyDetail } from '@open-matrix/browser-host/app-ready';
export class DemoBrowserActor extends MatrixActorHtmlElement {
static accepts = { 'ui.ping': {} };
describeReady(detail: MatrixAppReadyDetail): string {
return detail.appName;
}
}
`);
await writeFile(path.join(consumerDir, 'src', 'index.ts'), `import assert from 'node:assert/strict';
import {
InMemoryBroker,
InMemoryTransport,
MatrixActor,
MatrixRuntime,
TopicRouter,
} from '@open-matrix/core';
import { MatrixActor as SdkMatrixActor } from '@open-matrix/sdk';
class EchoActor extends MatrixActor {
static accepts = {
echo: {},
};
onEcho(payload: { message?: string }) {
return {
ok: true,
message: payload.message ?? null,
source: 'external-sdk-consumer',
};
}
}
async function request(transport: InMemoryTransport, mount: string, op: string, payload: unknown) {
const correlationId = 'consumer-proof-1';
const replyTo = '$replies.consumer-proof-1';
const response = new Promise<unknown>((resolve, reject) => {
const timeout = setTimeout(() => {
unsubscribe();
reject(new Error('request timed out'));
}, 2000);
const unsubscribe = transport.subscribe(replyTo, (value) => {
clearTimeout(timeout);
unsubscribe();
resolve(value);
});
});
transport.publish(TopicRouter.inbox(mount), {
op,
payload,
replyTo,
correlationId,
});
return response;
}
const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker, { name: 'external-consumer' });
const runtime = new MatrixRuntime({ transport, logging: false });
try {
assert.equal(SdkMatrixActor, MatrixActor);
await runtime.create(EchoActor, 'demo.echo');
const response = await request(transport, 'demo.echo', 'echo', { message: 'hello-sdk' });
assert.deepEqual(response, {
ok: true,
correlationId: 'consumer-proof-1',
result: {
ok: true,
message: 'hello-sdk',
source: 'external-sdk-consumer',
},
});
console.log(JSON.stringify({ ok: true, mount: 'demo.echo', op: 'echo' }));
} finally {
await runtime.shutdown();
}
`);
run('pnpm', ['install', '--ignore-scripts'], { cwd: consumerDir });
run('pnpm', ['exec', 'matrix', '--help'], { cwd: consumerDir, stdio: 'pipe' });
run('pnpm', ['run', 'build'], { cwd: consumerDir });
const runtimeProof = run('pnpm', ['run', 'run'], { cwd: consumerDir, stdio: 'pipe' });
const stdout = runtimeProof.stdout.trim();
const parsed = JSON.parse(stdout.split('\n').at(-1));
if (parsed.ok !== true || parsed.mount !== 'demo.echo' || parsed.op !== 'echo') {
throw new Error(`Unexpected runtime proof output: ${stdout}`);
}
const consumerFiles = [
await readFile(path.join(consumerDir, 'package.json'), 'utf8'),
await readFile(path.join(consumerDir, 'tsconfig.json'), 'utf8'),
await readFile(path.join(consumerDir, 'src', 'index.ts'), 'utf8'),
await readFile(path.join(consumerDir, 'src', 'browser-actor.ts'), 'utf8'),
].join('\n');
if (consumerFiles.includes('matrix-work-harness') || consumerFiles.includes('projects/matrix-3')) {
throw new Error('Consumer proof contains a Matrix-3/harness path reference');
}
if (existsSync(path.join(consumerDir, 'node_modules'))) {
await assertNoSymlinkToHarness(path.join(consumerDir, 'node_modules'));
}
console.log(JSON.stringify({
ok: true,
proof: 'external-consumer',
consumerDir,
packages,
runtime: parsed,
matrix3PathReferences: false,
}, null, 2));
} catch (error) {
keep = true;
throw error;
} finally {
if (!keep) {
await rm(proofRoot, { recursive: true, force: true });
} else {
console.error(`[prove-external-consumer] kept proof directory: ${proofRoot}`);
}
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack : error);
process.exit(1);
});