// SDK 包与推送工具的 npm 仓库(/npm/):包信息、最新版本、包文件、作用域名称的两种写法、安全审计 import { execFileSync } from 'node:child_process' import crypto from 'node:crypto' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import Fastify from 'fastify' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { NpmRegistry, registerNpmRegistry } from '../src/http/npm-registry' const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-registry-')) const f = Fastify() function pack(manifest: Record, file: string) { const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-registry-stage-')) fs.mkdirSync(path.join(stage, 'package')) fs.writeFileSync(path.join(stage, 'package', 'package.json'), JSON.stringify(manifest)) fs.writeFileSync(path.join(stage, 'package', 'index.js'), 'export {}\n') execFileSync('tar', ['czf', path.join(dir, file), 'package'], { cwd: stage }) } beforeAll(async () => { pack({ name: '@agent-server/client', version: '0.5.0' }, 'agent-server-client-0.5.0.tgz') pack({ name: '@agent-server/client', version: '0.10.0', peerDependencies: { '@agent-server/shared': '0.10.0' } }, 'agent-server-client-0.10.0.tgz') pack({ name: '@agent-server/skills-cli', version: '0.3.0', bin: { 'agent-skills': './agent-skills.mjs' } }, 'agent-server-skills-cli-0.3.0.tgz') fs.copyFileSync(path.join(dir, 'agent-server-skills-cli-0.3.0.tgz'), path.join(dir, 'agent-skills.tgz')) // 不带版本号的文件不算 fs.writeFileSync(path.join(dir, 'broken-1.0.0.tgz'), 'not a tarball') registerNpmRegistry(f, new NpmRegistry(() => dir)) await f.ready() }) afterAll(() => f.close()) describe('npm 仓库', () => { it('包信息:全部版本、最新版本按数字比较、对等依赖与 bin 原样保留、校验值与文件一致', async () => { for (const url of ['/npm/@agent-server%2fclient', '/npm/@agent-server%2Fclient', '/npm/@agent-server/client']) { const r = await f.inject({ url, headers: { host: 'agent.example.com' } }) expect(r.statusCode).toBe(200) expect(r.headers['cache-control']).toBe('no-cache') const doc = r.json() expect(doc['dist-tags']).toEqual({ latest: '0.10.0' }) expect(Object.keys(doc.versions).sort()).toEqual(['0.10.0', '0.5.0']) expect(doc.versions['0.10.0'].peerDependencies).toEqual({ '@agent-server/shared': '0.10.0' }) expect(doc.versions['0.10.0'].dist.tarball).toBe('http://agent.example.com/npm/-/agent-server-client-0.10.0.tgz') } const cli = (await f.inject({ url: '/npm/@agent-server%2fskills-cli' })).json() expect(Object.keys(cli.versions)).toEqual(['0.3.0']) expect(cli.versions['0.3.0'].bin).toEqual({ 'agent-skills': './agent-skills.mjs' }) const tgz = await f.inject({ url: '/npm/-/agent-server-skills-cli-0.3.0.tgz' }) expect(tgz.statusCode).toBe(200) expect(tgz.headers['cache-control']).toContain('immutable') expect(cli.versions['0.3.0'].dist.integrity).toBe(`sha512-${crypto.createHash('sha512').update(tgz.rawPayload).digest('base64')}`) }) it('不存在的包与文件为 404;旧的固定文件名不提供;安全审计返回空结果;最新版本列表', async () => { expect((await f.inject({ url: '/npm/@agent-server%2fnope' })).statusCode).toBe(404) expect((await f.inject({ url: '/npm/-/agent-skills.tgz' })).statusCode).toBe(404) expect((await f.inject({ url: '/npm/-/..%2f..%2fetc%2fpasswd' })).statusCode).toBe(404) expect((await f.inject({ method: 'POST', url: '/npm/-/npm/v1/security/advisories/bulk', payload: {} })).json()).toEqual({}) expect((await f.inject({ url: '/npm/-/latest' })).json()).toEqual(expect.arrayContaining([ { name: '@agent-server/client', version: '0.10.0', bin: false }, { name: '@agent-server/skills-cli', version: '0.3.0', bin: true }, ])) }) })