@vonzio/plugin-api 0.1.0
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 +661 -0
- package/README.md +5 -0
- package/capabilities.d.ts +69 -0
- package/capabilities.js +286 -0
- package/errors.d.ts +72 -0
- package/errors.js +106 -0
- package/frontend.d.ts +13 -0
- package/frontend.js +16 -0
- package/index.d.ts +945 -0
- package/index.js +24 -0
- package/manifest-validate.d.ts +44 -0
- package/manifest-validate.js +338 -0
- package/manifest.d.ts +121 -0
- package/manifest.js +37 -0
- package/package.json +49 -0
- package/policy.d.ts +98 -0
- package/policy.js +223 -0
- package/version.d.ts +16 -0
- package/version.js +54 -0
package/policy.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { MtlsSecretFiles, OperatorPolicy, PluginManifest, PluginSource, PolicyEntry } from "./manifest.js";
|
|
2
|
+
import type { PluginCapability } from "./capabilities.js";
|
|
3
|
+
import type { RefusalReason } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from this module's location to the monorepo root — the nearest
|
|
6
|
+
* ancestor package.json declaring a `workspaces` field. Robust to the
|
|
7
|
+
* process cwd (operators may launch from a subdir). Falls back to cwd.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findRepoRoot(startDir?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* SHA-256 (hex) of a package directory, excluding any `node_modules` subtree.
|
|
12
|
+
* Stable: files are visited in sorted relative-path order and each file's
|
|
13
|
+
* path + bytes are folded into the digest with separators, so the hash is
|
|
14
|
+
* insensitive to readdir ordering but sensitive to any content/path change.
|
|
15
|
+
* This is the operator-attested hash recorded in the policy file (§4) and the
|
|
16
|
+
* value the dashboard parity check compares against (§16).
|
|
17
|
+
*/
|
|
18
|
+
export declare function hashPackageDir(root: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Classify a resolved (realpath'd) package as builtin or external.
|
|
21
|
+
*
|
|
22
|
+
* Builtin ⇔ the real package root is a child of `<repoRoot>/packages/` AND the
|
|
23
|
+
* package name appears in the shipped builtins policy. Everything else is
|
|
24
|
+
* external. The name-in-builtins requirement (not just "inside packages/") is
|
|
25
|
+
* what stops an `npm link`/`file:` external whose realpath lands outside
|
|
26
|
+
* node_modules from being misclassified as a trusted builtin (deviation #3).
|
|
27
|
+
*/
|
|
28
|
+
export declare function classifySource(realPackageRoot: string, repoRoot: string, builtinNames: ReadonlySet<string>, packageName: string): PluginSource;
|
|
29
|
+
export interface LoadedPolicies {
|
|
30
|
+
/** Shipped builtins policy (always present; empty if the file is absent). */
|
|
31
|
+
builtins: OperatorPolicy;
|
|
32
|
+
/** Operator policy (null when no file is configured/found). */
|
|
33
|
+
operator: OperatorPolicy | null;
|
|
34
|
+
/** Resolved policy-file path actually read (for audit/parity), or null. */
|
|
35
|
+
operatorPath: string | null;
|
|
36
|
+
/** Set of builtin package names (keys of the builtins policy). */
|
|
37
|
+
builtinNames: ReadonlySet<string>;
|
|
38
|
+
/** Look up the effective entry for a package: operator wins over builtins. */
|
|
39
|
+
entryFor(packageName: string): PolicyEntry | null;
|
|
40
|
+
}
|
|
41
|
+
export interface LoadPoliciesOpts {
|
|
42
|
+
repoRoot: string;
|
|
43
|
+
/** Override for the operator policy path; defaults to $VONZIO_PLUGIN_POLICY
|
|
44
|
+
* then `<cwd>/vonzio-plugins.json`. */
|
|
45
|
+
operatorPolicyPath?: string;
|
|
46
|
+
cwd?: string;
|
|
47
|
+
env?: Record<string, string | undefined>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Load + validate the shipped builtins policy and the operator policy. A
|
|
51
|
+
* malformed file throws PolicyViolationError (operator-owned config — fail
|
|
52
|
+
* loud). Absent files are tolerated (builtins → empty; operator → null).
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadPolicies(opts: LoadPoliciesOpts): LoadedPolicies;
|
|
55
|
+
export type CrossCheckResult = {
|
|
56
|
+
ok: true;
|
|
57
|
+
grantedCapabilities: PluginCapability[];
|
|
58
|
+
grantedOutboundHosts: string[];
|
|
59
|
+
/** name -> host file paths, for each mtls secret the manifest declared
|
|
60
|
+
* and the policy provisioned. Empty when the plugin declares no mtls. */
|
|
61
|
+
grantedMtlsSecrets: Record<string, MtlsSecretFiles>;
|
|
62
|
+
frontendApproved: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
reason: RefusalReason;
|
|
66
|
+
message: string;
|
|
67
|
+
remediation?: string;
|
|
68
|
+
detail?: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
export interface CrossCheckArgs {
|
|
71
|
+
packageName: string;
|
|
72
|
+
manifest: PluginManifest;
|
|
73
|
+
entry: PolicyEntry | null;
|
|
74
|
+
/** Installed package version (from package.json "version"). */
|
|
75
|
+
installedVersion: string;
|
|
76
|
+
/** Hash of the installed package directory (hashPackageDir). */
|
|
77
|
+
installedHash: string;
|
|
78
|
+
/** "strict" (default) requires version-string equality; "loose" relaxes it
|
|
79
|
+
* to "hash match is authoritative" (VONZIO_PLUGIN_POLICY_TRACK_VERSIONS). */
|
|
80
|
+
trackVersions?: "strict" | "loose";
|
|
81
|
+
/**
|
|
82
|
+
* Source of the plugin. Built-ins are workspace-trusted OSS source, so their
|
|
83
|
+
* hash + version are NOT enforced — the shipped builtins-policy hash would
|
|
84
|
+
* otherwise drift on every source edit and break `make dev-oss` (deviation
|
|
85
|
+
* #9). Capability/host/frontend subset checks still apply. Defaults to
|
|
86
|
+
* "external" (full enforcement).
|
|
87
|
+
*/
|
|
88
|
+
source?: PluginSource;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The §3 step-16 policy cross-check: hash match, version match, capability
|
|
92
|
+
* subset, outbound-host subset, frontend approval. Returns the granted sets
|
|
93
|
+
* on success or a refusal with the §11 reason code. The external-only
|
|
94
|
+
* capability rules (db.access, db.scoped opt-in, root combos) are applied
|
|
95
|
+
* SEPARATELY by the loader before this (§3 steps 7-9).
|
|
96
|
+
*/
|
|
97
|
+
export declare function crossCheckPolicy(args: CrossCheckArgs): CrossCheckResult;
|
|
98
|
+
//# sourceMappingURL=policy.d.ts.map
|
package/policy.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Operator policy loading, package-directory hashing, builtin/external source
|
|
2
|
+
// classification, and the manifest<->policy cross-check. Node-only (node:fs /
|
|
3
|
+
// crypto / path) — exported from @vonzio/plugin-api as the SEPARATE `./policy`
|
|
4
|
+
// entry so it is shared, byte-identically, by BOTH consumers that need it:
|
|
5
|
+
// - core-server's loader (runtime §3 pipeline + the dashboard parity check)
|
|
6
|
+
// - the dashboard's Vite plugin (build-time frontend bundling, PR 3J.2)
|
|
7
|
+
// The package's main "." entry stays pure + browser-safe; this module is never
|
|
8
|
+
// pulled into the browser bundle. See docs/PLUGIN_LOADER_SPEC.md §3, §4, §16.
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { validatePolicy } from "./manifest-validate.js";
|
|
14
|
+
/**
|
|
15
|
+
* Walk up from this module's location to the monorepo root — the nearest
|
|
16
|
+
* ancestor package.json declaring a `workspaces` field. Robust to the
|
|
17
|
+
* process cwd (operators may launch from a subdir). Falls back to cwd.
|
|
18
|
+
*/
|
|
19
|
+
export function findRepoRoot(startDir) {
|
|
20
|
+
let dir = startDir ?? path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
// Bound the walk so a missing root can't loop to filesystem root forever.
|
|
22
|
+
for (let i = 0; i < 12; i++) {
|
|
23
|
+
const pkg = path.join(dir, "package.json");
|
|
24
|
+
if (existsSync(pkg)) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(readFileSync(pkg, "utf8"));
|
|
27
|
+
if (parsed.workspaces)
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore malformed package.json while walking
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const parent = path.dirname(dir);
|
|
35
|
+
if (parent === dir)
|
|
36
|
+
break;
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
return process.cwd();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* SHA-256 (hex) of a package directory, excluding any `node_modules` subtree.
|
|
43
|
+
* Stable: files are visited in sorted relative-path order and each file's
|
|
44
|
+
* path + bytes are folded into the digest with separators, so the hash is
|
|
45
|
+
* insensitive to readdir ordering but sensitive to any content/path change.
|
|
46
|
+
* This is the operator-attested hash recorded in the policy file (§4) and the
|
|
47
|
+
* value the dashboard parity check compares against (§16).
|
|
48
|
+
*/
|
|
49
|
+
export function hashPackageDir(root) {
|
|
50
|
+
const realRoot = path.resolve(root);
|
|
51
|
+
const files = [];
|
|
52
|
+
const walk = (dir) => {
|
|
53
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
54
|
+
for (const e of entries) {
|
|
55
|
+
if (e.name === "node_modules")
|
|
56
|
+
continue;
|
|
57
|
+
const abs = path.join(dir, e.name);
|
|
58
|
+
// REFUSE symlinks rather than silently skipping them. A skipped symlink
|
|
59
|
+
// is an attestation hole: a symlinked entry (e.g. index.js -> elsewhere
|
|
60
|
+
// in the package) would be executable yet invisible to the hash. Refusing
|
|
61
|
+
// is fail-closed; the loader turns this into a per-plugin refusal.
|
|
62
|
+
if (e.isSymbolicLink()) {
|
|
63
|
+
throw new Error(`refusing to hash package "${realRoot}": contains a symlink (${path.relative(realRoot, abs)}); symlinks are not allowed in plugin packages`);
|
|
64
|
+
}
|
|
65
|
+
if (e.isDirectory()) {
|
|
66
|
+
walk(abs);
|
|
67
|
+
}
|
|
68
|
+
else if (e.isFile()) {
|
|
69
|
+
files.push(path.relative(realRoot, abs));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(realRoot);
|
|
74
|
+
files.sort();
|
|
75
|
+
const hash = createHash("sha256");
|
|
76
|
+
for (const rel of files) {
|
|
77
|
+
// POSIX-normalize the path separator so the hash matches across OSes.
|
|
78
|
+
hash.update(rel.split(path.sep).join("/"));
|
|
79
|
+
hash.update("\0");
|
|
80
|
+
hash.update(readFileSync(path.join(realRoot, rel)));
|
|
81
|
+
hash.update("\0");
|
|
82
|
+
}
|
|
83
|
+
return hash.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Classify a resolved (realpath'd) package as builtin or external.
|
|
87
|
+
*
|
|
88
|
+
* Builtin ⇔ the real package root is a child of `<repoRoot>/packages/` AND the
|
|
89
|
+
* package name appears in the shipped builtins policy. Everything else is
|
|
90
|
+
* external. The name-in-builtins requirement (not just "inside packages/") is
|
|
91
|
+
* what stops an `npm link`/`file:` external whose realpath lands outside
|
|
92
|
+
* node_modules from being misclassified as a trusted builtin (deviation #3).
|
|
93
|
+
*/
|
|
94
|
+
export function classifySource(realPackageRoot, repoRoot, builtinNames, packageName) {
|
|
95
|
+
const packagesDir = path.join(path.resolve(repoRoot), "packages") + path.sep;
|
|
96
|
+
const inPackages = path.resolve(realPackageRoot).startsWith(packagesDir);
|
|
97
|
+
return inPackages && builtinNames.has(packageName) ? "builtin" : "external";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Load + validate the shipped builtins policy and the operator policy. A
|
|
101
|
+
* malformed file throws PolicyViolationError (operator-owned config — fail
|
|
102
|
+
* loud). Absent files are tolerated (builtins → empty; operator → null).
|
|
103
|
+
*/
|
|
104
|
+
export function loadPolicies(opts) {
|
|
105
|
+
const env = opts.env ?? process.env;
|
|
106
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
107
|
+
const builtinsPath = path.join(opts.repoRoot, "vonzio-plugins.builtins.json");
|
|
108
|
+
const builtins = existsSync(builtinsPath)
|
|
109
|
+
? validatePolicy(JSON.parse(readFileSync(builtinsPath, "utf8")), "vonzio-plugins.builtins.json")
|
|
110
|
+
: { policy_version: "1", plugins: {} };
|
|
111
|
+
const operatorPath = opts.operatorPolicyPath ??
|
|
112
|
+
env.VONZIO_PLUGIN_POLICY ??
|
|
113
|
+
path.join(cwd, "vonzio-plugins.json");
|
|
114
|
+
const operator = existsSync(operatorPath)
|
|
115
|
+
? validatePolicy(JSON.parse(readFileSync(operatorPath, "utf8")), path.basename(operatorPath))
|
|
116
|
+
: null;
|
|
117
|
+
const builtinNames = new Set(Object.keys(builtins.plugins));
|
|
118
|
+
return {
|
|
119
|
+
builtins,
|
|
120
|
+
operator,
|
|
121
|
+
operatorPath: existsSync(operatorPath) ? operatorPath : null,
|
|
122
|
+
builtinNames,
|
|
123
|
+
entryFor(packageName) {
|
|
124
|
+
return operator?.plugins[packageName] ?? builtins.plugins[packageName] ?? null;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The §3 step-16 policy cross-check: hash match, version match, capability
|
|
130
|
+
* subset, outbound-host subset, frontend approval. Returns the granted sets
|
|
131
|
+
* on success or a refusal with the §11 reason code. The external-only
|
|
132
|
+
* capability rules (db.access, db.scoped opt-in, root combos) are applied
|
|
133
|
+
* SEPARATELY by the loader before this (§3 steps 7-9).
|
|
134
|
+
*/
|
|
135
|
+
export function crossCheckPolicy(args) {
|
|
136
|
+
const { packageName, manifest, entry, installedVersion, installedHash } = args;
|
|
137
|
+
const trackVersions = args.trackVersions ?? "strict";
|
|
138
|
+
if (!entry) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
reason: "policy_missing",
|
|
142
|
+
message: `No operator policy entry for "${packageName}". Approve it before loading.`,
|
|
143
|
+
remediation: `run: vonzio plugin approve ${packageName}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Built-ins skip hash + version enforcement (workspace-trusted source;
|
|
147
|
+
// see CrossCheckArgs.source). Externals get the full attestation.
|
|
148
|
+
const enforceHash = (args.source ?? "external") === "external";
|
|
149
|
+
if (enforceHash && entry.approved_hash_sha256 !== installedHash) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: "policy_hash_mismatch",
|
|
153
|
+
message: `Installed package hash for "${packageName}" does not match the approved hash. The package was modified since approval, or tampered with.`,
|
|
154
|
+
remediation: `re-review and run: vonzio plugin approve ${packageName}`,
|
|
155
|
+
detail: { approved: entry.approved_hash_sha256, installed: installedHash },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (enforceHash && trackVersions === "strict" && entry.version !== installedVersion) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
reason: "policy_version_mismatch",
|
|
162
|
+
message: `Installed version ${installedVersion} of "${packageName}" differs from the approved version ${entry.version}.`,
|
|
163
|
+
remediation: `run: vonzio plugin approve ${packageName} (or set VONZIO_PLUGIN_POLICY_TRACK_VERSIONS=loose)`,
|
|
164
|
+
detail: { approved: entry.version, installed: installedVersion },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const approvedCaps = new Set(entry.approved_capabilities);
|
|
168
|
+
const capDrift = manifest.capabilities.filter((c) => !approvedCaps.has(c));
|
|
169
|
+
if (capDrift.length) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: "policy_capability_drift",
|
|
173
|
+
message: `"${packageName}" requests capabilities not in the approved set: ${capDrift.join(", ")}`,
|
|
174
|
+
remediation: `re-review and run: vonzio plugin approve ${packageName}`,
|
|
175
|
+
detail: { requested: manifest.capabilities, approved: entry.approved_capabilities, drift: capDrift },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const approvedHosts = new Set(entry.approved_outbound_hosts);
|
|
179
|
+
const hostDrift = (manifest.outboundHosts ?? []).filter((h) => !approvedHosts.has(h));
|
|
180
|
+
if (hostDrift.length) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
reason: "policy_outbound_host_drift",
|
|
184
|
+
message: `"${packageName}" requests outbound hosts not in the approved set: ${hostDrift.join(", ")}`,
|
|
185
|
+
remediation: `re-review and run: vonzio plugin approve ${packageName}`,
|
|
186
|
+
detail: { requested: manifest.outboundHosts ?? [], approved: entry.approved_outbound_hosts, drift: hostDrift },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (manifest.frontendEntry && entry.approved_frontend !== true) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "unapproved_frontend",
|
|
193
|
+
message: `"${packageName}" declares a frontend but the operator policy has not approved frontend bundling.`,
|
|
194
|
+
remediation: `run: vonzio plugin approve --frontend ${packageName}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Every mtls secret the manifest declares must be provisioned in policy with
|
|
198
|
+
// host file paths. A declared-but-unprovisioned name is drift (the operator
|
|
199
|
+
// hasn't mapped it to a cert/key yet).
|
|
200
|
+
const declaredMtls = manifest.mtlsSecrets ?? [];
|
|
201
|
+
const policyMtls = entry.mtls_secrets ?? {};
|
|
202
|
+
const mtlsDrift = declaredMtls.filter((name) => !(name in policyMtls));
|
|
203
|
+
if (mtlsDrift.length) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
reason: "policy_mtls_secret_drift",
|
|
207
|
+
message: `"${packageName}" declares mTLS secrets not provisioned in policy: ${mtlsDrift.join(", ")}`,
|
|
208
|
+
remediation: `add an mtls_secrets entry (cert/key paths) for each name, then re-approve ${packageName}`,
|
|
209
|
+
detail: { declared: declaredMtls, provisioned: Object.keys(policyMtls), drift: mtlsDrift },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const grantedMtlsSecrets = {};
|
|
213
|
+
for (const name of declaredMtls)
|
|
214
|
+
grantedMtlsSecrets[name] = policyMtls[name];
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
grantedCapabilities: [...manifest.capabilities],
|
|
218
|
+
grantedOutboundHosts: [...(manifest.outboundHosts ?? [])],
|
|
219
|
+
grantedMtlsSecrets,
|
|
220
|
+
frontendApproved: entry.approved_frontend === true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=policy.js.map
|
package/version.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw if the plugin's declared plugin-api version isn't compatible with
|
|
3
|
+
* core's. As of the external-loader contract (PLUGIN_API_VERSION 1.0.0)
|
|
4
|
+
* compatibility is BOTH major-equal AND minor-not-ahead:
|
|
5
|
+
*
|
|
6
|
+
* plugin.major === core.major (a different major is breaking either way)
|
|
7
|
+
* plugin.minor <= core.minor (a plugin built against a newer minor may
|
|
8
|
+
* rely on a surface this core doesn't ship)
|
|
9
|
+
*
|
|
10
|
+
* The minor check catches forward-incompatibility: capabilities are added in
|
|
11
|
+
* minor bumps (§5), so a plugin targeting a newer minor than core can declare
|
|
12
|
+
* a capability core doesn't understand. Same-major / not-ahead-minor is the
|
|
13
|
+
* compatible window. Called by the loader before importing each plugin.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertApiCompatible(pluginApiVersion: string, coreApiVersion?: string): void;
|
|
16
|
+
//# sourceMappingURL=version.d.ts.map
|
package/version.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PLUGIN_API_VERSION } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Throw if the plugin's declared plugin-api version isn't compatible with
|
|
4
|
+
* core's. As of the external-loader contract (PLUGIN_API_VERSION 1.0.0)
|
|
5
|
+
* compatibility is BOTH major-equal AND minor-not-ahead:
|
|
6
|
+
*
|
|
7
|
+
* plugin.major === core.major (a different major is breaking either way)
|
|
8
|
+
* plugin.minor <= core.minor (a plugin built against a newer minor may
|
|
9
|
+
* rely on a surface this core doesn't ship)
|
|
10
|
+
*
|
|
11
|
+
* The minor check catches forward-incompatibility: capabilities are added in
|
|
12
|
+
* minor bumps (§5), so a plugin targeting a newer minor than core can declare
|
|
13
|
+
* a capability core doesn't understand. Same-major / not-ahead-minor is the
|
|
14
|
+
* compatible window. Called by the loader before importing each plugin.
|
|
15
|
+
*/
|
|
16
|
+
export function assertApiCompatible(pluginApiVersion, coreApiVersion = PLUGIN_API_VERSION) {
|
|
17
|
+
const plugin = parseVersion(pluginApiVersion);
|
|
18
|
+
const core = parseVersion(coreApiVersion);
|
|
19
|
+
if (plugin === null) {
|
|
20
|
+
throw new Error(`Plugin declared invalid apiVersion: ${JSON.stringify(pluginApiVersion)}. Expected semver like "1.0".`);
|
|
21
|
+
}
|
|
22
|
+
if (core === null) {
|
|
23
|
+
// Should never trip in practice -- core's version is a literal in
|
|
24
|
+
// index.ts. Bail loudly if it somehow does.
|
|
25
|
+
throw new Error(`Internal: core apiVersion is malformed: ${coreApiVersion}`);
|
|
26
|
+
}
|
|
27
|
+
if (plugin.major > core.major) {
|
|
28
|
+
throw new Error(`Plugin requires plugin-api ^${plugin.major}.0.0 but core ships ${coreApiVersion}. ` +
|
|
29
|
+
`Upgrade vonzio core to v${plugin.major}.x, or use a plugin compatible with plugin-api ^${core.major}.0.0.`);
|
|
30
|
+
}
|
|
31
|
+
if (plugin.major < core.major) {
|
|
32
|
+
throw new Error(`Plugin built against plugin-api ^${plugin.major}.0.0 is incompatible with core's plugin-api ${coreApiVersion}. ` +
|
|
33
|
+
`Upgrade the plugin to a version targeting plugin-api ^${core.major}.0.0.`);
|
|
34
|
+
}
|
|
35
|
+
if (plugin.minor > core.minor) {
|
|
36
|
+
throw new Error(`Plugin targets plugin-api ${pluginApiVersion} but core ships ${coreApiVersion} ` +
|
|
37
|
+
`(plugin minor ${plugin.minor} > core minor ${core.minor}). ` +
|
|
38
|
+
`A newer-minor plugin may use a capability this core does not provide. ` +
|
|
39
|
+
`Upgrade vonzio core, or use a plugin targeting plugin-api <=${core.major}.${core.minor}.`);
|
|
40
|
+
}
|
|
41
|
+
// major equal AND plugin.minor <= core.minor: compatible.
|
|
42
|
+
}
|
|
43
|
+
/** Parse "MAJOR.MINOR(.PATCH)" into numeric parts; null if malformed. */
|
|
44
|
+
function parseVersion(version) {
|
|
45
|
+
const match = /^(\d+)\.(\d+)(?:\.\d+)?/.exec(version);
|
|
46
|
+
if (!match)
|
|
47
|
+
return null;
|
|
48
|
+
const major = Number(match[1]);
|
|
49
|
+
const minor = Number(match[2]);
|
|
50
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor))
|
|
51
|
+
return null;
|
|
52
|
+
return { major, minor };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=version.js.map
|