import { spawn } from 'node:child_process';
import { mkdir, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const RELAY_ORIGINS = [
'https://oauth-redirect.googleusercontent.com/',
'https://oauth-redirect-sandbox.googleusercontent.com/',
'https://oauth-redirect-test.googleusercontent.com/',
];
const compiled = new URL(import.meta.url).pathname.startsWith('/$bunfs/');
export function gatewayConfig(env = process.env) {
const external = new URL(env.EXTERNAL_URL || 'http://127.0.0.1:3000');
if (
external.username ||
external.password ||
external.search ||
external.hash ||
!['', '/'].includes(external.pathname) ||
(external.protocol !== 'https:' &&
!(external.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(external.hostname)))
)
throw new Error('EXTERNAL_URL must be an https origin (or loopback http for local testing)');
if (!env.MCP_ALLOWED_SUBJECT?.trim())
throw new Error('set MCP_ALLOWED_SUBJECT to your identity provider subject');
const dataPath = resolve(
env.DATA_PATH || join(env.HOME || homedir(), '.local', 'share', 'colab-mcp'),
);
if (dirname(dataPath) === dataPath) throw new Error('DATA_PATH must not be a filesystem root');
return {
...env,
EXTERNAL_URL: external.origin,
LISTEN: env.LISTEN || '127.0.0.1:3000',
NO_AUTO_TLS: env.NO_AUTO_TLS || 'true',
DATA_PATH: dataPath,
OIDC_ALLOWED_USERS: env.OIDC_ALLOWED_USERS || env.MCP_ALLOWED_SUBJECT,
OIDC_USER_ID_FIELD: env.OIDC_USER_ID_FIELD || '/sub',
MCP_REDIRECT_PREFIXES: env.MCP_REDIRECT_PREFIXES ?? RELAY_ORIGINS.join(','),
COLAB_MANAGED_CREDENTIALS: resolve(
env.COLAB_MANAGED_CREDENTIALS || join(dataPath, 'colab-google.json'),
),
COLAB_LOGIN_URL: new URL('/.auth/colab', external).href,
};
}
export async function serve({ env = process.env, spawnProcess = spawn, signals = process } = {}) {
const config = gatewayConfig(env);
await mkdir(config.DATA_PATH, { recursive: true, mode: 0o700 });
if ((await stat(config.DATA_PATH)).mode & 0o077)
throw new Error('DATA_PATH must be private (chmod 700)');
const base = compiled
? dirname(process.execPath)
: fileURLToPath(new URL('../dist/', import.meta.url));
const binary =
env.COLAB_GATEWAY_BIN ||
join(base, `colab-mcp-gateway${process.platform === 'win32' ? '.exe' : ''}`);
const worker = compiled
? [process.execPath]
: [process.execPath, fileURLToPath(new URL('./index.js', import.meta.url))];
const child = spawnProcess(binary, ['--', ...worker], { env: config, stdio: 'inherit' });
let deadline;
const stop = () => {
if (deadline) return;
child.kill('SIGTERM');
deadline = setTimeout(() => child.kill('SIGKILL'), 10_000);
deadline.unref();
};
signals.once('SIGINT', stop);
signals.once('SIGTERM', stop);
try {
return await new Promise((resolveExit, reject) => {
child.once('error', () =>
reject(
new Error(
'cannot start OAuth gateway; run npm run build:gateway or set COLAB_GATEWAY_BIN',
),
),
);
child.once('exit', (code, signal) => resolveExit(code ?? (signal ? 1 : 0)));
});
} finally {
clearTimeout(deadline);
signals.removeListener('SIGINT', stop);
signals.removeListener('SIGTERM', stop);
}
}