@notegen/plugin-cli 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 +21 -0
- package/README.md +349 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +398 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/lib/archive.d.ts +17 -0
- package/dist/lib/archive.js +257 -0
- package/dist/lib/constants.d.ts +16 -0
- package/dist/lib/constants.js +16 -0
- package/dist/lib/diagnostics.d.ts +25 -0
- package/dist/lib/diagnostics.js +49 -0
- package/dist/lib/files.d.ts +30 -0
- package/dist/lib/files.js +396 -0
- package/dist/lib/integrity.d.ts +20 -0
- package/dist/lib/integrity.js +162 -0
- package/dist/lib/manifest.d.ts +13 -0
- package/dist/lib/manifest.js +645 -0
- package/dist/lib/package.d.ts +21 -0
- package/dist/lib/package.js +229 -0
- package/dist/lib/path-rules.d.ts +23 -0
- package/dist/lib/path-rules.js +153 -0
- package/dist/lib/project.d.ts +21 -0
- package/dist/lib/project.js +209 -0
- package/dist/lib/scaffold.d.ts +22 -0
- package/dist/lib/scaffold.js +230 -0
- package/dist/lib/signing.d.ts +26 -0
- package/dist/lib/signing.js +181 -0
- package/dist/lib/strict-json.d.ts +26 -0
- package/dist/lib/strict-json.js +210 -0
- package/dist/lib/tasks.d.ts +70 -0
- package/dist/lib/tasks.js +241 -0
- package/dist/lib/watch.d.ts +6 -0
- package/dist/lib/watch.js +63 -0
- package/package.json +64 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { PLUGIN_API_VERSION } from '@notegen/plugin-api';
|
|
2
|
+
import { compare as compareSemver, valid as validSemver } from 'semver';
|
|
3
|
+
import { fail } from './diagnostics.js';
|
|
4
|
+
import { hasControlCharacter, packagePathCollisionKey, utf8ByteLength, validatePackagePath, } from './path-rules.js';
|
|
5
|
+
import { assertJsonIntegerToken, isJsonObject, parseStrictJson } from './strict-json.js';
|
|
6
|
+
const PLUGIN_ID_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$(?![\s\S])/u;
|
|
7
|
+
const PRERELEASE_IDENTIFIER = String.raw `(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
8
|
+
const SEMVER_SOURCE = String.raw `(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-${PRERELEASE_IDENTIFIER}(?:\.${PRERELEASE_IDENTIFIER})*)?`;
|
|
9
|
+
const SEMVER_PATTERN = new RegExp(String.raw `^${SEMVER_SOURCE}(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$(?![\s\S])`, 'u');
|
|
10
|
+
const API_SEMVER_PATTERN = new RegExp(String.raw `^${SEMVER_SOURCE}$(?![\s\S])`, 'u');
|
|
11
|
+
const LOCALIZATION_PATTERN = /^%([A-Za-z0-9][A-Za-z0-9._-]*)%$(?![\s\S])/u;
|
|
12
|
+
const NAMESPACED_ID_CHARACTERS = /^[A-Za-z0-9._-]+$(?![\s\S])/u;
|
|
13
|
+
const ICON_PATTERN = /^[A-Za-z0-9-]+$(?![\s\S])/u;
|
|
14
|
+
const SUPPORTED_PERMISSIONS = Object.freeze({
|
|
15
|
+
'editor.read': new Set(['active-editor']),
|
|
16
|
+
'editor.write': new Set(['active-editor']),
|
|
17
|
+
'notes.read': new Set(['workspace-file', 'workspace-files', 'workspace-folder']),
|
|
18
|
+
'attachments.read': new Set(['workspace-file', 'workspace-files', 'workspace-folder']),
|
|
19
|
+
'attachments.create': new Set(['workspace-folder']),
|
|
20
|
+
'notes.list': new Set(['workspace-folder']),
|
|
21
|
+
'notes.create': new Set(['workspace-folder']),
|
|
22
|
+
'notes.open': new Set(['workspace-folder']),
|
|
23
|
+
'notes.write': new Set(['workspace-file', 'workspace-files', 'workspace-folder']),
|
|
24
|
+
'notes.delete': new Set(['workspace-file', 'workspace-files', 'workspace-folder']),
|
|
25
|
+
'notes.move': new Set(['workspace-folder']),
|
|
26
|
+
'network.fetch': new Set(['network-origins']),
|
|
27
|
+
});
|
|
28
|
+
function objectValue(value, path) {
|
|
29
|
+
if (!isJsonObject(value))
|
|
30
|
+
fail('manifest.expected-object', `${path} must be an object`, path);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function arrayValue(value, path) {
|
|
34
|
+
if (!Array.isArray(value))
|
|
35
|
+
fail('manifest.expected-array', `${path} must be an array`, path);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function required(object, key, path) {
|
|
39
|
+
if (!Object.hasOwn(object, key)) {
|
|
40
|
+
fail('manifest.missing-field', `${path}.${key} is required`, `${path}.${key}`);
|
|
41
|
+
}
|
|
42
|
+
return object[key];
|
|
43
|
+
}
|
|
44
|
+
function assertAllowedKeys(object, allowed, path) {
|
|
45
|
+
const accepted = new Set(allowed);
|
|
46
|
+
const unknown = Object.keys(object).find((key) => !accepted.has(key));
|
|
47
|
+
if (unknown !== undefined) {
|
|
48
|
+
fail('manifest.unknown-field', `${path}.${unknown} is not supported`, `${path}.${unknown}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function stringValue(value, path) {
|
|
52
|
+
if (typeof value !== 'string')
|
|
53
|
+
fail('manifest.expected-string', `${path} must be a string`, path);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function booleanValue(value, path) {
|
|
57
|
+
if (typeof value !== 'boolean')
|
|
58
|
+
fail('manifest.expected-boolean', `${path} must be a boolean`, path);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function finiteNumber(value, path) {
|
|
62
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
63
|
+
fail('manifest.expected-number', `${path} must be a finite number`, path);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function integerValue(value, path) {
|
|
68
|
+
const number = finiteNumber(value, path);
|
|
69
|
+
if (!Number.isSafeInteger(number)) {
|
|
70
|
+
fail('manifest.expected-integer', `${path} must be a safe integer`, path);
|
|
71
|
+
}
|
|
72
|
+
return number;
|
|
73
|
+
}
|
|
74
|
+
function validateText(value, path, maximumBytes) {
|
|
75
|
+
const text = stringValue(value, path);
|
|
76
|
+
if (text.trim().length === 0 || utf8ByteLength(text) > maximumBytes || hasControlCharacter(text)) {
|
|
77
|
+
fail('manifest.invalid-text', `${path} must be non-empty, contain no control characters, and use at most ${maximumBytes} UTF-8 bytes`, path);
|
|
78
|
+
}
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
function localizationKey(text) {
|
|
82
|
+
const key = text.match(LOCALIZATION_PATTERN)?.[1];
|
|
83
|
+
return key !== undefined && utf8ByteLength(key) <= 160 ? key : undefined;
|
|
84
|
+
}
|
|
85
|
+
function validateLocalizedText(value, path) {
|
|
86
|
+
const text = validateText(value, path, 240);
|
|
87
|
+
if (text.startsWith('%') && localizationKey(text) === undefined) {
|
|
88
|
+
fail('manifest.invalid-localization', `${path} contains a malformed localization reference`, path);
|
|
89
|
+
}
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
function validateSemver(value, path, allowBuild) {
|
|
93
|
+
const version = stringValue(value, path);
|
|
94
|
+
if (utf8ByteLength(version) > 80
|
|
95
|
+
|| !(allowBuild ? SEMVER_PATTERN : API_SEMVER_PATTERN).test(version)
|
|
96
|
+
|| validSemver(version) === null) {
|
|
97
|
+
fail('manifest.invalid-semver', `${path} must be a canonical semantic version`, path);
|
|
98
|
+
}
|
|
99
|
+
return version;
|
|
100
|
+
}
|
|
101
|
+
function parseApiRequirement(value, path) {
|
|
102
|
+
const requirement = stringValue(value, path);
|
|
103
|
+
if (requirement.length === 0 || utf8ByteLength(requirement) > 80 || hasControlCharacter(requirement)) {
|
|
104
|
+
fail('manifest.invalid-api-range', `${path} is not a supported API version requirement`, path);
|
|
105
|
+
}
|
|
106
|
+
const operators = ['>=', '<=', '^', '~', '>', '<'];
|
|
107
|
+
const operator = operators.find((candidate) => requirement.startsWith(candidate)) ?? '';
|
|
108
|
+
const remainder = requirement.slice(operator.length);
|
|
109
|
+
const version = operator === '' ? remainder : remainder.replace(/^ +/u, '');
|
|
110
|
+
if (version.length === 0
|
|
111
|
+
|| version.endsWith(' ')
|
|
112
|
+
|| (operator === '' && version.length !== requirement.length)
|
|
113
|
+
|| !API_SEMVER_PATTERN.test(version)
|
|
114
|
+
|| validSemver(version) === null) {
|
|
115
|
+
fail('manifest.invalid-api-range', `${path} is not a supported API version requirement`, path);
|
|
116
|
+
}
|
|
117
|
+
return { operator, version };
|
|
118
|
+
}
|
|
119
|
+
export function satisfiesPluginApiRequirement(supported, requirement) {
|
|
120
|
+
if (!API_SEMVER_PATTERN.test(supported) || validSemver(supported) === null)
|
|
121
|
+
return false;
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = parseApiRequirement(requirement, 'apiVersion');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const comparison = compareSemver(supported, parsed.version);
|
|
130
|
+
const [supportedMajor, supportedMinor, supportedPatch] = supported.split('-')[0]?.split('.').map(Number) ?? [];
|
|
131
|
+
const [targetMajor, targetMinor, targetPatch] = parsed.version.split('-')[0]?.split('.').map(Number) ?? [];
|
|
132
|
+
switch (parsed.operator) {
|
|
133
|
+
case '': return comparison === 0;
|
|
134
|
+
case '>=': return comparison >= 0;
|
|
135
|
+
case '>': return comparison > 0;
|
|
136
|
+
case '<=': return comparison <= 0;
|
|
137
|
+
case '<': return comparison < 0;
|
|
138
|
+
case '~':
|
|
139
|
+
return comparison >= 0 && supportedMajor === targetMajor && supportedMinor === targetMinor;
|
|
140
|
+
case '^':
|
|
141
|
+
if ((targetMajor ?? 0) > 0)
|
|
142
|
+
return comparison >= 0 && supportedMajor === targetMajor;
|
|
143
|
+
if ((targetMinor ?? 0) > 0) {
|
|
144
|
+
return comparison >= 0 && supportedMajor === 0 && supportedMinor === targetMinor;
|
|
145
|
+
}
|
|
146
|
+
return comparison >= 0
|
|
147
|
+
&& supportedMajor === 0
|
|
148
|
+
&& supportedMinor === 0
|
|
149
|
+
&& supportedPatch === targetPatch;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function validatePluginId(value) {
|
|
153
|
+
const id = stringValue(value, '$.id');
|
|
154
|
+
const segments = id.split('.');
|
|
155
|
+
if (utf8ByteLength(id) < 3
|
|
156
|
+
|| utf8ByteLength(id) > 160
|
|
157
|
+
|| !PLUGIN_ID_PATTERN.test(id)
|
|
158
|
+
|| segments.some((segment) => utf8ByteLength(segment) > 63)) {
|
|
159
|
+
fail('manifest.invalid-id', 'Plugin id must be a lowercase reverse-domain identifier', '$.id');
|
|
160
|
+
}
|
|
161
|
+
if (id === 'app.notegen' || id.startsWith('app.notegen.')) {
|
|
162
|
+
fail('manifest.reserved-id', 'The app.notegen.* namespace is reserved for NoteGen host internals', '$.id');
|
|
163
|
+
}
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
function validateNamespacedId(value, pluginId, path) {
|
|
167
|
+
const id = stringValue(value, path);
|
|
168
|
+
const suffix = id.startsWith(pluginId) ? id.slice(pluginId.length) : '';
|
|
169
|
+
if (!suffix.startsWith('.')
|
|
170
|
+
|| suffix.length <= 1
|
|
171
|
+
|| utf8ByteLength(id) > 220
|
|
172
|
+
|| !NAMESPACED_ID_CHARACTERS.test(id)) {
|
|
173
|
+
fail('manifest.invalid-namespaced-id', `${path} must use the ${pluginId}. namespace`, path);
|
|
174
|
+
}
|
|
175
|
+
return id;
|
|
176
|
+
}
|
|
177
|
+
function validatePermissionDeclarations(value) {
|
|
178
|
+
const permissions = objectValue(value, '$.permissions');
|
|
179
|
+
assertAllowedKeys(permissions, Object.keys(SUPPORTED_PERMISSIONS), '$.permissions');
|
|
180
|
+
for (const [name, rawDeclaration] of Object.entries(permissions)) {
|
|
181
|
+
const path = `$.permissions.${name}`;
|
|
182
|
+
const declaration = objectValue(rawDeclaration, path);
|
|
183
|
+
assertAllowedKeys(declaration, ['scope', 'optional', 'description'], path);
|
|
184
|
+
const scope = stringValue(required(declaration, 'scope', path), `${path}.scope`);
|
|
185
|
+
const allowedScopes = SUPPORTED_PERMISSIONS[name];
|
|
186
|
+
if (!allowedScopes.has(scope)) {
|
|
187
|
+
fail('manifest.invalid-permission-scope', `${name} does not support the ${scope} scope`, `${path}.scope`);
|
|
188
|
+
}
|
|
189
|
+
if (Object.hasOwn(declaration, 'optional'))
|
|
190
|
+
booleanValue(declaration.optional, `${path}.optional`);
|
|
191
|
+
if (Object.hasOwn(declaration, 'description')) {
|
|
192
|
+
validateText(declaration.description, `${path}.description`, 240);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function validateSetting(value, pluginId, index, keys, localizedTexts) {
|
|
197
|
+
const path = `$.contributes.settings[${index}]`;
|
|
198
|
+
const setting = objectValue(value, path);
|
|
199
|
+
const type = stringValue(required(setting, 'type', path), `${path}.type`);
|
|
200
|
+
const common = ['key', 'type', 'scope', 'title', 'description', 'default'];
|
|
201
|
+
const allowedByType = {
|
|
202
|
+
boolean: common,
|
|
203
|
+
string: [...common, 'placeholder', 'maxLength', 'permissionPaths'],
|
|
204
|
+
number: [...common, 'min', 'max', 'step'],
|
|
205
|
+
select: [...common, 'options'],
|
|
206
|
+
'workspace-file': common,
|
|
207
|
+
'workspace-folder': common,
|
|
208
|
+
};
|
|
209
|
+
const allowed = allowedByType[type];
|
|
210
|
+
if (!allowed)
|
|
211
|
+
fail('manifest.invalid-setting-type', `${path}.type is unsupported`, `${path}.type`);
|
|
212
|
+
assertAllowedKeys(setting, allowed, path);
|
|
213
|
+
const key = validateNamespacedId(required(setting, 'key', path), pluginId, `${path}.key`);
|
|
214
|
+
if (keys.has(key))
|
|
215
|
+
fail('manifest.duplicate-setting', `Setting ${key} is declared more than once`, `${path}.key`);
|
|
216
|
+
keys.add(key);
|
|
217
|
+
const scope = stringValue(required(setting, 'scope', path), `${path}.scope`);
|
|
218
|
+
if (scope !== 'device' && scope !== 'workspace') {
|
|
219
|
+
fail('manifest.invalid-setting-scope', `${path}.scope must be device or workspace`, `${path}.scope`);
|
|
220
|
+
}
|
|
221
|
+
const title = validateLocalizedText(required(setting, 'title', path), `${path}.title`);
|
|
222
|
+
localizedTexts.push(title);
|
|
223
|
+
if (Object.hasOwn(setting, 'description')) {
|
|
224
|
+
localizedTexts.push(validateLocalizedText(setting.description, `${path}.description`));
|
|
225
|
+
}
|
|
226
|
+
const defaultValue = required(setting, 'default', path);
|
|
227
|
+
if (type === 'boolean') {
|
|
228
|
+
booleanValue(defaultValue, `${path}.default`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (type === 'string') {
|
|
232
|
+
const defaultText = stringValue(defaultValue, `${path}.default`);
|
|
233
|
+
let maximum = 65_536;
|
|
234
|
+
if (Object.hasOwn(setting, 'maxLength')) {
|
|
235
|
+
maximum = integerValue(setting.maxLength, `${path}.maxLength`);
|
|
236
|
+
assertJsonIntegerToken(setting, 'maxLength', `${path}.maxLength`);
|
|
237
|
+
if (maximum <= 0 || maximum > 65_536) {
|
|
238
|
+
fail('manifest.invalid-setting', `${path}.maxLength must be between 1 and 65536`, `${path}.maxLength`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (utf8ByteLength(defaultText) > maximum) {
|
|
242
|
+
fail('manifest.invalid-setting', `${path}.default exceeds maxLength`, `${path}.default`);
|
|
243
|
+
}
|
|
244
|
+
if (Object.hasOwn(setting, 'placeholder')) {
|
|
245
|
+
localizedTexts.push(validateLocalizedText(setting.placeholder, `${path}.placeholder`));
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (type === 'number') {
|
|
250
|
+
const defaultNumber = finiteNumber(defaultValue, `${path}.default`);
|
|
251
|
+
const minimum = Object.hasOwn(setting, 'min') ? finiteNumber(setting.min, `${path}.min`) : undefined;
|
|
252
|
+
const maximum = Object.hasOwn(setting, 'max') ? finiteNumber(setting.max, `${path}.max`) : undefined;
|
|
253
|
+
const step = Object.hasOwn(setting, 'step') ? finiteNumber(setting.step, `${path}.step`) : 1;
|
|
254
|
+
if (step <= 0 || (minimum !== undefined && maximum !== undefined && minimum > maximum)) {
|
|
255
|
+
fail('manifest.invalid-setting', `${path} has an invalid numeric range or step`, path);
|
|
256
|
+
}
|
|
257
|
+
if ((minimum !== undefined && defaultNumber < minimum)
|
|
258
|
+
|| (maximum !== undefined && defaultNumber > maximum)) {
|
|
259
|
+
fail('manifest.invalid-setting', `${path}.default is outside its numeric range`, `${path}.default`);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (type === 'select') {
|
|
264
|
+
const defaultText = stringValue(defaultValue, `${path}.default`);
|
|
265
|
+
const options = arrayValue(required(setting, 'options', path), `${path}.options`);
|
|
266
|
+
if (options.length === 0 || options.length > 100) {
|
|
267
|
+
fail('manifest.invalid-setting', `${path}.options must contain between 1 and 100 values`, `${path}.options`);
|
|
268
|
+
}
|
|
269
|
+
const values = new Set();
|
|
270
|
+
for (const [optionIndex, rawOption] of options.entries()) {
|
|
271
|
+
const optionPath = `${path}.options[${optionIndex}]`;
|
|
272
|
+
const option = objectValue(rawOption, optionPath);
|
|
273
|
+
assertAllowedKeys(option, ['label', 'value'], optionPath);
|
|
274
|
+
const label = validateLocalizedText(required(option, 'label', optionPath), `${optionPath}.label`);
|
|
275
|
+
localizedTexts.push(label);
|
|
276
|
+
const optionValue = stringValue(required(option, 'value', optionPath), `${optionPath}.value`);
|
|
277
|
+
if (optionValue.length === 0
|
|
278
|
+
|| utf8ByteLength(optionValue) > 160
|
|
279
|
+
|| hasControlCharacter(optionValue)
|
|
280
|
+
|| values.has(optionValue)) {
|
|
281
|
+
fail('manifest.invalid-setting', `${optionPath}.value is empty, invalid, or duplicated`, `${optionPath}.value`);
|
|
282
|
+
}
|
|
283
|
+
values.add(optionValue);
|
|
284
|
+
}
|
|
285
|
+
if (!values.has(defaultText)) {
|
|
286
|
+
fail('manifest.invalid-setting', `${path}.default must match an option value`, `${path}.default`);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (scope !== 'workspace') {
|
|
291
|
+
fail('manifest.invalid-setting', `${path}.scope must be workspace for ${type}`, `${path}.scope`);
|
|
292
|
+
}
|
|
293
|
+
const defaultPath = stringValue(defaultValue, `${path}.default`);
|
|
294
|
+
if (defaultPath.length > 0) {
|
|
295
|
+
validatePackagePath(defaultPath, {
|
|
296
|
+
directory: type === 'workspace-folder',
|
|
297
|
+
label: `${path}.default`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function validateContributions(value, pluginId) {
|
|
302
|
+
const contributes = objectValue(value, '$.contributes');
|
|
303
|
+
assertAllowedKeys(contributes, ['commands', 'settings', 'statusBar', 'menus', 'views'], '$.contributes');
|
|
304
|
+
const commandIds = new Set();
|
|
305
|
+
const localizedTexts = [];
|
|
306
|
+
const commands = Object.hasOwn(contributes, 'commands')
|
|
307
|
+
? arrayValue(contributes.commands, '$.contributes.commands')
|
|
308
|
+
: [];
|
|
309
|
+
if (commands.length > 100)
|
|
310
|
+
fail('manifest.too-many-contributions', 'A plugin may declare at most 100 commands', '$.contributes.commands');
|
|
311
|
+
for (const [index, rawCommand] of commands.entries()) {
|
|
312
|
+
const path = `$.contributes.commands[${index}]`;
|
|
313
|
+
const command = objectValue(rawCommand, path);
|
|
314
|
+
assertAllowedKeys(command, ['id', 'title', 'description', 'icon', 'suggestedShortcut'], path);
|
|
315
|
+
const id = validateNamespacedId(required(command, 'id', path), pluginId, `${path}.id`);
|
|
316
|
+
if (commandIds.has(id))
|
|
317
|
+
fail('manifest.duplicate-command', `Command ${id} is declared more than once`, `${path}.id`);
|
|
318
|
+
commandIds.add(id);
|
|
319
|
+
localizedTexts.push(validateLocalizedText(required(command, 'title', path), `${path}.title`));
|
|
320
|
+
if (Object.hasOwn(command, 'description')) {
|
|
321
|
+
localizedTexts.push(validateLocalizedText(command.description, `${path}.description`));
|
|
322
|
+
}
|
|
323
|
+
if (Object.hasOwn(command, 'icon')) {
|
|
324
|
+
const icon = stringValue(command.icon, `${path}.icon`);
|
|
325
|
+
if (utf8ByteLength(icon) === 0 || utf8ByteLength(icon) > 80 || !ICON_PATTERN.test(icon)) {
|
|
326
|
+
fail('manifest.invalid-icon', `${path}.icon must be a symbolic ASCII icon name`, `${path}.icon`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (Object.hasOwn(command, 'suggestedShortcut')) {
|
|
330
|
+
validateText(command.suggestedShortcut, `${path}.suggestedShortcut`, 80);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const settings = Object.hasOwn(contributes, 'settings')
|
|
334
|
+
? arrayValue(contributes.settings, '$.contributes.settings')
|
|
335
|
+
: [];
|
|
336
|
+
if (settings.length > 100)
|
|
337
|
+
fail('manifest.too-many-contributions', 'A plugin may declare at most 100 settings', '$.contributes.settings');
|
|
338
|
+
const settingKeys = new Set();
|
|
339
|
+
for (const [index, setting] of settings.entries()) {
|
|
340
|
+
validateSetting(setting, pluginId, index, settingKeys, localizedTexts);
|
|
341
|
+
}
|
|
342
|
+
const statusItems = Object.hasOwn(contributes, 'statusBar')
|
|
343
|
+
? arrayValue(contributes.statusBar, '$.contributes.statusBar')
|
|
344
|
+
: [];
|
|
345
|
+
if (statusItems.length > 30)
|
|
346
|
+
fail('manifest.too-many-contributions', 'A plugin may declare at most 30 status bar items', '$.contributes.statusBar');
|
|
347
|
+
const statusIds = new Set();
|
|
348
|
+
for (const [index, rawStatus] of statusItems.entries()) {
|
|
349
|
+
const path = `$.contributes.statusBar[${index}]`;
|
|
350
|
+
const status = objectValue(rawStatus, path);
|
|
351
|
+
assertAllowedKeys(status, ['id', 'alignment', 'priority', 'command'], path);
|
|
352
|
+
const id = validateNamespacedId(required(status, 'id', path), pluginId, `${path}.id`);
|
|
353
|
+
if (statusIds.has(id))
|
|
354
|
+
fail('manifest.duplicate-status', `Status item ${id} is declared more than once`, `${path}.id`);
|
|
355
|
+
statusIds.add(id);
|
|
356
|
+
const alignment = stringValue(required(status, 'alignment', path), `${path}.alignment`);
|
|
357
|
+
if (alignment !== 'left' && alignment !== 'right') {
|
|
358
|
+
fail('manifest.invalid-status', `${path}.alignment must be left or right`, `${path}.alignment`);
|
|
359
|
+
}
|
|
360
|
+
if (Object.hasOwn(status, 'priority')) {
|
|
361
|
+
const priority = integerValue(status.priority, `${path}.priority`);
|
|
362
|
+
assertJsonIntegerToken(status, 'priority', `${path}.priority`, { signed: true });
|
|
363
|
+
if (priority < -10_000 || priority > 10_000) {
|
|
364
|
+
fail('manifest.invalid-status', `${path}.priority is outside the supported range`, `${path}.priority`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (Object.hasOwn(status, 'command')) {
|
|
368
|
+
const command = stringValue(status.command, `${path}.command`);
|
|
369
|
+
if (!commandIds.has(command))
|
|
370
|
+
fail('manifest.unknown-command', `${path}.command is not declared`, `${path}.command`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const menus = Object.hasOwn(contributes, 'menus')
|
|
374
|
+
? arrayValue(contributes.menus, '$.contributes.menus')
|
|
375
|
+
: [];
|
|
376
|
+
if (menus.length > 100)
|
|
377
|
+
fail('manifest.too-many-contributions', 'A plugin may declare at most 100 menu items', '$.contributes.menus');
|
|
378
|
+
const menuLocations = new Set(['editor/slash', 'editor/context', 'file/context', 'mobile/writing/overflow']);
|
|
379
|
+
for (const [index, rawMenu] of menus.entries()) {
|
|
380
|
+
const path = `$.contributes.menus[${index}]`;
|
|
381
|
+
const menu = objectValue(rawMenu, path);
|
|
382
|
+
assertAllowedKeys(menu, ['location', 'command', 'when', 'group'], path);
|
|
383
|
+
const location = stringValue(required(menu, 'location', path), `${path}.location`);
|
|
384
|
+
const command = stringValue(required(menu, 'command', path), `${path}.command`);
|
|
385
|
+
if (!menuLocations.has(location) || !commandIds.has(command)) {
|
|
386
|
+
fail('manifest.invalid-menu', `${path} must use a supported location and declared command`, path);
|
|
387
|
+
}
|
|
388
|
+
if (Object.hasOwn(menu, 'when')) {
|
|
389
|
+
const condition = stringValue(menu.when, `${path}.when`);
|
|
390
|
+
const afterEditor = condition.startsWith('editor') ? condition.slice('editor'.length).trimStart() : '';
|
|
391
|
+
const supported = utf8ByteLength(condition) <= 240
|
|
392
|
+
&& afterEditor.startsWith('==')
|
|
393
|
+
&& afterEditor.slice(2).trimStart() === 'markdown';
|
|
394
|
+
if (!supported)
|
|
395
|
+
fail('manifest.invalid-menu-condition', `${path}.when is unsupported`, `${path}.when`);
|
|
396
|
+
}
|
|
397
|
+
if (Object.hasOwn(menu, 'group')) {
|
|
398
|
+
const group = stringValue(menu.group, `${path}.group`);
|
|
399
|
+
if (group.length === 0 || utf8ByteLength(group) > 80 || hasControlCharacter(group)) {
|
|
400
|
+
fail('manifest.invalid-menu-group', `${path}.group is invalid`, `${path}.group`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const views = Object.hasOwn(contributes, 'views')
|
|
405
|
+
? arrayValue(contributes.views, '$.contributes.views')
|
|
406
|
+
: [];
|
|
407
|
+
if (views.length > 30)
|
|
408
|
+
fail('manifest.too-many-contributions', 'A plugin may declare at most 30 views', '$.contributes.views');
|
|
409
|
+
const viewIds = new Set();
|
|
410
|
+
for (const [index, rawView] of views.entries()) {
|
|
411
|
+
const path = `$.contributes.views[${index}]`;
|
|
412
|
+
const view = objectValue(rawView, path);
|
|
413
|
+
assertAllowedKeys(view, ['id', 'title', 'location', 'icon'], path);
|
|
414
|
+
const id = validateNamespacedId(required(view, 'id', path), pluginId, `${path}.id`);
|
|
415
|
+
if (viewIds.has(id))
|
|
416
|
+
fail('manifest.duplicate-view', `View ${id} is declared more than once`, `${path}.id`);
|
|
417
|
+
viewIds.add(id);
|
|
418
|
+
localizedTexts.push(validateLocalizedText(required(view, 'title', path), `${path}.title`));
|
|
419
|
+
const location = stringValue(required(view, 'location', path), `${path}.location`);
|
|
420
|
+
if (location !== 'left-sidebar' && location !== 'right-sidebar' && location !== 'editor-tab') {
|
|
421
|
+
fail('manifest.invalid-view', `${path}.location must be left-sidebar, right-sidebar or editor-tab`, `${path}.location`);
|
|
422
|
+
}
|
|
423
|
+
if (Object.hasOwn(view, 'icon')) {
|
|
424
|
+
const icon = stringValue(view.icon, `${path}.icon`);
|
|
425
|
+
if (utf8ByteLength(icon) === 0 || utf8ByteLength(icon) > 80 || !ICON_PATTERN.test(icon)) {
|
|
426
|
+
fail('manifest.invalid-icon', `${path}.icon must be a symbolic ASCII icon name`, `${path}.icon`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { commandIds, localizedTexts };
|
|
431
|
+
}
|
|
432
|
+
function validateActivationEvents(value, commandIds) {
|
|
433
|
+
const events = arrayValue(value, '$.activationEvents');
|
|
434
|
+
if (events.length > 100)
|
|
435
|
+
fail('manifest.too-many-activation-events', 'A plugin may declare at most 100 activation events', '$.activationEvents');
|
|
436
|
+
const seen = new Set();
|
|
437
|
+
for (const [index, rawEvent] of events.entries()) {
|
|
438
|
+
const path = `$.activationEvents[${index}]`;
|
|
439
|
+
const event = stringValue(rawEvent, path);
|
|
440
|
+
const command = event.startsWith('onCommand:') ? event.slice('onCommand:'.length) : undefined;
|
|
441
|
+
if ((!['onEditor:markdown', 'onWorkspace:open', 'onNotes:change'].includes(event)
|
|
442
|
+
&& (command === undefined || !commandIds.has(command)))
|
|
443
|
+
|| seen.has(event)) {
|
|
444
|
+
fail('manifest.invalid-activation-event', `${path} is unsupported, duplicated, or references an unknown command`, path);
|
|
445
|
+
}
|
|
446
|
+
seen.add(event);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function isValidLocaleTag(value) {
|
|
450
|
+
if (utf8ByteLength(value) < 2 || utf8ByteLength(value) > 35 || !/^[\x00-\x7F]+$(?![\s\S])/u.test(value))
|
|
451
|
+
return false;
|
|
452
|
+
const [language, ...segments] = value.split('-');
|
|
453
|
+
return language !== undefined
|
|
454
|
+
&& language.length >= 2
|
|
455
|
+
&& language.length <= 8
|
|
456
|
+
&& /^[A-Za-z]+$(?![\s\S])/u.test(language)
|
|
457
|
+
&& segments.every((segment) => segment.length > 0 && segment.length <= 8 && /^[A-Za-z0-9]+$(?![\s\S])/u.test(segment));
|
|
458
|
+
}
|
|
459
|
+
export function validateLocaleMessages(value, path = 'locale') {
|
|
460
|
+
const messages = objectValue(value, path);
|
|
461
|
+
if (Object.keys(messages).length > 2_000) {
|
|
462
|
+
fail('manifest.too-many-locale-messages', `${path} contains more than 2000 messages`, path);
|
|
463
|
+
}
|
|
464
|
+
for (const [key, rawMessage] of Object.entries(messages)) {
|
|
465
|
+
if (key.length === 0 || utf8ByteLength(key) > 160 || hasControlCharacter(key) || typeof rawMessage !== 'string') {
|
|
466
|
+
fail('manifest.invalid-locale-message', `${path} contains an invalid key or non-string value`, `${path}.${key}`);
|
|
467
|
+
}
|
|
468
|
+
if (utf8ByteLength(rawMessage) > 4_096 || rawMessage.includes('\0')) {
|
|
469
|
+
fail('manifest.invalid-locale-message', `${path}.${key} exceeds its message limit`, `${path}.${key}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return messages;
|
|
473
|
+
}
|
|
474
|
+
function validateLocalization(manifest, localizedTexts, files) {
|
|
475
|
+
const defaultLocale = Object.hasOwn(manifest, 'defaultLocale')
|
|
476
|
+
? stringValue(manifest.defaultLocale, '$.defaultLocale')
|
|
477
|
+
: undefined;
|
|
478
|
+
const locales = Object.hasOwn(manifest, 'locales')
|
|
479
|
+
? objectValue(manifest.locales, '$.locales')
|
|
480
|
+
: {};
|
|
481
|
+
const localeEntries = Object.entries(locales);
|
|
482
|
+
if (localeEntries.length > 50)
|
|
483
|
+
fail('manifest.too-many-locales', 'A plugin may declare at most 50 locale resources', '$.locales');
|
|
484
|
+
if ((localeEntries.length === 0) !== (defaultLocale === undefined)) {
|
|
485
|
+
fail('manifest.invalid-locales', 'defaultLocale and non-empty locales must be declared together', '$.locales');
|
|
486
|
+
}
|
|
487
|
+
const references = new Set(localizedTexts.map(localizationKey).filter((key) => key !== undefined));
|
|
488
|
+
if (localeEntries.length === 0) {
|
|
489
|
+
if (references.size > 0)
|
|
490
|
+
fail('manifest.missing-locales', 'Localized contribution text requires locale resources', '$.locales');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (defaultLocale === undefined || !Object.hasOwn(locales, defaultLocale)) {
|
|
494
|
+
fail('manifest.invalid-locales', 'defaultLocale must reference an entry in locales', '$.defaultLocale');
|
|
495
|
+
}
|
|
496
|
+
const tags = new Set();
|
|
497
|
+
const paths = new Set();
|
|
498
|
+
let defaultMessages;
|
|
499
|
+
for (const [locale, rawPath] of localeEntries) {
|
|
500
|
+
if (!isValidLocaleTag(locale) || tags.has(locale.toLowerCase())) {
|
|
501
|
+
fail('manifest.invalid-locale-tag', `Invalid or duplicated locale tag: ${locale}`, `$.locales.${locale}`);
|
|
502
|
+
}
|
|
503
|
+
tags.add(locale.toLowerCase());
|
|
504
|
+
const path = validatePackagePath(stringValue(rawPath, `$.locales.${locale}`), { label: `$.locales.${locale}` });
|
|
505
|
+
const collisionKey = packagePathCollisionKey(path);
|
|
506
|
+
if (utf8ByteLength(path) > 240 || !path.toLowerCase().endsWith('.json') || paths.has(collisionKey)) {
|
|
507
|
+
fail('manifest.invalid-locale-path', `Invalid or duplicated locale resource: ${path}`, `$.locales.${locale}`);
|
|
508
|
+
}
|
|
509
|
+
paths.add(collisionKey);
|
|
510
|
+
if (files) {
|
|
511
|
+
const bytes = files.get(path);
|
|
512
|
+
if (!bytes)
|
|
513
|
+
fail('manifest.missing-locale-file', `Package is missing locale resource ${path}`, path);
|
|
514
|
+
const messages = validateLocaleMessages(parseStrictJson(bytes, path), path);
|
|
515
|
+
if (locale === defaultLocale)
|
|
516
|
+
defaultMessages = messages;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (files) {
|
|
520
|
+
if (!defaultMessages)
|
|
521
|
+
fail('manifest.missing-default-locale', 'The default locale could not be loaded', '$.defaultLocale');
|
|
522
|
+
const missing = [...references].find((key) => !Object.hasOwn(defaultMessages, key));
|
|
523
|
+
if (missing !== undefined) {
|
|
524
|
+
fail('manifest.missing-translation', `Default locale is missing contribution key ${missing}`, `$.locales.${defaultLocale}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function validateEntry(entry, files) {
|
|
529
|
+
if (utf8ByteLength(entry) > 240 || !entry.endsWith('.js')) {
|
|
530
|
+
fail('manifest.invalid-entry', 'Plugin entry must be a JavaScript .js file of at most 240 UTF-8 bytes', '$.entry');
|
|
531
|
+
}
|
|
532
|
+
if (!files)
|
|
533
|
+
return;
|
|
534
|
+
const bytes = files.get(entry);
|
|
535
|
+
if (!bytes)
|
|
536
|
+
fail('manifest.missing-entry', `Package is missing plugin entry ${entry}`, entry);
|
|
537
|
+
if (bytes.byteLength > 5 * 1_048_576)
|
|
538
|
+
fail('manifest.entry-too-large', 'Plugin entry exceeds 5 MiB', entry);
|
|
539
|
+
let source;
|
|
540
|
+
try {
|
|
541
|
+
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
fail('manifest.invalid-entry-encoding', 'Plugin entry must contain valid UTF-8 JavaScript', entry);
|
|
545
|
+
}
|
|
546
|
+
if (source.includes('\0'))
|
|
547
|
+
fail('manifest.invalid-entry', 'Plugin entry contains a null character', entry);
|
|
548
|
+
}
|
|
549
|
+
function validatePublicUrl(value, path) {
|
|
550
|
+
const raw = stringValue(value, path);
|
|
551
|
+
if (utf8ByteLength(raw) > 500)
|
|
552
|
+
fail('manifest.invalid-url', `${path} is too long`, path);
|
|
553
|
+
let url;
|
|
554
|
+
try {
|
|
555
|
+
url = new URL(raw);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
fail('manifest.invalid-url', `${path} must be a valid HTTP(S) URL`, path);
|
|
559
|
+
}
|
|
560
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
561
|
+
|| url.username.length > 0
|
|
562
|
+
|| url.password.length > 0
|
|
563
|
+
|| url.hostname.length === 0) {
|
|
564
|
+
fail('manifest.invalid-url', `${path} must be an HTTP(S) URL without credentials`, path);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
export function validatePluginManifest(value, options = {}) {
|
|
568
|
+
const manifest = objectValue(value, '$');
|
|
569
|
+
assertAllowedKeys(manifest, [
|
|
570
|
+
'manifestVersion', 'id', 'name', 'description', 'version', 'apiVersion',
|
|
571
|
+
'minAppVersion', 'platforms', 'entry', 'activationEvents', 'permissions',
|
|
572
|
+
'contributes', 'defaultLocale', 'locales', 'author', 'repository', 'license',
|
|
573
|
+
], '$');
|
|
574
|
+
const manifestVersion = integerValue(required(manifest, 'manifestVersion', '$'), '$.manifestVersion');
|
|
575
|
+
assertJsonIntegerToken(manifest, 'manifestVersion', '$.manifestVersion');
|
|
576
|
+
if (manifestVersion !== 1)
|
|
577
|
+
fail('manifest.unsupported-version', 'Only manifestVersion 1 is supported', '$.manifestVersion');
|
|
578
|
+
const pluginId = validatePluginId(required(manifest, 'id', '$'));
|
|
579
|
+
validateText(required(manifest, 'name', '$'), '$.name', 100);
|
|
580
|
+
if (Object.hasOwn(manifest, 'description'))
|
|
581
|
+
validateText(manifest.description, '$.description', 500);
|
|
582
|
+
validateSemver(required(manifest, 'version', '$'), '$.version', true);
|
|
583
|
+
const apiRequirement = stringValue(required(manifest, 'apiVersion', '$'), '$.apiVersion');
|
|
584
|
+
parseApiRequirement(apiRequirement, '$.apiVersion');
|
|
585
|
+
const minimumAppVersion = validateSemver(required(manifest, 'minAppVersion', '$'), '$.minAppVersion', true);
|
|
586
|
+
const platforms = arrayValue(required(manifest, 'platforms', '$'), '$.platforms');
|
|
587
|
+
if (platforms.length === 0)
|
|
588
|
+
fail('manifest.missing-platform', 'Plugin platforms must not be empty', '$.platforms');
|
|
589
|
+
const platformNames = platforms.map((platform, index) => stringValue(platform, `$.platforms[${index}]`));
|
|
590
|
+
if (platformNames.some((platform) => !['desktop', 'ios', 'android'].includes(platform))) {
|
|
591
|
+
fail('manifest.invalid-platform', 'Plugin platforms contains an unsupported value', '$.platforms');
|
|
592
|
+
}
|
|
593
|
+
if (new Set(platformNames).size !== platformNames.length) {
|
|
594
|
+
fail('manifest.duplicate-platform', 'Plugin platforms contains duplicate entries', '$.platforms');
|
|
595
|
+
}
|
|
596
|
+
if (!platformNames.includes('desktop')) {
|
|
597
|
+
fail('manifest.desktop-required', 'Community plugin packages must include desktop', '$.platforms');
|
|
598
|
+
}
|
|
599
|
+
const entry = validatePackagePath(stringValue(required(manifest, 'entry', '$'), '$.entry'), { label: '$.entry' });
|
|
600
|
+
validateEntry(entry, options.files);
|
|
601
|
+
validatePermissionDeclarations(required(manifest, 'permissions', '$'));
|
|
602
|
+
const contributions = validateContributions(required(manifest, 'contributes', '$'), pluginId);
|
|
603
|
+
validateActivationEvents(required(manifest, 'activationEvents', '$'), contributions.commandIds);
|
|
604
|
+
validateLocalization(manifest, contributions.localizedTexts, options.files);
|
|
605
|
+
if (!satisfiesPluginApiRequirement(options.apiVersion ?? PLUGIN_API_VERSION, apiRequirement)) {
|
|
606
|
+
fail('manifest.incompatible-api', `Plugin requires API ${apiRequirement}`, '$.apiVersion');
|
|
607
|
+
}
|
|
608
|
+
if (options.appVersion !== undefined) {
|
|
609
|
+
const appVersion = validateSemver(options.appVersion, 'appVersion', true);
|
|
610
|
+
if (compareSemver(minimumAppVersion, appVersion) > 0) {
|
|
611
|
+
fail('manifest.incompatible-app', `Plugin requires NoteGen ${minimumAppVersion} or newer`, '$.minAppVersion');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (Object.hasOwn(manifest, 'author')) {
|
|
615
|
+
const author = objectValue(manifest.author, '$.author');
|
|
616
|
+
assertAllowedKeys(author, ['name', 'url'], '$.author');
|
|
617
|
+
validateText(required(author, 'name', '$.author'), '$.author.name', 120);
|
|
618
|
+
if (Object.hasOwn(author, 'url'))
|
|
619
|
+
validatePublicUrl(author.url, '$.author.url');
|
|
620
|
+
}
|
|
621
|
+
if (Object.hasOwn(manifest, 'repository'))
|
|
622
|
+
validatePublicUrl(manifest.repository, '$.repository');
|
|
623
|
+
if (Object.hasOwn(manifest, 'license'))
|
|
624
|
+
validateText(manifest.license, '$.license', 80);
|
|
625
|
+
const typed = manifest;
|
|
626
|
+
let folderBindingSeen = false;
|
|
627
|
+
for (const setting of typed.contributes.settings ?? []) {
|
|
628
|
+
if (setting.type !== 'string' || setting.permissionPaths === undefined)
|
|
629
|
+
continue;
|
|
630
|
+
const paths = setting.permissionPaths;
|
|
631
|
+
if (folderBindingSeen || setting.scope !== 'workspace' || !Array.isArray(paths) || !paths.length || paths.length > 20
|
|
632
|
+
|| new Set(paths).size !== paths.length || paths.some(name => {
|
|
633
|
+
if (typeof name !== 'string' || !Object.hasOwn(typed.permissions, name))
|
|
634
|
+
return true;
|
|
635
|
+
const declaration = typed.permissions[name];
|
|
636
|
+
return !declaration || declaration.scope !== 'workspace-folder' || declaration.optional;
|
|
637
|
+
}))
|
|
638
|
+
fail('manifest.invalid-setting', 'Invalid workspace folder permission binding', '$.contributes.settings');
|
|
639
|
+
folderBindingSeen = true;
|
|
640
|
+
}
|
|
641
|
+
return typed;
|
|
642
|
+
}
|
|
643
|
+
export function parsePluginManifest(input, options = {}) {
|
|
644
|
+
return validatePluginManifest(parseStrictJson(input, 'plugin.json'), options);
|
|
645
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PluginManifestV1 } from '@notegen/plugin-api';
|
|
2
|
+
import { type IntegrityManifestV1, type PackageFileMap } from './integrity.js';
|
|
3
|
+
export interface PackageValidationOptions {
|
|
4
|
+
readonly apiVersion?: string;
|
|
5
|
+
readonly appVersion?: string;
|
|
6
|
+
readonly publicKey?: string;
|
|
7
|
+
readonly requireSignature?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ValidatedPluginPackage {
|
|
10
|
+
readonly manifest: PluginManifestV1;
|
|
11
|
+
readonly manifestValue: unknown;
|
|
12
|
+
readonly integrity: IntegrityManifestV1;
|
|
13
|
+
readonly integrityValue: unknown;
|
|
14
|
+
readonly files: PackageFileMap;
|
|
15
|
+
readonly signature: string | undefined;
|
|
16
|
+
readonly signatureVerified: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function validatePackageFiles(files: PackageFileMap, options?: PackageValidationOptions): ValidatedPluginPackage;
|
|
19
|
+
export declare function readPackageDirectory(directory: string): Promise<Map<string, Buffer>>;
|
|
20
|
+
/** Reads every entry in a complete package directory, unlike development import. */
|
|
21
|
+
export declare function readCompletePackageDirectory(directory: string): Promise<Map<string, Buffer>>;
|