Compare commits
No commits in common. "main" and "codex/alpha-product-ux" have entirely different histories.
main
...
codex/alph
10
AGENTS.md
10
AGENTS.md
|
|
@ -41,16 +41,6 @@ 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
|
||||
|
|
|
|||
61
README.md
61
README.md
|
|
@ -1,24 +1,12 @@
|
|||
<div align="center">
|
||||
|
||||
# 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.
|
||||
|
||||
> **Alpha contract.** This is the first public alpha. Keep SDK, Desktop and
|
||||
> official-plugin versions in the same release line while APIs are evolving.
|
||||
|
||||
## Install and verify
|
||||
|
||||
```bash
|
||||
|
|
@ -37,18 +25,6 @@ The build emits `dist/`. A packed npm artifact can be made locally with:
|
|||
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.
|
||||
|
|
@ -58,37 +34,6 @@ pushes the annotated version tag when needed and uploads the npm tarball and
|
|||
- 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
|
||||
|
||||
Verstak Desktop creates the real API with `createPluginAPI(pluginId)` and passes
|
||||
|
|
|
|||
105
README.ru.md
105
README.ru.md
|
|
@ -1,105 +0,0 @@
|
|||
<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).
|
||||
|
|
@ -27,8 +27,6 @@ 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;
|
||||
|
|
@ -38,47 +36,8 @@ 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;
|
||||
|
|
@ -174,7 +133,7 @@ export interface VerstakPluginAPI {
|
|||
};
|
||||
sync: {
|
||||
status(): Promise<SyncStatus>;
|
||||
configure(serverUrl: string, username: string, password: string, remoteVaultId?: string): Promise<void>;
|
||||
configure(serverUrl: string, username: string, password: string): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
||||
setInterval(minutes: number): Promise<void>;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA4OhD,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;AA8LhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,YAAY,EAAqB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ5G,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;IAC7C,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClE;AASD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,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"}
|
||||
{"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"}
|
||||
|
|
@ -421,7 +421,6 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
|||
status: vi.fn(async () => ({
|
||||
configured: false,
|
||||
serverUrl: '',
|
||||
vaultId: '',
|
||||
deviceId: '',
|
||||
deviceName: '',
|
||||
connected: false,
|
||||
|
|
@ -431,7 +430,6 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
|||
lastSyncAt: '',
|
||||
syncInterval: 0,
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
statusLabel: 'disabled',
|
||||
})),
|
||||
configure: vi.fn(async () => { }),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -43,53 +43,6 @@ 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;
|
||||
|
|
@ -97,7 +50,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' | 'notifications.schedule';
|
||||
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 interface PermissionEntry {
|
||||
name: Permission;
|
||||
description: string;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/capabilities.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/capabilities.json",
|
||||
"title": "Verstak Capability Registry",
|
||||
"description": "Known capability names and their descriptions for the Verstak platform",
|
||||
"type": "object",
|
||||
|
|
@ -27,16 +27,6 @@
|
|||
}
|
||||
},
|
||||
"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://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/contributions.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/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://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/events/browser.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/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://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/events/lifecycle.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/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://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/events/vault.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/events/vault.json",
|
||||
"title": "Verstak Vault Events",
|
||||
"description": "Event schemas for vault and case lifecycle events",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/hostname-normalization-v1.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/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": [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/manifest.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/manifest.json",
|
||||
"title": "Verstak Plugin Manifest",
|
||||
"description": "Schema for Verstak plugin.json manifest files",
|
||||
"type": "object",
|
||||
|
|
@ -126,8 +126,7 @@
|
|||
"secrets.read",
|
||||
"secrets.write",
|
||||
"sync.participate",
|
||||
"browser.receiver.manage",
|
||||
"notifications.schedule"
|
||||
"browser.receiver.manage"
|
||||
]
|
||||
},
|
||||
"minItems": 1
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/mirivlad/verstak-sdk/main/schemas/permissions.json",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/permissions.json",
|
||||
"title": "Verstak Permissions Registry",
|
||||
"description": "Known runtime permissions and their safety levels",
|
||||
"type": "object",
|
||||
|
|
@ -47,7 +47,6 @@
|
|||
{ "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": "notifications.schedule", "description": "Replace the plugin's scheduled native notifications", "dangerous": false }
|
||||
{ "name": "browser.receiver.manage", "description": "View and rotate the local browser receiver pairing token", "dangerous": true }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,158 +1,177 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$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.",
|
||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/sync.json",
|
||||
"title": "Verstak Sync Operations",
|
||||
"description": "Sync operation schemas for vault synchronization between devices",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Operation": { "$ref": "#/$defs/Operation" },
|
||||
"PushRequest": { "$ref": "#/$defs/PushRequest" },
|
||||
"PullRequest": { "$ref": "#/$defs/PullRequest" },
|
||||
"PullResponse": { "$ref": "#/$defs/PullResponse" },
|
||||
"BlobReference": { "$ref": "#/$defs/BlobReference" },
|
||||
"Snapshot": { "$ref": "#/$defs/Snapshot" },
|
||||
"PairingRequest": { "$ref": "#/$defs/PairingRequest" }
|
||||
"SyncOperation": {
|
||||
"$ref": "#/$defs/SyncOperation"
|
||||
},
|
||||
"SyncBatch": {
|
||||
"$ref": "#/$defs/SyncBatch"
|
||||
},
|
||||
"SyncManifest": {
|
||||
"$ref": "#/$defs/SyncManifest"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Operation": {
|
||||
"SyncOperation": {
|
||||
"type": "object",
|
||||
"required": ["op_id", "device_id", "entity_type", "entity_id", "op_type", "payload_json", "created_at"],
|
||||
"description": "A single sync operation entry",
|
||||
"required": ["op", "id", "timestamp"],
|
||||
"properties": {
|
||||
"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
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"FilePayload": {
|
||||
"SyncBatch": {
|
||||
"type": "object",
|
||||
"required": ["path"],
|
||||
"description": "A batch of sync operations sent between devices",
|
||||
"required": ["batchId", "deviceId", "operations", "timestamp"],
|
||||
"properties": {
|
||||
"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
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BlobReference": {
|
||||
"SyncManifest": {
|
||||
"type": "object",
|
||||
"required": ["sha256", "size"],
|
||||
"description": "Vault sync manifest for initial reconciliation",
|
||||
"required": ["deviceId", "entries"],
|
||||
"properties": {
|
||||
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"size": { "type": "integer", "minimum": 0 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
"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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkspacePayload": {
|
||||
"Conflict": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "path", "name"],
|
||||
"description": "Conflict record when two devices modify the same entity",
|
||||
"required": ["entityPath", "localHash", "remoteHash", "localTimestamp", "remoteTimestamp"],
|
||||
"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
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"PairingRequest": {
|
||||
"type": "object",
|
||||
"required": ["login", "password", "device_name", "vault_id"],
|
||||
"description": "Device pairing request payload",
|
||||
"required": ["deviceName", "deviceType", "publicKey"],
|
||||
"properties": {
|
||||
"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
|
||||
"deviceName": { "type": "string" },
|
||||
"deviceType": {
|
||||
"type": "string",
|
||||
"enum": ["desktop", "mobile"]
|
||||
},
|
||||
"publicKey": { "type": "string", "description": "Device public key for auth" },
|
||||
"clientVersion": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"VaultPath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"not": { "pattern": "(^|/)\\.verstak(/|$)" }
|
||||
},
|
||||
"WorkspacePath": {
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
#!/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,7 +3,6 @@ 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';
|
||||
|
|
@ -52,25 +51,6 @@ 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[];
|
||||
|
||||
|
|
@ -155,28 +135,7 @@ 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', () => {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ 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;
|
||||
|
|
@ -67,53 +65,9 @@ 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;
|
||||
|
|
@ -223,7 +177,7 @@ export interface VerstakPluginAPI {
|
|||
|
||||
sync: {
|
||||
status(): Promise<SyncStatus>;
|
||||
configure(serverUrl: string, username: string, password: string, remoteVaultId?: string): Promise<void>;
|
||||
configure(serverUrl: string, username: string, password: string): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
|
||||
setInterval(minutes: number): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -408,7 +408,6 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
|||
status: vi.fn(async () => ({
|
||||
configured: false,
|
||||
serverUrl: '',
|
||||
vaultId: '',
|
||||
deviceId: '',
|
||||
deviceName: '',
|
||||
connected: false,
|
||||
|
|
@ -418,7 +417,6 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
|||
lastSyncAt: '',
|
||||
syncInterval: 0,
|
||||
lastError: '',
|
||||
lastWarning: '',
|
||||
statusLabel: 'disabled',
|
||||
})),
|
||||
configure: vi.fn(async () => {}),
|
||||
|
|
|
|||
59
src/types.ts
59
src/types.ts
|
|
@ -55,62 +55,6 @@ 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;
|
||||
|
|
@ -145,8 +89,7 @@ export type Permission =
|
|||
| 'secrets.read'
|
||||
| 'secrets.write'
|
||||
| 'sync.participate'
|
||||
| 'browser.receiver.manage'
|
||||
| 'notifications.schedule';
|
||||
| 'browser.receiver.manage';
|
||||
|
||||
export interface PermissionEntry {
|
||||
name: Permission;
|
||||
|
|
|
|||
Loading…
Reference in New Issue