Align sync SDK with snapshot and workspace contracts
This commit is contained in:
parent
944f3742b6
commit
7ad07321b0
25
README.md
25
README.md
|
|
@ -46,6 +46,31 @@ pushes the annotated version tag when needed and uploads the npm tarball and
|
||||||
- Browser activity batches contain only a normalized hostname and bounded
|
- Browser activity batches contain only a normalized hostname and bounded
|
||||||
duration. Manual captures use a separate Inbox protocol.
|
duration. Manual captures use a separate Inbox protocol.
|
||||||
|
|
||||||
|
## Core sync contract
|
||||||
|
|
||||||
|
`schemas/sync.json` describes the operation-log wire format used by Desktop
|
||||||
|
core and sync-server. The server orders opaque operations by
|
||||||
|
`server_sequence`; it does not merge file contents or become the source of
|
||||||
|
truth. Sync plugins only use `api.sync` for configuration and status.
|
||||||
|
|
||||||
|
- File and folder operations are `create`, `update`, `delete`, or `move`.
|
||||||
|
File payloads carry a vault-relative path, a SHA-256 content hash, and the
|
||||||
|
existing bounded text/base64 representation when the file is supported.
|
||||||
|
- Workspace (`Deal`) operations are core-owned `workspace` entities with
|
||||||
|
`create`, `rename`, `trash`, and `restore`. Their payload carries the durable
|
||||||
|
`workspaceId`; `.verstak/workspace.json` remains unavailable to plugins and
|
||||||
|
is not ordinary file sync data.
|
||||||
|
- A pairing may name an existing remote `vaultId`. Omitting it creates/uses the
|
||||||
|
local vault identity. `SyncStatus.vaultId` reports the selected remote scope.
|
||||||
|
- `SyncStatus.lastWarning` reports a persistent unresolved scanner problem.
|
||||||
|
Files larger than the current 8 MB bounded transport or otherwise
|
||||||
|
unsupported are not marked synchronized and are retried on later scans.
|
||||||
|
|
||||||
|
The snapshot stored by core is implementation state, not a plugin API. It
|
||||||
|
excludes `.verstak`, trash, temporary files, and symlinks. Blob transport,
|
||||||
|
quotas, pagination, and retention are deliberately outside this contract and
|
||||||
|
remain a later milestone.
|
||||||
|
|
||||||
## Bundled Frontend API Contract
|
## Bundled Frontend API Contract
|
||||||
|
|
||||||
Verstak Desktop creates the real API with `createPluginAPI(pluginId)` and passes
|
Verstak Desktop creates the real API with `createPluginAPI(pluginId)` and passes
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ export interface PluginEvent<TPayload = Record<string, unknown>> {
|
||||||
export interface SyncStatus {
|
export interface SyncStatus {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
|
/** Remote sync scope selected during pairing. It can differ from this device's local vault UUID. */
|
||||||
|
vaultId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
|
@ -36,8 +38,47 @@ export interface SyncStatus {
|
||||||
lastSyncAt: string;
|
lastSyncAt: string;
|
||||||
syncInterval: number;
|
syncInterval: number;
|
||||||
lastError: string;
|
lastError: string;
|
||||||
|
/** A persistent unresolved scanner warning, for example an over-limit file. */
|
||||||
|
lastWarning: string;
|
||||||
statusLabel: string;
|
statusLabel: string;
|
||||||
}
|
}
|
||||||
|
/** Core operation-log contract. Plugins can display status but cannot create these operations. */
|
||||||
|
export type SyncEntityType = 'file' | 'folder' | 'workspace';
|
||||||
|
export type SyncOperationType = 'create' | 'update' | 'delete' | 'move' | 'rename' | 'trash' | 'restore';
|
||||||
|
export interface SyncOperation {
|
||||||
|
opId: string;
|
||||||
|
serverSequence?: number;
|
||||||
|
deviceId: string;
|
||||||
|
entityType: SyncEntityType;
|
||||||
|
entityId: string;
|
||||||
|
opType: SyncOperationType;
|
||||||
|
payloadJson: string;
|
||||||
|
createdAt: string;
|
||||||
|
clientSequence?: number;
|
||||||
|
lastSeenServerSeq?: number;
|
||||||
|
}
|
||||||
|
export interface SyncFilePayload {
|
||||||
|
path: string;
|
||||||
|
content?: string;
|
||||||
|
dataBase64?: string;
|
||||||
|
contentHash?: string;
|
||||||
|
fromPath?: string;
|
||||||
|
toPath?: string;
|
||||||
|
}
|
||||||
|
export interface SyncWorkspacePayload {
|
||||||
|
workspaceId: string;
|
||||||
|
path: string;
|
||||||
|
previousPath?: string;
|
||||||
|
name: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
export interface SyncSnapshotEntry {
|
||||||
|
path: string;
|
||||||
|
type: 'file' | 'folder';
|
||||||
|
size: number;
|
||||||
|
modifiedAt: string;
|
||||||
|
hash?: string;
|
||||||
|
}
|
||||||
export interface SyncConflict {
|
export interface SyncConflict {
|
||||||
op_id?: string;
|
op_id?: string;
|
||||||
opId?: string;
|
opId?: string;
|
||||||
|
|
@ -133,7 +174,7 @@ export interface VerstakPluginAPI {
|
||||||
};
|
};
|
||||||
sync: {
|
sync: {
|
||||||
status(): Promise<SyncStatus>;
|
status(): Promise<SyncStatus>;
|
||||||
configure(serverUrl: string, username: string, password: string): Promise<void>;
|
configure(serverUrl: string, username: string, password: string, remoteVaultId?: string): Promise<void>;
|
||||||
disconnect(): Promise<void>;
|
disconnect(): Promise<void>;
|
||||||
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
||||||
setInterval(minutes: number): Promise<void>;
|
setInterval(minutes: number): Promise<void>;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA8LhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
|
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA4OhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,YAAY,EAAqB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ5G,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;IAC7C,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClE;AASD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,CA0XlH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
|
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,YAAY,EAAqB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ5G,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;IAC7C,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClE;AASD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,CA4XlH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
|
||||||
|
|
@ -421,6 +421,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||||
status: vi.fn(async () => ({
|
status: vi.fn(async () => ({
|
||||||
configured: false,
|
configured: false,
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
|
vaultId: '',
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
deviceName: '',
|
deviceName: '',
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|
@ -430,6 +431,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||||
lastSyncAt: '',
|
lastSyncAt: '',
|
||||||
syncInterval: 0,
|
syncInterval: 0,
|
||||||
lastError: '',
|
lastError: '',
|
||||||
|
lastWarning: '',
|
||||||
statusLabel: 'disabled',
|
statusLabel: 'disabled',
|
||||||
})),
|
})),
|
||||||
configure: vi.fn(async () => { }),
|
configure: vi.fn(async () => { }),
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,177 +1,136 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/sync.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/sync.json",
|
||||||
"title": "Verstak Sync Operations",
|
"title": "Verstak core sync operation log",
|
||||||
"description": "Sync operation schemas for vault synchronization between devices",
|
"description": "Wire contracts for the core-owned operation log. Plugins may configure and display sync, but do not create operations.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"SyncOperation": {
|
"Operation": { "$ref": "#/$defs/Operation" },
|
||||||
"$ref": "#/$defs/SyncOperation"
|
"PushRequest": { "$ref": "#/$defs/PushRequest" },
|
||||||
},
|
"PullResponse": { "$ref": "#/$defs/PullResponse" },
|
||||||
"SyncBatch": {
|
"Snapshot": { "$ref": "#/$defs/Snapshot" },
|
||||||
"$ref": "#/$defs/SyncBatch"
|
"PairingRequest": { "$ref": "#/$defs/PairingRequest" }
|
||||||
},
|
|
||||||
"SyncManifest": {
|
|
||||||
"$ref": "#/$defs/SyncManifest"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"SyncOperation": {
|
"Operation": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A single sync operation entry",
|
"required": ["op_id", "device_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||||
"required": ["op", "id", "timestamp"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"op": {
|
"op_id": { "type": "string", "minLength": 1 },
|
||||||
"type": "string",
|
"server_sequence": { "type": "integer", "minimum": 1 },
|
||||||
"description": "Operation type",
|
"device_id": { "type": "string" },
|
||||||
"enum": ["add", "modify", "delete", "rename"]
|
"entity_type": { "enum": ["file", "folder", "workspace"] },
|
||||||
|
"entity_id": { "type": "string", "minLength": 1 },
|
||||||
|
"op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
|
||||||
|
"payload_json": { "type": "string" },
|
||||||
|
"created_at": { "type": "string", "format": "date-time" },
|
||||||
|
"client_sequence": { "type": "integer", "minimum": 0 },
|
||||||
|
"last_seen_server_seq": { "type": "integer", "minimum": 0 }
|
||||||
},
|
},
|
||||||
"id": {
|
"additionalProperties": false
|
||||||
"type": "string",
|
|
||||||
"description": "Unique operation identifier (UUID)"
|
|
||||||
},
|
},
|
||||||
"timestamp": {
|
"FilePayload": {
|
||||||
"type": "string",
|
|
||||||
"description": "ISO 8601 timestamp of the operation"
|
|
||||||
},
|
|
||||||
"deviceId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Originating device identifier"
|
|
||||||
},
|
|
||||||
"entityType": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Entity type being synced",
|
|
||||||
"enum": ["file", "note", "plugin_state", "vault_meta"]
|
|
||||||
},
|
|
||||||
"entityPath": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Entity path relative to vault root"
|
|
||||||
},
|
|
||||||
"hash": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Content hash (SHA-256) for content verification"
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "File size in bytes"
|
|
||||||
},
|
|
||||||
"mimeType": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "MIME type of the entity"
|
|
||||||
},
|
|
||||||
"pluginNamespace": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Plugin namespace, if syncing plugin state"
|
|
||||||
},
|
|
||||||
"oldPath": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Previous path for rename operations"
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Arbitrary metadata key-value pairs",
|
"required": ["path"],
|
||||||
"additionalProperties": { "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SyncBatch": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "A batch of sync operations sent between devices",
|
|
||||||
"required": ["batchId", "deviceId", "operations", "timestamp"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"batchId": {
|
"path": { "$ref": "#/$defs/VaultPath" },
|
||||||
"type": "string",
|
"content": { "type": "string" },
|
||||||
"description": "Unique batch identifier (UUID)"
|
"dataBase64": { "type": "string", "contentEncoding": "base64" },
|
||||||
|
"contentHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||||
|
"fromPath": { "$ref": "#/$defs/VaultPath" },
|
||||||
|
"toPath": { "$ref": "#/$defs/VaultPath" }
|
||||||
},
|
},
|
||||||
"deviceId": {
|
"additionalProperties": false
|
||||||
"type": "string",
|
|
||||||
"description": "Originating device identifier"
|
|
||||||
},
|
},
|
||||||
"operations": {
|
"WorkspacePayload": {
|
||||||
"type": "array",
|
|
||||||
"description": "List of sync operations in this batch",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/$defs/SyncOperation"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ISO 8601 timestamp of batch creation"
|
|
||||||
},
|
|
||||||
"lastSyncTimestamp": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Timestamp of the last successful sync from this device"
|
|
||||||
},
|
|
||||||
"sequence": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Sequence number for ordering batches"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SyncManifest": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Vault sync manifest for initial reconciliation",
|
"required": ["workspaceId", "path", "name"],
|
||||||
"required": ["deviceId", "entries"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"deviceId": { "type": "string" },
|
"workspaceId": { "type": "string", "format": "uuid" },
|
||||||
"entries": {
|
"path": { "$ref": "#/$defs/WorkspacePath" },
|
||||||
"type": "array",
|
"previousPath": { "$ref": "#/$defs/WorkspacePath" },
|
||||||
"items": {
|
"name": { "type": "string", "minLength": 1 },
|
||||||
"type": "object",
|
"metadata": { "type": "object", "additionalProperties": true }
|
||||||
"required": ["path", "hash", "updatedAt"],
|
|
||||||
"properties": {
|
|
||||||
"path": { "type": "string" },
|
|
||||||
"hash": { "type": "string" },
|
|
||||||
"size": { "type": "integer" },
|
|
||||||
"updatedAt": { "type": "string" },
|
|
||||||
"deleted": { "type": "boolean", "default": false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Conflict": {
|
"additionalProperties": false
|
||||||
"type": "object",
|
|
||||||
"description": "Conflict record when two devices modify the same entity",
|
|
||||||
"required": ["entityPath", "localHash", "remoteHash", "localTimestamp", "remoteTimestamp"],
|
|
||||||
"properties": {
|
|
||||||
"entityPath": { "type": "string" },
|
|
||||||
"localHash": { "type": "string" },
|
|
||||||
"remoteHash": { "type": "string" },
|
|
||||||
"localTimestamp": { "type": "string" },
|
|
||||||
"remoteTimestamp": { "type": "string" },
|
|
||||||
"resolution": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["local_wins", "remote_wins", "manual"],
|
|
||||||
"description": "How the conflict was resolved"
|
|
||||||
},
|
},
|
||||||
"resolvedAt": { "type": "string" }
|
"PushRequest": {
|
||||||
}
|
"type": "object",
|
||||||
|
"required": ["device_id", "ops"],
|
||||||
|
"properties": {
|
||||||
|
"device_id": { "type": "string", "minLength": 1 },
|
||||||
|
"idempotency_key": { "type": "string" },
|
||||||
|
"ops": { "type": "array", "items": { "$ref": "#/$defs/PushOperation" } }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"PullResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["server_sequence", "ops"],
|
||||||
|
"properties": {
|
||||||
|
"server_sequence": { "type": "integer", "minimum": 0 },
|
||||||
|
"ops": { "type": "array", "items": { "$ref": "#/$defs/Operation" } }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"PushOperation": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["op_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||||
|
"properties": {
|
||||||
|
"op_id": { "type": "string", "minLength": 1 },
|
||||||
|
"entity_type": { "enum": ["file", "folder", "workspace"] },
|
||||||
|
"entity_id": { "type": "string", "minLength": 1 },
|
||||||
|
"op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
|
||||||
|
"payload_json": { "type": "string" },
|
||||||
|
"created_at": { "type": "string", "format": "date-time" },
|
||||||
|
"client_sequence": { "type": "integer", "minimum": 0 },
|
||||||
|
"last_seen_server_seq": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"SnapshotEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["path", "type", "size", "modifiedAt"],
|
||||||
|
"properties": {
|
||||||
|
"path": { "$ref": "#/$defs/VaultPath" },
|
||||||
|
"type": { "enum": ["file", "folder"] },
|
||||||
|
"size": { "type": "integer", "minimum": 0 },
|
||||||
|
"modifiedAt": { "type": "string", "format": "date-time" },
|
||||||
|
"hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Snapshot": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "entries"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "integer", "minimum": 1 },
|
||||||
|
"entries": { "type": "object", "additionalProperties": { "$ref": "#/$defs/SnapshotEntry" } },
|
||||||
|
"unresolved": { "type": "object", "additionalProperties": { "type": "string" } }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
"PairingRequest": {
|
"PairingRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Device pairing request payload",
|
"required": ["login", "password", "device_name", "vault_id"],
|
||||||
"required": ["deviceName", "deviceType", "publicKey"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"deviceName": { "type": "string" },
|
"login": { "type": "string", "minLength": 1 },
|
||||||
"deviceType": {
|
"password": { "type": "string", "minLength": 1 },
|
||||||
|
"device_name": { "type": "string", "minLength": 1 },
|
||||||
|
"client_version": { "type": "string" },
|
||||||
|
"vault_id": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"VaultPath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["desktop", "mobile"]
|
"minLength": 1,
|
||||||
|
"not": { "pattern": "(^|/)\\.verstak(/|$)" }
|
||||||
},
|
},
|
||||||
"publicKey": { "type": "string", "description": "Device public key for auth" },
|
"WorkspacePath": {
|
||||||
"clientVersion": { "type": "string" }
|
"type": "string",
|
||||||
}
|
"minLength": 1,
|
||||||
},
|
"pattern": "^[^/]+$"
|
||||||
"PairingResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Device pairing response",
|
|
||||||
"required": ["deviceId", "token", "pairedAt"],
|
|
||||||
"properties": {
|
|
||||||
"deviceId": { "type": "string" },
|
|
||||||
"token": { "type": "string", "description": "Auth token for this device" },
|
|
||||||
"pairedAt": { "type": "string" },
|
|
||||||
"serverVersion": { "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||||
import capabilitiesSchema from '../schemas/capabilities.json';
|
import capabilitiesSchema from '../schemas/capabilities.json';
|
||||||
import manifestSchema from '../schemas/manifest.json';
|
import manifestSchema from '../schemas/manifest.json';
|
||||||
import permissionsSchema from '../schemas/permissions.json';
|
import permissionsSchema from '../schemas/permissions.json';
|
||||||
|
import syncSchema from '../schemas/sync.json';
|
||||||
import vaultEventsSchema from '../schemas/events/vault.json';
|
import vaultEventsSchema from '../schemas/events/vault.json';
|
||||||
import type { OpenProviderSupport, OpenResourceRequest, PluginManifest } from './types';
|
import type { OpenProviderSupport, OpenResourceRequest, PluginManifest } from './types';
|
||||||
import { createMockPluginAPI } from './test-utils';
|
import { createMockPluginAPI } from './test-utils';
|
||||||
|
|
@ -51,6 +52,22 @@ describe('VerstakPluginAPI contract', () => {
|
||||||
expect(typeof api.browserReceiver.rotateToken).toBe('function');
|
expect(typeof api.browserReceiver.rotateToken).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sync status exposes pairing scope and unresolved scanner warning', async () => {
|
||||||
|
const api = createMockPluginAPI('verstak.sync');
|
||||||
|
|
||||||
|
await api.sync.configure('https://sync.example.test', 'alice', 'secret', 'shared-vault-id');
|
||||||
|
const status = await api.sync.status();
|
||||||
|
expect(status).toMatchObject({ vaultId: '', lastWarning: '', configured: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sync schema describes the core operation log and workspace lifecycle', () => {
|
||||||
|
const defs = (syncSchema as any).$defs;
|
||||||
|
expect(defs.Operation.properties.entity_type.enum).toEqual(['file', 'folder', 'workspace']);
|
||||||
|
expect(defs.Operation.properties.op_type.enum).toContain('restore');
|
||||||
|
expect(defs.WorkspacePayload.required).toEqual(['workspaceId', 'path', 'name']);
|
||||||
|
expect(defs.Snapshot.properties.unresolved).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
test('manifest schema accepts files permissions used by platform-test', () => {
|
test('manifest schema accepts files permissions used by platform-test', () => {
|
||||||
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
|
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ export interface PluginEvent<TPayload = Record<string, unknown>> {
|
||||||
export interface SyncStatus {
|
export interface SyncStatus {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
|
/** Remote sync scope selected during pairing. It can differ from this device's local vault UUID. */
|
||||||
|
vaultId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
|
@ -65,9 +67,53 @@ export interface SyncStatus {
|
||||||
lastSyncAt: string;
|
lastSyncAt: string;
|
||||||
syncInterval: number;
|
syncInterval: number;
|
||||||
lastError: string;
|
lastError: string;
|
||||||
|
/** A persistent unresolved scanner warning, for example an over-limit file. */
|
||||||
|
lastWarning: string;
|
||||||
statusLabel: string;
|
statusLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Core operation-log contract. Plugins can display status but cannot create these operations. */
|
||||||
|
export type SyncEntityType = 'file' | 'folder' | 'workspace';
|
||||||
|
export type SyncOperationType = 'create' | 'update' | 'delete' | 'move' | 'rename' | 'trash' | 'restore';
|
||||||
|
|
||||||
|
export interface SyncOperation {
|
||||||
|
opId: string;
|
||||||
|
serverSequence?: number;
|
||||||
|
deviceId: string;
|
||||||
|
entityType: SyncEntityType;
|
||||||
|
entityId: string;
|
||||||
|
opType: SyncOperationType;
|
||||||
|
payloadJson: string;
|
||||||
|
createdAt: string;
|
||||||
|
clientSequence?: number;
|
||||||
|
lastSeenServerSeq?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncFilePayload {
|
||||||
|
path: string;
|
||||||
|
content?: string;
|
||||||
|
dataBase64?: string;
|
||||||
|
contentHash?: string;
|
||||||
|
fromPath?: string;
|
||||||
|
toPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncWorkspacePayload {
|
||||||
|
workspaceId: string;
|
||||||
|
path: string;
|
||||||
|
previousPath?: string;
|
||||||
|
name: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncSnapshotEntry {
|
||||||
|
path: string;
|
||||||
|
type: 'file' | 'folder';
|
||||||
|
size: number;
|
||||||
|
modifiedAt: string;
|
||||||
|
hash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SyncConflict {
|
export interface SyncConflict {
|
||||||
op_id?: string;
|
op_id?: string;
|
||||||
opId?: string;
|
opId?: string;
|
||||||
|
|
@ -177,7 +223,7 @@ export interface VerstakPluginAPI {
|
||||||
|
|
||||||
sync: {
|
sync: {
|
||||||
status(): Promise<SyncStatus>;
|
status(): Promise<SyncStatus>;
|
||||||
configure(serverUrl: string, username: string, password: string): Promise<void>;
|
configure(serverUrl: string, username: string, password: string, remoteVaultId?: string): Promise<void>;
|
||||||
disconnect(): Promise<void>;
|
disconnect(): Promise<void>;
|
||||||
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
||||||
setInterval(minutes: number): Promise<void>;
|
setInterval(minutes: number): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||||
status: vi.fn(async () => ({
|
status: vi.fn(async () => ({
|
||||||
configured: false,
|
configured: false,
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
|
vaultId: '',
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
deviceName: '',
|
deviceName: '',
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|
@ -417,6 +418,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||||
lastSyncAt: '',
|
lastSyncAt: '',
|
||||||
syncInterval: 0,
|
syncInterval: 0,
|
||||||
lastError: '',
|
lastError: '',
|
||||||
|
lastWarning: '',
|
||||||
statusLabel: 'disabled',
|
statusLabel: 'disabled',
|
||||||
})),
|
})),
|
||||||
configure: vi.fn(async () => {}),
|
configure: vi.fn(async () => {}),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue