@cervio/plugin-core 0.1.1 → 1.0.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 +1 -1
- package/dist/browser-Ddj__JFI.d.ts +141 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +166 -0
- package/dist/index.d.ts +63 -368
- package/dist/index.js +390 -155
- package/manifest.schema.json +159 -4
- package/package.json +36 -21
- package/dist/index.cjs +0 -549
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -427
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import JSZip from 'jszip';
|
|
1
|
+
import { unzipSync, strFromU8, zipSync } from 'fflate';
|
|
3
2
|
import { createHash } from 'crypto';
|
|
4
3
|
import { promises } from 'fs';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
|
|
7
6
|
// src/types.ts
|
|
8
|
-
var PLUGIN_ARTIFACT_BUCKET = "plugin-artifacts";
|
|
9
7
|
var MAX_PLUGIN_ZIP_BYTES = 50 * 1024 * 1024;
|
|
10
8
|
var MAX_PLUGIN_ZIP_ENTRIES = 5e3;
|
|
11
9
|
var MAX_PLUGIN_UNCOMPRESSED_BYTES = 250 * 1024 * 1024;
|
|
10
|
+
var PLUGIN_ARTIFACT_SOFT_WARN_BYTES = 1 * 1024 * 1024;
|
|
11
|
+
var PLUGIN_ARTIFACT_SOFT_STRONG_WARN_BYTES = 3 * 1024 * 1024;
|
|
12
12
|
var PLUGIN_KEY_RE = /^[a-z0-9][a-z0-9-_]{1,62}$/;
|
|
13
|
-
var PLUGIN_VERSION_SELECT = "id, plugin_id, version, changelog, artifact_path, artifact_entry_path, artifact_sha256, content_sha256, artifact_size_bytes, created_at, deleted_at, deleted_by";
|
|
14
13
|
var DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES = ["calendar"];
|
|
15
14
|
|
|
16
15
|
// src/entity-requirements.ts
|
|
17
16
|
var ENTITY_NAME_RE = /^[a-z][a-z0-9_-]{0,62}$/i;
|
|
18
17
|
var FIELD_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/i;
|
|
19
18
|
var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]);
|
|
19
|
+
var DECLARABLE_HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
20
|
+
var CERVIO_API_PREFIX = "/api/v1/";
|
|
21
|
+
var MAX_CERVIO_API_PREFIX_LEN = 256;
|
|
22
|
+
var CERVIO_API_PREFIX_SAFE_RE = /^\/api\/v1\/[a-zA-Z0-9][a-zA-Z0-9._/-]{0,253}$/;
|
|
20
23
|
function asStringArray(raw, nameRe) {
|
|
21
24
|
if (!Array.isArray(raw)) return [];
|
|
22
25
|
const out = [];
|
|
@@ -59,6 +62,61 @@ function parsePluginEntityRequirements(raw) {
|
|
|
59
62
|
}
|
|
60
63
|
return out;
|
|
61
64
|
}
|
|
65
|
+
function readCervioApiPath(row) {
|
|
66
|
+
return typeof row.path_prefix === "string" ? row.path_prefix.trim() : typeof row.pathPrefix === "string" ? row.pathPrefix.trim() : "";
|
|
67
|
+
}
|
|
68
|
+
function isDeclarableCervioApiPrefix(path2) {
|
|
69
|
+
if (!path2.startsWith(CERVIO_API_PREFIX)) return false;
|
|
70
|
+
if (path2.length > MAX_CERVIO_API_PREFIX_LEN) return false;
|
|
71
|
+
if (path2.includes("..") || path2.includes("//") || path2.includes("\\") || path2.includes("*") || path2.includes("?")) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!CERVIO_API_PREFIX_SAFE_RE.test(path2.replace(/\/$/, "") || path2)) return false;
|
|
75
|
+
const rest = path2.slice(CERVIO_API_PREFIX.length);
|
|
76
|
+
const firstSeg = rest.split("/").filter(Boolean)[0]?.toLowerCase();
|
|
77
|
+
if (!firstSeg) return false;
|
|
78
|
+
if (firstSeg === "admin") return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function isDeclarableCervioApiRequirement(row) {
|
|
82
|
+
return DECLARABLE_HTTP_METHODS.has(row.method.toUpperCase()) && isDeclarableCervioApiPrefix(row.path_prefix);
|
|
83
|
+
}
|
|
84
|
+
function pathMatchesCervioApiPrefix(pathname, prefix) {
|
|
85
|
+
if (!prefix.startsWith("/") || prefix.includes("..") || prefix.includes("//")) return false;
|
|
86
|
+
if (pathname === prefix) return true;
|
|
87
|
+
if (prefix.endsWith("/")) return pathname.startsWith(prefix);
|
|
88
|
+
return pathname.startsWith(`${prefix}/`);
|
|
89
|
+
}
|
|
90
|
+
function isDeclaredCervioApiRequest(method, pathname, declared) {
|
|
91
|
+
const m = method.trim().toUpperCase();
|
|
92
|
+
if (!declared.length) return false;
|
|
93
|
+
for (const row of declared) {
|
|
94
|
+
const declaredMethod = row.method.trim().toUpperCase();
|
|
95
|
+
const methodOk = declaredMethod === m || declaredMethod === "*" || m === "HEAD" && declaredMethod === "GET";
|
|
96
|
+
if (!methodOk) continue;
|
|
97
|
+
if (pathMatchesCervioApiPrefix(pathname, row.path_prefix)) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function validatePluginCervioApiRequirements(raw) {
|
|
102
|
+
if (raw == null) return { ok: true, value: [] };
|
|
103
|
+
if (!Array.isArray(raw)) return { ok: false, error: "manifest_cervio_apis_not_declarable" };
|
|
104
|
+
for (const item of raw) {
|
|
105
|
+
if (!item || typeof item !== "object") continue;
|
|
106
|
+
const row = item;
|
|
107
|
+
const path2 = readCervioApiPath(row);
|
|
108
|
+
if (!path2) continue;
|
|
109
|
+
if (!path2.startsWith("/")) continue;
|
|
110
|
+
const methodRaw = typeof row.method === "string" ? row.method.trim().toUpperCase() : "GET";
|
|
111
|
+
if (!DECLARABLE_HTTP_METHODS.has(methodRaw)) {
|
|
112
|
+
return { ok: false, error: "manifest_cervio_apis_not_declarable" };
|
|
113
|
+
}
|
|
114
|
+
if (!isDeclarableCervioApiPrefix(path2)) {
|
|
115
|
+
return { ok: false, error: "manifest_cervio_apis_not_declarable" };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, value: parsePluginCervioApiRequirements(raw) };
|
|
119
|
+
}
|
|
62
120
|
function parsePluginCervioApiRequirements(raw) {
|
|
63
121
|
if (!Array.isArray(raw)) return [];
|
|
64
122
|
const out = [];
|
|
@@ -68,7 +126,7 @@ function parsePluginCervioApiRequirements(raw) {
|
|
|
68
126
|
const row = item;
|
|
69
127
|
const methodRaw = typeof row.method === "string" ? row.method.trim().toUpperCase() : "GET";
|
|
70
128
|
const method = HTTP_METHODS.has(methodRaw) ? methodRaw : "GET";
|
|
71
|
-
const path2 =
|
|
129
|
+
const path2 = readCervioApiPath(row);
|
|
72
130
|
if (!path2 || !path2.startsWith("/")) continue;
|
|
73
131
|
const key = `${method}:${path2}`;
|
|
74
132
|
if (seen.has(key)) continue;
|
|
@@ -78,6 +136,152 @@ function parsePluginCervioApiRequirements(raw) {
|
|
|
78
136
|
return out;
|
|
79
137
|
}
|
|
80
138
|
|
|
139
|
+
// src/boot-requests.ts
|
|
140
|
+
var MAX_PLUGIN_BOOT_REQUESTS = 8;
|
|
141
|
+
var MAX_BOOT_PATH_LEN = 400;
|
|
142
|
+
function bootRequestPathname(path2) {
|
|
143
|
+
if (typeof path2 !== "string") return null;
|
|
144
|
+
const p = path2.trim();
|
|
145
|
+
if (!p.startsWith("/api/v1/") || p.length > MAX_BOOT_PATH_LEN || p.includes("#")) return null;
|
|
146
|
+
const pathname = p.split("?")[0] ?? "";
|
|
147
|
+
if (!isDeclarableCervioApiPrefix(pathname)) return null;
|
|
148
|
+
return pathname;
|
|
149
|
+
}
|
|
150
|
+
function validatePluginBootRequests(raw, cervioApis) {
|
|
151
|
+
if (raw == null) return { ok: true, value: [] };
|
|
152
|
+
if (!Array.isArray(raw)) return { ok: false, error: "manifest_boot_requests_invalid" };
|
|
153
|
+
if (raw.length > MAX_PLUGIN_BOOT_REQUESTS) return { ok: false, error: "manifest_boot_requests_too_many" };
|
|
154
|
+
const out = [];
|
|
155
|
+
const seen = /* @__PURE__ */ new Set();
|
|
156
|
+
for (const item of raw) {
|
|
157
|
+
const path2 = item && typeof item === "object" ? item.path : void 0;
|
|
158
|
+
const pathname = typeof path2 === "string" ? bootRequestPathname(path2) : null;
|
|
159
|
+
if (!pathname) return { ok: false, error: "manifest_boot_requests_invalid" };
|
|
160
|
+
if (!isDeclaredCervioApiRequest("GET", pathname, cervioApis)) {
|
|
161
|
+
return { ok: false, error: "manifest_boot_requests_not_declared" };
|
|
162
|
+
}
|
|
163
|
+
const p = path2.trim();
|
|
164
|
+
if (seen.has(p)) continue;
|
|
165
|
+
seen.add(p);
|
|
166
|
+
out.push({ path: p });
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, value: out };
|
|
169
|
+
}
|
|
170
|
+
function parsePluginBootRequests(raw) {
|
|
171
|
+
if (!Array.isArray(raw)) return [];
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const item of raw.slice(0, MAX_PLUGIN_BOOT_REQUESTS)) {
|
|
174
|
+
const path2 = item && typeof item === "object" ? item.path : void 0;
|
|
175
|
+
if (typeof path2 === "string" && bootRequestPathname(path2)) out.push({ path: path2.trim() });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
var MAX_COMPRESSION_RATIO = 100;
|
|
180
|
+
var RATIO_CHECK_FROM_BYTES = 1024 * 1024;
|
|
181
|
+
function checkEntry(entry) {
|
|
182
|
+
const { name, size, originalSize } = entry;
|
|
183
|
+
if (name.includes("..") || name.includes("\\") || name.startsWith("/")) {
|
|
184
|
+
return "zip_unsafe_entry_path";
|
|
185
|
+
}
|
|
186
|
+
if (/\.zip$/i.test(name)) return "zip_nested_archive";
|
|
187
|
+
if (originalSize > RATIO_CHECK_FROM_BYTES) {
|
|
188
|
+
if (size === 0 || originalSize / size > MAX_COMPRESSION_RATIO) {
|
|
189
|
+
return "zip_compression_ratio_too_high";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
async function readPluginZip(data) {
|
|
195
|
+
const input = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
196
|
+
if (input.byteLength > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: "file_too_large" };
|
|
197
|
+
let error = null;
|
|
198
|
+
let entries = 0;
|
|
199
|
+
let declaredTotal = 0;
|
|
200
|
+
let files;
|
|
201
|
+
try {
|
|
202
|
+
files = unzipSync(input, {
|
|
203
|
+
filter: (entry) => {
|
|
204
|
+
if (error) return false;
|
|
205
|
+
entries += 1;
|
|
206
|
+
if (entries > MAX_PLUGIN_ZIP_ENTRIES) {
|
|
207
|
+
error = "zip_too_many_entries";
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
error = checkEntry(entry);
|
|
211
|
+
if (error) return false;
|
|
212
|
+
if (entry.name.endsWith("/")) return false;
|
|
213
|
+
declaredTotal += entry.originalSize;
|
|
214
|
+
if (declaredTotal > MAX_PLUGIN_UNCOMPRESSED_BYTES) {
|
|
215
|
+
error = "zip_uncompressed_too_large";
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} catch {
|
|
222
|
+
files = null;
|
|
223
|
+
}
|
|
224
|
+
if (error) return { ok: false, error };
|
|
225
|
+
if (!files) return { ok: false, error: "zip_invalid" };
|
|
226
|
+
let actualTotal = 0;
|
|
227
|
+
for (const bytes of Object.values(files)) actualTotal += bytes.byteLength;
|
|
228
|
+
if (actualTotal > MAX_PLUGIN_UNCOMPRESSED_BYTES) {
|
|
229
|
+
return { ok: false, error: "zip_uncompressed_too_large" };
|
|
230
|
+
}
|
|
231
|
+
const paths = Object.keys(files);
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
value: {
|
|
235
|
+
paths,
|
|
236
|
+
bytes: (path2) => files[path2],
|
|
237
|
+
text: (path2) => {
|
|
238
|
+
const bytes = files[path2];
|
|
239
|
+
return bytes === void 0 ? void 0 : strFromU8(bytes);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function assertZipFile(file) {
|
|
245
|
+
const name = (file.name ?? "upload.zip").toLowerCase();
|
|
246
|
+
if (!name.endsWith(".zip")) return { ok: false, error: "invalid_file_extension" };
|
|
247
|
+
if (file.size <= 0) return { ok: false, error: "empty_file" };
|
|
248
|
+
if (file.size > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: "file_too_large" };
|
|
249
|
+
return { ok: true, value: true };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/semver.ts
|
|
253
|
+
var CORE_SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
|
|
254
|
+
function nextPatchVersion(latest) {
|
|
255
|
+
if (!latest || typeof latest !== "string") return "0.1.0";
|
|
256
|
+
const m = latest.trim().match(CORE_SEMVER_RE);
|
|
257
|
+
if (!m) return "0.1.0";
|
|
258
|
+
const major = Number(m[1]);
|
|
259
|
+
const minor = Number(m[2]);
|
|
260
|
+
const patch = Number(m[3]);
|
|
261
|
+
if (![major, minor, patch].every((n) => Number.isFinite(n) && n >= 0)) return "0.1.0";
|
|
262
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
263
|
+
}
|
|
264
|
+
function compareSemverDesc(a, b) {
|
|
265
|
+
const pa = a.match(CORE_SEMVER_RE);
|
|
266
|
+
const pb = b.match(CORE_SEMVER_RE);
|
|
267
|
+
if (!pa && !pb) return 0;
|
|
268
|
+
if (!pa) return 1;
|
|
269
|
+
if (!pb) return -1;
|
|
270
|
+
for (let i = 1; i <= 3; i++) {
|
|
271
|
+
const da = Number(pa[i]) - Number(pb[i]);
|
|
272
|
+
if (da !== 0) return da > 0 ? -1 : 1;
|
|
273
|
+
}
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/policy-types.ts
|
|
278
|
+
function isPolicyPluginType(t) {
|
|
279
|
+
return t === "policy" || t === "server";
|
|
280
|
+
}
|
|
281
|
+
function normalizeToPolicyPluginType(t) {
|
|
282
|
+
return isPolicyPluginType(t) ? "policy" : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
81
285
|
// src/connections.ts
|
|
82
286
|
var EXTERNAL_CONNECTION_ALLOWED_HOSTS = {
|
|
83
287
|
microsoft: ["graph.microsoft.com"],
|
|
@@ -178,102 +382,91 @@ function parsePluginConnections(raw) {
|
|
|
178
382
|
return out;
|
|
179
383
|
}
|
|
180
384
|
|
|
181
|
-
// src/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
}
|
|
385
|
+
// src/settings-requirements.ts
|
|
386
|
+
var SETTINGS_KEY_RE = /^[a-z][a-z0-9_]{0,62}$/i;
|
|
387
|
+
var SETTINGS_TYPES = /* @__PURE__ */ new Set(["string", "boolean", "select", "multiselect"]);
|
|
388
|
+
var RESERVED_SETTINGS_KEYS = /* @__PURE__ */ new Set(["debug"]);
|
|
389
|
+
function parseOptions(raw) {
|
|
390
|
+
if (!Array.isArray(raw)) return [];
|
|
391
|
+
const out = [];
|
|
392
|
+
const seen = /* @__PURE__ */ new Set();
|
|
393
|
+
for (const item of raw) {
|
|
394
|
+
if (!item || typeof item !== "object") continue;
|
|
395
|
+
const row = item;
|
|
396
|
+
const value = typeof row.value === "string" ? row.value.trim() : "";
|
|
397
|
+
const label = typeof row.label === "string" ? row.label.trim() : "";
|
|
398
|
+
if (!value || !label || seen.has(value)) continue;
|
|
399
|
+
seen.add(value);
|
|
400
|
+
out.push({ value, label });
|
|
197
401
|
}
|
|
198
|
-
return
|
|
402
|
+
return out;
|
|
199
403
|
}
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
205
|
-
|
|
404
|
+
function parseDefault(raw, type, options) {
|
|
405
|
+
const allowedValues = new Set(options.map((o) => o.value));
|
|
406
|
+
if (type === "boolean") return typeof raw === "boolean" ? raw : void 0;
|
|
407
|
+
if (type === "string") return typeof raw === "string" ? raw : void 0;
|
|
408
|
+
if (type === "select") {
|
|
409
|
+
return typeof raw === "string" && allowedValues.has(raw) ? raw : void 0;
|
|
410
|
+
}
|
|
411
|
+
if (type === "multiselect") {
|
|
412
|
+
if (!Array.isArray(raw)) return void 0;
|
|
413
|
+
const values = raw.filter((v) => typeof v === "string" && allowedValues.has(v));
|
|
414
|
+
return values.length ? values : void 0;
|
|
415
|
+
}
|
|
416
|
+
return void 0;
|
|
206
417
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
418
|
+
function parsePluginSettingsRequirements(raw) {
|
|
419
|
+
if (!Array.isArray(raw)) return [];
|
|
420
|
+
const out = [];
|
|
421
|
+
const seen = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const item of raw) {
|
|
423
|
+
if (!item || typeof item !== "object") continue;
|
|
424
|
+
const row = item;
|
|
425
|
+
const key = typeof row.key === "string" ? row.key.trim() : "";
|
|
426
|
+
if (!key || !SETTINGS_KEY_RE.test(key) || seen.has(key) || RESERVED_SETTINGS_KEYS.has(key)) continue;
|
|
427
|
+
const type = typeof row.type === "string" ? row.type.trim() : null;
|
|
428
|
+
if (!type || !SETTINGS_TYPES.has(type)) continue;
|
|
429
|
+
const label = typeof row.label === "string" ? row.label.trim() : "";
|
|
430
|
+
if (!label) continue;
|
|
431
|
+
const options = parseOptions(row.options);
|
|
432
|
+
if ((type === "select" || type === "multiselect") && options.length === 0) continue;
|
|
433
|
+
const hint = typeof row.hint === "string" && row.hint.trim() ? row.hint.trim() : void 0;
|
|
434
|
+
const placeholder = typeof row.placeholder === "string" && row.placeholder.trim() ? row.placeholder.trim() : void 0;
|
|
435
|
+
const defaultValue = parseDefault(row.default, type, options);
|
|
436
|
+
seen.add(key);
|
|
437
|
+
const field = { key, type, label };
|
|
438
|
+
if (hint) field.hint = hint;
|
|
439
|
+
if (placeholder) field.placeholder = placeholder;
|
|
440
|
+
if (defaultValue !== void 0) field.default = defaultValue;
|
|
441
|
+
if (options.length) field.options = options;
|
|
442
|
+
out.push(field);
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
219
445
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (da !== 0) return da > 0 ? -1 : 1;
|
|
446
|
+
|
|
447
|
+
// src/policy-entry.ts
|
|
448
|
+
var BARE_IMPORT_RE = /(?:from|import)\s*\(\s*['"](?![./]|data:)([^'"]+)['"]\s*\)|(?:from|import)\s+['"](?![./]|data:)([^'"]+)['"]/g;
|
|
449
|
+
function findBareModuleSpecifiers(source) {
|
|
450
|
+
const found = /* @__PURE__ */ new Set();
|
|
451
|
+
for (const match of source.matchAll(BARE_IMPORT_RE)) {
|
|
452
|
+
const spec = (match[1] || match[2] || "").trim();
|
|
453
|
+
if (spec) found.add(spec);
|
|
229
454
|
}
|
|
230
|
-
return
|
|
455
|
+
return [...found];
|
|
231
456
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
z.object({
|
|
244
|
-
id: z.string(),
|
|
245
|
-
provider: z.string(),
|
|
246
|
-
scopes: z.array(z.string()),
|
|
247
|
-
allowed_apis: z.array(
|
|
248
|
-
z.object({
|
|
249
|
-
host: z.string(),
|
|
250
|
-
methods: z.array(z.string()),
|
|
251
|
-
path_prefix: z.string().optional(),
|
|
252
|
-
pathPrefix: z.string().optional()
|
|
253
|
-
})
|
|
254
|
-
).optional(),
|
|
255
|
-
allowedApis: z.array(z.unknown()).optional()
|
|
256
|
-
})
|
|
257
|
-
).optional(),
|
|
258
|
-
entities: z.array(
|
|
259
|
-
z.object({
|
|
260
|
-
name: z.string(),
|
|
261
|
-
fields: z.array(z.string()).optional(),
|
|
262
|
-
forms: z.array(z.string()).optional(),
|
|
263
|
-
access: z.array(z.enum(["read", "write"])).optional()
|
|
264
|
-
})
|
|
265
|
-
).optional(),
|
|
266
|
-
cervio_apis: z.array(
|
|
267
|
-
z.object({
|
|
268
|
-
method: z.string().optional(),
|
|
269
|
-
path_prefix: z.string().optional(),
|
|
270
|
-
pathPrefix: z.string().optional()
|
|
271
|
-
})
|
|
272
|
-
).optional(),
|
|
273
|
-
cervioApis: z.array(z.unknown()).optional()
|
|
274
|
-
}).passthrough();
|
|
457
|
+
function assertPolicyEntrySelfContained(source) {
|
|
458
|
+
const specifiers = findBareModuleSpecifiers(source);
|
|
459
|
+
if (specifiers.length === 0) return { ok: true };
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
error: "policy_entry_not_self_contained",
|
|
463
|
+
specifiers
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/manifest.ts
|
|
275
468
|
function normalizePluginKey(raw) {
|
|
276
|
-
return raw.trim().toLowerCase().replace(/[
|
|
469
|
+
return raw.trim().toLowerCase().replace(/[/\s]+/g, "-").replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
277
470
|
}
|
|
278
471
|
function resolvePluginType(raw) {
|
|
279
472
|
const kindMap = {
|
|
@@ -281,11 +474,16 @@ function resolvePluginType(raw) {
|
|
|
281
474
|
task: "task",
|
|
282
475
|
form: "form",
|
|
283
476
|
task_form: "task_form",
|
|
477
|
+
// Legacy wire → canonical Policy Plugin type
|
|
478
|
+
server: "policy",
|
|
479
|
+
policy: "policy",
|
|
284
480
|
task_widget: "task",
|
|
285
481
|
record_panel: "form"
|
|
286
482
|
};
|
|
287
483
|
const candidate = kindMap[raw] ?? raw;
|
|
288
|
-
return ["task", "form", "task_form", "route"].includes(
|
|
484
|
+
return ["task", "form", "task_form", "route", "policy", "server"].includes(
|
|
485
|
+
candidate
|
|
486
|
+
) ? candidate : null;
|
|
289
487
|
}
|
|
290
488
|
function parseRequiredCapabilities(raw, allowed) {
|
|
291
489
|
if (!Array.isArray(raw)) return [];
|
|
@@ -319,9 +517,15 @@ function parseManifestObject(parsed, options) {
|
|
|
319
517
|
);
|
|
320
518
|
const connections = parsePluginConnections(parsed.connections);
|
|
321
519
|
const entities = parsePluginEntityRequirements(parsed.entities);
|
|
322
|
-
const
|
|
520
|
+
const cervioApisRes = validatePluginCervioApiRequirements(
|
|
323
521
|
parsed.cervio_apis ?? parsed.cervioApis ?? []
|
|
324
522
|
);
|
|
523
|
+
if (!cervioApisRes.ok) return cervioApisRes;
|
|
524
|
+
const cervioApis = cervioApisRes.value;
|
|
525
|
+
const settings = parsePluginSettingsRequirements(parsed.settings);
|
|
526
|
+
const bootRequestsRes = validatePluginBootRequests(parsed.boot_requests, cervioApis);
|
|
527
|
+
if (!bootRequestsRes.ok) return bootRequestsRes;
|
|
528
|
+
const bootRequests = bootRequestsRes.value;
|
|
325
529
|
return {
|
|
326
530
|
ok: true,
|
|
327
531
|
value: {
|
|
@@ -332,7 +536,9 @@ function parseManifestObject(parsed, options) {
|
|
|
332
536
|
requiredConnectionCapabilities,
|
|
333
537
|
connections,
|
|
334
538
|
entities,
|
|
335
|
-
cervioApis
|
|
539
|
+
cervioApis,
|
|
540
|
+
settings,
|
|
541
|
+
bootRequests
|
|
336
542
|
}
|
|
337
543
|
};
|
|
338
544
|
}
|
|
@@ -341,26 +547,14 @@ function manifestJsonForContentHash(raw) {
|
|
|
341
547
|
delete clone.version;
|
|
342
548
|
return JSON.stringify(clone);
|
|
343
549
|
}
|
|
344
|
-
async function loadZipFromBuffer(buffer) {
|
|
345
|
-
try {
|
|
346
|
-
const zip = await JSZip.loadAsync(buffer);
|
|
347
|
-
const safe = assertZipSafe(zip);
|
|
348
|
-
if (!safe.ok) return safe;
|
|
349
|
-
return { ok: true, value: zip };
|
|
350
|
-
} catch {
|
|
351
|
-
return { ok: false, error: "zip_invalid" };
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
550
|
async function parseZipManifest(file, options) {
|
|
355
|
-
const
|
|
356
|
-
const zipRes = await loadZipFromBuffer(buffer);
|
|
551
|
+
const zipRes = await readPluginZip(file instanceof Blob ? await file.arrayBuffer() : file);
|
|
357
552
|
if (!zipRes.ok) return zipRes;
|
|
358
553
|
const zip = zipRes.value;
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
const jsonText = await manifestEntry.async("text");
|
|
554
|
+
const has = (path2) => zip.bytes(path2) !== void 0;
|
|
555
|
+
const manifestEntryName = ["manifest.json", "manifest.example.json"].find(has) ?? zip.paths.find((p) => p.endsWith("/manifest.json") || p.endsWith("/manifest.example.json"));
|
|
556
|
+
if (!manifestEntryName) return { ok: false, error: "manifest_not_found_in_zip" };
|
|
557
|
+
const jsonText = zip.text(manifestEntryName) ?? "";
|
|
364
558
|
let parsed;
|
|
365
559
|
try {
|
|
366
560
|
parsed = JSON.parse(jsonText);
|
|
@@ -369,36 +563,51 @@ async function parseZipManifest(file, options) {
|
|
|
369
563
|
}
|
|
370
564
|
const fields = parseManifestObject(parsed, options);
|
|
371
565
|
if (!fields.ok) return fields;
|
|
372
|
-
const
|
|
373
|
-
if (!
|
|
374
|
-
|
|
375
|
-
|
|
566
|
+
const isPolicy = isPolicyPluginType(fields.value.pluginType);
|
|
567
|
+
if (!isPolicy) {
|
|
568
|
+
if (!has("dist/index.html")) return { ok: false, error: "build_index_missing" };
|
|
569
|
+
const hasDistAssets = zip.paths.some((name) => /^dist\/assets\/.+/.test(name));
|
|
570
|
+
if (!hasDistAssets) return { ok: false, error: "build_assets_missing" };
|
|
571
|
+
}
|
|
572
|
+
const entryFromHooks = isPolicy && Array.isArray(parsed.hooks) ? (() => {
|
|
573
|
+
for (const h of parsed.hooks) {
|
|
574
|
+
if (typeof h?.entry === "string" && h.entry.trim()) return h.entry.trim();
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
})() : null;
|
|
578
|
+
const artifactEntryPath = isPolicy ? entryFromHooks || "server/index.js" : "dist/index.html";
|
|
579
|
+
if (isPolicy) {
|
|
580
|
+
const entryName = [artifactEntryPath, artifactEntryPath.replace(/^\//, "")].find(has) ?? zip.paths.find((p) => p.endsWith(`/${artifactEntryPath}`));
|
|
581
|
+
if (!entryName) return { ok: false, error: "manifest_not_found_in_zip" };
|
|
582
|
+
const entrySource = zip.text(entryName) ?? "";
|
|
583
|
+
const selfContained = assertPolicyEntrySelfContained(entrySource);
|
|
584
|
+
if (!selfContained.ok) {
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: `${selfContained.error}:${selfContained.specifiers.join(",")}`
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
376
591
|
return {
|
|
377
592
|
ok: true,
|
|
378
593
|
value: {
|
|
379
594
|
...fields.value,
|
|
380
|
-
artifactEntryPath
|
|
595
|
+
artifactEntryPath,
|
|
381
596
|
rawManifest: parsed,
|
|
382
|
-
manifestEntryName
|
|
597
|
+
manifestEntryName
|
|
383
598
|
}
|
|
384
599
|
};
|
|
385
600
|
}
|
|
386
|
-
|
|
601
|
+
function computePluginContentSha256(zip, rawManifest) {
|
|
387
602
|
try {
|
|
388
603
|
const hash = createHash("sha256");
|
|
389
604
|
hash.update("manifest\0");
|
|
390
605
|
hash.update(manifestJsonForContentHash(rawManifest));
|
|
391
606
|
hash.update("\0");
|
|
392
|
-
const distPaths =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return normalized === "dist" || normalized.startsWith("dist/");
|
|
397
|
-
}).map((name) => name.replace(/^\.\//, "")).sort((a, b) => a.localeCompare(b));
|
|
398
|
-
for (const path2 of distPaths) {
|
|
399
|
-
const entry = zip.file(path2);
|
|
400
|
-
if (!entry || entry.dir) continue;
|
|
401
|
-
const bytes = await entry.async("uint8array");
|
|
607
|
+
const distPaths = zip.paths.map((name) => ({ name, normalized: name.replace(/^\.\//, "") })).filter(({ normalized }) => normalized === "dist" || normalized.startsWith("dist/")).sort((a, b) => a.normalized.localeCompare(b.normalized));
|
|
608
|
+
for (const { name, normalized: path2 } of distPaths) {
|
|
609
|
+
const bytes = zip.bytes(name);
|
|
610
|
+
if (!bytes) continue;
|
|
402
611
|
hash.update("file\0");
|
|
403
612
|
hash.update(path2);
|
|
404
613
|
hash.update("\0");
|
|
@@ -413,6 +622,7 @@ async function computePluginContentSha256(zip, rawManifest) {
|
|
|
413
622
|
function sha256Hex(buffer) {
|
|
414
623
|
return createHash("sha256").update(buffer).digest("hex");
|
|
415
624
|
}
|
|
625
|
+
var ZIP_MTIME = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
|
|
416
626
|
async function collectFiles(dir, base = "") {
|
|
417
627
|
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
418
628
|
const out = [];
|
|
@@ -439,14 +649,46 @@ async function packPlugin(opts) {
|
|
|
439
649
|
}
|
|
440
650
|
const fields = parseManifestObject(manifestRaw, opts.options);
|
|
441
651
|
if (!fields.ok) return fields;
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
652
|
+
const isPolicy = isPolicyPluginType(fields.value.pluginType);
|
|
653
|
+
if (!isPolicy) {
|
|
654
|
+
const distIndex = path.join(root, "dist", "index.html");
|
|
655
|
+
try {
|
|
656
|
+
await promises.access(distIndex);
|
|
657
|
+
} catch {
|
|
658
|
+
return { ok: false, error: "build_index_missing" };
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
const entry = (Array.isArray(manifestRaw.hooks) ? manifestRaw.hooks.find((h) => h?.entry)?.entry : null) || "server/index.js";
|
|
662
|
+
const entryAbs = path.join(
|
|
663
|
+
/*turbopackIgnore: true*/
|
|
664
|
+
root,
|
|
665
|
+
entry
|
|
666
|
+
);
|
|
667
|
+
try {
|
|
668
|
+
await promises.access(entryAbs);
|
|
669
|
+
} catch {
|
|
670
|
+
return { ok: false, error: "manifest_not_found_in_zip" };
|
|
671
|
+
}
|
|
672
|
+
let entrySource;
|
|
673
|
+
try {
|
|
674
|
+
entrySource = await promises.readFile(
|
|
675
|
+
/*turbopackIgnore: true*/
|
|
676
|
+
entryAbs,
|
|
677
|
+
"utf8"
|
|
678
|
+
);
|
|
679
|
+
} catch {
|
|
680
|
+
return { ok: false, error: "manifest_not_found_in_zip" };
|
|
681
|
+
}
|
|
682
|
+
const selfContained = assertPolicyEntrySelfContained(entrySource);
|
|
683
|
+
if (!selfContained.ok) {
|
|
684
|
+
return {
|
|
685
|
+
ok: false,
|
|
686
|
+
error: `${selfContained.error}:${selfContained.specifiers.join(",")}`
|
|
687
|
+
};
|
|
688
|
+
}
|
|
447
689
|
}
|
|
448
|
-
const
|
|
449
|
-
const files = await collectFiles(root);
|
|
690
|
+
const entries = {};
|
|
691
|
+
const files = (await collectFiles(root)).sort((a, b) => a.rel.localeCompare(b.rel));
|
|
450
692
|
let hasAssets = false;
|
|
451
693
|
for (const file of files) {
|
|
452
694
|
if (file.rel.startsWith(".cervio/") || file.rel === "node_modules" || file.rel.startsWith("node_modules/")) {
|
|
@@ -455,18 +697,15 @@ async function packPlugin(opts) {
|
|
|
455
697
|
if (file.rel.startsWith("dist/assets/") && file.rel !== "dist/assets/") {
|
|
456
698
|
hasAssets = true;
|
|
457
699
|
}
|
|
458
|
-
|
|
459
|
-
zip.file(file.rel, data);
|
|
700
|
+
entries[file.rel] = new Uint8Array(await promises.readFile(file.abs));
|
|
460
701
|
}
|
|
461
|
-
if (!hasAssets) return { ok: false, error: "build_assets_missing" };
|
|
462
|
-
const
|
|
463
|
-
if (!safe.ok) return safe;
|
|
464
|
-
const buffer = Buffer.from(await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" }));
|
|
702
|
+
if (!isPolicy && !hasAssets) return { ok: false, error: "build_assets_missing" };
|
|
703
|
+
const buffer = Buffer.from(zipSync(entries, { level: 6, mtime: ZIP_MTIME }));
|
|
465
704
|
const fileCheck = assertZipFile({ name: `${fields.value.pluginKey}.zip`, size: buffer.length });
|
|
466
705
|
if (!fileCheck.ok) return fileCheck;
|
|
467
|
-
const loaded = await
|
|
706
|
+
const loaded = await readPluginZip(buffer);
|
|
468
707
|
if (!loaded.ok) return loaded;
|
|
469
|
-
const hash =
|
|
708
|
+
const hash = computePluginContentSha256(loaded.value, manifestRaw);
|
|
470
709
|
if (!hash.ok) return hash;
|
|
471
710
|
return {
|
|
472
711
|
ok: true,
|
|
@@ -481,8 +720,7 @@ async function packPlugin(opts) {
|
|
|
481
720
|
async function validatePluginDir(dir, options) {
|
|
482
721
|
const packed = await packPlugin({ dir, options });
|
|
483
722
|
if (!packed.ok) return packed;
|
|
484
|
-
const
|
|
485
|
-
const parsed = await parseZipManifest(blob, options);
|
|
723
|
+
const parsed = await parseZipManifest(packed.value.buffer, options);
|
|
486
724
|
if (!parsed.ok) return parsed;
|
|
487
725
|
return {
|
|
488
726
|
ok: true,
|
|
@@ -495,12 +733,11 @@ async function validatePluginDir(dir, options) {
|
|
|
495
733
|
async function validatePluginZipBuffer(buffer, options) {
|
|
496
734
|
const fileCheck = assertZipFile({ name: "plugin.zip", size: buffer.length });
|
|
497
735
|
if (!fileCheck.ok) return fileCheck;
|
|
498
|
-
const
|
|
499
|
-
const parsed = await parseZipManifest(blob, options);
|
|
736
|
+
const parsed = await parseZipManifest(buffer, options);
|
|
500
737
|
if (!parsed.ok) return parsed;
|
|
501
|
-
const zipRes = await
|
|
738
|
+
const zipRes = await readPluginZip(buffer);
|
|
502
739
|
if (!zipRes.ok) return zipRes;
|
|
503
|
-
const hash =
|
|
740
|
+
const hash = computePluginContentSha256(zipRes.value, parsed.value.rawManifest);
|
|
504
741
|
if (!hash.ok) return hash;
|
|
505
742
|
return {
|
|
506
743
|
ok: true,
|
|
@@ -508,6 +745,4 @@ async function validatePluginZipBuffer(buffer, options) {
|
|
|
508
745
|
};
|
|
509
746
|
}
|
|
510
747
|
|
|
511
|
-
export { DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES,
|
|
512
|
-
//# sourceMappingURL=index.js.map
|
|
513
|
-
//# sourceMappingURL=index.js.map
|
|
748
|
+
export { DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES, MAX_PLUGIN_UNCOMPRESSED_BYTES, MAX_PLUGIN_ZIP_BYTES, MAX_PLUGIN_ZIP_ENTRIES, PLUGIN_ARTIFACT_SOFT_STRONG_WARN_BYTES, PLUGIN_ARTIFACT_SOFT_WARN_BYTES, PLUGIN_KEY_RE, assertZipFile, compareSemverDesc, computePluginContentSha256, isDeclarableCervioApiRequirement, isDeclaredCervioApiRequest, isPolicyPluginType, manifestJsonForContentHash, nextPatchVersion, normalizePluginKey, normalizeToPolicyPluginType, packPlugin, parseManifestObject, parsePluginBootRequests, parsePluginCervioApiRequirements, parsePluginEntityRequirements, parseZipManifest, readPluginZip, sha256Hex, validatePluginDir, validatePluginZipBuffer };
|