Compare commits
12
Commits
ad2dd975e7
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac29fa5c78 | ||
|
|
2b2936121d | ||
|
|
7ad07321b0 | ||
|
|
944f3742b6 | ||
|
|
35b62da2c7 | ||
|
|
43ac963923 | ||
|
|
1378fda7f2 | ||
|
|
9a4fd18456 | ||
|
|
a858a928bf | ||
|
|
69e15d2609 | ||
|
|
831415bebd | ||
|
|
d69214ea18 |
@@ -1 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
release/
|
||||
|
||||
@@ -41,6 +41,16 @@ verstak-sdk/
|
||||
## 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.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
|
||||
|
||||
@@ -40,3 +127,8 @@ or trash metadata.
|
||||
Bundled frontend plugins are trusted/cooperative and run in the desktop JS
|
||||
context. Current permission checks are contract checks, not a security boundary;
|
||||
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
+49
-1
@@ -1,6 +1,8 @@
|
||||
import type { CapabilityEntry, FileBytes, FileEntry, FileMetadata, MovePathOptions, OpenResourceRequest, OpenResourceResult, PluginSettings, RegisteredContributionPoints, RestoreTrashOptions, TrashEntry, TrashResult, WriteTextOptions } from './types';
|
||||
export type PluginCommandArgs = Record<string, unknown>;
|
||||
export type PluginDataJSON = Record<string, unknown>;
|
||||
export type PluginLocale = 'ru' | 'en';
|
||||
export type TranslationParams = Record<string, string | number>;
|
||||
export type PluginCommandHandler = (args: PluginCommandArgs, declaration: PluginCommandDeclaration) => unknown | Promise<unknown>;
|
||||
export type Unsubscribe = () => void;
|
||||
export interface PluginCommandDeclaration {
|
||||
@@ -25,6 +27,8 @@ export interface PluginEvent<TPayload = Record<string, unknown>> {
|
||||
export interface SyncStatus {
|
||||
configured: boolean;
|
||||
serverUrl: string;
|
||||
/** Remote sync scope selected during pairing. It can differ from this device's local vault UUID. */
|
||||
vaultId: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
connected: boolean;
|
||||
@@ -34,8 +38,47 @@ export interface SyncStatus {
|
||||
lastSyncAt: string;
|
||||
syncInterval: number;
|
||||
lastError: string;
|
||||
/** A persistent unresolved scanner warning, for example an over-limit file. */
|
||||
lastWarning: 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 {
|
||||
op_id?: string;
|
||||
opId?: string;
|
||||
@@ -61,6 +104,11 @@ export interface BrowserReceiverPairing {
|
||||
}
|
||||
export interface VerstakPluginAPI {
|
||||
readonly pluginId: string;
|
||||
i18n: {
|
||||
getLocale(): PluginLocale;
|
||||
t(key: string, params?: TranslationParams, fallback?: string): string;
|
||||
onDidChangeLocale(listener: (locale: PluginLocale) => void): Unsubscribe;
|
||||
};
|
||||
settings: {
|
||||
read(): Promise<PluginSettings>;
|
||||
read<T = unknown>(key: string): Promise<T | undefined>;
|
||||
@@ -126,7 +174,7 @@ export interface VerstakPluginAPI {
|
||||
};
|
||||
sync: {
|
||||
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>;
|
||||
testConnection(serverUrl: string, username: string, password: string): 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;AAsLhD,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
+4
-1
@@ -1,7 +1,10 @@
|
||||
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
||||
import type { VerstakPluginAPI } from './plugin-api';
|
||||
import type { PluginLocale, VerstakPluginAPI } from './plugin-api';
|
||||
export interface MockPluginAPIOptions {
|
||||
contributions?: RegisteredContributionPoints;
|
||||
locale?: PluginLocale;
|
||||
defaultLocale?: PluginLocale;
|
||||
messages?: Partial<Record<PluginLocale, Record<string, string>>>;
|
||||
}
|
||||
/**
|
||||
* Создать тестовый manifest для unit-тестов.
|
||||
|
||||
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,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ3E,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;CAC9C;AAED;;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,CA4WlH;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
+21
@@ -3,6 +3,11 @@ const mockCommandHandlers = new Map();
|
||||
function commandKey(pluginId, commandId) {
|
||||
return `${pluginId}:${commandId}`;
|
||||
}
|
||||
function interpolateMessage(message, params) {
|
||||
if (!params)
|
||||
return message;
|
||||
return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, (placeholder, name) => (Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : placeholder));
|
||||
}
|
||||
/**
|
||||
* Создать тестовый manifest для unit-тестов.
|
||||
*/
|
||||
@@ -39,6 +44,9 @@ export function createTestPluginState(overrides) {
|
||||
* Создать заглушку VerstakPluginAPI для тестов.
|
||||
*/
|
||||
export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||
const locale = options.locale || 'en';
|
||||
const defaultLocale = options.defaultLocale || 'en';
|
||||
const messages = options.messages || {};
|
||||
const settings = {};
|
||||
const pluginData = new Map();
|
||||
const commands = new Map();
|
||||
@@ -129,6 +137,17 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||
}
|
||||
return {
|
||||
pluginId,
|
||||
i18n: {
|
||||
getLocale: vi.fn(() => locale),
|
||||
t: vi.fn((key, params, fallback) => {
|
||||
const message = messages[locale]?.[key]
|
||||
?? messages[defaultLocale]?.[key]
|
||||
?? fallback
|
||||
?? key;
|
||||
return interpolateMessage(message, params);
|
||||
}),
|
||||
onDidChangeLocale: vi.fn((_listener) => () => { }),
|
||||
},
|
||||
settings: {
|
||||
read: vi.fn(async (key) => key ? settings[key] : { ...settings }),
|
||||
write: vi.fn(async (key, value) => {
|
||||
@@ -402,6 +421,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||
status: vi.fn(async () => ({
|
||||
configured: false,
|
||||
serverUrl: '',
|
||||
vaultId: '',
|
||||
deviceId: '',
|
||||
deviceName: '',
|
||||
connected: false,
|
||||
@@ -411,6 +431,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||
lastSyncAt: '',
|
||||
syncInterval: 0,
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
statusLabel: 'disabled',
|
||||
})),
|
||||
configure: vi.fn(async () => { }),
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+53
-1
@@ -1,4 +1,8 @@
|
||||
export type PluginSource = 'official' | 'local' | 'third-party';
|
||||
export interface PluginLocalizationConfig {
|
||||
defaultLocale: string;
|
||||
locales: Record<string, string>;
|
||||
}
|
||||
export interface PluginManifest {
|
||||
schemaVersion: 1;
|
||||
id: string;
|
||||
@@ -8,6 +12,7 @@ export interface PluginManifest {
|
||||
description?: string;
|
||||
source?: PluginSource;
|
||||
icon?: string;
|
||||
localization?: PluginLocalizationConfig;
|
||||
provides: string[];
|
||||
requires?: string[];
|
||||
optionalRequires?: string[];
|
||||
@@ -38,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;
|
||||
@@ -45,7 +97,7 @@ export interface CapabilityEntry {
|
||||
pluginId: string;
|
||||
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 {
|
||||
name: Permission;
|
||||
description: string;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
||||
"name": "@verstak/plugin-sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "Verstak Plugin SDK — TypeScript types and runtime API for plugin development",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"description": "Known capability names and their descriptions for the Verstak platform",
|
||||
"type": "object",
|
||||
@@ -27,6 +27,16 @@
|
||||
}
|
||||
},
|
||||
"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.markdown", "description": "Markdown text editing", "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",
|
||||
"$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",
|
||||
"description": "Known contribution points that plugins can register",
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"description": "Event schemas for browser extension and browser-inbox plugin communication",
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"description": "Event schemas for plugin and application lifecycle events",
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"description": "Event schemas for vault and case lifecycle events",
|
||||
"type": "object",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/hostname-normalization-v1.json",
|
||||
"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.",
|
||||
"version": 1,
|
||||
"bare": [
|
||||
{ "input": "example.com", "output": "example.com" },
|
||||
{ "input": " Example.COM. ", "output": "example.com" },
|
||||
{ "input": "пример.рф", "output": "xn--e1afmkfd.xn--p1ai" },
|
||||
{ "input": "bücher.example", "output": "xn--bcher-kva.example" },
|
||||
{ "input": "127.0.0.1", "output": "127.0.0.1" },
|
||||
{ "input": "[2001:db8::1]", "output": "2001:db8::1" },
|
||||
{ "input": "localhost", "output": "localhost" },
|
||||
{ "input": "intranet", "output": "intranet" },
|
||||
{ "input": "", "output": "" },
|
||||
{ "input": "https://example.com", "output": "" },
|
||||
{ "input": "example.com:443", "output": "" },
|
||||
{ "input": "user@example.com", "output": "" },
|
||||
{ "input": "bad host", "output": "" },
|
||||
{ "input": "example..com", "output": "" },
|
||||
{ "input": "example.com..", "output": "" },
|
||||
{ "input": "127.000.000.001", "output": "" },
|
||||
{ "input": "[2001:db8::1]:443", "output": "" },
|
||||
{ "input": "a...............................................................example", "output": "" }
|
||||
],
|
||||
"url": [
|
||||
{ "input": "https://пример.рф/path", "output": "xn--e1afmkfd.xn--p1ai" },
|
||||
{ "input": "http://Example.COM.:8080/path", "output": "example.com" },
|
||||
{ "input": "https://[2001:db8::1]/", "output": "2001:db8::1" },
|
||||
{ "input": "ftp://example.com/path", "output": "" },
|
||||
{ "input": "not a URL", "output": "" }
|
||||
]
|
||||
}
|
||||
+28
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"description": "Schema for Verstak plugin.json manifest files",
|
||||
"type": "object",
|
||||
@@ -51,6 +51,31 @@
|
||||
"type": "string",
|
||||
"description": "Path to plugin icon (relative to plugin root)"
|
||||
},
|
||||
"localization": {
|
||||
"type": "object",
|
||||
"description": "Plugin-owned localization catalogs",
|
||||
"properties": {
|
||||
"defaultLocale": {
|
||||
"type": "string",
|
||||
"description": "Default locale used when the selected locale is unavailable",
|
||||
"pattern": "^[a-z]{2}(?:-[a-z0-9]+)*$"
|
||||
},
|
||||
"locales": {
|
||||
"type": "object",
|
||||
"description": "Locale tags mapped to plugin-relative JSON catalog paths",
|
||||
"propertyNames": {
|
||||
"pattern": "^[a-z]{2}(?:-[a-z0-9]+)*$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"pattern": "^(?![\\\\/])(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
|
||||
},
|
||||
"minProperties": 1
|
||||
}
|
||||
},
|
||||
"required": ["defaultLocale", "locales"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"provides": {
|
||||
"type": "array",
|
||||
"description": "Capabilities this plugin provides",
|
||||
@@ -101,7 +126,8 @@
|
||||
"secrets.read",
|
||||
"secrets.write",
|
||||
"sync.participate",
|
||||
"browser.receiver.manage"
|
||||
"browser.receiver.manage",
|
||||
"notifications.schedule"
|
||||
]
|
||||
},
|
||||
"minItems": 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"description": "Known runtime permissions and their safety levels",
|
||||
"type": "object",
|
||||
@@ -47,6 +47,7 @@
|
||||
{ "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": "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 }
|
||||
]
|
||||
}
|
||||
|
||||
+133
-152
@@ -1,177 +1,158 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/sync.json",
|
||||
"title": "Verstak Sync Operations",
|
||||
"description": "Sync operation schemas for vault synchronization between devices",
|
||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/sync.json",
|
||||
"title": "Verstak core sync operation log",
|
||||
"description": "Wire contracts for the core-owned operation log. Plugins may configure and display sync, but do not create operations.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"SyncOperation": {
|
||||
"$ref": "#/$defs/SyncOperation"
|
||||
},
|
||||
"SyncBatch": {
|
||||
"$ref": "#/$defs/SyncBatch"
|
||||
},
|
||||
"SyncManifest": {
|
||||
"$ref": "#/$defs/SyncManifest"
|
||||
}
|
||||
"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" }
|
||||
},
|
||||
"$defs": {
|
||||
"SyncOperation": {
|
||||
"Operation": {
|
||||
"type": "object",
|
||||
"description": "A single sync operation entry",
|
||||
"required": ["op", "id", "timestamp"],
|
||||
"required": ["op_id", "device_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||
"properties": {
|
||||
"op": {
|
||||
"type": "string",
|
||||
"description": "Operation type",
|
||||
"enum": ["add", "modify", "delete", "rename"]
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique operation identifier (UUID)"
|
||||
},
|
||||
"timestamp": {
|
||||
"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",
|
||||
"description": "Arbitrary metadata key-value pairs",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
}
|
||||
"op_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"server_sequence": { "type": "integer", "minimum": 1 },
|
||||
"device_id": { "type": "string", "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
|
||||
},
|
||||
"SyncBatch": {
|
||||
"FilePayload": {
|
||||
"type": "object",
|
||||
"description": "A batch of sync operations sent between devices",
|
||||
"required": ["batchId", "deviceId", "operations", "timestamp"],
|
||||
"required": ["path"],
|
||||
"properties": {
|
||||
"batchId": {
|
||||
"type": "string",
|
||||
"description": "Unique batch identifier (UUID)"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string",
|
||||
"description": "Originating device identifier"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"path": { "$ref": "#/$defs/VaultPath" },
|
||||
"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
|
||||
},
|
||||
"SyncManifest": {
|
||||
"BlobReference": {
|
||||
"type": "object",
|
||||
"description": "Vault sync manifest for initial reconciliation",
|
||||
"required": ["deviceId", "entries"],
|
||||
"required": ["sha256", "size"],
|
||||
"properties": {
|
||||
"deviceId": { "type": "string" },
|
||||
"entries": {
|
||||
"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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"size": { "type": "integer", "minimum": 0 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Conflict": {
|
||||
"WorkspacePayload": {
|
||||
"type": "object",
|
||||
"description": "Conflict record when two devices modify the same entity",
|
||||
"required": ["entityPath", "localHash", "remoteHash", "localTimestamp", "remoteTimestamp"],
|
||||
"required": ["workspaceId", "path", "name"],
|
||||
"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" }
|
||||
}
|
||||
"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": {
|
||||
"type": "object",
|
||||
"description": "Device pairing request payload",
|
||||
"required": ["deviceName", "deviceType", "publicKey"],
|
||||
"required": ["login", "password", "device_name", "vault_id"],
|
||||
"properties": {
|
||||
"deviceName": { "type": "string" },
|
||||
"deviceType": {
|
||||
"type": "string",
|
||||
"enum": ["desktop", "mobile"]
|
||||
},
|
||||
"publicKey": { "type": "string", "description": "Device public key for auth" },
|
||||
"clientVersion": { "type": "string" }
|
||||
}
|
||||
"login": { "type": "string", "minLength": 1, "maxLength": 320 },
|
||||
"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
|
||||
},
|
||||
"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" }
|
||||
}
|
||||
"VaultPath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"not": { "pattern": "(^|/)\\.verstak(/|$)" }
|
||||
},
|
||||
"WorkspacePath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[^/]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 manifestSchema from '../schemas/manifest.json';
|
||||
import permissionsSchema from '../schemas/permissions.json';
|
||||
import syncSchema from '../schemas/sync.json';
|
||||
import vaultEventsSchema from '../schemas/events/vault.json';
|
||||
import type { OpenProviderSupport, OpenResourceRequest, PluginManifest } from './types';
|
||||
import { createMockPluginAPI } from './test-utils';
|
||||
@@ -16,6 +17,9 @@ describe('VerstakPluginAPI contract', () => {
|
||||
expect(typeof api.settings.write).toBe('function');
|
||||
expect(typeof api.storage.data.read).toBe('function');
|
||||
expect(typeof api.storage.data.write).toBe('function');
|
||||
expect(typeof api.i18n.getLocale).toBe('function');
|
||||
expect(typeof api.i18n.t).toBe('function');
|
||||
expect(typeof api.i18n.onDidChangeLocale).toBe('function');
|
||||
expect(typeof api.ui.openSettings).toBe('function');
|
||||
expect(typeof api.capabilities.list).toBe('function');
|
||||
expect(typeof api.commands.register).toBe('function');
|
||||
@@ -48,6 +52,25 @@ describe('VerstakPluginAPI contract', () => {
|
||||
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', () => {
|
||||
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
|
||||
|
||||
@@ -58,6 +81,56 @@ describe('VerstakPluginAPI contract', () => {
|
||||
expect(permissionEnum).toContain('workbench.open');
|
||||
});
|
||||
|
||||
test('manifest schema declares safe plugin localization catalogs', () => {
|
||||
const localization = (manifestSchema as any).properties.localization;
|
||||
|
||||
expect(localization.type).toBe('object');
|
||||
expect(localization.required).toEqual(['defaultLocale', 'locales']);
|
||||
expect(localization.properties.defaultLocale.pattern).toBe('^[a-z]{2}(?:-[a-z0-9]+)*$');
|
||||
expect(localization.properties.locales.propertyNames.pattern).toBe('^[a-z]{2}(?:-[a-z0-9]+)*$');
|
||||
expect(localization.properties.locales.additionalProperties.pattern).toBe('^(?![\\\\/])(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$');
|
||||
});
|
||||
|
||||
test('manifest types accept plugin-owned localization catalogs', () => {
|
||||
const manifest: PluginManifest = {
|
||||
schemaVersion: 1,
|
||||
id: 'localized.plugin',
|
||||
name: 'Localized Plugin',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
provides: ['localized.example'],
|
||||
permissions: ['ui.register'],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
locales: {
|
||||
en: 'locales/en.json',
|
||||
ru: 'locales/ru.json',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(manifest.localization?.defaultLocale).toBe('en');
|
||||
expect(manifest.localization?.locales.ru).toBe('locales/ru.json');
|
||||
});
|
||||
|
||||
test('mock i18n translates with fallback and named interpolation', () => {
|
||||
const api = createMockPluginAPI('localized.plugin', {
|
||||
locale: 'ru',
|
||||
messages: {
|
||||
en: { greeting: 'Hello, {name}!', onlyEnglish: 'English fallback' },
|
||||
ru: { greeting: 'Привет, {name}!' },
|
||||
},
|
||||
defaultLocale: 'en',
|
||||
});
|
||||
|
||||
expect(api.i18n.getLocale()).toBe('ru');
|
||||
expect(api.i18n.t('greeting', { name: 'Мир' })).toBe('Привет, Мир!');
|
||||
expect(api.i18n.t('onlyEnglish')).toBe('English fallback');
|
||||
expect(api.i18n.t('missing', undefined, 'Fallback')).toBe('Fallback');
|
||||
expect(api.i18n.t('unknown')).toBe('unknown');
|
||||
expect(typeof api.i18n.onDidChangeLocale(() => {})).toBe('function');
|
||||
});
|
||||
|
||||
test('secrets capability and permissions are declared as dangerous platform contract', () => {
|
||||
const capabilities = ((capabilitiesSchema as any).capabilities || []) as Array<{ name: string; status: string }>;
|
||||
const permissions = ((permissionsSchema as any).permissions || []) as Array<{ name: string; dangerous: boolean }>;
|
||||
@@ -82,7 +155,28 @@ describe('VerstakPluginAPI contract', () => {
|
||||
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: 'notifications.schedule', dangerous: false }));
|
||||
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', () => {
|
||||
|
||||
+55
-1
@@ -23,6 +23,8 @@ import type {
|
||||
|
||||
export type PluginCommandArgs = Record<string, unknown>;
|
||||
export type PluginDataJSON = Record<string, unknown>;
|
||||
export type PluginLocale = 'ru' | 'en';
|
||||
export type TranslationParams = Record<string, string | number>;
|
||||
export type PluginCommandHandler = (
|
||||
args: PluginCommandArgs,
|
||||
declaration: PluginCommandDeclaration
|
||||
@@ -54,6 +56,8 @@ export interface PluginEvent<TPayload = Record<string, unknown>> {
|
||||
export interface SyncStatus {
|
||||
configured: boolean;
|
||||
serverUrl: string;
|
||||
/** Remote sync scope selected during pairing. It can differ from this device's local vault UUID. */
|
||||
vaultId: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
connected: boolean;
|
||||
@@ -63,9 +67,53 @@ export interface SyncStatus {
|
||||
lastSyncAt: string;
|
||||
syncInterval: number;
|
||||
lastError: string;
|
||||
/** A persistent unresolved scanner warning, for example an over-limit file. */
|
||||
lastWarning: 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 {
|
||||
op_id?: string;
|
||||
opId?: string;
|
||||
@@ -95,6 +143,12 @@ export interface BrowserReceiverPairing {
|
||||
export interface VerstakPluginAPI {
|
||||
readonly pluginId: string;
|
||||
|
||||
i18n: {
|
||||
getLocale(): PluginLocale;
|
||||
t(key: string, params?: TranslationParams, fallback?: string): string;
|
||||
onDidChangeLocale(listener: (locale: PluginLocale) => void): Unsubscribe;
|
||||
};
|
||||
|
||||
settings: {
|
||||
read(): Promise<PluginSettings>;
|
||||
read<T = unknown>(key: string): Promise<T | undefined>;
|
||||
@@ -169,7 +223,7 @@ export interface VerstakPluginAPI {
|
||||
|
||||
sync: {
|
||||
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>;
|
||||
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
||||
setInterval(minutes: number): Promise<void>;
|
||||
|
||||
+27
-1
@@ -1,7 +1,7 @@
|
||||
// Verstak Plugin SDK — Test Utilities
|
||||
|
||||
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
||||
import type { PluginCommandHandler, VerstakPluginAPI } from './plugin-api';
|
||||
import type { PluginCommandHandler, PluginLocale, TranslationParams, VerstakPluginAPI } from './plugin-api';
|
||||
|
||||
const mockCommandHandlers = new Map<string, PluginCommandHandler>();
|
||||
|
||||
@@ -11,6 +11,16 @@ function commandKey(pluginId: string, commandId: string): string {
|
||||
|
||||
export interface MockPluginAPIOptions {
|
||||
contributions?: RegisteredContributionPoints;
|
||||
locale?: PluginLocale;
|
||||
defaultLocale?: PluginLocale;
|
||||
messages?: Partial<Record<PluginLocale, Record<string, string>>>;
|
||||
}
|
||||
|
||||
function interpolateMessage(message: string, params?: TranslationParams): string {
|
||||
if (!params) return message;
|
||||
return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, (placeholder, name: string) => (
|
||||
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : placeholder
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +61,9 @@ export function createTestPluginState(overrides?: Partial<PluginState>): PluginS
|
||||
* Создать заглушку VerstakPluginAPI для тестов.
|
||||
*/
|
||||
export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPluginAPIOptions = {}): VerstakPluginAPI {
|
||||
const locale = options.locale || 'en';
|
||||
const defaultLocale = options.defaultLocale || 'en';
|
||||
const messages = options.messages || {};
|
||||
const settings: Record<string, unknown> = {};
|
||||
const pluginData = new Map<string, Record<string, unknown>>();
|
||||
const commands = new Map<string, PluginCommandHandler>();
|
||||
@@ -139,6 +152,17 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
i18n: {
|
||||
getLocale: vi.fn(() => locale),
|
||||
t: vi.fn((key: string, params?: TranslationParams, fallback?: string) => {
|
||||
const message = messages[locale]?.[key]
|
||||
?? messages[defaultLocale]?.[key]
|
||||
?? fallback
|
||||
?? key;
|
||||
return interpolateMessage(message, params);
|
||||
}),
|
||||
onDidChangeLocale: vi.fn((_listener: (nextLocale: PluginLocale) => void) => () => {}),
|
||||
},
|
||||
settings: {
|
||||
read: vi.fn(async (key?: string) => key ? settings[key] : { ...settings }) as VerstakPluginAPI['settings']['read'],
|
||||
write: vi.fn(async (key: string, value: unknown) => {
|
||||
@@ -384,6 +408,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||
status: vi.fn(async () => ({
|
||||
configured: false,
|
||||
serverUrl: '',
|
||||
vaultId: '',
|
||||
deviceId: '',
|
||||
deviceName: '',
|
||||
connected: false,
|
||||
@@ -393,6 +418,7 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||
lastSyncAt: '',
|
||||
syncInterval: 0,
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
statusLabel: 'disabled',
|
||||
})),
|
||||
configure: vi.fn(async () => {}),
|
||||
|
||||
+64
-1
@@ -4,6 +4,11 @@
|
||||
|
||||
export type PluginSource = 'official' | 'local' | 'third-party';
|
||||
|
||||
export interface PluginLocalizationConfig {
|
||||
defaultLocale: string;
|
||||
locales: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PluginManifest {
|
||||
schemaVersion: 1;
|
||||
id: string;
|
||||
@@ -13,6 +18,7 @@ export interface PluginManifest {
|
||||
description?: string;
|
||||
source?: PluginSource;
|
||||
icon?: string;
|
||||
localization?: PluginLocalizationConfig;
|
||||
provides: string[];
|
||||
requires?: string[];
|
||||
optionalRequires?: string[];
|
||||
@@ -49,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;
|
||||
@@ -83,7 +145,8 @@ export type Permission =
|
||||
| 'secrets.read'
|
||||
| 'secrets.write'
|
||||
| 'sync.participate'
|
||||
| 'browser.receiver.manage';
|
||||
| 'browser.receiver.manage'
|
||||
| 'notifications.schedule';
|
||||
|
||||
export interface PermissionEntry {
|
||||
name: Permission;
|
||||
|
||||
Reference in New Issue
Block a user