mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-26 01:25:35 +00:00
187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
PrivateFileTooPermissiveError,
|
|
readPrivateFile,
|
|
writePrivateFile,
|
|
} from '#/services/auth/privateFiles';
|
|
import { loadOrCreateServerToken, rotateServerToken } from '#/services/auth/persistentToken';
|
|
import { createTokenStore } from '#/services/auth/tokenStore';
|
|
import { resolvePasswordHash, verifyPassword } from '#/services/auth/password';
|
|
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = mkdtempSync(join(tmpdir(), 'kimi-auth-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('privateFiles', () => {
|
|
it.skipIf(process.platform === 'win32')('writes a file with mode 0600', async () => {
|
|
const p = join(tmpDir, 'secret');
|
|
await writePrivateFile(p, 'hello');
|
|
expect(statSync(p).mode & 0o777).toBe(0o600);
|
|
});
|
|
|
|
it.skipIf(process.platform === 'win32')('creates an absent parent dir with mode 0700', async () => {
|
|
const p = join(tmpDir, 'nested', 'dir', 'secret');
|
|
await writePrivateFile(p, 'hello');
|
|
expect(statSync(join(tmpDir, 'nested', 'dir')).mode & 0o777).toBe(0o700);
|
|
});
|
|
|
|
it('round-trips string content through readPrivateFile', async () => {
|
|
const p = join(tmpDir, 'secret');
|
|
await writePrivateFile(p, 's3cr3t-value');
|
|
const buf = await readPrivateFile(p);
|
|
expect(buf.toString('utf8')).toBe('s3cr3t-value');
|
|
});
|
|
|
|
it('round-trips Buffer content through readPrivateFile', async () => {
|
|
const p = join(tmpDir, 'bin');
|
|
const data = Buffer.from([0, 1, 2, 254, 255]);
|
|
await writePrivateFile(p, data);
|
|
const buf = await readPrivateFile(p);
|
|
expect(buf.equals(data)).toBe(true);
|
|
});
|
|
|
|
it.skipIf(process.platform === 'win32')('readPrivateFile throws on a 0644 file', async () => {
|
|
const p = join(tmpDir, 'leaky');
|
|
writeFileSync(p, 'x', { mode: 0o644 });
|
|
chmodSync(p, 0o644);
|
|
await expect(readPrivateFile(p)).rejects.toThrowError(
|
|
PrivateFileTooPermissiveError,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('tokenStore', () => {
|
|
it('returns the same token from repeated getToken() calls', async () => {
|
|
const store = await createTokenStore(join(tmpDir, 'home'));
|
|
expect(store.getToken()).toBe(store.getToken());
|
|
await store.dispose();
|
|
});
|
|
|
|
it('produces different tokens for different home dirs', async () => {
|
|
const a = await createTokenStore(join(tmpDir, 'home-a'));
|
|
const b = await createTokenStore(join(tmpDir, 'home-b'));
|
|
expect(a.getToken()).not.toBe(b.getToken());
|
|
await a.dispose();
|
|
await b.dispose();
|
|
});
|
|
|
|
it('reuses the same persistent token across stores in one home dir', async () => {
|
|
const home = join(tmpDir, 'home');
|
|
const a = await createTokenStore(home);
|
|
const token = a.getToken();
|
|
await a.dispose();
|
|
const b = await createTokenStore(home);
|
|
expect(b.getToken()).toBe(token);
|
|
await b.dispose();
|
|
});
|
|
|
|
it.skipIf(process.platform === 'win32')('writes the token file with mode 0600 at server.token', async () => {
|
|
const home = join(tmpDir, 'home');
|
|
const store = await createTokenStore(home);
|
|
expect(store.tokenPath).toBe(join(home, 'server.token'));
|
|
expect(statSync(store.tokenPath).mode & 0o777).toBe(0o600);
|
|
await store.dispose();
|
|
});
|
|
|
|
it('isValid accepts the token and rejects wrong / empty / same-length candidates', async () => {
|
|
const store = await createTokenStore(join(tmpDir, 'home'));
|
|
const token = store.getToken();
|
|
expect(store.isValid(token)).toBe(true);
|
|
expect(store.isValid('wrong')).toBe(false);
|
|
expect(store.isValid('')).toBe(false);
|
|
|
|
const other = await createTokenStore(join(tmpDir, 'home-other'));
|
|
expect(other.getToken().length).toBe(token.length);
|
|
expect(store.isValid(other.getToken())).toBe(false);
|
|
await store.dispose();
|
|
await other.dispose();
|
|
});
|
|
|
|
it('dispose() keeps the persistent token file on disk', async () => {
|
|
const store = await createTokenStore(join(tmpDir, 'home'));
|
|
expect(existsSync(store.tokenPath)).toBe(true);
|
|
await store.dispose();
|
|
expect(existsSync(store.tokenPath)).toBe(true);
|
|
});
|
|
|
|
it('re-reads the token after the file is rewritten (live rotation)', async () => {
|
|
const home = join(tmpDir, 'home');
|
|
const store = await createTokenStore(home);
|
|
const original = store.getToken();
|
|
|
|
// Rewrite the same way `rotateServerToken` does (atomic rename → new
|
|
// inode/mtime). Use a distinct, same-length value so the length check in
|
|
// isValid does not short-circuit.
|
|
const rotated = 'r'.repeat(original.length);
|
|
await writePrivateFile(store.tokenPath, rotated);
|
|
|
|
expect(store.getToken()).toBe(rotated);
|
|
expect(store.isValid(rotated)).toBe(true);
|
|
expect(store.isValid(original)).toBe(false);
|
|
await store.dispose();
|
|
});
|
|
});
|
|
|
|
describe('persistentToken', () => {
|
|
it('loadOrCreateServerToken generates once and reuses thereafter', async () => {
|
|
const home = join(tmpDir, 'home');
|
|
const a = await loadOrCreateServerToken(home);
|
|
const b = await loadOrCreateServerToken(home);
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it.skipIf(process.platform === 'win32')('writes server.token with mode 0600', async () => {
|
|
const home = join(tmpDir, 'home');
|
|
await loadOrCreateServerToken(home);
|
|
expect(statSync(join(home, 'server.token')).mode & 0o777).toBe(0o600);
|
|
});
|
|
|
|
it('rotateServerToken writes a new, different token to server.token', async () => {
|
|
const home = join(tmpDir, 'home');
|
|
const original = await loadOrCreateServerToken(home);
|
|
const rotated = await rotateServerToken(home);
|
|
expect(rotated).not.toBe(original);
|
|
expect(readFileSync(join(home, 'server.token'), 'utf8').trim()).toBe(rotated);
|
|
});
|
|
});
|
|
|
|
describe('password', () => {
|
|
it('resolvePasswordHash returns undefined when env is unset or empty', async () => {
|
|
expect(await resolvePasswordHash({})).toBeUndefined();
|
|
expect(await resolvePasswordHash({ KIMI_CODE_PASSWORD: '' })).toBeUndefined();
|
|
});
|
|
|
|
it('hashes a set password with bcrypt and verifies correctly', async () => {
|
|
const passwordHash = await resolvePasswordHash({
|
|
KIMI_CODE_PASSWORD: 'correct-horse-battery-staple',
|
|
});
|
|
expect(passwordHash?.startsWith('$2')).toBe(true);
|
|
expect(await verifyPassword('correct-horse-battery-staple', passwordHash)).toBe(
|
|
true,
|
|
);
|
|
expect(await verifyPassword('wrong-password', passwordHash)).toBe(false);
|
|
});
|
|
|
|
it('verifyPassword returns false when the hash is undefined', async () => {
|
|
expect(await verifyPassword('anything', undefined)).toBe(false);
|
|
});
|
|
});
|