@cervio/plugin-core 0.1.4 → 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/dist/index.js CHANGED
@@ -1,22 +1,25 @@
1
- import { z } from 'zod';
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 = typeof row.path_prefix === "string" ? row.path_prefix.trim() : typeof row.pathPrefix === "string" ? row.pathPrefix.trim() : "";
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/settings-requirements.ts
82
- var SETTINGS_KEY_RE = /^[a-z][a-z0-9_]{0,62}$/i;
83
- var SETTINGS_TYPES = /* @__PURE__ */ new Set(["string", "boolean", "select", "multiselect"]);
84
- var RESERVED_SETTINGS_KEYS = /* @__PURE__ */ new Set(["debug"]);
85
- function parseOptions(raw) {
86
- if (!Array.isArray(raw)) return [];
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
- if (!item || typeof item !== "object") continue;
91
- const row = item;
92
- const value = typeof row.value === "string" ? row.value.trim() : "";
93
- const label = typeof row.label === "string" ? row.label.trim() : "";
94
- if (!value || !label || seen.has(value)) continue;
95
- seen.add(value);
96
- out.push({ value, label });
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
- function parseDefault(raw, type, options) {
101
- const allowedValues = new Set(options.map((o) => o.value));
102
- if (type === "boolean") return typeof raw === "boolean" ? raw : void 0;
103
- if (type === "string") return typeof raw === "string" ? raw : void 0;
104
- if (type === "select") {
105
- return typeof raw === "string" && allowedValues.has(raw) ? raw : void 0;
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 (type === "multiselect") {
108
- if (!Array.isArray(raw)) return void 0;
109
- const values = raw.filter((v) => typeof v === "string" && allowedValues.has(v));
110
- return values.length ? values : void 0;
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 void 0;
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 parsePluginSettingsRequirements(raw) {
115
- if (!Array.isArray(raw)) return [];
116
- const out = [];
117
- const seen = /* @__PURE__ */ new Set();
118
- for (const item of raw) {
119
- if (!item || typeof item !== "object") continue;
120
- const row = item;
121
- const key = typeof row.key === "string" ? row.key.trim() : "";
122
- if (!key || !SETTINGS_KEY_RE.test(key) || seen.has(key) || RESERVED_SETTINGS_KEYS.has(key)) continue;
123
- const type = typeof row.type === "string" ? row.type.trim() : null;
124
- if (!type || !SETTINGS_TYPES.has(type)) continue;
125
- const label = typeof row.label === "string" ? row.label.trim() : "";
126
- if (!label) continue;
127
- const options = parseOptions(row.options);
128
- if ((type === "select" || type === "multiselect") && options.length === 0) continue;
129
- const hint = typeof row.hint === "string" && row.hint.trim() ? row.hint.trim() : void 0;
130
- const placeholder = typeof row.placeholder === "string" && row.placeholder.trim() ? row.placeholder.trim() : void 0;
131
- const defaultValue = parseDefault(row.default, type, options);
132
- seen.add(key);
133
- const field = { key, type, label };
134
- if (hint) field.hint = hint;
135
- if (placeholder) field.placeholder = placeholder;
136
- if (defaultValue !== void 0) field.default = defaultValue;
137
- if (options.length) field.options = options;
138
- out.push(field);
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 out;
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/zip-safety.ts
244
- var MAX_PLUGIN_COMPRESSION_RATIO = 100;
245
- function assertZipSafe(zip) {
246
- const names = Object.keys(zip.files);
247
- if (names.length > MAX_PLUGIN_ZIP_ENTRIES) {
248
- return { ok: false, error: "zip_too_many_entries" };
249
- }
250
- let totalUncompressed = 0;
251
- for (const name of names) {
252
- if (name.includes("..") || name.includes("\\") || name.startsWith("/")) {
253
- return { ok: false, error: "zip_unsafe_entry_path" };
254
- }
255
- if (/\.zip$/i.test(name) && !zip.files[name]?.dir) {
256
- return { ok: false, error: "zip_nested_archive" };
257
- }
258
- const entry = zip.files[name];
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 { ok: true, value: true };
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 compareSemverDesc(a, b) {
297
- const pa = a.match(CORE_SEMVER_RE);
298
- const pb = b.match(CORE_SEMVER_RE);
299
- if (!pa && !pb) return 0;
300
- if (!pa) return 1;
301
- if (!pb) return -1;
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
- return 0;
307
- }
308
- var pluginManifestZodSchema = z.object({
309
- plugin_key: z.string().regex(/^[a-z0-9][a-z0-9-_]{1,62}$/).optional().describe("Stable plugin id. Prefer this over legacy `id`."),
310
- id: z.string().optional().describe("Legacy alias for plugin_key (slashes become dashes)."),
311
- plugin_name: z.string().optional(),
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 normalizeToPolicyPluginType(t) {
382
- return isPolicyPluginType(t) ? "policy" : null;
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(/[\/\s]+/g, "-").replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
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 cervioApis = parsePluginCervioApiRequirements(
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 buffer = await file.arrayBuffer();
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 manifestEntry = zip.file("manifest.json") ?? zip.file("manifest.example.json") ?? Object.values(zip.files).find(
496
- (f) => f.name.endsWith("/manifest.json") || f.name.endsWith("/manifest.example.json")
497
- );
498
- if (!manifestEntry) return { ok: false, error: "manifest_not_found_in_zip" };
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
- const hasDistIndex = !!zip.file("dist/index.html");
511
- if (!hasDistIndex) return { ok: false, error: "build_index_missing" };
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 entryFile = zip.file(artifactEntryPath) || zip.file(artifactEntryPath.replace(/^\//, "")) || Object.values(zip.files).find((f) => f.name.endsWith(`/${artifactEntryPath}`));
524
- if (!entryFile) return { ok: false, error: "manifest_not_found_in_zip" };
525
- const entrySource = await entryFile.async("text");
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: manifestEntry.name
597
+ manifestEntryName
541
598
  }
542
599
  };
543
600
  }
544
- async function computePluginContentSha256(zip, rawManifest) {
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 = Object.keys(zip.files).filter((name) => {
551
- const entry = zip.files[name];
552
- if (!entry || entry.dir) return false;
553
- const normalized = name.replace(/^\.\//, "");
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(root, entry);
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(entryAbs, "utf8");
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 zip = new JSZip();
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
- const data = await promises.readFile(file.abs);
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 safe = assertZipSafe(zip);
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 loadZipFromBuffer(buffer);
706
+ const loaded = await readPluginZip(buffer);
650
707
  if (!loaded.ok) return loaded;
651
- const hash = await computePluginContentSha256(loaded.value, manifestRaw);
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 blob = new Blob([packed.value.buffer], { type: "application/zip" });
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 blob = new Blob([buffer], { type: "application/zip" });
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 loadZipFromBuffer(buffer);
738
+ const zipRes = await readPluginZip(buffer);
684
739
  if (!zipRes.ok) return zipRes;
685
- const hash = await computePluginContentSha256(zipRes.value, parsed.value.rawManifest);
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, EXTERNAL_CONNECTION_ALLOWED_HOSTS, EXTERNAL_CONNECTION_HTTP_METHODS, MAX_PLUGIN_UNCOMPRESSED_BYTES, MAX_PLUGIN_ZIP_BYTES, MAX_PLUGIN_ZIP_ENTRIES, OAUTH_PROVIDER_KEY_RE, PLUGIN_ARTIFACT_BUCKET, PLUGIN_KEY_RE, PLUGIN_VERSION_SELECT, assertPolicyEntrySelfContained, assertZipFile, assertZipSafe, compareSemverDesc, computePluginContentSha256, findBareModuleSpecifiers, isExternalConnectionProvider, isPolicyPluginType, loadZipFromBuffer, manifestJsonForContentHash, nextPatchVersion, normalizeExactApiHost, normalizePluginKey, normalizeToPolicyPluginType, packPlugin, parseManifestObject, parsePluginCervioApiRequirements, parsePluginConnections, parsePluginEntityRequirements, parsePluginSettingsRequirements, parseZipManifest, pluginManifestZodSchema, sha256Hex, validatePluginDir, validatePluginZipBuffer };
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 };