import { z } from "zod"; export const SyncFileKindSchema = z.enum(["text", "binary"]); export type SyncFileKind = z.infer; export const EncryptionEnvelopeSchema = z.object({ algorithm: z.literal("AES-GCM-256"), keyId: z.string().min(1), iv: z.string().min(1), ciphertext: z.string().min(1) }); export type EncryptionEnvelope = z.infer; export const FileManifestSchema = z.object({ path: z.string().min(1), kind: SyncFileKindSchema, contentHash: z.string().min(1), revisionId: z.string().min(1), baseRevisionId: z.string().min(1).optional(), updatedAt: z.string().datetime(), sizeBytes: z.number().int().nonnegative(), deviceId: z.string().min(1) }); export type FileManifest = z.infer; export const SyncFileRecordSchema = z.object({ manifest: FileManifestSchema, envelope: EncryptionEnvelopeSchema }); export type SyncFileRecord = z.infer; export const TombstoneSchema = z.object({ path: z.string().min(1), revisionId: z.string().min(1), baseRevisionId: z.string().min(1).optional(), deletedAt: z.string().datetime(), deviceId: z.string().min(1) }); export type Tombstone = z.infer; export const SyncConflictSchema = z.object({ path: z.string().min(1), serverRevisionId: z.string().min(1), clientRevisionId: z.string().min(1), reason: z.enum(["revision-mismatch"]) }); export type SyncConflict = z.infer; export const SyncChangeSchema = z .object({ serverRevision: z.number().int().nonnegative(), file: SyncFileRecordSchema.optional(), tombstone: TombstoneSchema.optional() }) .refine((value) => Number(Boolean(value.file)) + Number(Boolean(value.tombstone)) === 1, { message: "A change must include either a file or a tombstone." }); export type SyncChange = z.infer; export const RegisterDeviceRequestSchema = z.object({ vaultId: z.string().min(1), deviceName: z.string().min(1) }); export type RegisterDeviceRequest = z.infer; export const RegisterDeviceResponseSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1), token: z.string().min(1), issuedAt: z.string().datetime() }); export type RegisterDeviceResponse = z.infer; export const RecoveryBundleSchema = z.object({ version: z.literal(1), serverUrl: z.string().min(1), vaultId: z.string().min(1), keyId: z.string().min(1), exportedVaultKey: z.string().min(1), generatedAt: z.string().datetime() }); export type RecoveryBundle = z.infer; export const DeviceRecordSchema = z.object({ deviceId: z.string().min(1), deviceName: z.string().min(1), issuedAt: z.string().datetime(), revokedAt: z.string().datetime().optional() }); export type DeviceRecord = z.infer; export const ListDevicesRequestSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1) }); export type ListDevicesRequest = z.infer; export const ListDevicesResponseSchema = z.object({ devices: z.array(DeviceRecordSchema), activeKeyId: z.string().min(1).optional(), keyRotatedAt: z.string().datetime().optional() }); export type ListDevicesResponse = z.infer; export const RevokeDeviceRequestSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1), targetDeviceId: z.string().min(1) }); export type RevokeDeviceRequest = z.infer; export const RevokeDeviceResponseSchema = z.object({ targetDeviceId: z.string().min(1), revokedAt: z.string().datetime() }); export type RevokeDeviceResponse = z.infer; export const RotateVaultKeyRequestSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1), nextKeyId: z.string().min(1), previousKeyId: z.string().min(1).optional() }); export type RotateVaultKeyRequest = z.infer; export const RotateVaultKeyResponseSchema = z.object({ activeKeyId: z.string().min(1), rotatedAt: z.string().datetime() }); export type RotateVaultKeyResponse = z.infer; export const SyncPullRequestSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1), sinceServerRevision: z.number().int().nonnegative(), limit: z.number().int().positive().max(500).optional() }); export type SyncPullRequest = z.infer; export const SyncPullResponseSchema = z.object({ serverRevision: z.number().int().nonnegative(), changes: z.array(SyncChangeSchema), hasMore: z.boolean(), nextSinceServerRevision: z.number().int().nonnegative(), activeKeyId: z.string().min(1).optional(), keyRotatedAt: z.string().datetime().optional() }); export type SyncPullResponse = z.infer; export const SyncPushRequestSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1), knownServerRevision: z.number().int().nonnegative(), files: z.array(SyncFileRecordSchema), tombstones: z.array(TombstoneSchema) }); export type SyncPushRequest = z.infer; export const SyncPushResponseSchema = z.object({ acceptedServerRevision: z.number().int().nonnegative(), acceptedFilePaths: z.array(z.string()), acceptedTombstones: z.array(z.string()), conflicts: z.array(SyncConflictSchema), activeKeyId: z.string().min(1).optional(), keyRotatedAt: z.string().datetime().optional() }); export type SyncPushResponse = z.infer; export const ClientLogEntrySchema = z.object({ level: z.enum(["debug", "info", "warn", "error"]), message: z.string().min(1), timestamp: z.string().datetime(), deviceId: z.string().min(1), context: z.record(z.string(), z.unknown()).optional() }); export type ClientLogEntry = z.infer; export const ClientLogUploadRequestSchema = z.object({ vaultId: z.string().min(1), deviceId: z.string().min(1), runId: z.string().min(1), entries: z.array(ClientLogEntrySchema) }); export type ClientLogUploadRequest = z.infer; export const ClientLogUploadResponseSchema = z.object({ accepted: z.number().int().nonnegative(), requestId: z.string().min(1) }); export type ClientLogUploadResponse = z.infer; export function parseWithSchema(schema: z.ZodSchema, value: unknown): T { return schema.parse(value); }