Compare commits

..

No commits in common. "main" and "codex/alpha-product-ux" have entirely different histories.

26 changed files with 174 additions and 739 deletions

View File

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

View File

@ -1,24 +1,12 @@
<div align="center">
# Verstak Plugin SDK
### TypeScript API, JSON schemas and contract tests for Verstak plugins.
**English** · [Русский](README.ru.md)
[![Release](https://img.shields.io/github/v/release/mirivlad/verstak-sdk?include_prereleases\&label=release)](https://github.com/mirivlad/verstak-sdk/releases)
![Status](https://img.shields.io/badge/status-alpha-orange)
[![License](https://img.shields.io/github/license/mirivlad/verstak-sdk)](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

View File

@ -1,105 +0,0 @@
<div align="center">
# Verstak Plugin SDK
### TypeScript API, JSON-схемы и контрактные тесты для плагинов Верстака.
[English](README.md) · **Русский**
[![Release](https://img.shields.io/github/v/release/mirivlad/verstak-sdk?include_prereleases\&label=release)](https://github.com/mirivlad/verstak-sdk/releases)
![Статус](https://img.shields.io/badge/status-alpha-orange)
[![Лицензия](https://img.shields.io/github/license/mirivlad/verstak-sdk)](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).

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

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

View File

@ -1 +1 @@
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;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"}

View File

@ -1 +1 @@
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,YAAY,EAAqB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ5G,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;IAC7C,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClE;AASD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,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"}

2
dist/test-utils.js vendored
View File

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

49
dist/types.d.ts vendored
View File

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

2
dist/types.d.ts.map vendored

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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