feat(server): W2-A login gate, secrets, settings, instances, audit, engine panel API and PG-backed SSE bus
- 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]>
This commit is contained in:
1 parent
07db0dac5b
commit
ff230e5da7
34 files changed
+4363
-78
No files matched your search
@@ -192,3 +192,115 @@ export const SecretCreate = z.object({
|
||||
export type SecretCreate = z.infer<typeof SecretCreate>;
|
||||
export const SecretUpdate = z.object({ plaintext: z.string().min(1).max(16384) });
|
||||
export type SecretUpdate = z.infer<typeof SecretUpdate>;
|
||||
|
||||
/** GET /audit-events 过滤:按 actor / action / target;游标分页(新 → 旧) */
|
||||
export const AuditEventQuery = z.object({
|
||||
cursor: z.string().min(1).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
actorType: z.string().max(40).optional(),
|
||||
actorId: z.string().max(200).optional(),
|
||||
/** 精确匹配;以 `.*` 结尾时按前缀匹配(如 `admin.*`) */
|
||||
action: z.string().max(80).optional(),
|
||||
targetType: z.string().max(80).optional(),
|
||||
targetId: z.string().max(200).optional(),
|
||||
});
|
||||
export type AuditEventQuery = z.infer<typeof AuditEventQuery>;
|
||||
|
||||
// ── 引擎面板(只读直查 openworkflow schema,仅 admin;docs/design/openworkflow-dashboard-reference.md) ──
|
||||
|
||||
/** 引擎 run 状态(含 legacy sleeping/succeeded) */
|
||||
export const EngineRunStatus = z.enum([
|
||||
"pending",
|
||||
"running",
|
||||
"sleeping",
|
||||
"succeeded",
|
||||
"completed",
|
||||
"failed",
|
||||
"canceled",
|
||||
]);
|
||||
export type EngineRunStatus = z.infer<typeof EngineRunStatus>;
|
||||
export const EngineStepKind = z.enum(["function", "sleep", "workflow", "signal-send", "signal-wait"]);
|
||||
export type EngineStepKind = z.infer<typeof EngineStepKind>;
|
||||
|
||||
/** 引擎 ID 是 openworkflow 自己生成的文本 ID(非本地 UUID) */
|
||||
const EngineId = z.string().min(1).max(200);
|
||||
|
||||
export const EngineRun = z.object({
|
||||
id: EngineId,
|
||||
workflowName: z.string(),
|
||||
version: z.string().nullable(),
|
||||
status: z.string(),
|
||||
attempts: z.number().int(),
|
||||
workerId: z.string().nullable(),
|
||||
parentStepAttemptId: z.string().nullable(),
|
||||
availableAt: Timestamp.nullable(),
|
||||
deadlineAt: Timestamp.nullable(),
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
createdAt: Timestamp,
|
||||
updatedAt: Timestamp,
|
||||
});
|
||||
export type EngineRun = z.infer<typeof EngineRun>;
|
||||
|
||||
export const EngineRunQuery = z.object({
|
||||
cursor: z.string().min(1).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
status: EngineRunStatus.optional(),
|
||||
workflowName: z.string().max(200).optional(),
|
||||
/** created_at 下界(含) */
|
||||
from: Timestamp.optional(),
|
||||
/** created_at 上界(不含) */
|
||||
to: Timestamp.optional(),
|
||||
});
|
||||
export type EngineRunQuery = z.infer<typeof EngineRunQuery>;
|
||||
|
||||
/** step attempt:不含 config;context 只露 signal-wait 的信号名与超时(排查卡等待用) */
|
||||
export const EngineStepAttempt = z.object({
|
||||
id: EngineId,
|
||||
stepName: z.string(),
|
||||
kind: z.string(),
|
||||
status: z.string(),
|
||||
output: z.unknown(),
|
||||
error: z.unknown(),
|
||||
waitSignal: z.string().nullable(),
|
||||
timeoutAt: z.string().nullable(),
|
||||
childWorkflowRunId: z.string().nullable(),
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
createdAt: Timestamp,
|
||||
});
|
||||
export type EngineStepAttempt = z.infer<typeof EngineStepAttempt>;
|
||||
|
||||
export const EngineSignal = z.object({
|
||||
id: EngineId,
|
||||
signal: z.string(),
|
||||
data: z.unknown(),
|
||||
stepAttemptId: z.string(),
|
||||
createdAt: Timestamp,
|
||||
});
|
||||
export type EngineSignal = z.infer<typeof EngineSignal>;
|
||||
|
||||
/** run 详情:input/output/error 经敏感字段脱敏;不返回 config/context */
|
||||
export const EngineRunDetail = EngineRun.extend({
|
||||
input: z.unknown(),
|
||||
output: z.unknown(),
|
||||
error: z.unknown(),
|
||||
stepAttempts: z.array(EngineStepAttempt),
|
||||
signals: z.array(EngineSignal),
|
||||
});
|
||||
export type EngineRunDetail = z.infer<typeof EngineRunDetail>;
|
||||
|
||||
export const EngineStats = z.object({
|
||||
/** 全表按状态计数(不受列表筛选影响) */
|
||||
byStatus: z.record(z.string(), z.number().int().nonnegative()),
|
||||
/** 最老的待处理 run(pending,按 available_at/created_at) */
|
||||
oldestPending: z
|
||||
.object({ id: EngineId, workflowName: z.string(), since: Timestamp, ageSec: z.number().nonnegative() })
|
||||
.nullable(),
|
||||
/** 当前持有租约的 worker 快照(非权威注册表:引擎没有 worker 表) */
|
||||
activeWorkers: z.array(z.object({ workerId: z.string(), running: z.number().int(), since: Timestamp.nullable() })),
|
||||
});
|
||||
export type EngineStats = z.infer<typeof EngineStats>;
|
||||
|
||||
export const EngineCancelRequest = z.object({ reason: z.string().max(500).optional() });
|
||||
export const EngineCancelResult = z.object({ canceled: z.boolean() });
|
||||
Reference in new issue
Block a user