@cervio/plugin-core 0.1.4 → 1.0.1
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 +62 -7
- package/dist/browser-Ddj__JFI.d.ts +141 -0
- package/dist/browser.d.ts +1 -2
- package/dist/browser.js +148 -31
- package/dist/index.d.ts +8 -467
- package/dist/index.js +302 -249
- package/manifest.schema.json +41 -0
- package/package.json +27 -17
- package/dist/browser-CbWn_0Kk.d.cts +0 -118
- package/dist/browser-CbWn_0Kk.d.ts +0 -118
- package/dist/browser.cjs +0 -56
- package/dist/browser.cjs.map +0 -1
- package/dist/browser.d.cts +0 -2
- package/dist/browser.js.map +0 -1
- package/dist/index.cjs +0 -736
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -581
- 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,66 +136,150 @@ function parsePluginCervioApiRequirements(raw) {
|
|
|
78
136
|
return out;
|
|
79
137
|
}
|
|
80
138
|
|
|
81
|
-
// src/
|
|
82
|
-
var
|
|
83
|
-
var
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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" };
|
|
87
154
|
const out = [];
|
|
88
155
|
const seen = /* @__PURE__ */ new Set();
|
|
89
156
|
for (const item of raw) {
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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() });
|
|
97
176
|
}
|
|
98
177
|
return out;
|
|
99
178
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
return
|
|
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";
|
|
106
185
|
}
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|
|
111
191
|
}
|
|
112
|
-
return
|
|
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
|
+
};
|
|
113
243
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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;
|
|
139
273
|
}
|
|
140
|
-
return
|
|
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;
|
|
141
283
|
}
|
|
142
284
|
|
|
143
285
|
// src/connections.ts
|
|
@@ -240,146 +382,66 @@ function parsePluginConnections(raw) {
|
|
|
240
382
|
return out;
|
|
241
383
|
}
|
|
242
384
|
|
|
243
|
-
// src/
|
|
244
|
-
var
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const uncompressed = entry._data?.uncompressedSize ?? 0;
|
|
260
|
-
const compressed = entry._data?.compressedSize ?? 0;
|
|
261
|
-
totalUncompressed += uncompressed;
|
|
262
|
-
if (totalUncompressed > MAX_PLUGIN_UNCOMPRESSED_BYTES) {
|
|
263
|
-
return { ok: false, error: "zip_uncompressed_too_large" };
|
|
264
|
-
}
|
|
265
|
-
if (compressed > 0 && uncompressed > 1024 * 1024) {
|
|
266
|
-
const ratio = uncompressed / compressed;
|
|
267
|
-
if (ratio > MAX_PLUGIN_COMPRESSION_RATIO) {
|
|
268
|
-
return { ok: false, error: "zip_compression_ratio_too_high" };
|
|
269
|
-
}
|
|
270
|
-
} else if (compressed === 0 && uncompressed > 1024 * 1024) {
|
|
271
|
-
return { ok: false, error: "zip_compression_ratio_too_high" };
|
|
272
|
-
}
|
|
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 });
|
|
273
401
|
}
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
function assertZipFile(file) {
|
|
277
|
-
const name = (file.name ?? "upload.zip").toLowerCase();
|
|
278
|
-
if (!name.endsWith(".zip")) return { ok: false, error: "invalid_file_extension" };
|
|
279
|
-
if (file.size <= 0) return { ok: false, error: "empty_file" };
|
|
280
|
-
if (file.size > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: "file_too_large" };
|
|
281
|
-
return { ok: true, value: true };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/semver.ts
|
|
285
|
-
var CORE_SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
|
|
286
|
-
function nextPatchVersion(latest) {
|
|
287
|
-
if (!latest || typeof latest !== "string") return "0.1.0";
|
|
288
|
-
const m = latest.trim().match(CORE_SEMVER_RE);
|
|
289
|
-
if (!m) return "0.1.0";
|
|
290
|
-
const major = Number(m[1]);
|
|
291
|
-
const minor = Number(m[2]);
|
|
292
|
-
const patch = Number(m[3]);
|
|
293
|
-
if (![major, minor, patch].every((n) => Number.isFinite(n) && n >= 0)) return "0.1.0";
|
|
294
|
-
return `${major}.${minor}.${patch + 1}`;
|
|
402
|
+
return out;
|
|
295
403
|
}
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
for (let i = 1; i <= 3; i++) {
|
|
303
|
-
const da = Number(pa[i]) - Number(pb[i]);
|
|
304
|
-
if (da !== 0) return da > 0 ? -1 : 1;
|
|
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;
|
|
305
410
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
name: z.string().optional().describe("Legacy alias for plugin_name."),
|
|
313
|
-
plugin_type: z.enum(["task", "form", "task_form", "route", "policy", "server"]).optional().describe("Use `policy` for Policy Plugins. `server` is a legacy alias for `policy`."),
|
|
314
|
-
kind: z.string().optional().describe("Legacy alias for plugin_type (task_widget\u2192task, record_panel\u2192form)."),
|
|
315
|
-
version: z.string().optional().describe("Ignored by Cervio - the host assigns release versions."),
|
|
316
|
-
hooks: z.array(
|
|
317
|
-
z.object({
|
|
318
|
-
event: z.enum(["before_create", "before_update"]),
|
|
319
|
-
entities: z.array(z.string()).optional(),
|
|
320
|
-
blocking: z.boolean().optional(),
|
|
321
|
-
identity: z.enum(["caller", "plugin_principal"]).optional(),
|
|
322
|
-
entry: z.string().optional()
|
|
323
|
-
})
|
|
324
|
-
).optional().describe("Policy Plugin hook declarations (plugin_type=policy; legacy server)."),
|
|
325
|
-
required_connection_capabilities: z.array(z.string()).optional(),
|
|
326
|
-
requiredConnectionCapabilities: z.array(z.string()).optional(),
|
|
327
|
-
connections: z.array(
|
|
328
|
-
z.object({
|
|
329
|
-
id: z.string(),
|
|
330
|
-
provider: z.string(),
|
|
331
|
-
scopes: z.array(z.string()),
|
|
332
|
-
allowed_apis: z.array(
|
|
333
|
-
z.object({
|
|
334
|
-
host: z.string(),
|
|
335
|
-
methods: z.array(z.string()),
|
|
336
|
-
path_prefix: z.string().optional(),
|
|
337
|
-
pathPrefix: z.string().optional()
|
|
338
|
-
})
|
|
339
|
-
).optional(),
|
|
340
|
-
allowedApis: z.array(z.unknown()).optional()
|
|
341
|
-
})
|
|
342
|
-
).optional(),
|
|
343
|
-
entities: z.array(
|
|
344
|
-
z.object({
|
|
345
|
-
name: z.string(),
|
|
346
|
-
fields: z.array(z.string()).optional(),
|
|
347
|
-
forms: z.array(z.string()).optional(),
|
|
348
|
-
access: z.array(z.enum(["read", "write"])).optional()
|
|
349
|
-
})
|
|
350
|
-
).optional(),
|
|
351
|
-
cervio_apis: z.array(
|
|
352
|
-
z.object({
|
|
353
|
-
method: z.string().optional(),
|
|
354
|
-
path_prefix: z.string().optional(),
|
|
355
|
-
pathPrefix: z.string().optional()
|
|
356
|
-
})
|
|
357
|
-
).optional(),
|
|
358
|
-
cervioApis: z.array(z.unknown()).optional(),
|
|
359
|
-
settings: z.array(
|
|
360
|
-
z.object({
|
|
361
|
-
key: z.string(),
|
|
362
|
-
type: z.enum(["string", "boolean", "select", "multiselect"]),
|
|
363
|
-
label: z.string(),
|
|
364
|
-
hint: z.string().optional(),
|
|
365
|
-
placeholder: z.string().optional(),
|
|
366
|
-
default: z.union([z.string(), z.boolean(), z.array(z.string())]).optional(),
|
|
367
|
-
options: z.array(
|
|
368
|
-
z.object({
|
|
369
|
-
value: z.string(),
|
|
370
|
-
label: z.string()
|
|
371
|
-
})
|
|
372
|
-
).optional().describe("Required for `select` / `multiselect`.")
|
|
373
|
-
})
|
|
374
|
-
).optional().describe("Admin-editable settings fields (drives Impostazioni dynamic form).")
|
|
375
|
-
}).passthrough();
|
|
376
|
-
|
|
377
|
-
// src/policy-types.ts
|
|
378
|
-
function isPolicyPluginType(t) {
|
|
379
|
-
return t === "policy" || t === "server";
|
|
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;
|
|
380
417
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
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;
|
|
383
445
|
}
|
|
384
446
|
|
|
385
447
|
// src/policy-entry.ts
|
|
@@ -401,8 +463,10 @@ function assertPolicyEntrySelfContained(source) {
|
|
|
401
463
|
specifiers
|
|
402
464
|
};
|
|
403
465
|
}
|
|
466
|
+
|
|
467
|
+
// src/manifest.ts
|
|
404
468
|
function normalizePluginKey(raw) {
|
|
405
|
-
return raw.trim().toLowerCase().replace(/[
|
|
469
|
+
return raw.trim().toLowerCase().replace(/[/\s]+/g, "-").replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
406
470
|
}
|
|
407
471
|
function resolvePluginType(raw) {
|
|
408
472
|
const kindMap = {
|
|
@@ -453,10 +517,15 @@ function parseManifestObject(parsed, options) {
|
|
|
453
517
|
);
|
|
454
518
|
const connections = parsePluginConnections(parsed.connections);
|
|
455
519
|
const entities = parsePluginEntityRequirements(parsed.entities);
|
|
456
|
-
const
|
|
520
|
+
const cervioApisRes = validatePluginCervioApiRequirements(
|
|
457
521
|
parsed.cervio_apis ?? parsed.cervioApis ?? []
|
|
458
522
|
);
|
|
523
|
+
if (!cervioApisRes.ok) return cervioApisRes;
|
|
524
|
+
const cervioApis = cervioApisRes.value;
|
|
459
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;
|
|
460
529
|
return {
|
|
461
530
|
ok: true,
|
|
462
531
|
value: {
|
|
@@ -468,7 +537,8 @@ function parseManifestObject(parsed, options) {
|
|
|
468
537
|
connections,
|
|
469
538
|
entities,
|
|
470
539
|
cervioApis,
|
|
471
|
-
settings
|
|
540
|
+
settings,
|
|
541
|
+
bootRequests
|
|
472
542
|
}
|
|
473
543
|
};
|
|
474
544
|
}
|
|
@@ -477,26 +547,14 @@ function manifestJsonForContentHash(raw) {
|
|
|
477
547
|
delete clone.version;
|
|
478
548
|
return JSON.stringify(clone);
|
|
479
549
|
}
|
|
480
|
-
async function loadZipFromBuffer(buffer) {
|
|
481
|
-
try {
|
|
482
|
-
const zip = await JSZip.loadAsync(buffer);
|
|
483
|
-
const safe = assertZipSafe(zip);
|
|
484
|
-
if (!safe.ok) return safe;
|
|
485
|
-
return { ok: true, value: zip };
|
|
486
|
-
} catch {
|
|
487
|
-
return { ok: false, error: "zip_invalid" };
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
550
|
async function parseZipManifest(file, options) {
|
|
491
|
-
const
|
|
492
|
-
const zipRes = await loadZipFromBuffer(buffer);
|
|
551
|
+
const zipRes = await readPluginZip(file instanceof Blob ? await file.arrayBuffer() : file);
|
|
493
552
|
if (!zipRes.ok) return zipRes;
|
|
494
553
|
const zip = zipRes.value;
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
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) ?? "";
|
|
500
558
|
let parsed;
|
|
501
559
|
try {
|
|
502
560
|
parsed = JSON.parse(jsonText);
|
|
@@ -507,9 +565,8 @@ async function parseZipManifest(file, options) {
|
|
|
507
565
|
if (!fields.ok) return fields;
|
|
508
566
|
const isPolicy = isPolicyPluginType(fields.value.pluginType);
|
|
509
567
|
if (!isPolicy) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const hasDistAssets = Object.keys(zip.files).some((name) => /^dist\/assets\/.+/.test(name));
|
|
568
|
+
if (!has("dist/index.html")) return { ok: false, error: "build_index_missing" };
|
|
569
|
+
const hasDistAssets = zip.paths.some((name) => /^dist\/assets\/.+/.test(name));
|
|
513
570
|
if (!hasDistAssets) return { ok: false, error: "build_assets_missing" };
|
|
514
571
|
}
|
|
515
572
|
const entryFromHooks = isPolicy && Array.isArray(parsed.hooks) ? (() => {
|
|
@@ -520,9 +577,9 @@ async function parseZipManifest(file, options) {
|
|
|
520
577
|
})() : null;
|
|
521
578
|
const artifactEntryPath = isPolicy ? entryFromHooks || "server/index.js" : "dist/index.html";
|
|
522
579
|
if (isPolicy) {
|
|
523
|
-
const
|
|
524
|
-
if (!
|
|
525
|
-
const entrySource =
|
|
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) ?? "";
|
|
526
583
|
const selfContained = assertPolicyEntrySelfContained(entrySource);
|
|
527
584
|
if (!selfContained.ok) {
|
|
528
585
|
return {
|
|
@@ -537,26 +594,20 @@ async function parseZipManifest(file, options) {
|
|
|
537
594
|
...fields.value,
|
|
538
595
|
artifactEntryPath,
|
|
539
596
|
rawManifest: parsed,
|
|
540
|
-
manifestEntryName
|
|
597
|
+
manifestEntryName
|
|
541
598
|
}
|
|
542
599
|
};
|
|
543
600
|
}
|
|
544
|
-
|
|
601
|
+
function computePluginContentSha256(zip, rawManifest) {
|
|
545
602
|
try {
|
|
546
603
|
const hash = createHash("sha256");
|
|
547
604
|
hash.update("manifest\0");
|
|
548
605
|
hash.update(manifestJsonForContentHash(rawManifest));
|
|
549
606
|
hash.update("\0");
|
|
550
|
-
const distPaths =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return normalized === "dist" || normalized.startsWith("dist/");
|
|
555
|
-
}).map((name) => name.replace(/^\.\//, "")).sort((a, b) => a.localeCompare(b));
|
|
556
|
-
for (const path2 of distPaths) {
|
|
557
|
-
const entry = zip.file(path2);
|
|
558
|
-
if (!entry || entry.dir) continue;
|
|
559
|
-
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;
|
|
560
611
|
hash.update("file\0");
|
|
561
612
|
hash.update(path2);
|
|
562
613
|
hash.update("\0");
|
|
@@ -571,6 +622,7 @@ async function computePluginContentSha256(zip, rawManifest) {
|
|
|
571
622
|
function sha256Hex(buffer) {
|
|
572
623
|
return createHash("sha256").update(buffer).digest("hex");
|
|
573
624
|
}
|
|
625
|
+
var ZIP_MTIME = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
|
|
574
626
|
async function collectFiles(dir, base = "") {
|
|
575
627
|
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
576
628
|
const out = [];
|
|
@@ -607,7 +659,11 @@ async function packPlugin(opts) {
|
|
|
607
659
|
}
|
|
608
660
|
} else {
|
|
609
661
|
const entry = (Array.isArray(manifestRaw.hooks) ? manifestRaw.hooks.find((h) => h?.entry)?.entry : null) || "server/index.js";
|
|
610
|
-
const entryAbs = path.join(
|
|
662
|
+
const entryAbs = path.join(
|
|
663
|
+
/*turbopackIgnore: true*/
|
|
664
|
+
root,
|
|
665
|
+
entry
|
|
666
|
+
);
|
|
611
667
|
try {
|
|
612
668
|
await promises.access(entryAbs);
|
|
613
669
|
} catch {
|
|
@@ -615,7 +671,11 @@ async function packPlugin(opts) {
|
|
|
615
671
|
}
|
|
616
672
|
let entrySource;
|
|
617
673
|
try {
|
|
618
|
-
entrySource = await promises.readFile(
|
|
674
|
+
entrySource = await promises.readFile(
|
|
675
|
+
/*turbopackIgnore: true*/
|
|
676
|
+
entryAbs,
|
|
677
|
+
"utf8"
|
|
678
|
+
);
|
|
619
679
|
} catch {
|
|
620
680
|
return { ok: false, error: "manifest_not_found_in_zip" };
|
|
621
681
|
}
|
|
@@ -627,8 +687,8 @@ async function packPlugin(opts) {
|
|
|
627
687
|
};
|
|
628
688
|
}
|
|
629
689
|
}
|
|
630
|
-
const
|
|
631
|
-
const files = await collectFiles(root);
|
|
690
|
+
const entries = {};
|
|
691
|
+
const files = (await collectFiles(root)).sort((a, b) => a.rel.localeCompare(b.rel));
|
|
632
692
|
let hasAssets = false;
|
|
633
693
|
for (const file of files) {
|
|
634
694
|
if (file.rel.startsWith(".cervio/") || file.rel === "node_modules" || file.rel.startsWith("node_modules/")) {
|
|
@@ -637,18 +697,15 @@ async function packPlugin(opts) {
|
|
|
637
697
|
if (file.rel.startsWith("dist/assets/") && file.rel !== "dist/assets/") {
|
|
638
698
|
hasAssets = true;
|
|
639
699
|
}
|
|
640
|
-
|
|
641
|
-
zip.file(file.rel, data);
|
|
700
|
+
entries[file.rel] = new Uint8Array(await promises.readFile(file.abs));
|
|
642
701
|
}
|
|
643
702
|
if (!isPolicy && !hasAssets) return { ok: false, error: "build_assets_missing" };
|
|
644
|
-
const
|
|
645
|
-
if (!safe.ok) return safe;
|
|
646
|
-
const buffer = Buffer.from(await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" }));
|
|
703
|
+
const buffer = Buffer.from(zipSync(entries, { level: 6, mtime: ZIP_MTIME }));
|
|
647
704
|
const fileCheck = assertZipFile({ name: `${fields.value.pluginKey}.zip`, size: buffer.length });
|
|
648
705
|
if (!fileCheck.ok) return fileCheck;
|
|
649
|
-
const loaded = await
|
|
706
|
+
const loaded = await readPluginZip(buffer);
|
|
650
707
|
if (!loaded.ok) return loaded;
|
|
651
|
-
const hash =
|
|
708
|
+
const hash = computePluginContentSha256(loaded.value, manifestRaw);
|
|
652
709
|
if (!hash.ok) return hash;
|
|
653
710
|
return {
|
|
654
711
|
ok: true,
|
|
@@ -663,8 +720,7 @@ async function packPlugin(opts) {
|
|
|
663
720
|
async function validatePluginDir(dir, options) {
|
|
664
721
|
const packed = await packPlugin({ dir, options });
|
|
665
722
|
if (!packed.ok) return packed;
|
|
666
|
-
const
|
|
667
|
-
const parsed = await parseZipManifest(blob, options);
|
|
723
|
+
const parsed = await parseZipManifest(packed.value.buffer, options);
|
|
668
724
|
if (!parsed.ok) return parsed;
|
|
669
725
|
return {
|
|
670
726
|
ok: true,
|
|
@@ -677,12 +733,11 @@ async function validatePluginDir(dir, options) {
|
|
|
677
733
|
async function validatePluginZipBuffer(buffer, options) {
|
|
678
734
|
const fileCheck = assertZipFile({ name: "plugin.zip", size: buffer.length });
|
|
679
735
|
if (!fileCheck.ok) return fileCheck;
|
|
680
|
-
const
|
|
681
|
-
const parsed = await parseZipManifest(blob, options);
|
|
736
|
+
const parsed = await parseZipManifest(buffer, options);
|
|
682
737
|
if (!parsed.ok) return parsed;
|
|
683
|
-
const zipRes = await
|
|
738
|
+
const zipRes = await readPluginZip(buffer);
|
|
684
739
|
if (!zipRes.ok) return zipRes;
|
|
685
|
-
const hash =
|
|
740
|
+
const hash = computePluginContentSha256(zipRes.value, parsed.value.rawManifest);
|
|
686
741
|
if (!hash.ok) return hash;
|
|
687
742
|
return {
|
|
688
743
|
ok: true,
|
|
@@ -690,6 +745,4 @@ async function validatePluginZipBuffer(buffer, options) {
|
|
|
690
745
|
};
|
|
691
746
|
}
|
|
692
747
|
|
|
693
|
-
export { DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES,
|
|
694
|
-
//# sourceMappingURL=index.js.map
|
|
695
|
-
//# 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 };
|