- Admin login gate (docs/design/auth-reference.md): admin_user/admin_session/login_attempt
(migration 0015), one-time setup token logged once + conditional UPDATE, argon2id via
hash-wasm (WASM; compiled binary has no --allow-ffi), sha256-stored sessions in HttpOnly
SameSite=Lax cookie umt_session (fixed 12h TTL, soft revoke), HMAC-derived double-submit
CSRF, IP (5) + account (15) failure limits per 15 min, password change revokes other
sessions, optional TOTP with replay counter, AuditAction enum.
- Unified auth middleware: cookie session or Bearer API key -> c.var.actor
{type: admin|api_key, id, name, scopes}; requireScope works for both; requireAdminSession.
- ctx.secrets: AES-256-GCM, per-secret HKDF-SHA256 key from SECRET_MASTER_KEY, AAD bound
to (id, purpose); ctx.settings: 30s cache invalidated by settings.changed events.
- Endpoints: /auth/*, /settings, /api-keys, /secrets, /audit-events, library-roots,
downloader/media-server instances (+test), path-mappings (+verify),
PUT /sites/{id}/credentials, /engine/runs|stats|cancel (read-only openworkflow schema).
- SSE: PgEventBus (sse_event table, migration 0016, LISTEN/NOTIFY umt_events, catch-up by
seq, pruning, resync); EventBusPort is now async.
- Tests: login/rate limit/revocation/CSRF/TOTP, secrets round trip and no log leakage,
admin APIs against mock qB/Emby, SSE across two Deno subprocesses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
44 lines
1.4 KiB
TypeScript
44 lines
1.4 KiB
TypeScript
// SSE 多进程测试用子进程:sub 模式订阅并逐行输出收到的事件 JSON,收满 n 条退出;pub 模式发布 n 条后退出。
|
|
// 用法:deno run -A test/fixtures/sse-bus-proc.ts <sub|pub> <schema> <channel> <n>
|
|
import type { DB } from "@umt/db";
|
|
import { Kysely, PostgresDialect } from "kysely";
|
|
import pg from "pg";
|
|
import { PgEventBus } from "../../src/http/sse-pg.ts";
|
|
import { createLogger } from "../../src/logger.ts";
|
|
|
|
const [mode, schema, channel, nRaw] = Deno.args;
|
|
const n = Number(nRaw);
|
|
const url = Deno.env.get("DATABASE_URL") ?? "postgres://umt:umt@localhost:55433/umt";
|
|
const db = new Kysely<DB>({
|
|
dialect: new PostgresDialect({
|
|
pool: new pg.Pool({ connectionString: url, max: 2, options: `-c search_path=${schema},public` }),
|
|
}),
|
|
});
|
|
const bus = await PgEventBus.start({
|
|
db,
|
|
connectionString: url,
|
|
logger: createLogger({ level: "warn" }),
|
|
channel: channel!,
|
|
});
|
|
|
|
if (mode === "sub") {
|
|
let got = 0;
|
|
const done = new Promise<void>((resolve) => {
|
|
bus.subscribe((e) => {
|
|
console.log(JSON.stringify(e));
|
|
if (++got >= n) resolve();
|
|
});
|
|
});
|
|
console.log("READY");
|
|
const timeout = setTimeout(() => {
|
|
console.error("timeout");
|
|
Deno.exit(3);
|
|
}, 20_000);
|
|
await done;
|
|
clearTimeout(timeout);
|
|
} else {
|
|
for (let i = 0; i < n; i++) await bus.publish("test.ping", { i, pid: Deno.pid });
|
|
}
|
|
await bus.stop();
|
|
await db.destroy();
|