SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+571
View File
@@ -0,0 +1,571 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
evaluatePasswordRules,
isDevAuthMode,
isStrongPassword,
PASSWORD_RULES,
prepareWebuiAuthStateForRestore,
WebuiAuth,
} from '../src/webui/auth';
import {
confirmTotpEnrollment,
unwrapTotpSecret,
type TotpPersistedState,
} from '../src/webui/totp';
const TS = '2026-06-18T00:00:00.000Z';
const HASH = 'ab'.repeat(64);
const SALT = 'cd'.repeat(16);
const STRONG = 'Correct-Horse-1!';
const STRONG_NEXT = 'Correct-Horse-2!';
const RFC_SECRET = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ';
const T59_MS = 59_000;
const T59_CODE = '287082';
const VALID_TOTP: TotpPersistedState = {
wrapSalt: '11'.repeat(16),
iv: '22'.repeat(12),
ciphertext: '33'.repeat(10),
authTag: '44'.repeat(16),
recoveryCodeHashes: ['55'.repeat(32), '66'.repeat(32)],
lastUsedStep: 7,
label: 'SnowLuma (testhost)',
};
const AUTH_ENV = ['SNOWLUMA_DEV_MODE', 'SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD'] as const;
const savedEnv: Record<string, string | undefined> = {};
beforeEach(() => {
for (const key of AUTH_ENV) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
});
afterEach(() => {
for (const key of AUTH_ENV) {
if (savedEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = savedEnv[key];
}
}
});
describe('PASSWORD_RULES / evaluatePasswordRules / isStrongPassword', () => {
it('exposes the five published rule ids and labels in order', () => {
expect(PASSWORD_RULES.map((rule) => ({ id: rule.id, label: rule.label }))).toEqual([
{ id: 'length', label: '长度不少于 10 位' },
{ id: 'lower', label: '包含小写字母' },
{ id: 'upper', label: '包含大写字母' },
{ id: 'special', label: '包含特殊符号 (!@#$%…)' },
{ id: 'no-space', label: '不包含空格' },
]);
});
it('marks every rule ok for a 10-character mixed password with a symbol', () => {
expect(evaluatePasswordRules('Abcdefghi!')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: true },
{ id: 'lower', label: '包含小写字母', ok: true },
{ id: 'upper', label: '包含大写字母', ok: true },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: true },
{ id: 'no-space', label: '不包含空格', ok: true },
]);
expect(isStrongPassword('Abcdefghi!')).toBe(true);
});
it('fails only length when the password is 9 characters', () => {
expect(evaluatePasswordRules('Abcdefgh!')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: false },
{ id: 'lower', label: '包含小写字母', ok: true },
{ id: 'upper', label: '包含大写字母', ok: true },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: true },
{ id: 'no-space', label: '不包含空格', ok: true },
]);
expect(isStrongPassword('Abcdefgh!')).toBe(false);
});
it('fails only lower when every letter is uppercase', () => {
expect(evaluatePasswordRules('ABCDEFGHI!')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: true },
{ id: 'lower', label: '包含小写字母', ok: false },
{ id: 'upper', label: '包含大写字母', ok: true },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: true },
{ id: 'no-space', label: '不包含空格', ok: true },
]);
expect(isStrongPassword('ABCDEFGHI!')).toBe(false);
});
it('fails only upper when every letter is lowercase', () => {
expect(evaluatePasswordRules('abcdefghi!')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: true },
{ id: 'lower', label: '包含小写字母', ok: true },
{ id: 'upper', label: '包含大写字母', ok: false },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: true },
{ id: 'no-space', label: '不包含空格', ok: true },
]);
expect(isStrongPassword('abcdefghi!')).toBe(false);
});
it('fails only special when the password is letters and digits', () => {
expect(evaluatePasswordRules('Abcdefgh12')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: true },
{ id: 'lower', label: '包含小写字母', ok: true },
{ id: 'upper', label: '包含大写字母', ok: true },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: false },
{ id: 'no-space', label: '不包含空格', ok: true },
]);
expect(isStrongPassword('Abcdefgh12')).toBe(false);
});
it('fails special and no-space when the only extra character is a space', () => {
expect(evaluatePasswordRules('Abcdefgh i')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: true },
{ id: 'lower', label: '包含小写字母', ok: true },
{ id: 'upper', label: '包含大写字母', ok: true },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: false },
{ id: 'no-space', label: '不包含空格', ok: false },
]);
expect(isStrongPassword('Abcdefgh i')).toBe(false);
});
it('fails every rule for an empty string, including no-space', () => {
expect(evaluatePasswordRules('')).toEqual([
{ id: 'length', label: '长度不少于 10 位', ok: false },
{ id: 'lower', label: '包含小写字母', ok: false },
{ id: 'upper', label: '包含大写字母', ok: false },
{ id: 'special', label: '包含特殊符号 (!@#$%…)', ok: false },
{ id: 'no-space', label: '不包含空格', ok: false },
]);
expect(isStrongPassword('')).toBe(false);
});
});
describe('isDevAuthMode', () => {
it('is true only when SNOWLUMA_DEV_MODE is the string 1', () => {
expect(isDevAuthMode()).toBe(false);
process.env.SNOWLUMA_DEV_MODE = '';
expect(isDevAuthMode()).toBe(false);
process.env.SNOWLUMA_DEV_MODE = '0';
expect(isDevAuthMode()).toBe(false);
process.env.SNOWLUMA_DEV_MODE = 'true';
expect(isDevAuthMode()).toBe(false);
process.env.SNOWLUMA_DEV_MODE = '1';
expect(isDevAuthMode()).toBe(true);
});
});
describe('prepareWebuiAuthStateForRestore', () => {
const base = {
passwordHash: HASH,
passwordSalt: SALT,
mustChangePassword: false,
generatedAt: TS,
updatedAt: TS,
};
it('accepts a complete credential object and omits totp when it is absent or undefined', () => {
expect(prepareWebuiAuthStateForRestore(base)).toEqual({
passwordHash: HASH,
passwordSalt: SALT,
mustChangePassword: false,
generatedAt: TS,
updatedAt: TS,
});
expect(prepareWebuiAuthStateForRestore({ ...base, totp: undefined })).toEqual({
passwordHash: HASH,
passwordSalt: SALT,
mustChangePassword: false,
generatedAt: TS,
updatedAt: TS,
});
});
it('keeps mixed-case hash and salt hex and attaches a valid totp object', () => {
const mixedHash = 'AB'.repeat(64);
const mixedSalt = 'CD'.repeat(16);
expect(
prepareWebuiAuthStateForRestore({
passwordHash: mixedHash,
passwordSalt: mixedSalt,
mustChangePassword: true,
generatedAt: TS,
updatedAt: '2026-06-19T12:34:56.000Z',
totp: VALID_TOTP,
}),
).toEqual({
passwordHash: mixedHash,
passwordSalt: mixedSalt,
mustChangePassword: true,
generatedAt: TS,
updatedAt: '2026-06-19T12:34:56.000Z',
totp: {
wrapSalt: '11'.repeat(16),
iv: '22'.repeat(12),
ciphertext: '33'.repeat(10),
authTag: '44'.repeat(16),
recoveryCodeHashes: ['55'.repeat(32), '66'.repeat(32)],
lastUsedStep: 7,
label: 'SnowLuma (testhost)',
},
});
});
it('rejects a non-object credential state', () => {
expect(() => prepareWebuiAuthStateForRestore(null)).toThrow(
'credential state must be an object',
);
expect(() => prepareWebuiAuthStateForRestore(undefined)).toThrow(
'credential state must be an object',
);
expect(() => prepareWebuiAuthStateForRestore([])).toThrow(
'credential state must be an object',
);
expect(() => prepareWebuiAuthStateForRestore('x')).toThrow(
'credential state must be an object',
);
});
it('rejects a passwordHash that is not exactly 128 hexadecimal characters', () => {
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, passwordHash: 'ab'.repeat(63) }),
).toThrow('passwordHash must be exactly 128 hexadecimal characters');
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, passwordHash: `${HASH}0` }),
).toThrow('passwordHash must be exactly 128 hexadecimal characters');
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, passwordHash: 'gg'.repeat(64) }),
).toThrow('passwordHash must be exactly 128 hexadecimal characters');
expect(() => prepareWebuiAuthStateForRestore({ ...base, passwordHash: 1 })).toThrow(
'passwordHash must be exactly 128 hexadecimal characters',
);
});
it('rejects a passwordSalt that is not exactly 32 hexadecimal characters', () => {
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, passwordSalt: 'cd'.repeat(15) }),
).toThrow('passwordSalt must be exactly 32 hexadecimal characters');
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, passwordSalt: 'zz'.repeat(16) }),
).toThrow('passwordSalt must be exactly 32 hexadecimal characters');
});
it('rejects a non-boolean mustChangePassword', () => {
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, mustChangePassword: 'false' }),
).toThrow('mustChangePassword must be a boolean');
});
it('rejects generatedAt or updatedAt that are not parseable timestamps', () => {
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, generatedAt: 'not-a-timestamp' }),
).toThrow('generatedAt must be a valid timestamp');
expect(() => prepareWebuiAuthStateForRestore({ ...base, generatedAt: 0 })).toThrow(
'generatedAt must be a valid timestamp',
);
expect(() => prepareWebuiAuthStateForRestore({ ...base, updatedAt: '' })).toThrow(
'updatedAt must be a valid timestamp',
);
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, updatedAt: 'Invalid Date' }),
).toThrow('updatedAt must be a valid timestamp');
});
it('rejects an unknown top-level field', () => {
expect(() => prepareWebuiAuthStateForRestore({ ...base, note: 'x' })).toThrow(
'unknown field $.note',
);
});
it('rejects a malformed totp object and a null totp value', () => {
expect(() =>
prepareWebuiAuthStateForRestore({ ...base, totp: { wrapSalt: 'zz' } }),
).toThrow('totp wrapped secret fields are invalid');
expect(() => prepareWebuiAuthStateForRestore({ ...base, totp: null })).toThrow(
'totp state must be an object',
);
});
});
describe('WebuiAuth', () => {
const configPath = path.join('config', 'webui.json');
let tmp: string;
let prevCwd: string;
beforeEach(() => {
prevCwd = process.cwd();
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-webui-auth-'));
process.chdir(tmp);
});
afterEach(() => {
process.chdir(prevCwd);
fs.rmSync(tmp, { recursive: true, force: true });
});
function readAuthFile(): Record<string, unknown> {
return JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;
}
function writeAuthFile(value: string | Record<string, unknown>): void {
fs.mkdirSync('config', { recursive: true });
fs.writeFileSync(
configPath,
typeof value === 'string' ? value : JSON.stringify(value, null, 2),
);
}
function backupNames(): string[] {
if (!fs.existsSync('config')) {
return [];
}
return fs
.readdirSync('config')
.filter((name) => name.startsWith('webui.json.bak.'))
.sort();
}
it('publishes the fixed dev password', () => {
expect(WebuiAuth.devPassword).toBe('snowluma-dev');
});
it('creates config/webui.json with a one-shot hex bootstrap password', () => {
const auth = WebuiAuth.load();
const initial = auth.takeInitialPassword();
expect(initial).toMatch(/^[0-9a-f]{16}$/);
expect(auth.takeInitialPassword()).toBeNull();
expect(auth.mustChangePassword()).toBe(true);
expect(auth.isDevMode()).toBe(false);
expect(auth.verify(initial!)).toBe(true);
expect(auth.verify('wrong-password')).toBe(false);
expect(auth.verify('')).toBe(false);
expect(auth.verify(undefined as unknown as string)).toBe(false);
expect(auth.totpEnabled()).toBe(false);
expect(auth.totpStatus()).toEqual({ enabled: false });
expect(auth.totpState()).toBeUndefined();
const disk = readAuthFile();
expect(disk.mustChangePassword).toBe(true);
expect(disk.totp).toBeUndefined();
expect(disk.passwordHash).toMatch(/^[0-9a-f]{128}$/);
expect(disk.passwordSalt).toMatch(/^[0-9a-f]{32}$/);
expect(disk.generatedAt).toBe(disk.updatedAt);
expect(Number.isFinite(Date.parse(disk.generatedAt as string))).toBe(true);
if (process.platform !== 'win32') {
expect(fs.statSync(configPath).mode & 0o777).toBe(0o600);
}
});
it('rotates a leftover must-change bootstrap password on the next load', () => {
const first = WebuiAuth.load();
const oldPassword = first.takeInitialPassword();
expect(oldPassword).toMatch(/^[0-9a-f]{16}$/);
const second = WebuiAuth.load();
const rotated = second.takeInitialPassword();
expect(rotated).toMatch(/^[0-9a-f]{16}$/);
expect(rotated).not.toBe(oldPassword);
expect(second.mustChangePassword()).toBe(true);
expect(second.verify(oldPassword!)).toBe(false);
expect(second.verify(rotated!)).toBe(true);
});
it('seeds credentials from SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD when it is at least 8 characters', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = '12345678';
const auth = WebuiAuth.load();
expect(auth.takeInitialPassword()).toBeNull();
expect(auth.mustChangePassword()).toBe(false);
expect(auth.verify('12345678')).toBe(true);
expect(readAuthFile().mustChangePassword).toBe(false);
});
it('ignores a bootstrap env value shorter than 8 characters', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = '1234567';
const auth = WebuiAuth.load();
expect(auth.mustChangePassword()).toBe(true);
expect(auth.verify('1234567')).toBe(false);
expect(auth.takeInitialPassword()).toMatch(/^[0-9a-f]{16}$/);
});
it('reloads an already-rotated file without regenerating', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = 'seed-pass';
WebuiAuth.load();
delete process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD;
const again = WebuiAuth.load();
expect(again.takeInitialPassword()).toBeNull();
expect(again.mustChangePassword()).toBe(false);
expect(again.verify('seed-pass')).toBe(true);
expect(backupNames()).toEqual([]);
});
it('backs up invalid JSON and regenerates credentials', () => {
writeAuthFile('{not-json');
const auth = WebuiAuth.load();
const names = backupNames();
expect(names).toEqual([expect.stringMatching(/^webui\.json\.bak\.\d+$/)]);
expect(fs.readFileSync(path.join('config', names[0]!), 'utf8')).toBe('{not-json');
expect(auth.takeInitialPassword()).toMatch(/^[0-9a-f]{16}$/);
expect(auth.mustChangePassword()).toBe(true);
});
it('backs up a schema-invalid object and can reseed from the bootstrap env', () => {
writeAuthFile({ passwordHash: 'zz', passwordSalt: '00', mustChangePassword: false });
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = 'env-password';
const auth = WebuiAuth.load();
expect(backupNames()).toHaveLength(1);
expect(auth.takeInitialPassword()).toBeNull();
expect(auth.mustChangePassword()).toBe(false);
expect(auth.verify('env-password')).toBe(true);
});
it('keeps password credentials when the live totp field is invalid', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = STRONG;
WebuiAuth.load();
delete process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD;
const disk = readAuthFile();
writeAuthFile({ ...disk, totp: { wrapSalt: 'zz' } });
const auth = WebuiAuth.load();
expect(auth.verify(STRONG)).toBe(true);
expect(auth.mustChangePassword()).toBe(false);
expect(auth.totpEnabled()).toBe(false);
expect(auth.totpStatus()).toEqual({ enabled: false });
expect(auth.totpState()).toBeUndefined();
expect(backupNames()).toEqual([]);
});
it('loads a persisted totp object from disk', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = STRONG;
const first = WebuiAuth.load();
delete process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD;
first.persistTotp(VALID_TOTP);
const auth = WebuiAuth.load();
expect(auth.totpEnabled()).toBe(true);
expect(auth.totpStatus()).toEqual({
enabled: true,
remainingRecoveryCodes: 2,
label: 'SnowLuma (testhost)',
});
expect(auth.totpState()).toEqual({
wrapSalt: '11'.repeat(16),
iv: '22'.repeat(12),
ciphertext: '33'.repeat(10),
authTag: '44'.repeat(16),
recoveryCodeHashes: ['55'.repeat(32), '66'.repeat(32)],
lastUsedStep: 7,
label: 'SnowLuma (testhost)',
});
});
it('setPassword writes a new hash, clears must-change, and keeps generatedAt', () => {
const auth = WebuiAuth.load();
const initial = auth.takeInitialPassword()!;
const generatedAt = readAuthFile().generatedAt;
expect(() => auth.setPassword('too-weak')).toThrow('密码不符合强度要求');
auth.setPassword(STRONG);
expect(auth.mustChangePassword()).toBe(false);
expect(auth.verify(initial)).toBe(false);
expect(auth.verify(STRONG)).toBe(true);
const disk = readAuthFile();
expect(disk.mustChangePassword).toBe(false);
expect(disk.generatedAt).toBe(generatedAt);
expect(disk.updatedAt).not.toBe(generatedAt);
expect(disk.totp).toBeUndefined();
});
it('setPassword rewraps totp with the current password', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = STRONG;
const auth = WebuiAuth.load();
delete process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD;
const enabled = confirmTotpEnrollment({
password: STRONG,
secret: RFC_SECRET,
code: T59_CODE,
label: 'SnowLuma (testhost)',
atMs: T59_MS,
});
auth.persistTotp(enabled.state);
expect(() => auth.setPassword(STRONG_NEXT)).toThrow(
'修改密码需要当前密码以重新保护 2FA 密钥',
);
expect(() => auth.setPassword(STRONG_NEXT, '')).toThrow(
'修改密码需要当前密码以重新保护 2FA 密钥',
);
expect(() => auth.setPassword(STRONG_NEXT, 'Wrong-Password-3!')).toThrow(
'无法使用当前密码解开 2FA 密钥',
);
auth.setPassword(STRONG_NEXT, STRONG);
expect(auth.verify(STRONG)).toBe(false);
expect(auth.verify(STRONG_NEXT)).toBe(true);
expect(auth.totpEnabled()).toBe(true);
expect(unwrapTotpSecret(STRONG_NEXT, auth.totpState()!)).toBe(RFC_SECRET);
expect(unwrapTotpSecret(STRONG, auth.totpState()!)).toBeNull();
});
it('persistTotp writes, reports, and then removes 2FA state', () => {
process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD = STRONG;
const auth = WebuiAuth.load();
delete process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD;
auth.persistTotp(VALID_TOTP);
expect(auth.totpEnabled()).toBe(true);
expect(auth.totpStatus()).toEqual({
enabled: true,
remainingRecoveryCodes: 2,
label: 'SnowLuma (testhost)',
});
expect(auth.totpState()).toEqual(VALID_TOTP);
expect(readAuthFile().totp).toEqual(VALID_TOTP);
auth.persistTotp(undefined);
expect(auth.totpEnabled()).toBe(false);
expect(auth.totpStatus()).toEqual({ enabled: false });
expect(auth.totpState()).toBeUndefined();
expect(Object.prototype.hasOwnProperty.call(readAuthFile(), 'totp')).toBe(false);
});
it('uses the in-memory dev password and never reads or writes webui.json', () => {
writeAuthFile('should-not-be-read');
process.env.SNOWLUMA_DEV_MODE = '1';
const auth = WebuiAuth.load();
delete process.env.SNOWLUMA_DEV_MODE;
expect(isDevAuthMode()).toBe(false);
expect(auth.isDevMode()).toBe(true);
expect(auth.mustChangePassword()).toBe(false);
expect(auth.takeInitialPassword()).toBeNull();
expect(auth.verify('snowluma-dev')).toBe(true);
expect(auth.verify(STRONG)).toBe(false);
expect(auth.totpEnabled()).toBe(false);
expect(() => auth.setPassword(STRONG)).toThrow(
'开发模式 (SNOWLUMA_DEV_MODE=1) 已禁用密码修改',
);
expect(() => auth.persistTotp(VALID_TOTP)).toThrow(
'开发模式 (SNOWLUMA_DEV_MODE=1) 已禁用 2FA',
);
expect(fs.readFileSync(configPath, 'utf8')).toBe('should-not-be-read');
});
it('does not create config/webui.json when loading in dev mode on a fresh directory', () => {
process.env.SNOWLUMA_DEV_MODE = '1';
WebuiAuth.load();
expect(fs.existsSync('config')).toBe(false);
});
});