195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const SyncFileKindSchema = z.enum(["text", "binary"]);
|
|
export type SyncFileKind = z.infer<typeof SyncFileKindSchema>;
|
|
|
|
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<typeof EncryptionEnvelopeSchema>;
|
|
|
|
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<typeof FileManifestSchema>;
|
|
|
|
export const SyncFileRecordSchema = z.object({
|
|
manifest: FileManifestSchema,
|
|
envelope: EncryptionEnvelopeSchema
|
|
});
|
|
export type SyncFileRecord = z.infer<typeof SyncFileRecordSchema>;
|
|
|
|
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<typeof TombstoneSchema>;
|
|
|
|
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<typeof SyncConflictSchema>;
|
|
|
|
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<typeof SyncChangeSchema>;
|
|
|
|
export const RegisterDeviceRequestSchema = z.object({
|
|
vaultId: z.string().min(1),
|
|
deviceName: z.string().min(1)
|
|
});
|
|
export type RegisterDeviceRequest = z.infer<typeof RegisterDeviceRequestSchema>;
|
|
|
|
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<typeof RegisterDeviceResponseSchema>;
|
|
|
|
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<typeof RecoveryBundleSchema>;
|
|
|
|
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<typeof DeviceRecordSchema>;
|
|
|
|
export const ListDevicesRequestSchema = z.object({
|
|
vaultId: z.string().min(1),
|
|
deviceId: z.string().min(1)
|
|
});
|
|
export type ListDevicesRequest = z.infer<typeof ListDevicesRequestSchema>;
|
|
|
|
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<typeof ListDevicesResponseSchema>;
|
|
|
|
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<typeof RevokeDeviceRequestSchema>;
|
|
|
|
export const RevokeDeviceResponseSchema = z.object({
|
|
targetDeviceId: z.string().min(1),
|
|
revokedAt: z.string().datetime()
|
|
});
|
|
export type RevokeDeviceResponse = z.infer<typeof RevokeDeviceResponseSchema>;
|
|
|
|
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<typeof RotateVaultKeyRequestSchema>;
|
|
|
|
export const RotateVaultKeyResponseSchema = z.object({
|
|
activeKeyId: z.string().min(1),
|
|
rotatedAt: z.string().datetime()
|
|
});
|
|
export type RotateVaultKeyResponse = z.infer<typeof RotateVaultKeyResponseSchema>;
|
|
|
|
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<typeof SyncPullRequestSchema>;
|
|
|
|
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<typeof SyncPullResponseSchema>;
|
|
|
|
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<typeof SyncPushRequestSchema>;
|
|
|
|
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<typeof SyncPushResponseSchema>;
|
|
|
|
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<typeof ClientLogEntrySchema>;
|
|
|
|
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<typeof ClientLogUploadRequestSchema>;
|
|
|
|
export const ClientLogUploadResponseSchema = z.object({
|
|
accepted: z.number().int().nonnegative(),
|
|
requestId: z.string().min(1)
|
|
});
|
|
export type ClientLogUploadResponse = z.infer<typeof ClientLogUploadResponseSchema>;
|
|
|
|
export function parseWithSchema<T>(schema: z.ZodSchema<T>, value: unknown): T {
|
|
return schema.parse(value);
|
|
}
|