Compare commits
10
Commits
831415bebd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac29fa5c78 | ||
|
|
2b2936121d | ||
|
|
7ad07321b0 | ||
|
|
944f3742b6 | ||
|
|
35b62da2c7 | ||
|
|
43ac963923 | ||
|
|
1378fda7f2 | ||
|
|
9a4fd18456 | ||
|
|
a858a928bf | ||
|
|
69e15d2609 |
@@ -1 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
dist/
|
||||||
|
release/
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ verstak-sdk/
|
|||||||
## Capability Registry (известные)
|
## Capability Registry (известные)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
verstak/core/plugin-manager/v1
|
||||||
|
verstak/core/capability-registry/v1
|
||||||
|
verstak/core/contribution-registry/v1
|
||||||
|
verstak/core/permissions/v1
|
||||||
|
verstak/core/events/v1
|
||||||
|
verstak/core/files/v1
|
||||||
|
verstak/core/workbench/v1
|
||||||
|
verstak/core/notifications/v1
|
||||||
|
verstak/core/vault/v1
|
||||||
|
verstak/core/workspace/v1
|
||||||
editor.text
|
editor.text
|
||||||
editor.text.markdown
|
editor.text.markdown
|
||||||
editor.note.markdown
|
editor.note.markdown
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Verstak Plugin SDK
|
||||||
|
Copyright (C) 2026 Verstak contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
The complete license text is available at:
|
||||||
|
https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
@@ -1,6 +1,93 @@
|
|||||||
# verstak-sdk
|
<div align="center">
|
||||||
|
|
||||||
Verstak Plugin SDK — manifest schema, TypeScript SDK, RPC protocol, capability contracts, event schemas, test helpers, packaging tools
|
# Verstak Plugin SDK
|
||||||
|
|
||||||
|
### TypeScript API, JSON schemas and contract tests for Verstak plugins.
|
||||||
|
|
||||||
|
**English** · [Русский](README.ru.md)
|
||||||
|
|
||||||
|
[](https://github.com/mirivlad/verstak-sdk/releases)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> **Alpha contract.** Keep SDK, Desktop and official-plugin versions in the
|
||||||
|
> same release line while APIs are evolving.
|
||||||
|
|
||||||
|
TypeScript API, JSON schemas and contract tests for plugins running in Verstak
|
||||||
|
Desktop. The SDK is versioned independently so plugin authors can validate
|
||||||
|
their manifests and compile against the public host API.
|
||||||
|
|
||||||
|
## Install and verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run lint
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The build emits `dist/`. A packed npm artifact can be made locally with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/release.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
It validates the requested version against `package.json`, then writes an npm
|
||||||
|
tarball and `SHA256SUMS` to `release/`.
|
||||||
|
|
||||||
|
## Publish a GitHub Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/publish-github-release.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs the same local packaging command, then requires a clean, up-to-date
|
||||||
|
`main` and an authenticated [`gh`](https://cli.github.com/) CLI. It creates and
|
||||||
|
pushes the annotated version tag when needed and uploads the npm tarball and
|
||||||
|
`SHA256SUMS` to GitHub Releases. The requested version must match
|
||||||
|
`package.json`.
|
||||||
|
|
||||||
|
## Contracts relevant to the alpha
|
||||||
|
|
||||||
|
- Workspaces have durable UUID identities; paths are addresses, not identity.
|
||||||
|
- Activity may be scoped to `workspaceId` or to explicit `unassigned` work.
|
||||||
|
- The `hostname-normalization-v1.json` vectors define the shared canonical
|
||||||
|
browser-domain representation used by Desktop and the extension.
|
||||||
|
- Browser activity batches contain only a normalized hostname and bounded
|
||||||
|
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`.
|
||||||
|
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.
|
||||||
|
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 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
|
## Bundled Frontend API Contract
|
||||||
|
|
||||||
@@ -40,3 +127,8 @@ or trash metadata.
|
|||||||
Bundled frontend plugins are trusted/cooperative and run in the desktop JS
|
Bundled frontend plugins are trusted/cooperative and run in the desktop JS
|
||||||
context. Current permission checks are contract checks, not a security boundary;
|
context. Current permission checks are contract checks, not a security boundary;
|
||||||
real isolation belongs to a later sidecar/sandbox milestone.
|
real isolation belongs to a later sidecar/sandbox milestone.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2026 Verstak contributors. Licensed under
|
||||||
|
[GNU AGPLv3 or later](LICENSE).
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# Verstak Plugin SDK
|
||||||
|
|
||||||
|
### TypeScript API, JSON-схемы и контрактные тесты для плагинов Верстака.
|
||||||
|
|
||||||
|
[English](README.md) · **Русский**
|
||||||
|
|
||||||
|
[](https://github.com/mirivlad/verstak-sdk/releases)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> **Контракт alpha-версии.** Держите SDK, Desktop и официальные плагины
|
||||||
|
> в одной релизной линейке, пока API развиваются.
|
||||||
|
|
||||||
|
TypeScript API, JSON-схемы и контрактные тесты для плагинов в Verstak Desktop.
|
||||||
|
SDK версионируется независимо, чтобы авторы плагинов могли валидировать
|
||||||
|
свои manifest и компилироваться против публичного host API.
|
||||||
|
|
||||||
|
## Установка и проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run lint
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Сборка создаёт `dist/`. Упакованный npm-артефакт можно сделать локально:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/release.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт проверяет версию на соответствие `package.json`, затем записывает
|
||||||
|
npm-тарбол и `SHA256SUMS` в `release/`.
|
||||||
|
|
||||||
|
## Публикация GitHub Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/publish-github-release.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Выполняет локальную упаковку, затем требует чистый актуальный `main` и
|
||||||
|
авторизованный [`gh`](https://cli.github.com/) CLI. Создаёт и отправляет
|
||||||
|
аннотированный тег, затем загружает npm-тарбол и `SHA256SUMS` в GitHub Releases.
|
||||||
|
Запрошенная версия должна совпадать с `package.json`.
|
||||||
|
|
||||||
|
## Контракты, актуальные для alpha
|
||||||
|
|
||||||
|
- Дела имеют постоянные UUID-идентификаторы; пути — это адреса, а не идентификаторы.
|
||||||
|
- Активность может быть привязана к `workspaceId` или к явной области `unassigned`.
|
||||||
|
- `hostname-normalization-v1.json` определяет общее каноническое представление
|
||||||
|
доменов браузера, используемое Desktop и расширением.
|
||||||
|
- Пакеты активности браузера содержат только нормализованный домен и ограниченную
|
||||||
|
длительность. Ручные захваты используют отдельный протокол Inbox.
|
||||||
|
|
||||||
|
## Контракт синхронизации
|
||||||
|
|
||||||
|
`schemas/sync.json` описывает формат журнала операций, используемый Desktop core
|
||||||
|
и sync-сервером. Сервер упорядочивает операции по `server_sequence`, не сливает
|
||||||
|
содержимое файлов и не становится источником истины.
|
||||||
|
|
||||||
|
- Операции с файлами и папками: `create`, `update`, `delete`, `move`. Небольшой
|
||||||
|
UTF-8 текст может быть встроенным; бинарные и большие файлы содержат ссылку
|
||||||
|
`blob` `{sha256,size}`.
|
||||||
|
- Операции с делами (`Deal`) — это сущности `workspace`, принадлежащие core:
|
||||||
|
`create`, `rename`, `trash`, `restore` с постоянным `workspaceId`.
|
||||||
|
- Pull использует `since_sequence` и `page_limit`; клиент сохраняет курсор
|
||||||
|
только после успешного применения каждой операции и останавливается на первой
|
||||||
|
неудачной.
|
||||||
|
- Файл, превышающий лимит blob или иначе неподдерживаемый, не помечается
|
||||||
|
синхронизированным и повторяется при следующих сканированиях.
|
||||||
|
|
||||||
|
## Контракт Frontend API
|
||||||
|
|
||||||
|
Verstak Desktop создаёт реальный API через `createPluginAPI(pluginId)` и
|
||||||
|
передаёт его компонентам плагинов при монтировании. SDK экспортирует
|
||||||
|
TypeScript-типы для этого объекта:
|
||||||
|
|
||||||
|
- `settings.read/write/writeAll`
|
||||||
|
- `capabilities.list/get/has`
|
||||||
|
- `commands.register/execute/executeFor`
|
||||||
|
- `contributions.list`
|
||||||
|
- `events.publish/subscribe`
|
||||||
|
- `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/listTrash/restoreTrash/deleteTrash`
|
||||||
|
- `workbench.openResource/editResource`
|
||||||
|
- опциональный `dispose`
|
||||||
|
|
||||||
|
Пути к файлам — канонические vault-относительные слеш-пути. Обратные слеши,
|
||||||
|
абсолютные пути Windows/UNC, переходы по директориям, нулевые байты, варианты
|
||||||
|
`.verstak` и операции через symlink отклоняются. Чтение/запись текста — только
|
||||||
|
UTF-8; `readText` ограничен 2 МБ, `readBytes` возвращает base64 для обычных
|
||||||
|
файлов до 8 МБ.
|
||||||
|
|
||||||
|
Bundled frontend-плагины являются доверенными и выполняются в контексте JS
|
||||||
|
рабочего стола. Текущие проверки прав — это контрактные проверки, а не граница
|
||||||
|
безопасности; настоящая изоляция — в будущем milestone sidecar/sandbox.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Copyright © 2026 Verstak contributors. Распространяется на условиях
|
||||||
|
[GNU AGPLv3 или новее](LICENSE).
|
||||||
Vendored
+42
-1
@@ -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>;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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"}
|
||||||
Vendored
+1
-1
@@ -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"}
|
||||||
Vendored
+2
@@ -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 () => { }),
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+48
-1
@@ -43,6 +43,53 @@ export interface SyncConfig {
|
|||||||
namespaces?: string[];
|
namespaces?: string[];
|
||||||
participate?: boolean;
|
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 type CapabilityName = string;
|
||||||
export interface CapabilityEntry {
|
export interface CapabilityEntry {
|
||||||
name: CapabilityName;
|
name: CapabilityName;
|
||||||
@@ -50,7 +97,7 @@ export interface CapabilityEntry {
|
|||||||
pluginId: string;
|
pluginId: string;
|
||||||
status: 'stable' | 'draft' | 'deprecated';
|
status: 'stable' | 'draft' | 'deprecated';
|
||||||
}
|
}
|
||||||
export type Permission = 'vault.read' | 'vault.write' | 'vault.watch' | 'files.read' | 'files.write' | 'files.delete' | 'files.openExternal' | 'workbench.open' | 'storage.namespace' | 'storage.migrations' | 'events.publish' | 'events.subscribe' | 'ui.register' | 'commands.register' | 'network.local' | 'network.remote' | 'process.spawn' | 'secrets.read' | 'secrets.write' | 'sync.participate' | 'browser.receiver.manage';
|
export type Permission = 'vault.read' | 'vault.write' | 'vault.watch' | 'files.read' | 'files.write' | 'files.delete' | 'files.openExternal' | 'workbench.open' | 'storage.namespace' | 'storage.migrations' | 'events.publish' | 'events.subscribe' | 'ui.register' | 'commands.register' | 'network.local' | 'network.remote' | 'process.spawn' | 'secrets.read' | 'secrets.write' | 'sync.participate' | 'browser.receiver.manage' | 'notifications.schedule';
|
||||||
export interface PermissionEntry {
|
export interface PermissionEntry {
|
||||||
name: Permission;
|
name: Permission;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
|||||||
"name": "@verstak/plugin-sdk",
|
"name": "@verstak/plugin-sdk",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Verstak Plugin SDK — TypeScript types and runtime API for plugin development",
|
"description": "Verstak Plugin SDK — TypeScript types and runtime API for plugin development",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/capabilities.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/capabilities.json",
|
||||||
"title": "Verstak Capability Registry",
|
"title": "Verstak Capability Registry",
|
||||||
"description": "Known capability names and their descriptions for the Verstak platform",
|
"description": "Known capability names and their descriptions for the Verstak platform",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -27,6 +27,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
|
{ "name": "verstak/core/plugin-manager/v1", "description": "Core plugin discovery and lifecycle management", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/capability-registry/v1", "description": "Core capability registry access", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/contribution-registry/v1", "description": "Core plugin contribution registry", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/permissions/v1", "description": "Core plugin permission enforcement", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/events/v1", "description": "Core plugin event bus", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/files/v1", "description": "Core vault files API", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/workbench/v1", "description": "Core Workbench resource routing", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/notifications/v1", "description": "Core native notification scheduling", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/vault/v1", "description": "Core vault lifecycle", "status": "draft" },
|
||||||
|
{ "name": "verstak/core/workspace/v1", "description": "Core case lifecycle", "status": "draft" },
|
||||||
{ "name": "editor.text", "description": "Text editing capability (any format)", "status": "draft" },
|
{ "name": "editor.text", "description": "Text editing capability (any format)", "status": "draft" },
|
||||||
{ "name": "editor.text.markdown", "description": "Markdown text editing", "status": "draft" },
|
{ "name": "editor.text.markdown", "description": "Markdown text editing", "status": "draft" },
|
||||||
{ "name": "editor.note.markdown", "description": "Markdown note editing (extends editor.text.markdown with note metadata)", "status": "draft" },
|
{ "name": "editor.note.markdown", "description": "Markdown note editing (extends editor.text.markdown with note metadata)", "status": "draft" },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/contributions.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/contributions.json",
|
||||||
"title": "Verstak Contribution Points Registry",
|
"title": "Verstak Contribution Points Registry",
|
||||||
"description": "Known contribution points that plugins can register",
|
"description": "Known contribution points that plugins can register",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/events/browser.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/events/browser.json",
|
||||||
"title": "Verstak Browser Events",
|
"title": "Verstak Browser Events",
|
||||||
"description": "Event schemas for browser extension and browser-inbox plugin communication",
|
"description": "Event schemas for browser extension and browser-inbox plugin communication",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/events/lifecycle.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/events/lifecycle.json",
|
||||||
"title": "Verstak Lifecycle Events",
|
"title": "Verstak Lifecycle Events",
|
||||||
"description": "Event schemas for plugin and application lifecycle events",
|
"description": "Event schemas for plugin and application lifecycle events",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/events/vault.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/events/vault.json",
|
||||||
"title": "Verstak Vault Events",
|
"title": "Verstak Vault Events",
|
||||||
"description": "Event schemas for vault and case lifecycle events",
|
"description": "Event schemas for vault and case lifecycle events",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/hostname-normalization-v1.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/hostname-normalization-v1.json",
|
||||||
"title": "Verstak canonical hostname normalization v1 test vectors",
|
"title": "Verstak canonical hostname normalization v1 test vectors",
|
||||||
|
"type": "object",
|
||||||
"description": "Canonical hostnames are lowercase ASCII A-labels without a port or trailing DNS dot. Bare hostnames accept DNS names, IPv4, bracketed IPv6, localhost, and internal single-label names. URL inputs accept only HTTP(S). Invalid or excessively long input normalizes to an empty string.",
|
"description": "Canonical hostnames are lowercase ASCII A-labels without a port or trailing DNS dot. Bare hostnames accept DNS names, IPv4, bracketed IPv6, localhost, and internal single-label names. URL inputs accept only HTTP(S). Invalid or excessively long input normalizes to an empty string.",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"bare": [
|
"bare": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/manifest.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/manifest.json",
|
||||||
"title": "Verstak Plugin Manifest",
|
"title": "Verstak Plugin Manifest",
|
||||||
"description": "Schema for Verstak plugin.json manifest files",
|
"description": "Schema for Verstak plugin.json manifest files",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -126,7 +126,8 @@
|
|||||||
"secrets.read",
|
"secrets.read",
|
||||||
"secrets.write",
|
"secrets.write",
|
||||||
"sync.participate",
|
"sync.participate",
|
||||||
"browser.receiver.manage"
|
"browser.receiver.manage",
|
||||||
|
"notifications.schedule"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"minItems": 1
|
"minItems": 1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/permissions.json",
|
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/permissions.json",
|
||||||
"title": "Verstak Permissions Registry",
|
"title": "Verstak Permissions Registry",
|
||||||
"description": "Known runtime permissions and their safety levels",
|
"description": "Known runtime permissions and their safety levels",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
{ "name": "secrets.read", "description": "Read secrets from the secret store", "dangerous": true },
|
{ "name": "secrets.read", "description": "Read secrets from the secret store", "dangerous": true },
|
||||||
{ "name": "secrets.write", "description": "Write secrets to the secret store", "dangerous": true },
|
{ "name": "secrets.write", "description": "Write secrets to the secret store", "dangerous": true },
|
||||||
{ "name": "sync.participate", "description": "Participate in vault sync", "dangerous": true },
|
{ "name": "sync.participate", "description": "Participate in vault sync", "dangerous": true },
|
||||||
{ "name": "browser.receiver.manage", "description": "View and rotate the local browser receiver pairing token", "dangerous": true }
|
{ "name": "browser.receiver.manage", "description": "View and rotate the local browser receiver pairing token", "dangerous": true },
|
||||||
|
{ "name": "notifications.schedule", "description": "Replace the plugin's scheduled native notifications", "dangerous": false }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-150
@@ -1,177 +1,158 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/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" },
|
||||||
},
|
"PullRequest": { "$ref": "#/$defs/PullRequest" },
|
||||||
"SyncBatch": {
|
"PullResponse": { "$ref": "#/$defs/PullResponse" },
|
||||||
"$ref": "#/$defs/SyncBatch"
|
"BlobReference": { "$ref": "#/$defs/BlobReference" },
|
||||||
},
|
"Snapshot": { "$ref": "#/$defs/Snapshot" },
|
||||||
"SyncManifest": {
|
"PairingRequest": { "$ref": "#/$defs/PairingRequest" }
|
||||||
"$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, "maxLength": 128 },
|
||||||
"type": "string",
|
"server_sequence": { "type": "integer", "minimum": 1 },
|
||||||
"description": "Operation type",
|
"device_id": { "type": "string", "maxLength": 128 },
|
||||||
"enum": ["add", "modify", "delete", "rename"]
|
"entity_type": { "enum": ["file", "folder", "workspace"] },
|
||||||
|
"entity_id": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||||
|
"op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
|
||||||
|
"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 }
|
||||||
},
|
},
|
||||||
"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": "Bounded inline UTF-8 text only." },
|
||||||
"description": "Unique batch identifier (UUID)"
|
"blob": { "$ref": "#/$defs/BlobReference" },
|
||||||
|
"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": {
|
"BlobReference": {
|
||||||
"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": ["sha256", "size"],
|
||||||
"required": ["deviceId", "entries"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"deviceId": { "type": "string" },
|
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||||
"entries": {
|
"size": { "type": "integer", "minimum": 0 }
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"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" }
|
"WorkspacePayload": {
|
||||||
}
|
"type": "object",
|
||||||
|
"required": ["workspaceId", "path", "name"],
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": { "type": "string", "format": "uuid" },
|
||||||
|
"path": { "$ref": "#/$defs/WorkspacePath" },
|
||||||
|
"previousPath": { "$ref": "#/$defs/WorkspacePath" },
|
||||||
|
"name": { "type": "string", "minLength": 1 },
|
||||||
|
"metadata": { "type": "object", "additionalProperties": true }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"PushRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["device_id", "ops"],
|
||||||
|
"properties": {
|
||||||
|
"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", "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
|
||||||
|
},
|
||||||
|
"PushOperation": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["op_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||||
|
"properties": {
|
||||||
|
"op_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"entity_type": { "enum": ["file", "folder", "workspace"] },
|
||||||
|
"entity_id": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||||
|
"op_type": { "enum": ["create", "update", "delete", "move", "rename", "trash", "restore"] },
|
||||||
|
"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 }
|
||||||
|
},
|
||||||
|
"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, "maxLength": 320 },
|
||||||
"deviceType": {
|
"password": { "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
|
||||||
|
},
|
||||||
|
"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" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
VERSION="${1:-}"
|
||||||
|
REPOSITORY="mirivlad/verstak-sdk"
|
||||||
|
RELEASE_SCRIPT="${VERSTAK_RELEASE_SCRIPT:-$ROOT/scripts/release.sh}"
|
||||||
|
RELEASE_DIR="${VERSTAK_RELEASE_DIR:-$ROOT/release}"
|
||||||
|
GIT_BIN="${GIT_BIN:-git}"
|
||||||
|
GH_BIN="${GH_BIN:-gh}"
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" || ! "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
||||||
|
echo "usage: $0 <version>" >&2
|
||||||
|
echo "example: $0 v0.1.0" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! command -v "$GH_BIN" >/dev/null; then
|
||||||
|
echo "gh CLI is required to publish a GitHub Release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$("$GIT_BIN" branch --show-current)" != "main" ]]; then
|
||||||
|
echo "GitHub releases must be published from main" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$("$GIT_BIN" status --porcelain)" ]]; then
|
||||||
|
echo "working tree must be clean before publishing a release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$GH_BIN" auth status
|
||||||
|
"$GIT_BIN" fetch origin main --tags
|
||||||
|
HEAD="$("$GIT_BIN" rev-parse HEAD)"
|
||||||
|
if [[ "$HEAD" != "$("$GIT_BIN" rev-parse origin/main)" ]]; then
|
||||||
|
echo "local main must match origin/main before publishing a release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$RELEASE_SCRIPT" "$VERSION"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
ASSETS=("$RELEASE_DIR"/*.tar.gz "$RELEASE_DIR"/*.tgz)
|
||||||
|
if [[ -f "$RELEASE_DIR/SHA256SUMS" ]]; then
|
||||||
|
ASSETS+=("$RELEASE_DIR/SHA256SUMS")
|
||||||
|
fi
|
||||||
|
if [[ ${#ASSETS[@]} -eq 0 ]]; then
|
||||||
|
echo "no release assets found in $RELEASE_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "$GIT_BIN" rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
|
||||||
|
if [[ "$("$GIT_BIN" rev-parse "${VERSION}^{commit}")" != "$HEAD" ]]; then
|
||||||
|
echo "existing tag $VERSION does not point at HEAD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
"$GIT_BIN" tag -a "$VERSION" -m "Release $VERSION"
|
||||||
|
"$GIT_BIN" push origin "refs/tags/$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "$GH_BIN" release view "$VERSION" --repo "$REPOSITORY" >/dev/null 2>&1; then
|
||||||
|
"$GH_BIN" release upload "$VERSION" "${ASSETS[@]}" --repo "$REPOSITORY" --clobber
|
||||||
|
else
|
||||||
|
"$GH_BIN" release create "$VERSION" "${ASSETS[@]}" \
|
||||||
|
--repo "$REPOSITORY" \
|
||||||
|
--title "Verstak Plugin SDK $VERSION" \
|
||||||
|
--generate-notes \
|
||||||
|
--latest \
|
||||||
|
--verify-tag
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "GitHub release:"
|
||||||
|
"$GH_BIN" release view "$VERSION" --repo "$REPOSITORY" --json url --jq .url
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
REQUESTED_VERSION="${1:-}"
|
||||||
|
PACKAGE_VERSION="$(node -p "require('$ROOT/package.json').version")"
|
||||||
|
|
||||||
|
if [[ -z "$REQUESTED_VERSION" ]]; then
|
||||||
|
echo "usage: $0 <version>" >&2
|
||||||
|
echo "example: $0 v0.1.0" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ "${REQUESTED_VERSION#v}" != "$PACKAGE_VERSION" ]]; then
|
||||||
|
echo "requested version $REQUESTED_VERSION does not match package.json $PACKAGE_VERSION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== verstak sdk release $PACKAGE_VERSION ==="
|
||||||
|
(cd "$ROOT" && npm ci --no-audit --no-fund && npm run lint && npm test && npm run build)
|
||||||
|
|
||||||
|
RELEASE_ROOT="$ROOT/release"
|
||||||
|
rm -rf "$RELEASE_ROOT"
|
||||||
|
mkdir -p "$RELEASE_ROOT"
|
||||||
|
(cd "$ROOT" && npm pack --pack-destination "$RELEASE_ROOT")
|
||||||
|
(cd "$RELEASE_ROOT" && sha256sum ./*.tgz > SHA256SUMS)
|
||||||
|
|
||||||
|
echo "release package: $RELEASE_ROOT"
|
||||||
|
echo "checksums: $RELEASE_ROOT/SHA256SUMS"
|
||||||
Executable
+91
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PUBLISHER="$ROOT/scripts/publish-github-release.sh"
|
||||||
|
VERSION="v0.0.0-test"
|
||||||
|
REPOSITORY="mirivlad/verstak-sdk"
|
||||||
|
ASSET_NAME="verstak-plugin-sdk-0.0.0-test.tgz"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
if [[ ! -x "$PUBLISHER" ]]; then
|
||||||
|
echo "publisher is missing or not executable: $PUBLISHER" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$WORK/bin" "$WORK/release" "$WORK/state"
|
||||||
|
LOG="$WORK/log"
|
||||||
|
export LOG
|
||||||
|
|
||||||
|
cat > "$WORK/release.sh" <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
printf 'release:%s\n' "$1" >> "$LOG"
|
||||||
|
printf 'package\n' > "$VERSTAK_RELEASE_DIR/verstak-plugin-sdk-${1#v}.tgz"
|
||||||
|
printf 'checksum\n' > "$VERSTAK_RELEASE_DIR/SHA256SUMS"
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$WORK/release.sh"
|
||||||
|
|
||||||
|
cat > "$WORK/bin/git" <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "$PWD" != "${EXPECTED_ROOT:?}" ]]; then
|
||||||
|
echo "publisher did not enter repository root: $PWD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
case "${1:-}" in
|
||||||
|
status) exit 0 ;;
|
||||||
|
branch) echo main ;;
|
||||||
|
fetch) printf 'fetch\n' >> "$LOG" ;;
|
||||||
|
rev-parse)
|
||||||
|
if [[ "${2:-}" == "-q" ]]; then
|
||||||
|
if [[ -f "$TEST_STATE/tag" ]]; then echo test-commit; exit 0; fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo test-commit
|
||||||
|
;;
|
||||||
|
tag) touch "$TEST_STATE/tag"; printf 'tag:%s\n' "${3:-}" >> "$LOG" ;;
|
||||||
|
push) printf 'push:%s:%s\n' "${2:-}" "${3:-}" >> "$LOG" ;;
|
||||||
|
*) echo "unexpected git invocation: $*" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$WORK/bin/git"
|
||||||
|
|
||||||
|
cat > "$WORK/bin/gh" <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
case "${1:-}:${2:-}" in
|
||||||
|
auth:status) printf 'auth\n' >> "$LOG" ;;
|
||||||
|
release:view)
|
||||||
|
if [[ -f "$TEST_STATE/release" ]]; then echo https://github.example/release; else exit 1; fi
|
||||||
|
;;
|
||||||
|
release:create) touch "$TEST_STATE/release"; printf 'create:%s\n' "$*" >> "$LOG" ;;
|
||||||
|
release:upload) printf 'upload:%s\n' "$*" >> "$LOG" ;;
|
||||||
|
*) echo "unexpected gh invocation: $*" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$WORK/bin/gh"
|
||||||
|
|
||||||
|
run_publisher() {
|
||||||
|
VERSTAK_RELEASE_SCRIPT="$WORK/release.sh" \
|
||||||
|
VERSTAK_RELEASE_DIR="$WORK/release" \
|
||||||
|
GIT_BIN="$WORK/bin/git" \
|
||||||
|
GH_BIN="$WORK/bin/gh" \
|
||||||
|
EXPECTED_ROOT="$ROOT" \
|
||||||
|
TEST_STATE="$WORK/state" \
|
||||||
|
"$PUBLISHER" "$VERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_publisher
|
||||||
|
grep -Fqx "release:$VERSION" "$LOG"
|
||||||
|
grep -Fqx "tag:$VERSION" "$LOG"
|
||||||
|
grep -Fqx "push:origin:refs/tags/$VERSION" "$LOG"
|
||||||
|
grep -F "release create $VERSION" "$LOG" >/dev/null
|
||||||
|
grep -F "$ASSET_NAME" "$LOG" >/dev/null
|
||||||
|
grep -F "SHA256SUMS" "$LOG" >/dev/null
|
||||||
|
|
||||||
|
run_publisher
|
||||||
|
grep -F "release upload $VERSION" "$LOG" >/dev/null
|
||||||
|
|
||||||
|
echo "GitHub release publisher test passed"
|
||||||
@@ -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,25 @@ 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();
|
||||||
|
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', () => {
|
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[];
|
||||||
|
|
||||||
@@ -135,7 +155,28 @@ describe('VerstakPluginAPI contract', () => {
|
|||||||
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
|
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
|
||||||
|
|
||||||
expect(permissions).toContainEqual(expect.objectContaining({ name: 'browser.receiver.manage', dangerous: true }));
|
expect(permissions).toContainEqual(expect.objectContaining({ name: 'browser.receiver.manage', dangerous: true }));
|
||||||
|
expect(permissions).toContainEqual(expect.objectContaining({ name: 'notifications.schedule', dangerous: false }));
|
||||||
expect(permissionEnum).toContain('browser.receiver.manage');
|
expect(permissionEnum).toContain('browser.receiver.manage');
|
||||||
|
expect(permissionEnum).toContain('notifications.schedule');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('desktop core capabilities used by bundled plugins are declared', () => {
|
||||||
|
const capabilities = ((capabilitiesSchema as any).capabilities || []) as Array<{ name: string; status: string }>;
|
||||||
|
|
||||||
|
for (const name of [
|
||||||
|
'verstak/core/plugin-manager/v1',
|
||||||
|
'verstak/core/capability-registry/v1',
|
||||||
|
'verstak/core/contribution-registry/v1',
|
||||||
|
'verstak/core/permissions/v1',
|
||||||
|
'verstak/core/events/v1',
|
||||||
|
'verstak/core/files/v1',
|
||||||
|
'verstak/core/workbench/v1',
|
||||||
|
'verstak/core/notifications/v1',
|
||||||
|
'verstak/core/vault/v1',
|
||||||
|
'verstak/core/workspace/v1',
|
||||||
|
]) {
|
||||||
|
expect(capabilities).toContainEqual(expect.objectContaining({ name, status: 'draft' }));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('file.changed schema documents watcher refresh payload', () => {
|
test('file.changed schema documents watcher refresh payload', () => {
|
||||||
|
|||||||
+47
-1
@@ -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 () => {}),
|
||||||
|
|||||||
+58
-1
@@ -55,6 +55,62 @@ export interface SyncConfig {
|
|||||||
participate?: boolean;
|
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 ────────────────────────────────────────────
|
// ─── Capabilities ────────────────────────────────────────────
|
||||||
|
|
||||||
export type CapabilityName = string;
|
export type CapabilityName = string;
|
||||||
@@ -89,7 +145,8 @@ export type Permission =
|
|||||||
| 'secrets.read'
|
| 'secrets.read'
|
||||||
| 'secrets.write'
|
| 'secrets.write'
|
||||||
| 'sync.participate'
|
| 'sync.participate'
|
||||||
| 'browser.receiver.manage';
|
| 'browser.receiver.manage'
|
||||||
|
| 'notifications.schedule';
|
||||||
|
|
||||||
export interface PermissionEntry {
|
export interface PermissionEntry {
|
||||||
name: Permission;
|
name: Permission;
|
||||||
|
|||||||
Reference in New Issue
Block a user