@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/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,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/zip-safety.ts
182
- function assertZipSafe(zip) {
183
- const names = Object.keys(zip.files);
184
- if (names.length > MAX_PLUGIN_ZIP_ENTRIES) {
185
- return { ok: false, error: "zip_too_many_entries" };
186
- }
187
- let totalUncompressed = 0;
188
- for (const name of names) {
189
- if (name.includes("..") || name.includes("\\") || name.startsWith("/")) {
190
- return { ok: false, error: "zip_unsafe_entry_path" };
191
- }
192
- const entry = zip.files[name];
193
- totalUncompressed += entry._data?.uncompressedSize ?? 0;
194
- if (totalUncompressed > MAX_PLUGIN_UNCOMPRESSED_BYTES) {
195
- return { ok: false, error: "zip_uncompressed_too_large" };
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 { ok: true, value: true };
402
+ return out;
199
403
  }
200
- function assertZipFile(file) {
201
- const name = (file.name ?? "upload.zip").toLowerCase();
202
- if (!name.endsWith(".zip")) return { ok: false, error: "invalid_file_extension" };
203
- if (file.size <= 0) return { ok: false, error: "empty_file" };
204
- if (file.size > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: "file_too_large" };
205
- return { ok: true, value: true };
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
- // src/semver.ts
209
- var CORE_SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
210
- function nextPatchVersion(latest) {
211
- if (!latest || typeof latest !== "string") return "0.1.0";
212
- const m = latest.trim().match(CORE_SEMVER_RE);
213
- if (!m) return "0.1.0";
214
- const major = Number(m[1]);
215
- const minor = Number(m[2]);
216
- const patch = Number(m[3]);
217
- if (![major, minor, patch].every((n) => Number.isFinite(n) && n >= 0)) return "0.1.0";
218
- return `${major}.${minor}.${patch + 1}`;
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
- function compareSemverDesc(a, b) {
221
- const pa = a.match(CORE_SEMVER_RE);
222
- const pb = b.match(CORE_SEMVER_RE);
223
- if (!pa && !pb) return 0;
224
- if (!pa) return 1;
225
- if (!pb) return -1;
226
- for (let i = 1; i <= 3; i++) {
227
- const da = Number(pa[i]) - Number(pb[i]);
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 0;
455
+ return [...found];
231
456
  }
232
- var pluginManifestZodSchema = z.object({
233
- plugin_key: z.string().regex(/^[a-z0-9][a-z0-9-_]{1,62}$/).optional().describe("Stable plugin id. Prefer this over legacy `id`."),
234
- id: z.string().optional().describe("Legacy alias for plugin_key (slashes become dashes)."),
235
- plugin_name: z.string().optional(),
236
- name: z.string().optional().describe("Legacy alias for plugin_name."),
237
- plugin_type: z.enum(["task", "form", "task_form", "route"]).optional(),
238
- kind: z.string().optional().describe("Legacy alias for plugin_type (task_widget\u2192task, record_panel\u2192form)."),
239
- version: z.string().optional().describe("Ignored by Cervio \u2014 the host assigns release versions."),
240
- required_connection_capabilities: z.array(z.string()).optional(),
241
- requiredConnectionCapabilities: z.array(z.string()).optional(),
242
- connections: z.array(
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(/[\/\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, "");
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(candidate) ? candidate : null;
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 cervioApis = parsePluginCervioApiRequirements(
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 buffer = await file.arrayBuffer();
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 manifestEntry = zip.file("manifest.json") ?? zip.file("manifest.example.json") ?? Object.values(zip.files).find(
360
- (f) => f.name.endsWith("/manifest.json") || f.name.endsWith("/manifest.example.json")
361
- );
362
- if (!manifestEntry) return { ok: false, error: "manifest_not_found_in_zip" };
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 hasDistIndex = !!zip.file("dist/index.html");
373
- if (!hasDistIndex) return { ok: false, error: "build_index_missing" };
374
- const hasDistAssets = Object.keys(zip.files).some((name) => /^dist\/assets\/.+/.test(name));
375
- if (!hasDistAssets) return { ok: false, error: "build_assets_missing" };
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: "dist/index.html",
595
+ artifactEntryPath,
381
596
  rawManifest: parsed,
382
- manifestEntryName: manifestEntry.name
597
+ manifestEntryName
383
598
  }
384
599
  };
385
600
  }
386
- async function computePluginContentSha256(zip, rawManifest) {
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 = Object.keys(zip.files).filter((name) => {
393
- const entry = zip.files[name];
394
- if (!entry || entry.dir) return false;
395
- const normalized = name.replace(/^\.\//, "");
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 distIndex = path.join(root, "dist", "index.html");
443
- try {
444
- await promises.access(distIndex);
445
- } catch {
446
- return { ok: false, error: "build_index_missing" };
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 zip = new JSZip();
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
- const data = await promises.readFile(file.abs);
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 safe = assertZipSafe(zip);
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 loadZipFromBuffer(buffer);
706
+ const loaded = await readPluginZip(buffer);
468
707
  if (!loaded.ok) return loaded;
469
- const hash = await computePluginContentSha256(loaded.value, manifestRaw);
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 blob = new Blob([packed.value.buffer], { type: "application/zip" });
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 blob = new Blob([buffer], { type: "application/zip" });
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 loadZipFromBuffer(buffer);
738
+ const zipRes = await readPluginZip(buffer);
502
739
  if (!zipRes.ok) return zipRes;
503
- const hash = await computePluginContentSha256(zipRes.value, parsed.value.rawManifest);
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, 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, assertZipFile, assertZipSafe, compareSemverDesc, computePluginContentSha256, isExternalConnectionProvider, loadZipFromBuffer, manifestJsonForContentHash, nextPatchVersion, normalizeExactApiHost, normalizePluginKey, packPlugin, parseManifestObject, parsePluginCervioApiRequirements, parsePluginConnections, parsePluginEntityRequirements, parseZipManifest, pluginManifestZodSchema, sha256Hex, validatePluginDir, validatePluginZipBuffer };
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 };