import { afterEach, describe, it } from 'node:test'; import { strict as assert } from 'node:assert'; import * as fs from 'node:fs'; import * as http from 'node:http'; import * as os from 'node:os'; import * as path from 'node:path'; import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js'; import { createMatrixPackage } from '../helpers/package-fixtures.js'; interface INpmPublishRegistryFixture { readonly registryUrl: string; readonly close: () => Promise; readonly publishRequests: () => number; readonly tarballRequests: () => number; readonly authorizationHeaders: () => readonly string[]; } interface IPublishedPackage { readonly name: string; readonly version: string; readonly fileName: string; readonly tarball: Buffer; } function asRecord(value: unknown): Record | null { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function readString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null; } function addressPort(server: http.Server): number { const address = server.address(); if (!address || typeof address === 'string') { throw new Error('Fixture registry did not bind a TCP port'); } return address.port; } async function readRequestBody(req: http.IncomingMessage): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks).toString('utf8'); } async function startNpmPublishRegistryFixture(): Promise { let publishRequests = 0; let tarballRequests = 0; let published: IPublishedPackage | null = null; const authorizationHeaders: string[] = []; const server = http.createServer((req, res) => { void (async () => { const auth = req.headers.authorization; if (typeof auth === 'string') { authorizationHeaders.push(auth); } const registryUrl = `http://127.0.0.1:${addressPort(server)}/`; const url = new URL(req.url ?? '/', registryUrl); if (req.method === 'PUT') { publishRequests += 1; const body = asRecord(JSON.parse(await readRequestBody(req)) as unknown); const packageName = readString(body?.name); const distTags = asRecord(body?.['dist-tags']); const version = readString(distTags?.latest); const attachments = asRecord(body?._attachments); const attachmentEntry = attachments ? Object.entries(attachments)[0] : undefined; const attachment = asRecord(attachmentEntry?.[1]); const data = readString(attachment?.data); if (!packageName || !version || !attachmentEntry || !data) { res.writeHead(400, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'invalid publish payload' })); return; } published = { name: packageName, version, fileName: attachmentEntry[0], tarball: Buffer.from(data, 'base64'), }; res.writeHead(201, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: true })); return; } if (req.method === 'GET' && url.pathname.endsWith('.tgz')) { tarballRequests += 1; if (!published) { res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'no tarball published' })); return; } res.writeHead(200, { 'content-type': 'application/octet-stream' }); res.end(published.tarball); return; } const decodedPath = decodeURIComponent(url.pathname); if (req.method === 'GET' && published && decodedPath === `/${published.name}`) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ name: published.name, 'dist-tags': { latest: published.version }, versions: { [published.version]: { name: published.name, version: published.version, dist: { tarball: new URL(`tarballs/${published.fileName}`, registryUrl).toString(), }, }, }, })); return; } res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not found', path: url.pathname })); })().catch((error: unknown) => { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); }); }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', () => { server.off('error', reject); resolve(); }); }); return { registryUrl: `http://127.0.0.1:${addressPort(server)}/`, close: async () => { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); }, publishRequests: () => publishRequests, tarballRequests: () => tarballRequests, authorizationHeaders: () => authorizationHeaders, }; } describe('FP-30 mx auth + publish integration', { concurrency: false }, () => { const tempDirs: string[] = []; const restoreEnvFns: Array<() => void> = []; afterEach(() => { while (restoreEnvFns.length > 0) { const restore = restoreEnvFns.pop(); if (restore) { restore(); } } while (tempDirs.length > 0) { const dir = tempDirs.pop(); if (dir) { fs.rmSync(dir, { recursive: true, force: true }); } } }); function useMatrixHome(matrixHome: string): void { const previousMatrixHome = process.env.MATRIX_HOME; const previousCliConfig = process.env.MATRIX_CLI_CONFIG; const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY; const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL; const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG; const previousNodeAuthToken = process.env.NODE_AUTH_TOKEN; const previousNpmToken = process.env.NPM_TOKEN; process.env.MATRIX_HOME = matrixHome; delete process.env.MATRIX_CLI_CONFIG; delete process.env.MATRIX_PACKAGE_REGISTRY; delete process.env.MATRIX_REGISTRY_URL; delete process.env.NPM_CONFIG_USERCONFIG; delete process.env.NODE_AUTH_TOKEN; delete process.env.NPM_TOKEN; restoreEnvFns.push(() => { restoreEnv('MATRIX_HOME', previousMatrixHome); restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig); restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry); restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl); restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig); restoreEnv('NODE_AUTH_TOKEN', previousNodeAuthToken); restoreEnv('NPM_TOKEN', previousNpmToken); }); } function restoreEnv(key: string, value: string | undefined): void { if (value === undefined) { delete process.env[key]; return; } process.env[key] = value; } it('requires login before publish and enables registry install after publish', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-auth-publish-')); tempDirs.push(root); const workspaceDir = path.join(root, 'workspace'); fs.mkdirSync(workspaceDir, { recursive: true }); const packagesDir = path.join(root, '.matrix', 'packages'); const registryDir = path.join(root, '.matrix', 'registry'); const credentialsFile = path.join(root, '.matrix', 'credentials.json'); const homeDir = path.join(root, 'home'); const previousHome = process.env.HOME; fs.mkdirSync(homeDir, { recursive: true }); process.env.HOME = homeDir; try { const fixture = createMatrixPackage(path.join(root, 'source'), { packageName: '@acme/fp30-auth-publish', version: '1.0.0', componentType: 'AuthPublishActor', mount: 'auth.publish', }); const publishWithoutLogin = await runMxCli([ 'node', 'mx', 'publish', '--registry-dir', registryDir, '--credentials-file', credentialsFile, '--json', ], { cwd: fixture.packageDir }); assertExitCode(publishWithoutLogin, 1); assert.equal(publishWithoutLogin.errors.some((line) => line.includes('MX_AUTH_REQUIRED')), true); const loginResult = await runMxCli([ 'node', 'mx', 'login', '--username', 'alice', '--token', 'token-auth-publish', '--credentials-file', credentialsFile, '--json', ], { cwd: workspaceDir }); assertExitCode(loginResult, 0); const publishResult = await runMxCli([ 'node', 'mx', 'publish', '--registry-dir', registryDir, '--credentials-file', credentialsFile, '--json', ], { cwd: fixture.packageDir }); assertExitCode(publishResult, 0); const installFromRegistry = await runMxCli([ 'node', 'mx', 'install', `${fixture.packageName}@${fixture.version}`, '--packages-dir', packagesDir, '--registry-dir', registryDir, '--json', ], { cwd: workspaceDir }); assertExitCode(installFromRegistry, 0); const payload = parseLastJson(installFromRegistry.logs) as { targetDir: string }; assert.equal(fs.existsSync(payload.targetDir), true); } finally { restoreEnv('HOME', previousHome); } }); it('publishes to an npm registry API and installs from the published tarball', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npm-publish-')); tempDirs.push(root); const workspaceDir = path.join(root, 'workspace'); fs.mkdirSync(workspaceDir, { recursive: true }); const packagesDir = path.join(root, '.matrix', 'packages'); const credentialsFile = path.join(root, '.matrix', 'credentials.json'); const registry = await startNpmPublishRegistryFixture(); try { const fixture = createMatrixPackage(path.join(root, 'source'), { packageName: '@acme/fp30-npm-publish', version: '1.0.0', componentType: 'NpmPublishActor', mount: 'npm.publish', }); const loginResult = await runMxCli([ 'node', 'mx', 'login', '--username', 'alice', '--token', 'token-npm-publish', '--registry', registry.registryUrl, '--credentials-file', credentialsFile, '--json', ], { cwd: workspaceDir }); assertExitCode(loginResult, 0); const publishResult = await runMxCli([ 'node', 'mx', 'publish', '--registry', registry.registryUrl, '--credentials-file', credentialsFile, '--json', ], { cwd: fixture.packageDir }); assertExitCode(publishResult, 0); const publishPayload = parseLastJson(publishResult.logs) as { packageName: string; version: string; registryUrl?: string; }; assert.equal(publishPayload.packageName, fixture.packageName); assert.equal(publishPayload.version, fixture.version); assert.equal(publishPayload.registryUrl, registry.registryUrl); assert.equal(registry.publishRequests(), 1); assert.equal( registry.authorizationHeaders().some((header) => header.includes('token-npm-publish')), true ); const installResult = await runMxCli([ 'node', 'mx', 'install', `${fixture.packageName}@${fixture.version}`, '--packages-dir', packagesDir, '--registry', registry.registryUrl, '--credentials-file', credentialsFile, '--json', ], { cwd: workspaceDir }); assertExitCode(installResult, 0); const installPayload = parseLastJson(installResult.logs) as { targetDir: string }; assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true); assert.equal(registry.tarballRequests(), 1); } finally { await registry.close(); } }); it('keeps Matrix scoped packages on the requested npm registry API', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-scoped-npm-publish-')); tempDirs.push(root); const workspaceDir = path.join(root, 'workspace'); const homeDir = path.join(root, 'home'); fs.mkdirSync(workspaceDir, { recursive: true }); fs.mkdirSync(homeDir, { recursive: true }); const packagesDir = path.join(root, '.matrix', 'packages'); const credentialsFile = path.join(root, '.matrix', 'credentials.json'); const registry = await startNpmPublishRegistryFixture(); const previousHome = process.env.HOME; const previousGlobalConfig = process.env.NPM_CONFIG_GLOBALCONFIG; const wrongRegistry = 'http://127.0.0.1:9/wrong/npm/'; const poisonedGlobalConfig = path.join(root, 'poisoned-global-npmrc'); fs.writeFileSync( path.join(homeDir, '.npmrc'), [ `@matrix:registry=${wrongRegistry}`, `@open-matrix:registry=${wrongRegistry}`, `@omega:registry=${wrongRegistry}`, '', ].join('\n'), 'utf8', ); fs.writeFileSync( poisonedGlobalConfig, [ `@matrix:registry=${wrongRegistry}`, `@open-matrix:registry=${wrongRegistry}`, `@omega:registry=${wrongRegistry}`, '', ].join('\n'), 'utf8', ); process.env.HOME = homeDir; process.env.NPM_CONFIG_GLOBALCONFIG = poisonedGlobalConfig; try { const fixture = createMatrixPackage(path.join(root, 'source'), { packageName: '@matrix/fp30-scoped-npm-publish', version: '1.0.0', componentType: 'ScopedNpmPublishActor', mount: 'scoped.npm.publish', }); const loginResult = await runMxCli([ 'node', 'mx', 'login', '--username', 'alice', '--token', 'token-scoped-npm-publish', '--registry', registry.registryUrl, '--credentials-file', credentialsFile, '--json', ], { cwd: workspaceDir }); assertExitCode(loginResult, 0); const publishResult = await runMxCli([ 'node', 'mx', 'publish', '--registry', registry.registryUrl, '--credentials-file', credentialsFile, '--json', ], { cwd: fixture.packageDir }); assertExitCode(publishResult, 0); assert.equal(registry.publishRequests(), 1); assert.equal( registry.authorizationHeaders().some((header) => header.includes('token-scoped-npm-publish')), true, ); const installResult = await runMxCli([ 'node', 'mx', 'install', `${fixture.packageName}@${fixture.version}`, '--packages-dir', packagesDir, '--registry', registry.registryUrl, '--credentials-file', credentialsFile, '--json', ], { cwd: workspaceDir }); assertExitCode(installResult, 0); const installPayload = parseLastJson(installResult.logs) as { targetDir: string }; assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true); assert.equal(registry.tarballRequests(), 1); } finally { restoreEnv('HOME', previousHome); restoreEnv('NPM_CONFIG_GLOBALCONFIG', previousGlobalConfig); await registry.close(); } }); it('publishes to the configured npm registry API without a per-command registry flag', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-configured-npm-publish-')); tempDirs.push(root); useMatrixHome(path.join(root, '.matrix')); const workspaceDir = path.join(root, 'workspace'); fs.mkdirSync(workspaceDir, { recursive: true }); const credentialsFile = path.join(root, '.matrix', 'credentials.json'); const registry = await startNpmPublishRegistryFixture(); try { const fixture = createMatrixPackage(path.join(root, 'source'), { packageName: '@acme/fp30-configured-npm-publish', version: '1.0.0', componentType: 'ConfiguredNpmPublishActor', mount: 'configured.npm.publish', }); const setConfig = await runMxCli([ 'node', 'mx', 'config', 'set', 'registry', registry.registryUrl, '--json', ], { cwd: workspaceDir }); assertExitCode(setConfig, 0); const loginResult = await runMxCli([ 'node', 'mx', 'login', '--username', 'alice', '--token', 'token-configured-npm-publish', '--credentials-file', credentialsFile, '--json', ], { cwd: workspaceDir }); assertExitCode(loginResult, 0); const publishResult = await runMxCli([ 'node', 'mx', 'publish', '--credentials-file', credentialsFile, '--json', ], { cwd: fixture.packageDir }); assertExitCode(publishResult, 0); const publishPayload = parseLastJson(publishResult.logs) as { packageName: string; version: string; registryUrl?: string; }; assert.equal(publishPayload.packageName, fixture.packageName); assert.equal(publishPayload.version, fixture.version); assert.equal(publishPayload.registryUrl, registry.registryUrl); assert.equal(registry.publishRequests(), 1); assert.equal( registry.authorizationHeaders().some((header) => header.includes('token-configured-npm-publish')), true, ); } finally { await registry.close(); } }); it('publishes to an npm registry API using an npmrc token', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npmrc-publish-')); tempDirs.push(root); const workspaceDir = path.join(root, 'workspace'); fs.mkdirSync(workspaceDir, { recursive: true }); const packagesDir = path.join(root, '.matrix', 'packages'); const fixture = createMatrixPackage(path.join(root, 'source'), { packageName: '@acme/fp30-npmrc-publish', version: '1.0.0', componentType: 'NpmrcPublishActor', mount: 'npmrc.publish', }); const registry = await startNpmPublishRegistryFixture(); const npmrcPath = path.join(root, '.npmrc'); const registryKey = registry.registryUrl.replace(/^https?:/, ''); const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG; restoreEnvFns.push(() => restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig)); fs.writeFileSync( npmrcPath, `${registryKey}:_authToken=npmrc-token-publish\n`, 'utf8', ); process.env.NPM_CONFIG_USERCONFIG = npmrcPath; try { const publishResult = await runMxCli([ 'node', 'mx', 'publish', '--registry', registry.registryUrl, '--json', ], { cwd: fixture.packageDir }); assertExitCode(publishResult, 0); const installResult = await runMxCli([ 'node', 'mx', 'install', `${fixture.packageName}@${fixture.version}`, '--packages-dir', packagesDir, '--registry', registry.registryUrl, '--json', ], { cwd: workspaceDir }); assertExitCode(installResult, 0); assert.equal(registry.publishRequests(), 1); assert.equal(registry.tarballRequests(), 1); assert.equal(registry.authorizationHeaders().includes('Bearer npmrc-token-publish'), true); } finally { await registry.close(); } }); });