245 lines
8.6 KiB
TypeScript
245 lines
8.6 KiB
TypeScript
import { describe, it } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import * as fs from 'node:fs';
|
|
import * as net from 'node:net';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
|
|
import type { ILoadedMatrixPackageEnvironment } from '../../src/utils/package-environment.js';
|
|
import type { IResolvedRunnerTransportPlan } from '../../src/utils/runner-transport.js';
|
|
import type { ILoadedMatrixWebappManifest } from '../../src/utils/webapp-manifest.js';
|
|
import { startStandaloneWebappServer } from '../../src/utils/standalone-webapp-server.js';
|
|
|
|
async function getAvailablePort(): Promise<number> {
|
|
const server = await new Promise<net.Server>((resolve, reject) => {
|
|
const candidate = net.createServer();
|
|
candidate.once('error', reject);
|
|
candidate.listen(0, '127.0.0.1', () => resolve(candidate));
|
|
});
|
|
const address = server.address();
|
|
assert.ok(address && typeof address === 'object');
|
|
const port = address.port;
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
return port;
|
|
}
|
|
|
|
describe('standalone webapp server', () => {
|
|
it('advertises JWT browser auth when the runner transport uses credentialsRef', async () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'standalone-webapp-server-'));
|
|
const packageDir = path.join(tempRoot, 'package');
|
|
const distDir = path.join(packageDir, 'dist');
|
|
fs.mkdirSync(distDir, { recursive: true });
|
|
const httpPort = await getAvailablePort();
|
|
const credentialsPath = path.join(tempRoot, 'credentials.json');
|
|
fs.writeFileSync(credentialsPath, JSON.stringify({
|
|
jwt: 'sample.jwt.value',
|
|
seed: 'SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
}), 'utf8');
|
|
fs.writeFileSync(
|
|
path.join(distDir, 'index.html'),
|
|
'<!doctype html><html><body>fixture</body></html>',
|
|
'utf8',
|
|
);
|
|
|
|
const loadedEnvironment: ILoadedMatrixPackageEnvironment = {
|
|
packageDir,
|
|
envName: 'host',
|
|
environmentPath: path.join(packageDir, '.matrix', 'host.environment.json'),
|
|
environment: {
|
|
name: 'host',
|
|
runtime: { root: 'COM.TEST.EXTERNAL' },
|
|
nats: {
|
|
mode: 'external',
|
|
url: 'nats://example.test:4222',
|
|
wsUrl: 'wss://example.test/nats-ws',
|
|
credentialsRef: `file:${credentialsPath}`,
|
|
},
|
|
http: {
|
|
enabled: true,
|
|
port: httpPort,
|
|
},
|
|
},
|
|
};
|
|
const webapp: ILoadedMatrixWebappManifest = {
|
|
packageDir,
|
|
packageName: '@acme/webapp',
|
|
manifestPath: path.join(packageDir, 'matrix.json'),
|
|
distDir,
|
|
entryFile: 'index.html',
|
|
appName: 'webapp',
|
|
};
|
|
const runnerTransport: IResolvedRunnerTransportPlan = {
|
|
envName: 'host',
|
|
root: 'COM.TEST.EXTERNAL',
|
|
mode: 'external',
|
|
url: 'nats://example.test:4222',
|
|
wsUrl: 'wss://example.test/nats-ws',
|
|
credentialsRef: `file:${credentialsPath}`,
|
|
};
|
|
|
|
const server = await startStandaloneWebappServer({
|
|
loadedEnvironment,
|
|
webapp,
|
|
runnerTransport,
|
|
});
|
|
try {
|
|
const bootstrap = await fetch(`${server.baseUrl}/api/bootstrap`);
|
|
assert.equal(bootstrap.status, 200);
|
|
const bootstrapPayload = await bootstrap.json() as {
|
|
authorityContext?: {
|
|
assetHostRoot?: string;
|
|
platformServiceRoot?: string;
|
|
sessionAuthorityRoot?: string | null;
|
|
selectedAuthorityRoot?: string | null;
|
|
runtimeInstanceRoot?: string | null;
|
|
isAuthenticated?: boolean;
|
|
isPlatformAdmin?: boolean;
|
|
selectableAuthorityRoots?: string[];
|
|
};
|
|
authorityRoot?: string;
|
|
busRoot?: string;
|
|
runtimeInstanceRoot?: string | null;
|
|
transportPlan?: {
|
|
authMode?: string;
|
|
authEndpoint?: string | null;
|
|
wsPath?: string;
|
|
};
|
|
sources?: {
|
|
transportAuthMode?: string;
|
|
};
|
|
};
|
|
assert.deepEqual(bootstrapPayload.authorityContext, {
|
|
assetHostRoot: 'COM.TEST.EXTERNAL',
|
|
platformServiceRoot: 'COM.TEST.EXTERNAL',
|
|
sessionAuthorityRoot: 'COM.TEST.EXTERNAL',
|
|
selectedAuthorityRoot: 'COM.TEST.EXTERNAL',
|
|
runtimeInstanceRoot: null,
|
|
isAuthenticated: true,
|
|
isPlatformAdmin: false,
|
|
selectableAuthorityRoots: ['COM.TEST.EXTERNAL'],
|
|
});
|
|
assert.equal(bootstrapPayload.authorityRoot, 'COM.TEST.EXTERNAL');
|
|
assert.equal(bootstrapPayload.busRoot, 'COM.TEST.EXTERNAL');
|
|
assert.equal(bootstrapPayload.runtimeInstanceRoot, null);
|
|
assert.equal(bootstrapPayload.transportPlan?.wsPath, '/nats-ws');
|
|
assert.equal(bootstrapPayload.transportPlan?.authMode, 'jwt');
|
|
assert.equal(bootstrapPayload.transportPlan?.authEndpoint, '/api/nats-jwt');
|
|
assert.equal(bootstrapPayload.sources?.transportAuthMode, 'credentials-ref');
|
|
|
|
const auth = await fetch(`${server.baseUrl}/api/nats-jwt`, { method: 'POST' });
|
|
assert.equal(auth.status, 200);
|
|
const authPayload = await auth.json() as {
|
|
mode?: string;
|
|
jwt?: string;
|
|
seed?: string;
|
|
};
|
|
assert.equal(authPayload.mode, 'jwt');
|
|
assert.equal(authPayload.jwt, 'sample.jwt.value');
|
|
assert.equal(authPayload.seed, 'SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
|
|
} finally {
|
|
await server.shutdown();
|
|
}
|
|
});
|
|
|
|
it('forwards explicit upstream WebSocket paths without duplicating /nats-ws', async () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'standalone-webapp-server-proxy-'));
|
|
const packageDir = path.join(tempRoot, 'package');
|
|
const distDir = path.join(packageDir, 'dist');
|
|
fs.mkdirSync(distDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(distDir, 'index.html'),
|
|
'<!doctype html><html><body>fixture</body></html>',
|
|
'utf8',
|
|
);
|
|
const httpPort = await getAvailablePort();
|
|
const upstreamPort = await getAvailablePort();
|
|
let capturedRequest = '';
|
|
const upstream = net.createServer();
|
|
const captured = new Promise<void>((resolve) => {
|
|
upstream.on('connection', (socket) => {
|
|
socket.once('data', (chunk: Buffer) => {
|
|
capturedRequest = chunk.toString('utf8');
|
|
socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
upstream.once('error', reject);
|
|
upstream.listen(upstreamPort, '127.0.0.1', () => {
|
|
upstream.off('error', reject);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
const loadedEnvironment: ILoadedMatrixPackageEnvironment = {
|
|
packageDir,
|
|
envName: 'host',
|
|
environmentPath: path.join(packageDir, '.matrix', 'host.environment.json'),
|
|
environment: {
|
|
name: 'host',
|
|
runtime: { root: 'COM.TEST.EXTERNAL' },
|
|
nats: {
|
|
mode: 'external',
|
|
url: `nats://127.0.0.1:${upstreamPort}`,
|
|
wsUrl: `ws://127.0.0.1:${upstreamPort}/nats-ws`,
|
|
},
|
|
http: {
|
|
enabled: true,
|
|
port: httpPort,
|
|
},
|
|
},
|
|
};
|
|
const webapp: ILoadedMatrixWebappManifest = {
|
|
packageDir,
|
|
packageName: '@acme/webapp',
|
|
manifestPath: path.join(packageDir, 'matrix.json'),
|
|
distDir,
|
|
entryFile: 'index.html',
|
|
appName: 'webapp',
|
|
};
|
|
const runnerTransport: IResolvedRunnerTransportPlan = {
|
|
envName: 'host',
|
|
root: 'COM.TEST.EXTERNAL',
|
|
mode: 'external',
|
|
url: `nats://127.0.0.1:${upstreamPort}`,
|
|
wsUrl: `ws://127.0.0.1:${upstreamPort}/nats-ws`,
|
|
};
|
|
|
|
const server = await startStandaloneWebappServer({
|
|
loadedEnvironment,
|
|
webapp,
|
|
runnerTransport,
|
|
});
|
|
const client = net.connect(httpPort, '127.0.0.1');
|
|
try {
|
|
await new Promise<void>((resolve) => {
|
|
client.once('connect', resolve);
|
|
});
|
|
client.write([
|
|
'GET /nats-ws?probe=1 HTTP/1.1',
|
|
`Host: 127.0.0.1:${httpPort}`,
|
|
'Connection: Upgrade',
|
|
'Upgrade: websocket',
|
|
'Origin: http://127.0.0.1:9999',
|
|
'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==',
|
|
'Sec-WebSocket-Version: 13',
|
|
'',
|
|
'',
|
|
].join('\r\n'));
|
|
await captured;
|
|
assert.match(capturedRequest, /^GET \/nats-ws\?probe=1 HTTP\/1\.1\r\n/);
|
|
assert.doesNotMatch(capturedRequest, /\/nats-ws\/nats-ws/);
|
|
assert.doesNotMatch(capturedRequest, /\r\nOrigin:/i);
|
|
} finally {
|
|
client.destroy();
|
|
await server.shutdown();
|
|
await new Promise<void>((resolve) => {
|
|
upstream.close(() => resolve());
|
|
});
|
|
}
|
|
});
|
|
});
|