Initial commit
This commit is contained in:
14
packages/sync-protocol/package.json
Normal file
14
packages/sync-protocol/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@obsidian-sync/sync-protocol",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"typecheck": "tsc -b --pretty false"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
194
packages/sync-protocol/src/index.ts
Normal file
194
packages/sync-protocol/src/index.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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);
|
||||
}
|
||||
13
packages/sync-protocol/tsconfig.json
Normal file
13
packages/sync-protocol/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"module": "CommonJS"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user