import { z } from "zod"; import { ActorType, ApiScope, EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common"; // 运维域:校验运行、媒体服务器库存映射、API Key、审计、outbox、凭证(REQ §8.1、§7.4、§13.2) export const ReconcileKind = z.enum(["identity", "metadata", "asset", "acquisition", "server"]); export type ReconcileKind = z.infer; /** read_only:恢复后默认只读对账模式(REQ §8.5) */ export const ReconcileMode = z.enum(["normal", "read_only"]); export type ReconcileMode = z.infer; export const ReconcileStatus = z.enum(["queued", "running", "succeeded", "partial", "failed", "cancelled"]); export type ReconcileStatus = z.infer; export const ReconcileRun = EntityBase.extend({ kind: ReconcileKind, mode: ReconcileMode, status: ReconcileStatus, scope: JsonObject, /** 中断续扫位置 */ resumeCursor: JsonObject, stats: JsonObject, taskId: Id.nullable(), error: JsonObject.nullable(), startedAt: Timestamp.nullable(), finishedAt: Timestamp.nullable(), }); export type ReconcileRun = z.infer; export const ReconcileRunCreate = ReconcileRun.pick({ kind: true, mode: true, scope: true }).partial({ mode: true, scope: true, }); export type ReconcileRunCreate = z.infer; export const ReconcileRunUpdate = ReconcileRun.pick({ status: true, resumeCursor: true, stats: true, taskId: true, error: true, startedAt: true, finishedAt: true, }).partial(); export type ReconcileRunUpdate = z.infer; export const ServerItemType = z.enum(["movie", "series", "season", "episode", "other"]); export type ServerItemType = z.infer; /** 未知 ≠ 缺失:stale/unmatched 不能推出"没有资源" */ export const ServerMatchStatus = z.enum(["matched", "ambiguous", "unmatched", "stale"]); export type ServerMatchStatus = z.infer; /** Emby 兼容库存条目与本地对象的映射(辅助证据,不构成文件纳管授权) */ export const ServerMapping = EntityBase.extend({ mediaServerInstanceId: Id, serverItemId: z.string().min(1), itemType: ServerItemType, mediaEntityId: Id.nullable(), contentUnitId: Id.nullable(), mediaAssetId: Id.nullable(), serverPath: z.string().nullable(), pathMappingId: Id.nullable(), matchStatus: ServerMatchStatus, versionInfo: JsonObject, evidence: JsonObject, observedAt: Timestamp, }); export type ServerMapping = z.infer; export const ServerMappingCreate = ServerMapping.omit(SERVER_MANAGED).partial({ mediaEntityId: true, contentUnitId: true, mediaAssetId: true, serverPath: true, pathMappingId: true, matchStatus: true, versionInfo: true, evidence: true, observedAt: true, }); export type ServerMappingCreate = z.infer; export const ServerMappingUpdate = ServerMappingCreate.omit({ mediaServerInstanceId: true, serverItemId: true, }).partial(); export type ServerMappingUpdate = z.infer; /** API Key(不含 key_hash) */ export const ApiKey = EntityBase.extend({ name: z.string().min(1), scopes: z.array(ApiScope), revokedAt: Timestamp.nullable(), lastUsedAt: Timestamp.nullable(), }); export type ApiKey = z.infer; export const ApiKeyCreate = z.object({ name: z.string().min(1).max(80), scopes: z.array(ApiScope).min(1), }); export type ApiKeyCreate = z.infer; /** 创建响应:明文 key 只在此返回一次 */ export const ApiKeyCreated = z.object({ apiKey: ApiKey, plaintextKey: z.string().min(16) }); export type ApiKeyCreated = z.infer; export const ApiKeyUpdate = z.object({ name: z.string().min(1).max(80).optional(), scopes: z.array(ApiScope).min(1).optional(), revoked: z.literal(true).optional(), }); export type ApiKeyUpdate = z.infer; /** 审计事件(只追加) */ export const AuditEvent = z.object({ id: Id, at: Timestamp, actorType: z.string(), actorId: z.string().nullable(), action: z.string().min(1), targetType: z.string().nullable(), targetId: z.string().nullable(), payload: JsonObject, traceId: z.string().nullable(), }); export type AuditEvent = z.infer; export const AuditEventCreate = AuditEvent.omit({ id: true, at: true }) .partial({ actorId: true, targetType: true, targetId: true, payload: true, traceId: true }) .extend({ actorType: ActorType }); export type AuditEventCreate = z.infer; export const OutboxStatus = z.enum(["pending", "dispatched", "failed", "dead"]); export type OutboxStatus = z.infer; /** outbox:业务事务内写入,分发器轮询 pending 并以 idempotencyKey 入队(ADR-002 §2.1) */ export const OutboxEvent = z.object({ id: Id, at: Timestamp, type: z.string().min(1), payload: JsonObject, publishedAt: Timestamp.nullable(), idempotencyKey: z.string().nullable(), status: OutboxStatus, attempts: NonNegInt, nextAttemptAt: Timestamp, lastError: JsonObject.nullable(), }); export type OutboxEvent = z.infer; export const OutboxEventCreate = OutboxEvent.pick({ type: true, payload: true, idempotencyKey: true, nextAttemptAt: true, }).partial({ payload: true, idempotencyKey: true, nextAttemptAt: true, }); export type OutboxEventCreate = z.infer; export const OutboxEventUpdate = OutboxEvent.pick({ status: true, attempts: true, nextAttemptAt: true, lastError: true, publishedAt: true, }).partial(); export type OutboxEventUpdate = z.infer; export const SecretPurpose = z.enum([ "site_cookie", "site_passkey", "site_api_key", "site_password", "feed_url", "downloader_password", "media_server_api_key", "metadata_api_key", "llm_api_key", "other", ]); export type SecretPurpose = z.infer; /** 加密凭证的元数据视图:密文/nonce 永不出 API */ export const Secret = EntityBase.extend({ name: z.string().min(1), purpose: SecretPurpose, algorithm: z.string(), kdf: z.string(), kdfParams: JsonObject, keyVersion: z.number().int().min(1), rotatedAt: Timestamp.nullable(), }); export type Secret = z.infer; /** plaintext 只写;服务端用 SECRET_MASTER_KEY 派生密钥加密后存 ciphertext/nonce */ export const SecretCreate = z.object({ name: z.string().min(1).max(120), purpose: SecretPurpose, plaintext: z.string().min(1).max(16384), }); export type SecretCreate = z.infer; export const SecretUpdate = z.object({ plaintext: z.string().min(1).max(16384) }); export type SecretUpdate = z.infer;