f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
5.56 kB
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
import { createLogger } from '../logging/logger.js';
import { APIError, ErrorCode } from './constants.js';
import { toUrlSafeBase64, fromUrlSafeBase64 } from './general.js';
import { config as appConfig, settingsStore } from '../config/index.js';
const logger = createLogger('auth');
const CONFIG_ACCESS_KEY_SETTING = 'api.configAccessKey';
const AUTH_REQUIRED_SETTING = 'api.authRequired';
export interface SessionUser {
username: string;
isAdmin: boolean;
}
interface SessionPayload {
u: string;
a: boolean;
exp: number;
}
function constantTimeEquals(a: string, b: string): boolean {
const ab = Buffer.from(a);
const bb = Buffer.from(b);
if (ab.length !== bb.length) return false;
return timingSafeEqual(ab, bb);
}
/**
* Validate a username/password pair against the AIOSTREAMS_AUTH credential
* map. This is the same map used by the built-in proxy and NZB-grab proxying.
*/
export function validateCredentials(
username: string,
password: string
): boolean {
const stored = appConfig.bootstrap.auth?.get(username);
if (stored === undefined) return false;
return constantTimeEquals(stored, password);
}
/**
* Whether a username is an admin. If AIOSTREAMS_AUTH_ADMINS is unset/empty,
* every authenticated user is an admin (matches the documented env behaviour).
*/
export function isAdminUser(username: string): boolean {
const admins = appConfig.bootstrap.authAdmins;
if (!admins || admins.length === 0) return true;
return admins.includes(username);
}
/**
* Whether a username is allowed to use the built-in proxy.
* If AIOSTREAMS_AUTH_PROXY is unset/empty, all authenticated users may use it.
*/
export function canUseProxy(username: string): boolean {
const allowed = appConfig.bootstrap.authProxy;
if (!allowed || allowed.length === 0) return true;
return allowed.includes(username);
}
function sign(data: string): string {
return createHmac('sha256', appConfig.bootstrap.secretKey)
.update(data)
.digest('base64url');
}
/**
* Issue a stateless, HMAC-signed session token (JWT-like) for a username.
*/
export function issueSession(username: string): string {
const ttl = appConfig.api.sessionTtlSeconds;
const payload: SessionPayload = {
u: username,
a: isAdminUser(username),
exp: Math.floor(Date.now() / 1000) + ttl,
};
const body = toUrlSafeBase64(JSON.stringify(payload));
return `${body}.${sign(body)}`;
}
/**
* Verify a session token. Returns the session user on success, null on any
* failure (bad signature, malformed, expired).
*/
export function verifySession(token: string | undefined): SessionUser | null {
if (!token) return null;
const dot = token.lastIndexOf('.');
if (dot <= 0) return null;
const body = token.slice(0, dot);
const sig = token.slice(dot + 1);
if (!constantTimeEquals(sig, sign(body))) return null;
try {
const payload = JSON.parse(fromUrlSafeBase64(body)) as SessionPayload;
if (
typeof payload.u !== 'string' ||
typeof payload.exp !== 'number' ||
payload.exp < Math.floor(Date.now() / 1000)
) {
return null;
}
return { username: payload.u, isAdmin: !!payload.a };
} catch {
return null;
}
}
/**
* The active config access key, or null when the config-write gate is
* disabled (authRequired is false).
*/
export function getConfigAccessKey(): string | null {
if (!appConfig.api.authRequired) return null;
const key = appConfig.api.configAccessKey;
return key && key.length > 0 ? key : null;
}
function regenerateAccessKey(): string {
const newKey = randomBytes(24).toString('hex');
settingsStore.set(CONFIG_ACCESS_KEY_SETTING, newKey, 'system:auth');
return newKey;
}
/**
* Ensure a config access key exists. Call once at startup.
*
*/
export async function ensureConfigAccessKey(): Promise<void> {
if (appConfig.api.configAccessKey) return;
if (process.env.CONFIG_ACCESS_KEY !== undefined) return; // env-managed
const legacy = process.env.ADDON_PASSWORD;
const passwords = legacy
?.split(',')
.map((p) => p.trim())
.filter((p) => p.length > 0);
if (legacy && legacy.length > 0 && passwords && passwords.length > 0) {
await settingsStore.set(
CONFIG_ACCESS_KEY_SETTING,
passwords[0],
'system:auth'
);
if (!appConfig.api.authRequired) {
await settingsStore.set(AUTH_REQUIRED_SETTING, true, 'system:auth');
}
logger.warn(
'Migrated legacy ADDON_PASSWORD env into the config access key setting. ADDON_PASSWORD is deprecated; use CONFIG_ACCESS_KEY or manage the key from the dashboard.'
);
return;
}
if (!appConfig.api.authRequired) return;
regenerateAccessKey();
logger.info(
'Generated and persisted a config access key (CONFIG_ACCESS_KEY was not set).'
);
}
/**
* Enforce the config-write gate. When the gate is active, the config must
* carry the current access key in its `accessKey` field. Throws
* ADDON_PASSWORD_INVALID otherwise. No-op when the gate is disabled.
*/
export function assertConfigAccessKey(config: { accessKey?: string }): void {
let key = getConfigAccessKey();
if (!key) {
if (appConfig.api.authRequired) {
logger.warn(
'Config access key is missing but auth is required; a new key is being generated'
);
key = regenerateAccessKey();
} else {
return;
}
}
if (!config.accessKey || !constantTimeEquals(config.accessKey, key)) {
throw new APIError(ErrorCode.ADDON_PASSWORD_INVALID);
}
}