Define paginated blob sync contract
This commit is contained in:
parent
7ad07321b0
commit
2b2936121d
20
README.md
20
README.md
|
|
@ -54,22 +54,28 @@ core and sync-server. The server orders opaque operations by
|
|||
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.
|
||||
Small UTF-8 text may be inline; binary and large files carry a `blob`
|
||||
`{sha256,size}` reference. The bytes are uploaded/downloaded through the
|
||||
scoped Blob API before an operation is accepted/applied, never base64 in the
|
||||
operation log.
|
||||
- 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.
|
||||
- Pull uses `since_sequence` and a bounded `page_limit`; each response has
|
||||
`page_last_sequence` and `has_more`. Clients persist a cursor only after an
|
||||
operation is safely applied and stop at the first failed sequence.
|
||||
- `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.
|
||||
A file over the configured blob limit or otherwise unsupported is not marked
|
||||
synchronized and is 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.
|
||||
excludes `.verstak`, trash, temporary files, and symlinks. Blob ownership,
|
||||
quotas and pagination are part of the current wire contract. Operation-log
|
||||
retention remains a future checkpoint milestone: deleting it now could prevent
|
||||
a newly paired device from reconstructing a vault.
|
||||
|
||||
## Bundled Frontend API Contract
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,53 @@ export interface SyncConfig {
|
|||
namespaces?: string[];
|
||||
participate?: boolean;
|
||||
}
|
||||
/** A content-addressed binary payload. Blob bytes are uploaded before the
|
||||
* operation that references them and are never base64-embedded in the log. */
|
||||
export interface SyncBlobReference {
|
||||
sha256: string;
|
||||
size: number;
|
||||
}
|
||||
export interface SyncFilePayload {
|
||||
path: string;
|
||||
content?: string;
|
||||
blob?: SyncBlobReference;
|
||||
contentHash?: string;
|
||||
fromPath?: string;
|
||||
toPath?: string;
|
||||
}
|
||||
export interface SyncOperation {
|
||||
op_id: string;
|
||||
server_sequence?: number;
|
||||
device_id?: string;
|
||||
entity_type: 'file' | 'folder' | 'workspace';
|
||||
entity_id: string;
|
||||
op_type: 'create' | 'update' | 'delete' | 'move' | 'rename' | 'trash' | 'restore';
|
||||
payload_json: string;
|
||||
created_at: string;
|
||||
client_sequence?: number;
|
||||
last_seen_server_seq?: number;
|
||||
}
|
||||
export interface SyncPushRequest {
|
||||
device_id: string;
|
||||
idempotency_key?: string;
|
||||
ops: Omit<SyncOperation, 'server_sequence' | 'device_id'>[];
|
||||
}
|
||||
export interface SyncPullRequest {
|
||||
since_sequence: number;
|
||||
page_limit?: number;
|
||||
}
|
||||
export interface SyncPullResponse {
|
||||
server_sequence: number;
|
||||
page_last_sequence: number;
|
||||
has_more: boolean;
|
||||
ops: SyncOperation[];
|
||||
}
|
||||
/** Stable public errors. UI must map `code` to localized copy rather than
|
||||
* displaying server diagnostics. */
|
||||
export interface SyncServerError {
|
||||
error: string;
|
||||
code: string;
|
||||
}
|
||||
export type CapabilityName = string;
|
||||
export interface CapabilityEntry {
|
||||
name: CapabilityName;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,9 @@
|
|||
"properties": {
|
||||
"Operation": { "$ref": "#/$defs/Operation" },
|
||||
"PushRequest": { "$ref": "#/$defs/PushRequest" },
|
||||
"PullRequest": { "$ref": "#/$defs/PullRequest" },
|
||||
"PullResponse": { "$ref": "#/$defs/PullResponse" },
|
||||
"BlobReference": { "$ref": "#/$defs/BlobReference" },
|
||||
"Snapshot": { "$ref": "#/$defs/Snapshot" },
|
||||
"PairingRequest": { "$ref": "#/$defs/PairingRequest" }
|
||||
},
|
||||
|
|
@ -16,13 +18,13 @@
|
|||
"type": "object",
|
||||
"required": ["op_id", "device_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||
"properties": {
|
||||
"op_id": { "type": "string", "minLength": 1 },
|
||||
"op_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"server_sequence": { "type": "integer", "minimum": 1 },
|
||||
"device_id": { "type": "string" },
|
||||
"device_id": { "type": "string", "maxLength": 128 },
|
||||
"entity_type": { "enum": ["file", "folder", "workspace"] },
|
||||
"entity_id": { "type": "string", "minLength": 1 },
|
||||
"entity_id": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||
"op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
|
||||
"payload_json": { "type": "string" },
|
||||
"payload_json": { "type": "string", "maxLength": 262144 },
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"client_sequence": { "type": "integer", "minimum": 0 },
|
||||
"last_seen_server_seq": { "type": "integer", "minimum": 0 }
|
||||
|
|
@ -34,14 +36,23 @@
|
|||
"required": ["path"],
|
||||
"properties": {
|
||||
"path": { "$ref": "#/$defs/VaultPath" },
|
||||
"content": { "type": "string" },
|
||||
"dataBase64": { "type": "string", "contentEncoding": "base64" },
|
||||
"content": { "type": "string", "description": "Bounded inline UTF-8 text only." },
|
||||
"blob": { "$ref": "#/$defs/BlobReference" },
|
||||
"contentHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"fromPath": { "$ref": "#/$defs/VaultPath" },
|
||||
"toPath": { "$ref": "#/$defs/VaultPath" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"BlobReference": {
|
||||
"type": "object",
|
||||
"required": ["sha256", "size"],
|
||||
"properties": {
|
||||
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"size": { "type": "integer", "minimum": 0 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"WorkspacePayload": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "path", "name"],
|
||||
|
|
@ -58,17 +69,28 @@
|
|||
"type": "object",
|
||||
"required": ["device_id", "ops"],
|
||||
"properties": {
|
||||
"device_id": { "type": "string", "minLength": 1 },
|
||||
"idempotency_key": { "type": "string" },
|
||||
"ops": { "type": "array", "items": { "$ref": "#/$defs/PushOperation" } }
|
||||
"device_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"idempotency_key": { "type": "string", "maxLength": 128 },
|
||||
"ops": { "type": "array", "maxItems": 100, "items": { "$ref": "#/$defs/PushOperation" } }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PullRequest": {
|
||||
"type": "object",
|
||||
"required": ["since_sequence"],
|
||||
"properties": {
|
||||
"since_sequence": { "type": "integer", "minimum": 0 },
|
||||
"page_limit": { "type": "integer", "minimum": 1, "maximum": 100 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PullResponse": {
|
||||
"type": "object",
|
||||
"required": ["server_sequence", "ops"],
|
||||
"required": ["server_sequence", "page_last_sequence", "has_more", "ops"],
|
||||
"properties": {
|
||||
"server_sequence": { "type": "integer", "minimum": 0 },
|
||||
"page_last_sequence": { "type": "integer", "minimum": 0 },
|
||||
"has_more": { "type": "boolean" },
|
||||
"ops": { "type": "array", "items": { "$ref": "#/$defs/Operation" } }
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
@ -77,11 +99,11 @@
|
|||
"type": "object",
|
||||
"required": ["op_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||
"properties": {
|
||||
"op_id": { "type": "string", "minLength": 1 },
|
||||
"op_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"entity_type": { "enum": ["file", "folder", "workspace"] },
|
||||
"entity_id": { "type": "string", "minLength": 1 },
|
||||
"entity_id": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||
"op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
|
||||
"payload_json": { "type": "string" },
|
||||
"payload_json": { "type": "string", "maxLength": 262144 },
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"client_sequence": { "type": "integer", "minimum": 0 },
|
||||
"last_seen_server_seq": { "type": "integer", "minimum": 0 }
|
||||
|
|
@ -114,11 +136,11 @@
|
|||
"type": "object",
|
||||
"required": ["login", "password", "device_name", "vault_id"],
|
||||
"properties": {
|
||||
"login": { "type": "string", "minLength": 1 },
|
||||
"login": { "type": "string", "minLength": 1, "maxLength": 320 },
|
||||
"password": { "type": "string", "minLength": 1 },
|
||||
"device_name": { "type": "string", "minLength": 1 },
|
||||
"client_version": { "type": "string" },
|
||||
"vault_id": { "type": "string", "minLength": 1 }
|
||||
"device_name": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"client_version": { "type": "string", "maxLength": 256 },
|
||||
"vault_id": { "type": "string", "minLength": 1, "maxLength": 256 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ describe('VerstakPluginAPI contract', () => {
|
|||
expect(defs.Operation.properties.op_type.enum).toContain('restore');
|
||||
expect(defs.WorkspacePayload.required).toEqual(['workspaceId', 'path', 'name']);
|
||||
expect(defs.Snapshot.properties.unresolved).toBeDefined();
|
||||
expect(defs.FilePayload.properties.blob.$ref).toBe('#/$defs/BlobReference');
|
||||
expect(defs.FilePayload.properties.dataBase64).toBeUndefined();
|
||||
expect(defs.PullResponse.required).toContain('has_more');
|
||||
});
|
||||
|
||||
test('manifest schema accepts files permissions used by platform-test', () => {
|
||||
|
|
|
|||
56
src/types.ts
56
src/types.ts
|
|
@ -55,6 +55,62 @@ export interface SyncConfig {
|
|||
participate?: boolean;
|
||||
}
|
||||
|
||||
// ─── Core sync wire contract ────────────────────────────────
|
||||
|
||||
/** A content-addressed binary payload. Blob bytes are uploaded before the
|
||||
* operation that references them and are never base64-embedded in the log. */
|
||||
export interface SyncBlobReference {
|
||||
sha256: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface SyncFilePayload {
|
||||
path: string;
|
||||
content?: string;
|
||||
blob?: SyncBlobReference;
|
||||
contentHash?: string;
|
||||
fromPath?: string;
|
||||
toPath?: string;
|
||||
}
|
||||
|
||||
export interface SyncOperation {
|
||||
op_id: string;
|
||||
server_sequence?: number;
|
||||
device_id?: string;
|
||||
entity_type: 'file' | 'folder' | 'workspace';
|
||||
entity_id: string;
|
||||
op_type: 'create' | 'update' | 'delete' | 'move' | 'rename' | 'trash' | 'restore';
|
||||
payload_json: string;
|
||||
created_at: string;
|
||||
client_sequence?: number;
|
||||
last_seen_server_seq?: number;
|
||||
}
|
||||
|
||||
export interface SyncPushRequest {
|
||||
device_id: string;
|
||||
idempotency_key?: string;
|
||||
ops: Omit<SyncOperation, 'server_sequence' | 'device_id'>[];
|
||||
}
|
||||
|
||||
export interface SyncPullRequest {
|
||||
since_sequence: number;
|
||||
page_limit?: number;
|
||||
}
|
||||
|
||||
export interface SyncPullResponse {
|
||||
server_sequence: number;
|
||||
page_last_sequence: number;
|
||||
has_more: boolean;
|
||||
ops: SyncOperation[];
|
||||
}
|
||||
|
||||
/** Stable public errors. UI must map `code` to localized copy rather than
|
||||
* displaying server diagnostics. */
|
||||
export interface SyncServerError {
|
||||
error: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
// ─── Capabilities ────────────────────────────────────────────
|
||||
|
||||
export type CapabilityName = string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue