@vormio/plugin-sdk 0.0.0-stage → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vormio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,232 @@
1
- # Temporary Holding Version
1
+ # @vormio/plugin-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The manifest schema, runtime helpers and packaging command for writing Vormio plugins, plus the commands that publish a signed plugin catalog. The package is versioned with Vormio-Core (currently 0.0.5) and published to npm under the MIT license: `npm install @vormio/plugin-sdk@0.0.5`.
4
+
5
+ Plugin contract version: `apiVersion: 1`. During development (0.0.x) there is no cross-version compatibility promise: the host loads only plugins with the same `apiVersion` as its own.
6
+
7
+ ## What a plugin can do
8
+
9
+ A plugin provides three capabilities for the formats it declares. They are independent: declaring one does not grant the others.
10
+
11
+ | Extension point | Runs in | Result |
12
+ | --------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
13
+ | Thumbnail | A hidden sandboxed page; Canvas and WebGL work | One PNG whose longest edge is at most the host's `maxEdge` (currently 320); the host then caps it at 512×512 and 4 MiB |
14
+ | Viewer | A sandboxed page over Vormio's viewing area | Shows the file read-only; the user can interact with it |
15
+ | Analyzer | A hidden sandboxed page | A composition report: the file's internal subresources, the files it references and diagnostics; see [Analyzers](#analyzers) |
16
+
17
+ A plugin runs only after it is enabled in a library. Each plugin has its own memory partition:
18
+
19
+ - No access to the network, `file:` or other plugins.
20
+ - No Node and no `eval`.
21
+ - WebAssembly and Workers are available.
22
+ - It can read only the file the host binds to it, plus companion files in the same linked folder whose extensions are listed in `companionFormats`.
23
+ - It cannot modify any file.
24
+
25
+ ## Package layout
26
+
27
+ A plugin is a folder: a `manifest.json` at the root plus the built static files.
28
+
29
+ ```json
30
+ {
31
+ "apiVersion": 1,
32
+ "id": "acme.model-viewer",
33
+ "version": "0.1.0",
34
+ "name": "Model Viewer",
35
+ "description": "Example.",
36
+ "contributions": {
37
+ "thumbnails": [
38
+ {
39
+ "id": "model",
40
+ "entry": "thumbnail.html",
41
+ "formats": ["gltf", "glb"],
42
+ "companionFormats": ["bin", "png", "jpg"]
43
+ }
44
+ ],
45
+ "viewers": [
46
+ {
47
+ "id": "model",
48
+ "entry": "viewer.html",
49
+ "formats": ["gltf", "glb"],
50
+ "companionFormats": ["bin", "png", "jpg"]
51
+ }
52
+ ],
53
+ "analyzers": [{ "id": "model", "entry": "analyzer.html", "formats": ["gltf", "glb"] }]
54
+ }
55
+ }
56
+ ```
57
+
58
+ - `thumbnails`, `viewers` and `analyzers` are each optional, but there must be at least one extension point. Each kind allows up to 8, with unique IDs within the kind.
59
+ - `id` is `publisher.name`, using only lowercase letters, digits and hyphens. The `vormio` publisher is reserved for the host.
60
+ - `formats` and `companionFormats` are lowercase extensions without the dot. Wildcards are not allowed.
61
+ - `entry` is an HTML file in the package. Pages run under a strict Content Security Policy: scripts must be separate files, not inline.
62
+ - Unknown fields are rejected.
63
+
64
+ **Allowed files**: `html js mjs css json wasm png jpg jpeg webp svg woff2 txt md`. Links, `..` paths, Windows reserved names and names that differ only by case are not allowed.
65
+
66
+ **Limits**: at most 1,024 files, 16 MiB per file and 64 MiB in total; the `.vormioplugin` file itself is at most 96 MiB.
67
+
68
+ **Grants**: a library records what the user approved for each extension point (analyzers included): its ID, `formats` and `companionFormats`. A version that reads no more than that keeps running without asking again, including:
69
+
70
+ - an upgrade that changes only code, name or version
71
+ - an upgrade that reads less
72
+ - a rollback
73
+
74
+ Reading more formats or companion formats, or adding an extension point, requires the user to enable the plugin again, and that approval is added to what was approved before.
75
+
76
+ ## Runtime API
77
+
78
+ ```ts
79
+ import { analyzer, readCompanion, readInput, thumbnail, viewer } from '@vormio/plugin-sdk'
80
+
81
+ // Viewer: called first and on every file change. Returning means ready; throwing reports a failure.
82
+ viewer(async (input, signal) => {
83
+ const bytes = await readInput(input)
84
+ const texture = await readCompanion(input, 'textures/base.png')
85
+ if (texture.status !== 'ok') console.warn(texture.status)
86
+ // …render; signal aborts when the viewer moves to the next file.
87
+ })
88
+
89
+ // Thumbnail: return a Blob, HTMLCanvasElement or OffscreenCanvas.
90
+ thumbnail(async (input, { maxEdge }) => {
91
+ const canvas = new OffscreenCanvas(maxEdge, maxEdge)
92
+ // …draw
93
+ return canvas
94
+ })
95
+
96
+ // Analyzer: return the file's composition report; see the next section.
97
+ analyzer(async (input) => {
98
+ const gltf = JSON.parse(new TextDecoder().decode(await readInput(input)))
99
+ // …
100
+ return { subresources: [], references: [], diagnostics: [] }
101
+ })
102
+ ```
103
+
104
+ - `input.url` is the main file's address. Companion files resolve by relative address, just as on disk: for `textures/a.png` in a glTF, `fetch(new URL('textures/a.png', input.url))` works, and loaders can use these addresses directly. The addresses support Range requests.
105
+ - Read results:
106
+
107
+ | Status | Meaning | HTTP |
108
+ | --------------- | ----------------------------------------------------------------------------------------------------------- | ----- |
109
+ | `ok` | Read succeeded | 200 |
110
+ | `out-of-bounds` | Invalid path | 400 |
111
+ | `unauthorized` | The extension is not in `companionFormats` | 403 |
112
+ | `over-limit` | More than 256 companion requests in one binding | 429 |
113
+ | `missing` | The file is not in the linked folder, or it was skipped by the scan, removed from the library or is missing | 404 |
114
+ | `changed` | The file changed on disk and no longer matches the library's record | 409 |
115
+ | `unavailable` | Any other reason, such as an offline linked folder or an expired binding | Other |
116
+
117
+ - Pages that do not use these helpers can call `window.vormio` directly; see the `PluginBridge` type in `@vormio/plugin-sdk/bridge`. A thumbnail handler returns the PNG as an `ArrayBuffer`, and an analyzer handler (`onAnalyzeRequest`) returns the composition report.
118
+ - While a plugin view has focus, unmodified Esc, ← and → are handled by the host (back to the grid, previous, next); the plugin does not receive these keys.
119
+
120
+ ## Analyzers
121
+
122
+ Once a plugin is enabled in a library, the host analyzes its declared formats in the background. Matching files join the queue when linking and scanning find new files or versions, when the plugin is enabled, and when the plugin's code changes. The file whose details the user is viewing goes first, and the user can also re-analyze manually. At most 2 analyses run at once, each with a 30-second timeout. A thrown error, a timeout or an invalid report counts as a failed analysis and shows the reason; the same plugin code is not retried on the same file version.
123
+
124
+ A report describes one version of the analyzed file:
125
+
126
+ ```ts
127
+ interface CompositionReport {
128
+ /** Locatable objects inside the file, at most 10,000. */
129
+ subresources: {
130
+ key: string // Unique within this file version, chosen by the plugin, e.g. "materials/0"
131
+ kind: string // Display category, e.g. "Material"; the host uses it only for grouping
132
+ name: string // Display name; may be an empty string
133
+ parent?: string // Key of the parent subresource, for display hierarchy
134
+ }[]
135
+ /** References, at most 50,000. */
136
+ references: {
137
+ from?: string // Key of the referencing subresource; omitted means the file itself
138
+ uri?: string // Points to another file: the raw address written in the file
139
+ key?: string // Points to a subresource in this file; give exactly one of uri and key
140
+ role: string // Display role, e.g. "Base color texture"
141
+ evidence?: string // Location in the file, e.g. "/images/2/uri"
142
+ }[]
143
+ /** Problems found during analysis, at most 500. */
144
+ diagnostics: { severity: 'error' | 'warning' | 'info'; message: string; key?: string }[]
145
+ /** Only part of the analysis completed. */
146
+ partial?: boolean
147
+ }
148
+ ```
149
+
150
+ - The host validates reports with `compositionReportSchema` (`parseReport` also checks the size); plugins can check themselves against the same schema before returning. The report serialized as JSON is at most 4 MiB (counted in UTF-8 bytes). Names and keys are at most 200 characters, kinds and roles 80, evidence 300, diagnostics 500 and addresses 2,048. Names, kinds, roles and evidence must not contain control characters.
151
+ - The report is invalid if it has duplicate keys, a `parent`, `from` or `key` that points to a missing subresource, a cyclic hierarchy, or a reference with both or neither of `uri` and `key`.
152
+ - Write `uri` exactly as it appears in the file; do not resolve it yourself. The host decodes percent-encoding once, ignores everything after `?` and `#`, treats `\` as `/`, resolves it relative to the analyzed file and classifies it: a file in the linked folder, out of bounds (`..` leaving the linked folder), embedded (`data:`), external (absolute paths, drive letters, other schemes) or invalid (undecodable, containing control characters, or pointing to a folder). Whether the target exists is decided by the host at query time.
153
+ - For embedded data (`data:`), report only the header up to the comma, e.g. `data:image/png;base64`. The host classifies it as embedded by prefix and does not store the content; a full `data:` address often exceeds 2,048 characters and would invalidate the whole report.
154
+ - Reporting a reference does not require reading the target file, so you do not need `companionFormats` just to report references. Declare them only when the analysis actually reads companion files.
155
+
156
+ ## Packaging and development
157
+
158
+ The package declares `"sideEffects": false`: when a plugin page uses only the runtime helpers, bundlers (esbuild, Vite, Rollup, webpack) drop the manifest schema and its zod dependency, leaving about 1 KB of runtime code for a minimal page. Node-side packaging and validation are imported from `@vormio/plugin-sdk/node`.
159
+
160
+ ```bash
161
+ npx vormio-plugin pack <build-output-folder> <output-file.vormioplugin>
162
+ ```
163
+
164
+ The command validates the manifest and every file, then writes a zip-format `.vormioplugin`. It never overwrites an existing output file.
165
+
166
+ During development, open the **插件** (Plugins) page in Vormio, choose **添加开发目录…** (Add development folder…) and point it at the build output folder. The host watches the folder: every change is validated again, and a valid build reloads the plugin. If validation fails, the last valid version stays loaded and the error is shown on the plugin card. Vormio never runs build commands.
167
+
168
+ ## Plugin catalogs
169
+
170
+ A plugin catalog is a signed `catalog.json` plus the plugin packages it lists, on static hosting. Vormio installs and updates plugins from it. Publishers produce one with two commands:
171
+
172
+ ```bash
173
+ npx vormio-plugin keygen <key-path-prefix>
174
+ npx vormio-plugin catalog <release-config.json> --key <private-key.pem> --out <new-folder>
175
+ ```
176
+
177
+ - `keygen` generates an Ed25519 key pair: `<prefix>.private.pem` (PKCS#8) and `<prefix>.public.txt` (the base64 of the 32-byte public key; hosts build this line in). It refuses to write inside a Git working tree and never overwrites a file. The private key stays on the publisher's machine: never commit or upload it.
178
+ - The release configuration lists every package in the new catalog, relative to the configuration file:
179
+
180
+ ```json
181
+ {
182
+ "previous": "published/3/catalog.json",
183
+ "packages": ["artifacts/model-viewer-0.2.0.vormioplugin"]
184
+ }
185
+ ```
186
+
187
+ `previous` is the catalog currently online. The new catalog's sequence is its sequence plus 1, and it must be signed with the same key. A first publication has no previous catalog and uses `"initialSequence": 1` instead.
188
+
189
+ - `catalog` does the following:
190
+ - Validates every package with the rules a host applies when installing it.
191
+ - Copies each package to `packages/<plugin ID>-<version>-<first 12 hex digits of its SHA-256>.vormioplugin`.
192
+ - Writes and signs `catalog.json`, then verifies it with the public key.
193
+
194
+ The output folder must be new or empty. If any check fails, nothing is written.
195
+
196
+ - Upload `packages/` first and `catalog.json` last. Do not cache `catalog.json`. Package file names change with their content, so packages can be cached for a long time.
197
+
198
+ **Format**: `catalog.json` is an envelope `{ format: 1, keyId, payload, signature }`:
199
+
200
+ - `payload` is the base64 of the content's UTF-8 JSON.
201
+ - `signature` is the Ed25519 signature of those bytes.
202
+ - `keyId` is the first 16 hex digits of the public key's SHA-256.
203
+
204
+ The content:
205
+
206
+ ```ts
207
+ interface CatalogPayload {
208
+ sequence: number // Grows by 1 per publication; hosts refuse a sequence lower than one they have seen
209
+ publishedAt: string // ISO 8601, UTC
210
+ plugins: {
211
+ id: string
212
+ releases: {
213
+ manifest: object // The package's manifest.json, as is
214
+ file: string // packages/<name>.vormioplugin, relative to catalog.json
215
+ bytes: number
216
+ sha256: string // SHA-256 of the whole package file, lowercase hex
217
+ }[]
218
+ }[]
219
+ }
220
+ ```
221
+
222
+ - Limits:
223
+ - `catalog.json` is at most 1 MiB.
224
+ - At most 256 plugins, with at most 8 releases each.
225
+ - Package files are at most 96 MiB.
226
+
227
+ Plugin IDs, and the versions of one plugin, must not repeat. `manifest.id` must match the plugin ID.
228
+
229
+ - One plugin can list releases for hosts of different `apiVersion`s side by side. A host installs only releases with its own `apiVersion` whose manifest passes its own validation; it shows the rest as needing a newer Vormio.
230
+ - Hosts verify the signature before reading the content. A downloaded package must match `bytes` and `sha256`, then pass the rules of a local install, and its manifest must equal the one in the catalog.
231
+
232
+ `@vormio/plugin-sdk/node` exports the same functionality: `verifyCatalog`, `signCatalog`, `writeCatalog`, `writeCatalogKeys`, `releaseManifest` and `compareVersions`.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * What the host binds a plugin page to: one observed version of one file. `url` serves its bytes
3
+ * (with byte ranges); companion files resolve relative to it, as they do on disk.
4
+ */
5
+ export interface PluginInput {
6
+ url: string;
7
+ name: string;
8
+ extension: string;
9
+ size: number;
10
+ }
11
+ export type CompanionStatus = 'out-of-bounds' | 'unauthorized' | 'over-limit' | 'missing' | 'changed' | 'unavailable';
12
+ export type CompanionResult = {
13
+ status: 'ok';
14
+ url: string;
15
+ bytes: ArrayBuffer;
16
+ } | {
17
+ status: CompanionStatus;
18
+ url: string;
19
+ };
20
+ /** An object inside the analyzed file that can be located, such as a mesh or a material. */
21
+ export interface ReportSubresource {
22
+ /** Unique and stable within this version of the file; chosen by the plugin. */
23
+ key: string;
24
+ /** Display label used for grouping, such as "Material"; the host does not interpret it. */
25
+ kind: string;
26
+ name: string;
27
+ /** Key of the subresource this one belongs to, for display as a hierarchy. */
28
+ parent?: string;
29
+ }
30
+ /** A reference to another file (`uri`) or to a subresource of this file (`key`): exactly one. */
31
+ export interface ReportReference {
32
+ /** Key of the subresource that refers; omitted when the file itself refers. */
33
+ from?: string;
34
+ /** The address as written in the file, relative to the analyzed file. */
35
+ uri?: string;
36
+ /** A subresource of the analyzed file. */
37
+ key?: string;
38
+ /** Display label, such as "Base color texture". */
39
+ role: string;
40
+ /** Where in the file the reference is, such as "/images/2/uri". */
41
+ evidence?: string;
42
+ }
43
+ export interface ReportDiagnostic {
44
+ severity: 'error' | 'warning' | 'info';
45
+ message: string;
46
+ /** The subresource the diagnostic is about. */
47
+ key?: string;
48
+ }
49
+ /** What an analyzer found in one observed version of one file. */
50
+ export interface CompositionReport {
51
+ subresources: ReportSubresource[];
52
+ references: ReportReference[];
53
+ diagnostics: ReportDiagnostic[];
54
+ /** The analyzer could only finish part of the file. */
55
+ partial?: boolean;
56
+ }
57
+ /** The frozen object the host's plugin preload exposes as `window.vormio`. */
58
+ export interface PluginBridge {
59
+ /** The viewer's current input. */
60
+ getInput(): Promise<PluginInput>;
61
+ /** Called when the viewer moves to another file; returns the function that unsubscribes. */
62
+ onInput(listener: (input: PluginInput) => void): () => void;
63
+ /** The viewer shows `input`; the host removes its loading state. */
64
+ ready(input: PluginInput): void;
65
+ /** The viewer cannot show `input`; the host shows `message` as plain text. */
66
+ fail(input: PluginInput, message: string): void;
67
+ /**
68
+ * Thumbnail pages: handles each request from the host and returns PNG bytes no larger than
69
+ * `maxEdge` on either side. (Blobs cannot cross the bridge; `thumbnail()` converts them.)
70
+ */
71
+ onThumbnailRequest(handler: (input: PluginInput, size: {
72
+ maxEdge: number;
73
+ }) => Promise<ArrayBuffer>): void;
74
+ /** Analyzer pages: handles each request from the host and returns the composition report. */
75
+ onAnalyzeRequest(handler: (input: PluginInput) => Promise<CompositionReport>): void;
76
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,2 @@
1
+ // Types shared by plugin pages and the host's plugin preload. No DOM types, so Node code can use them.
2
+ export {};
@@ -0,0 +1,5 @@
1
+ export { contributionsOf, manifestSchema, missingEntries, parseManifest, permissionSummary, pluginApiVersion, type Contribution, type ContributionKind, type ManifestResult, type PluginManifest, } from './manifest.ts';
2
+ export { checkPackageFiles, checkPackagePath, packageExtensions, packageLimits, type PackageFile, } from './package-rules.ts';
3
+ export { compositionReportSchema, parseReport, reportLimits, type ReportResult } from './report.ts';
4
+ export type { ReportDiagnostic, ReportReference, ReportSubresource } from './bridge.ts';
5
+ export { analyzer, bridge, readCompanion, readInput, thumbnail, viewer, type CompanionResult, type CompanionStatus, type CompositionReport, type PluginBridge, type PluginInput, } from './runtime.ts';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { contributionsOf, manifestSchema, missingEntries, parseManifest, permissionSummary, pluginApiVersion, } from "./manifest.js";
2
+ export { checkPackageFiles, checkPackagePath, packageExtensions, packageLimits, } from "./package-rules.js";
3
+ export { compositionReportSchema, parseReport, reportLimits } from "./report.js";
4
+ export { analyzer, bridge, readCompanion, readInput, thumbnail, viewer, } from "./runtime.js";
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ /** The plugin contract version this SDK describes. Hosts load only plugins with this version. */
3
+ export declare const pluginApiVersion = 1;
4
+ declare const contributionSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ entry: z.ZodString;
7
+ formats: z.ZodArray<z.ZodString>;
8
+ companionFormats: z.ZodDefault<z.ZodArray<z.ZodString>>;
9
+ }, z.core.$strict>;
10
+ export type Contribution = z.output<typeof contributionSchema>;
11
+ /** `publisher.name`. The `vormio` publisher is reserved for the host. */
12
+ export declare const pluginIdSchema: z.ZodString;
13
+ /** A semantic version without build metadata. */
14
+ export declare const versionSchema: z.ZodString;
15
+ export declare const manifestSchema: z.ZodObject<{
16
+ apiVersion: z.ZodLiteral<1>;
17
+ id: z.ZodString;
18
+ version: z.ZodString;
19
+ name: z.ZodString;
20
+ description: z.ZodDefault<z.ZodString>;
21
+ contributions: z.ZodObject<{
22
+ thumbnails: z.ZodDefault<z.ZodArray<z.ZodObject<{
23
+ id: z.ZodString;
24
+ entry: z.ZodString;
25
+ formats: z.ZodArray<z.ZodString>;
26
+ companionFormats: z.ZodDefault<z.ZodArray<z.ZodString>>;
27
+ }, z.core.$strict>>>;
28
+ viewers: z.ZodDefault<z.ZodArray<z.ZodObject<{
29
+ id: z.ZodString;
30
+ entry: z.ZodString;
31
+ formats: z.ZodArray<z.ZodString>;
32
+ companionFormats: z.ZodDefault<z.ZodArray<z.ZodString>>;
33
+ }, z.core.$strict>>>;
34
+ analyzers: z.ZodDefault<z.ZodArray<z.ZodObject<{
35
+ id: z.ZodString;
36
+ entry: z.ZodString;
37
+ formats: z.ZodArray<z.ZodString>;
38
+ companionFormats: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
+ }, z.core.$strict>>>;
40
+ }, z.core.$strict>;
41
+ }, z.core.$strict>;
42
+ export type PluginManifest = z.output<typeof manifestSchema>;
43
+ export type ContributionKind = 'thumbnail' | 'viewer' | 'analyzer';
44
+ /** Every contribution of a manifest with its kind. */
45
+ export declare function contributionsOf(manifest: PluginManifest): {
46
+ kind: ContributionKind;
47
+ contribution: Contribution;
48
+ }[];
49
+ export type ManifestResult = {
50
+ ok: true;
51
+ manifest: PluginManifest;
52
+ } | {
53
+ ok: false;
54
+ problems: string[];
55
+ };
56
+ /** Validates parsed `manifest.json` content and reports every problem with its field path. */
57
+ export declare function parseManifest(value: unknown): ManifestResult;
58
+ /** Entry files the manifest names but the package does not contain. */
59
+ export declare function missingEntries(manifest: PluginManifest, paths: ReadonlySet<string>): string[];
60
+ /**
61
+ * What a plugin may do, in a canonical form: every contribution's kind, ID, formats and companion
62
+ * formats, sorted. Hosts hash it into the permission fingerprint that a library's grant records;
63
+ * names, descriptions, versions and code are not part of it.
64
+ */
65
+ export declare function permissionSummary(manifest: PluginManifest): string;
66
+ export {};
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { checkPackagePath } from "./package-rules.js";
3
+ /** The plugin contract version this SDK describes. Hosts load only plugins with this version. */
4
+ export const pluginApiVersion = 1;
5
+ const unique = (values) => new Set(values).size === values.length;
6
+ const extension = z.string().regex(/^[a-z0-9]{1,16}$/, 'Use a lower-case extension without a dot');
7
+ const extensions = (min) => z.array(extension).min(min).max(64).refine(unique, 'Extensions must not repeat');
8
+ const contributionSchema = z.strictObject({
9
+ /** Unique within its kind; lower case, digits and hyphens. */
10
+ id: z.string().regex(/^[a-z][a-z0-9-]{0,31}$/),
11
+ /** An HTML file in the package that the host loads in a sandboxed page. */
12
+ entry: z.string().refine((path) => checkPackagePath(path) === null && path.endsWith('.html'), {
13
+ message: 'The entry must be an .html file inside the package',
14
+ }),
15
+ /** Extensions of the main files this contribution handles. */
16
+ formats: extensions(1),
17
+ /** Extensions of other files it may read next to the main file. */
18
+ companionFormats: extensions(0).default([]),
19
+ });
20
+ const contributionList = z
21
+ .array(contributionSchema)
22
+ .max(8)
23
+ .default([])
24
+ .refine((items) => unique(items.map((item) => item.id)), 'Contribution IDs must not repeat');
25
+ /** `publisher.name`. The `vormio` publisher is reserved for the host. */
26
+ export const pluginIdSchema = z
27
+ .string()
28
+ .regex(/^[a-z0-9][a-z0-9-]{0,39}\.[a-z0-9][a-z0-9-]{0,39}$/, 'Use publisher.name')
29
+ .refine((id) => !id.startsWith('vormio.'), 'The vormio publisher is reserved');
30
+ /** A semantic version without build metadata. */
31
+ export const versionSchema = z
32
+ .string()
33
+ .regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/, 'Use a semantic version');
34
+ export const manifestSchema = z.strictObject({
35
+ apiVersion: z.literal(pluginApiVersion),
36
+ id: pluginIdSchema,
37
+ version: versionSchema,
38
+ name: z.string().trim().min(1).max(80),
39
+ description: z.string().max(400).default(''),
40
+ contributions: z
41
+ .strictObject({
42
+ thumbnails: contributionList,
43
+ viewers: contributionList,
44
+ analyzers: contributionList,
45
+ })
46
+ .refine((contributions) => contributions.thumbnails.length +
47
+ contributions.viewers.length +
48
+ contributions.analyzers.length >
49
+ 0, 'Declare at least one contribution'),
50
+ });
51
+ /** Every contribution of a manifest with its kind. */
52
+ export function contributionsOf(manifest) {
53
+ return [
54
+ ...manifest.contributions.thumbnails.map((contribution) => ({
55
+ kind: 'thumbnail',
56
+ contribution,
57
+ })),
58
+ ...manifest.contributions.viewers.map((contribution) => ({
59
+ kind: 'viewer',
60
+ contribution,
61
+ })),
62
+ ...manifest.contributions.analyzers.map((contribution) => ({
63
+ kind: 'analyzer',
64
+ contribution,
65
+ })),
66
+ ];
67
+ }
68
+ /** Validates parsed `manifest.json` content and reports every problem with its field path. */
69
+ export function parseManifest(value) {
70
+ const result = manifestSchema.safeParse(value);
71
+ if (result.success)
72
+ return { ok: true, manifest: result.data };
73
+ return {
74
+ ok: false,
75
+ problems: result.error.issues.map((issue) => `${issue.path.length ? issue.path.join('.') : 'manifest'}: ${issue.message}`),
76
+ };
77
+ }
78
+ /** Entry files the manifest names but the package does not contain. */
79
+ export function missingEntries(manifest, paths) {
80
+ return contributionsOf(manifest)
81
+ .map(({ contribution }) => contribution.entry)
82
+ .filter((entry) => !paths.has(entry));
83
+ }
84
+ /**
85
+ * What a plugin may do, in a canonical form: every contribution's kind, ID, formats and companion
86
+ * formats, sorted. Hosts hash it into the permission fingerprint that a library's grant records;
87
+ * names, descriptions, versions and code are not part of it.
88
+ */
89
+ export function permissionSummary(manifest) {
90
+ const entries = contributionsOf(manifest)
91
+ .map(({ kind, contribution }) => ({
92
+ kind,
93
+ id: contribution.id,
94
+ formats: [...contribution.formats].sort(),
95
+ companionFormats: [...contribution.companionFormats].sort(),
96
+ }))
97
+ .sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`));
98
+ return JSON.stringify({
99
+ apiVersion: manifest.apiVersion,
100
+ id: manifest.id,
101
+ contributions: entries,
102
+ });
103
+ }
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ import { type ManifestResult } from '../manifest.ts';
3
+ /** Limits for a signed plugin catalog. */
4
+ export declare const catalogLimits: {
5
+ readonly maxCatalogBytes: number;
6
+ readonly maxPlugins: 256;
7
+ readonly maxReleasesPerPlugin: 8;
8
+ };
9
+ /** Which check a catalog failed: its shape, the key it was signed with, or the signature itself. */
10
+ export type CatalogErrorCode = 'format' | 'key' | 'signature';
11
+ export declare class CatalogError extends Error {
12
+ readonly code: CatalogErrorCode;
13
+ readonly problems: readonly string[];
14
+ constructor(code: CatalogErrorCode, problems: readonly string[]);
15
+ }
16
+ declare const payloadSchema: z.ZodObject<{
17
+ sequence: z.ZodNumber;
18
+ publishedAt: z.ZodISODateTime;
19
+ plugins: z.ZodArray<z.ZodObject<{
20
+ id: z.ZodString;
21
+ releases: z.ZodArray<z.ZodObject<{
22
+ manifest: z.ZodObject<{
23
+ apiVersion: z.ZodNumber;
24
+ id: z.ZodString;
25
+ version: z.ZodString;
26
+ name: z.ZodString;
27
+ description: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$loose>;
29
+ file: z.ZodString;
30
+ bytes: z.ZodNumber;
31
+ sha256: z.ZodString;
32
+ }, z.core.$strict>>;
33
+ }, z.core.$strict>>;
34
+ }, z.core.$strict>;
35
+ /** What `signCatalog` signs. */
36
+ export type CatalogPayload = z.input<typeof payloadSchema>;
37
+ /** A catalog whose signature has been verified. */
38
+ export type Catalog = z.output<typeof payloadSchema> & {
39
+ keyId: string;
40
+ };
41
+ export type CatalogPlugin = Catalog['plugins'][number];
42
+ export type CatalogRelease = CatalogPlugin['releases'][number];
43
+ /** The first 16 hexadecimal digits of the public key's SHA-256; catalogs name their key by it. */
44
+ export declare function catalogKeyId(publicKey: string): string;
45
+ /** The public key that belongs to a PEM private key. */
46
+ export declare function catalogPublicKey(privateKey: string): string;
47
+ export interface CatalogKeys {
48
+ /** Base64 of the raw key; hosts build it in. */
49
+ publicKey: string;
50
+ /** PKCS#8 PEM. */
51
+ privateKey: string;
52
+ keyId: string;
53
+ }
54
+ export declare function generateCatalogKeys(): CatalogKeys;
55
+ /** Validates a payload and returns the signed `catalog.json` bytes. */
56
+ export declare function signCatalog(payload: CatalogPayload, privateKey: string): Buffer;
57
+ /**
58
+ * Checks `catalog.json` against a public key: shape of the envelope, key ID, signature, and only
59
+ * then the payload. Throws `CatalogError` at the first check that fails.
60
+ */
61
+ export declare function verifyCatalog(bytes: Uint8Array, publicKey: string): Catalog;
62
+ /**
63
+ * The release's manifest when this SDK's hosts can install it: same apiVersion, and valid under
64
+ * this SDK's manifest schema. Otherwise the reasons, typically that the release needs a newer host.
65
+ */
66
+ export declare function releaseManifest(release: CatalogRelease): ManifestResult;
67
+ /** Orders two semantic versions by precedence: negative, zero or positive, like a sort callback. */
68
+ export declare function compareVersions(a: string, b: string): number;
69
+ export {};