refactor: rombak dari monorepo ke project tunggal — flat src/{domain,application,infrastructure,interfaces}, satu package.json tanpa workspaces, tsconfig paths → src/*, update install.sh/Dockerfile/CI
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Shared application state for the REST API server.
|
||||
* Mirrors `apps/interfaces/api/src/state.rs`.
|
||||
*
|
||||
* `ApiState` holds all service implementations wired to concrete
|
||||
* infrastructure adapters. Constructed once at startup (composition root)
|
||||
* and shared across all requests.
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
SessionServiceImpl,
|
||||
ConversationServiceImpl,
|
||||
SettingsServiceImpl,
|
||||
MemoryServiceImpl,
|
||||
} from "@zesdex/application";
|
||||
import {
|
||||
JsonSettingsRepository,
|
||||
JsonAppConfigRepository,
|
||||
JsonConversationRepository,
|
||||
MarkdownMemoryRepository,
|
||||
FileSystemSessionRepository,
|
||||
FileSystemSessionLockRepository,
|
||||
Argon2PasswordService,
|
||||
Hs256TokenService,
|
||||
LlmClient,
|
||||
} from "@zesdex/infrastructure";
|
||||
|
||||
/**
|
||||
* Sliding-window rate limiter for auth endpoints.
|
||||
* Mirrors `infrastructure::middleware::rate_limit::RateLimiter`.
|
||||
*/
|
||||
export class RateLimiter {
|
||||
private hits = new Map<string, number[]>();
|
||||
|
||||
/** Check whether `key` is within `max` requests per `windowSecs`. Returns true if allowed. */
|
||||
check(key: string, max: number, windowSecs: number): boolean {
|
||||
const now = Date.now();
|
||||
const cutoff = now - windowSecs * 1000;
|
||||
const list = (this.hits.get(key) ?? []).filter((t) => t > cutoff);
|
||||
if (list.length >= max) {
|
||||
this.hits.set(key, list);
|
||||
return false;
|
||||
}
|
||||
list.push(now);
|
||||
this.hits.set(key, list);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Concrete session repository wired from infrastructure. */
|
||||
const sessionRepo = new FileSystemSessionRepository();
|
||||
const sessionLockRepo = new FileSystemSessionLockRepository();
|
||||
const conversationRepo = new JsonConversationRepository();
|
||||
const settingsRepo = new JsonSettingsRepository();
|
||||
const appConfigRepo = new JsonAppConfigRepository();
|
||||
const memoryRepo = new MarkdownMemoryRepository();
|
||||
|
||||
/** Rooted, long-lived shared API state. */
|
||||
export interface ApiState {
|
||||
store_base_dir: string;
|
||||
jwt_secret: string;
|
||||
session_service: SessionServiceImpl;
|
||||
conversation_service: ConversationServiceImpl;
|
||||
settings_service: SettingsServiceImpl;
|
||||
memory_service: MemoryServiceImpl;
|
||||
password_service: Argon2PasswordService;
|
||||
token_service: Hs256TokenService;
|
||||
auth_rate_limiter: RateLimiter;
|
||||
/** Serializes read-modify-write of `users.json` (TOCTOU race guard). */
|
||||
users_lock: { lock: () => () => void };
|
||||
llm_client: LlmClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op lock that returns an unlock function.
|
||||
* Bun is single-threaded per process, so the JS event loop already
|
||||
* serialises synchronous read-modify-write of users.json — the lock is
|
||||
* retained for structural parity with the Rust mutex guard.
|
||||
*/
|
||||
function noopLock(): { lock: () => () => void } {
|
||||
return { lock: () => () => {} };
|
||||
}
|
||||
|
||||
/** Construct a new API state with all services wired to their defaults. */
|
||||
export function newApiState(
|
||||
baseDir: string,
|
||||
jwtSecret: string,
|
||||
llmApiKey: string,
|
||||
llmModel: string,
|
||||
llmBaseUrl?: string,
|
||||
): ApiState {
|
||||
const sessionsDir = path.join(baseDir, "sessions");
|
||||
const memoryDir = path.join(baseDir, "memories");
|
||||
|
||||
const session_service = new SessionServiceImpl(sessionRepo, sessionLockRepo, baseDir);
|
||||
const conversation_service = new ConversationServiceImpl(conversationRepo, sessionsDir);
|
||||
const settings_service = new SettingsServiceImpl(settingsRepo, appConfigRepo, baseDir);
|
||||
const memory_service = new MemoryServiceImpl(memoryRepo, memoryDir);
|
||||
|
||||
const token_service = new Hs256TokenService(jwtSecret);
|
||||
const llm = new LlmClient(llmApiKey, llmModel, llmBaseUrl);
|
||||
|
||||
return {
|
||||
store_base_dir: baseDir,
|
||||
jwt_secret: jwtSecret,
|
||||
session_service,
|
||||
conversation_service,
|
||||
settings_service,
|
||||
memory_service,
|
||||
password_service: new Argon2PasswordService(),
|
||||
token_service,
|
||||
auth_rate_limiter: new RateLimiter(),
|
||||
users_lock: noopLock(),
|
||||
llm_client: llm,
|
||||
};
|
||||
}
|
||||
|
||||
/** Load the `users.json` map (username → password hash), or null if absent. */
|
||||
export function loadUsers(baseDir: string): Record<string, string> | null {
|
||||
const p = path.join(baseDir, "users.json");
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(p, "utf8")) as Record<string, string>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist the users map to `users.json` (pretty-printed). */
|
||||
export function saveUsers(baseDir: string, users: Record<string, string>): void {
|
||||
const p = path.join(baseDir, "users.json");
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, JSON.stringify(users, null, 2));
|
||||
}
|
||||
Reference in New Issue
Block a user