Initial commit

This commit is contained in:
2026-04-08 11:55:27 +01:00
commit 470a1c15b8
36 changed files with 4932 additions and 0 deletions

View 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"
}
}

View 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);
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"module": "CommonJS"
},
"include": [
"src/**/*.ts"
]
}