Align sync SDK with snapshot and workspace contracts

This commit is contained in:
mirivlad 2026-07-17 04:11:39 +08:00
parent 944f3742b6
commit 7ad07321b0
11 changed files with 249 additions and 157 deletions

View File

@ -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

43
dist/plugin-api.d.ts vendored
View File

@ -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

View File

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

View File

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

2
dist/test-utils.js vendored
View File

@ -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

View File

@ -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 },
"id": { "op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
"type": "string", "payload_json": { "type": "string" },
"description": "Unique operation identifier (UUID)" "created_at": { "type": "string", "format": "date-time" },
}, "client_sequence": { "type": "integer", "minimum": 0 },
"timestamp": { "last_seen_server_seq": { "type": "integer", "minimum": 0 }
"type": "string", },
"description": "ISO 8601 timestamp of the operation" "additionalProperties": false
},
"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",
"description": "Arbitrary metadata key-value pairs",
"additionalProperties": { "type": "string" }
}
}
}, },
"SyncBatch": { "FilePayload": {
"type": "object", "type": "object",
"description": "A batch of sync operations sent between devices", "required": ["path"],
"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}$" },
"deviceId": { "fromPath": { "$ref": "#/$defs/VaultPath" },
"type": "string", "toPath": { "$ref": "#/$defs/VaultPath" }
"description": "Originating device identifier" },
}, "additionalProperties": false
"operations": {
"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": { "WorkspacePayload": {
"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": { "additionalProperties": false
"path": { "type": "string" },
"hash": { "type": "string" },
"size": { "type": "integer" },
"updatedAt": { "type": "string" },
"deleted": { "type": "boolean", "default": false }
}
}
}
}
}, },
"Conflict": { "PushRequest": {
"type": "object", "type": "object",
"description": "Conflict record when two devices modify the same entity", "required": ["device_id", "ops"],
"required": ["entityPath", "localHash", "remoteHash", "localTimestamp", "remoteTimestamp"],
"properties": { "properties": {
"entityPath": { "type": "string" }, "device_id": { "type": "string", "minLength": 1 },
"localHash": { "type": "string" }, "idempotency_key": { "type": "string" },
"remoteHash": { "type": "string" }, "ops": { "type": "array", "items": { "$ref": "#/$defs/PushOperation" } }
"localTimestamp": { "type": "string" }, },
"remoteTimestamp": { "type": "string" }, "additionalProperties": false
"resolution": { },
"type": "string", "PullResponse": {
"enum": ["local_wins", "remote_wins", "manual"], "type": "object",
"description": "How the conflict was resolved" "required": ["server_sequence", "ops"],
}, "properties": {
"resolvedAt": { "type": "string" } "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 },
"type": "string", "device_name": { "type": "string", "minLength": 1 },
"enum": ["desktop", "mobile"] "client_version": { "type": "string" },
}, "vault_id": { "type": "string", "minLength": 1 }
"publicKey": { "type": "string", "description": "Device public key for auth" }, },
"clientVersion": { "type": "string" } "additionalProperties": false
}
}, },
"PairingResponse": { "VaultPath": {
"type": "object", "type": "string",
"description": "Device pairing response", "minLength": 1,
"required": ["deviceId", "token", "pairedAt"], "not": { "pattern": "(^|/)\\.verstak(/|$)" }
"properties": { },
"deviceId": { "type": "string" }, "WorkspacePath": {
"token": { "type": "string", "description": "Auth token for this device" }, "type": "string",
"pairedAt": { "type": "string" }, "minLength": 1,
"serverVersion": { "type": "string" } "pattern": "^[^/]+$"
}
} }
} }
} }

View File

@ -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[];

View File

@ -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>;

View File

@ -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 () => {}),