@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.
@@ -212,6 +212,24 @@
212
212
  "type": "array",
213
213
  "items": {}
214
214
  },
215
+ "boot_requests": {
216
+ "maxItems": 8,
217
+ "type": "array",
218
+ "items": {
219
+ "type": "object",
220
+ "properties": {
221
+ "path": {
222
+ "type": "string",
223
+ "description": "GET path + optional query under /api/v1/, covered by a GET `cervio_apis` prefix."
224
+ }
225
+ },
226
+ "required": [
227
+ "path"
228
+ ],
229
+ "additionalProperties": false
230
+ },
231
+ "description": "GET calls the plugin makes at start. The host starts them while the plugin loads and answers the plugin's own fetch of the same path + query from that response (once). `/api/v1/entities/<technical name>/...` also matches the entity id. Hint only: nothing changes for other requests."
232
+ },
215
233
  "settings": {
216
234
  "type": "array",
217
235
  "items": {
@@ -283,6 +301,29 @@
283
301
  "additionalProperties": false
284
302
  },
285
303
  "description": "Admin-editable settings fields (drives Impostazioni dynamic form)."
304
+ },
305
+ "task_widget": {
306
+ "type": "object",
307
+ "properties": {
308
+ "preferred_size": {
309
+ "type": "string",
310
+ "enum": [
311
+ "compact",
312
+ "standard",
313
+ "tall",
314
+ "fill"
315
+ ],
316
+ "description": "Default box height preset in a task layout (standard = 34rem)."
317
+ },
318
+ "preferred_height_px": {
319
+ "type": "integer",
320
+ "minimum": 240,
321
+ "maximum": 1600,
322
+ "description": "Default box height in px; wins over preferred_size."
323
+ }
324
+ },
325
+ "additionalProperties": false,
326
+ "description": "task / task_form plugins: suggested size of the plugin box in a task layout. Default only: the layout widget size set by the tenant admin always wins. Invalid values are ignored, never fatal."
286
327
  }
287
328
  },
288
329
  "additionalProperties": true
package/package.json CHANGED
@@ -1,47 +1,57 @@
1
1
  {
2
2
  "name": "@cervio/plugin-core",
3
- "version": "0.1.4",
3
+ "version": "1.0.1",
4
4
  "description": "Shared Cervio plugin manifest validation, ZIP safety, content hash, and packing.",
5
+ "keywords": [
6
+ "cervio",
7
+ "plugin"
8
+ ],
9
+ "license": "MIT",
10
+ "homepage": "https://github.com/cervio-app/cervio-devkit/tree/main/packages/plugin-core#readme",
11
+ "bugs": "https://github.com/cervio-app/cervio-devkit/issues",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/cervio-app/cervio-devkit.git",
15
+ "directory": "packages/plugin-core"
16
+ },
5
17
  "type": "module",
18
+ "sideEffects": false,
6
19
  "main": "./dist/index.js",
7
20
  "types": "./dist/index.d.ts",
8
21
  "exports": {
9
22
  ".": {
10
23
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js",
12
- "require": "./dist/index.cjs"
24
+ "import": "./dist/index.js"
13
25
  },
14
26
  "./browser": {
15
27
  "types": "./dist/browser.d.ts",
16
- "import": "./dist/browser.js",
17
- "require": "./dist/browser.cjs"
28
+ "import": "./dist/browser.js"
18
29
  },
19
30
  "./manifest.schema.json": "./manifest.schema.json"
20
31
  },
21
32
  "files": [
22
33
  "dist",
23
34
  "manifest.schema.json",
24
- "README.md"
35
+ "README.md",
36
+ "LICENSE"
25
37
  ],
38
+ "engines": {
39
+ "node": ">=26"
40
+ },
26
41
  "dependencies": {
27
- "jszip": "^3.10.1",
28
- "zod": "^3.25.76",
29
- "zod-to-json-schema": "^3.24.6"
42
+ "fflate": "^0.8.3",
43
+ "zod": "^4.6.7"
30
44
  },
31
45
  "devDependencies": {
32
- "@types/node": "^22.15.0",
33
- "tsup": "^8.5.0",
46
+ "@types/node": "^26.0.0",
47
+ "jszip": "^3.10.1",
48
+ "tsup": "^9.0.0",
34
49
  "typescript": "^5.8.3",
35
- "vitest": "^3.2.4"
50
+ "vitest": "^5.0.5"
36
51
  },
37
52
  "publishConfig": {
38
53
  "access": "public"
39
54
  },
40
- "license": "UNLICENSED",
41
- "keywords": [
42
- "cervio",
43
- "plugin"
44
- ],
45
55
  "scripts": {
46
56
  "build": "tsup --config tsup.config.ts && node scripts/emit-manifest-schema.mjs",
47
57
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -1,118 +0,0 @@
1
- import JSZip from 'jszip';
2
-
3
- /** Canonical Policy Plugin type is `policy`; `server` is a silent legacy alias. */
4
- type PluginType = 'task' | 'form' | 'task_form' | 'route' | 'policy' | 'server';
5
- type PluginActionResult<T> = {
6
- ok: true;
7
- value: T;
8
- } | {
9
- ok: false;
10
- error: string;
11
- };
12
- interface PluginVersionRow {
13
- id: string;
14
- plugin_id: string;
15
- version: string;
16
- changelog: string | null;
17
- artifact_path: string | null;
18
- artifact_entry_path?: string | null;
19
- artifact_sha256: string | null;
20
- content_sha256?: string | null;
21
- artifact_size_bytes: number | null;
22
- created_at: string;
23
- /** Soft-delete timestamp; when set the row is history-only (strikethrough in Admin). */
24
- deleted_at?: string | null;
25
- deleted_by?: string | null;
26
- }
27
- interface ParsedPluginManifest {
28
- pluginKey: string;
29
- pluginName: string | null;
30
- pluginType: PluginType | null;
31
- /** Present in ZIP but ignored for assignment; Cervio owns release versions. */
32
- manifestVersion: string | null;
33
- /** Catalog capability ids the plugin needs (e.g. calendar). */
34
- requiredConnectionCapabilities: string[];
35
- /** External Connections requirements (OAuth broker). */
36
- connections: PluginConnectionRequirement[];
37
- /** Declared Cervio entity deps (name-based; for Admin + future export). */
38
- entities: PluginEntityRequirement[];
39
- /** Declared Cervio API path prefixes (documentation / future allowlist). */
40
- cervioApis: PluginCervioApiRequirement[];
41
- /** Declared Admin-editable settings fields (drives Impostazioni dynamic form). */
42
- settings: PluginSettingsFieldRequirement[];
43
- artifactEntryPath: string;
44
- rawManifest: Record<string, unknown>;
45
- manifestEntryName: string;
46
- }
47
- type PluginEntityAccess = 'read' | 'write';
48
- type PluginEntityRequirement = {
49
- name: string;
50
- fields: string[];
51
- forms: string[];
52
- access: PluginEntityAccess[];
53
- };
54
- type PluginCervioApiRequirement = {
55
- method: string;
56
- path_prefix: string;
57
- };
58
- type PluginSettingsFieldType = 'string' | 'boolean' | 'select' | 'multiselect';
59
- type PluginSettingsFieldOption = {
60
- value: string;
61
- label: string;
62
- };
63
- type PluginSettingsFieldRequirement = {
64
- key: string;
65
- type: PluginSettingsFieldType;
66
- label: string;
67
- hint?: string;
68
- placeholder?: string;
69
- default?: string | boolean | string[];
70
- /** Required for `select` / `multiselect`. */
71
- options?: PluginSettingsFieldOption[];
72
- };
73
- type ExternalConnectionHttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
74
- type AllowedApiRule = {
75
- host: string;
76
- methods: ExternalConnectionHttpMethod[];
77
- path_prefix: string;
78
- };
79
- type PluginConnectionRequirement = {
80
- id: string;
81
- provider: string;
82
- consent: 'user';
83
- scopes: string[];
84
- allowed_apis: AllowedApiRule[];
85
- };
86
- type ParseManifestOptions = {
87
- /**
88
- * Capability ids a plugin may declare in `required_connection_capabilities`.
89
- * Host passes the live catalog; CLI uses {@link DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES}.
90
- */
91
- allowedCapabilities?: readonly string[];
92
- };
93
- declare const PLUGIN_ARTIFACT_BUCKET = "plugin-artifacts";
94
- declare const MAX_PLUGIN_ZIP_BYTES: number;
95
- declare const MAX_PLUGIN_ZIP_ENTRIES = 5000;
96
- declare const MAX_PLUGIN_UNCOMPRESSED_BYTES: number;
97
- declare const PLUGIN_KEY_RE: RegExp;
98
- declare const 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";
99
- /**
100
- * Static default for CLI validate. Generated from host `CONNECTION_CAPABILITY_CATALOG`
101
- * where `pluginDeclarable === true`. Guarded by drift test against the host catalog.
102
- */
103
- declare const DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES: readonly ["calendar"];
104
-
105
- /** Max uncompressed/compressed ratio per entry (zip-bomb heuristic). */
106
- declare const MAX_PLUGIN_COMPRESSION_RATIO = 100;
107
- /**
108
- * Validates a loaded plugin ZIP before any entry is inflated (CERVIO-SEC-014):
109
- * caps the entry count and total declared uncompressed size to stop zip bombs,
110
- * rejects nested archives, and rejects path-traversal / absolute entry names.
111
- */
112
- declare function assertZipSafe(zip: JSZip): PluginActionResult<true>;
113
- declare function assertZipFile(file: {
114
- name?: string;
115
- size: number;
116
- }): PluginActionResult<true>;
117
-
118
- export { type AllowedApiRule as A, DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES as D, type ExternalConnectionHttpMethod as E, MAX_PLUGIN_UNCOMPRESSED_BYTES as M, type PluginCervioApiRequirement as P, type PluginEntityRequirement as a, type PluginSettingsFieldRequirement as b, type PluginConnectionRequirement as c, type PluginActionResult as d, type ParseManifestOptions as e, type PluginType as f, type ParsedPluginManifest as g, MAX_PLUGIN_ZIP_BYTES as h, MAX_PLUGIN_ZIP_ENTRIES as i, PLUGIN_ARTIFACT_BUCKET as j, PLUGIN_KEY_RE as k, PLUGIN_VERSION_SELECT as l, type PluginEntityAccess as m, type PluginSettingsFieldOption as n, type PluginSettingsFieldType as o, type PluginVersionRow as p, assertZipFile as q, assertZipSafe as r, MAX_PLUGIN_COMPRESSION_RATIO as s };
@@ -1,118 +0,0 @@
1
- import JSZip from 'jszip';
2
-
3
- /** Canonical Policy Plugin type is `policy`; `server` is a silent legacy alias. */
4
- type PluginType = 'task' | 'form' | 'task_form' | 'route' | 'policy' | 'server';
5
- type PluginActionResult<T> = {
6
- ok: true;
7
- value: T;
8
- } | {
9
- ok: false;
10
- error: string;
11
- };
12
- interface PluginVersionRow {
13
- id: string;
14
- plugin_id: string;
15
- version: string;
16
- changelog: string | null;
17
- artifact_path: string | null;
18
- artifact_entry_path?: string | null;
19
- artifact_sha256: string | null;
20
- content_sha256?: string | null;
21
- artifact_size_bytes: number | null;
22
- created_at: string;
23
- /** Soft-delete timestamp; when set the row is history-only (strikethrough in Admin). */
24
- deleted_at?: string | null;
25
- deleted_by?: string | null;
26
- }
27
- interface ParsedPluginManifest {
28
- pluginKey: string;
29
- pluginName: string | null;
30
- pluginType: PluginType | null;
31
- /** Present in ZIP but ignored for assignment; Cervio owns release versions. */
32
- manifestVersion: string | null;
33
- /** Catalog capability ids the plugin needs (e.g. calendar). */
34
- requiredConnectionCapabilities: string[];
35
- /** External Connections requirements (OAuth broker). */
36
- connections: PluginConnectionRequirement[];
37
- /** Declared Cervio entity deps (name-based; for Admin + future export). */
38
- entities: PluginEntityRequirement[];
39
- /** Declared Cervio API path prefixes (documentation / future allowlist). */
40
- cervioApis: PluginCervioApiRequirement[];
41
- /** Declared Admin-editable settings fields (drives Impostazioni dynamic form). */
42
- settings: PluginSettingsFieldRequirement[];
43
- artifactEntryPath: string;
44
- rawManifest: Record<string, unknown>;
45
- manifestEntryName: string;
46
- }
47
- type PluginEntityAccess = 'read' | 'write';
48
- type PluginEntityRequirement = {
49
- name: string;
50
- fields: string[];
51
- forms: string[];
52
- access: PluginEntityAccess[];
53
- };
54
- type PluginCervioApiRequirement = {
55
- method: string;
56
- path_prefix: string;
57
- };
58
- type PluginSettingsFieldType = 'string' | 'boolean' | 'select' | 'multiselect';
59
- type PluginSettingsFieldOption = {
60
- value: string;
61
- label: string;
62
- };
63
- type PluginSettingsFieldRequirement = {
64
- key: string;
65
- type: PluginSettingsFieldType;
66
- label: string;
67
- hint?: string;
68
- placeholder?: string;
69
- default?: string | boolean | string[];
70
- /** Required for `select` / `multiselect`. */
71
- options?: PluginSettingsFieldOption[];
72
- };
73
- type ExternalConnectionHttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
74
- type AllowedApiRule = {
75
- host: string;
76
- methods: ExternalConnectionHttpMethod[];
77
- path_prefix: string;
78
- };
79
- type PluginConnectionRequirement = {
80
- id: string;
81
- provider: string;
82
- consent: 'user';
83
- scopes: string[];
84
- allowed_apis: AllowedApiRule[];
85
- };
86
- type ParseManifestOptions = {
87
- /**
88
- * Capability ids a plugin may declare in `required_connection_capabilities`.
89
- * Host passes the live catalog; CLI uses {@link DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES}.
90
- */
91
- allowedCapabilities?: readonly string[];
92
- };
93
- declare const PLUGIN_ARTIFACT_BUCKET = "plugin-artifacts";
94
- declare const MAX_PLUGIN_ZIP_BYTES: number;
95
- declare const MAX_PLUGIN_ZIP_ENTRIES = 5000;
96
- declare const MAX_PLUGIN_UNCOMPRESSED_BYTES: number;
97
- declare const PLUGIN_KEY_RE: RegExp;
98
- declare const 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";
99
- /**
100
- * Static default for CLI validate. Generated from host `CONNECTION_CAPABILITY_CATALOG`
101
- * where `pluginDeclarable === true`. Guarded by drift test against the host catalog.
102
- */
103
- declare const DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES: readonly ["calendar"];
104
-
105
- /** Max uncompressed/compressed ratio per entry (zip-bomb heuristic). */
106
- declare const MAX_PLUGIN_COMPRESSION_RATIO = 100;
107
- /**
108
- * Validates a loaded plugin ZIP before any entry is inflated (CERVIO-SEC-014):
109
- * caps the entry count and total declared uncompressed size to stop zip bombs,
110
- * rejects nested archives, and rejects path-traversal / absolute entry names.
111
- */
112
- declare function assertZipSafe(zip: JSZip): PluginActionResult<true>;
113
- declare function assertZipFile(file: {
114
- name?: string;
115
- size: number;
116
- }): PluginActionResult<true>;
117
-
118
- export { type AllowedApiRule as A, DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES as D, type ExternalConnectionHttpMethod as E, MAX_PLUGIN_UNCOMPRESSED_BYTES as M, type PluginCervioApiRequirement as P, type PluginEntityRequirement as a, type PluginSettingsFieldRequirement as b, type PluginConnectionRequirement as c, type PluginActionResult as d, type ParseManifestOptions as e, type PluginType as f, type ParsedPluginManifest as g, MAX_PLUGIN_ZIP_BYTES as h, MAX_PLUGIN_ZIP_ENTRIES as i, PLUGIN_ARTIFACT_BUCKET as j, PLUGIN_KEY_RE as k, PLUGIN_VERSION_SELECT as l, type PluginEntityAccess as m, type PluginSettingsFieldOption as n, type PluginSettingsFieldType as o, type PluginVersionRow as p, assertZipFile as q, assertZipSafe as r, MAX_PLUGIN_COMPRESSION_RATIO as s };
package/dist/browser.cjs DELETED
@@ -1,56 +0,0 @@
1
- 'use strict';
2
-
3
- // src/types.ts
4
- var MAX_PLUGIN_ZIP_BYTES = 50 * 1024 * 1024;
5
- var MAX_PLUGIN_ZIP_ENTRIES = 5e3;
6
- var MAX_PLUGIN_UNCOMPRESSED_BYTES = 250 * 1024 * 1024;
7
-
8
- // src/zip-safety.ts
9
- var MAX_PLUGIN_COMPRESSION_RATIO = 100;
10
- function assertZipSafe(zip) {
11
- const names = Object.keys(zip.files);
12
- if (names.length > MAX_PLUGIN_ZIP_ENTRIES) {
13
- return { ok: false, error: "zip_too_many_entries" };
14
- }
15
- let totalUncompressed = 0;
16
- for (const name of names) {
17
- if (name.includes("..") || name.includes("\\") || name.startsWith("/")) {
18
- return { ok: false, error: "zip_unsafe_entry_path" };
19
- }
20
- if (/\.zip$/i.test(name) && !zip.files[name]?.dir) {
21
- return { ok: false, error: "zip_nested_archive" };
22
- }
23
- const entry = zip.files[name];
24
- const uncompressed = entry._data?.uncompressedSize ?? 0;
25
- const compressed = entry._data?.compressedSize ?? 0;
26
- totalUncompressed += uncompressed;
27
- if (totalUncompressed > MAX_PLUGIN_UNCOMPRESSED_BYTES) {
28
- return { ok: false, error: "zip_uncompressed_too_large" };
29
- }
30
- if (compressed > 0 && uncompressed > 1024 * 1024) {
31
- const ratio = uncompressed / compressed;
32
- if (ratio > MAX_PLUGIN_COMPRESSION_RATIO) {
33
- return { ok: false, error: "zip_compression_ratio_too_high" };
34
- }
35
- } else if (compressed === 0 && uncompressed > 1024 * 1024) {
36
- return { ok: false, error: "zip_compression_ratio_too_high" };
37
- }
38
- }
39
- return { ok: true, value: true };
40
- }
41
- function assertZipFile(file) {
42
- const name = (file.name ?? "upload.zip").toLowerCase();
43
- if (!name.endsWith(".zip")) return { ok: false, error: "invalid_file_extension" };
44
- if (file.size <= 0) return { ok: false, error: "empty_file" };
45
- if (file.size > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: "file_too_large" };
46
- return { ok: true, value: true };
47
- }
48
-
49
- exports.MAX_PLUGIN_COMPRESSION_RATIO = MAX_PLUGIN_COMPRESSION_RATIO;
50
- exports.MAX_PLUGIN_UNCOMPRESSED_BYTES = MAX_PLUGIN_UNCOMPRESSED_BYTES;
51
- exports.MAX_PLUGIN_ZIP_BYTES = MAX_PLUGIN_ZIP_BYTES;
52
- exports.MAX_PLUGIN_ZIP_ENTRIES = MAX_PLUGIN_ZIP_ENTRIES;
53
- exports.assertZipFile = assertZipFile;
54
- exports.assertZipSafe = assertZipSafe;
55
- //# sourceMappingURL=browser.cjs.map
56
- //# sourceMappingURL=browser.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/types.ts","../src/zip-safety.ts"],"names":[],"mappings":";;;AAmGO,IAAM,oBAAA,GAAuB,KAAK,IAAA,GAAO;AACzC,IAAM,sBAAA,GAAyB;AAC/B,IAAM,6BAAA,GAAgC,MAAM,IAAA,GAAO;;;AC5FnD,IAAM,4BAAA,GAA+B;AAOrC,SAAS,cAAc,GAAA,EAAsC;AAClE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AACnC,EAAA,IAAI,KAAA,CAAM,SAAS,sBAAA,EAAwB;AACzC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,sBAAA,EAAuB;AAAA,EACpD;AACA,EAAA,IAAI,iBAAA,GAAoB,CAAA;AACxB,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,IAAK,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,IAAK,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AACtE,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,uBAAA,EAAwB;AAAA,IACrD;AAEA,IAAA,IAAI,SAAA,CAAU,KAAK,IAAI,CAAA,IAAK,CAAC,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,EAAG,GAAA,EAAK;AACjD,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,oBAAA,EAAqB;AAAA,IAClD;AACA,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AAG5B,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,KAAA,EAAO,gBAAA,IAAoB,CAAA;AACtD,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,KAAA,EAAO,cAAA,IAAkB,CAAA;AAClD,IAAA,iBAAA,IAAqB,YAAA;AACrB,IAAA,IAAI,oBAAoB,6BAAA,EAA+B;AACrD,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,4BAAA,EAA6B;AAAA,IAC1D;AACA,IAAA,IAAI,UAAA,GAAa,CAAA,IAAK,YAAA,GAAe,IAAA,GAAO,IAAA,EAAM;AAChD,MAAA,MAAM,QAAQ,YAAA,GAAe,UAAA;AAC7B,MAAA,IAAI,QAAQ,4BAAA,EAA8B;AACxC,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,gCAAA,EAAiC;AAAA,MAC9D;AAAA,IACF,CAAA,MAAA,IAAW,UAAA,KAAe,CAAA,IAAK,YAAA,GAAe,OAAO,IAAA,EAAM;AACzD,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,gCAAA,EAAiC;AAAA,IAC9D;AAAA,EACF;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK;AACjC;AAEO,SAAS,cAAc,IAAA,EAAiE;AAC7F,EAAA,MAAM,IAAA,GAAA,CAAQ,IAAA,CAAK,IAAA,IAAQ,YAAA,EAAc,WAAA,EAAY;AACrD,EAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA,SAAU,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,wBAAA,EAAyB;AAChF,EAAA,IAAI,IAAA,CAAK,QAAQ,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,OAAO,YAAA,EAAa;AAC5D,EAAA,IAAI,IAAA,CAAK,OAAO,oBAAA,EAAsB,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,OAAO,gBAAA,EAAiB;AAClF,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK;AACjC","file":"browser.cjs","sourcesContent":["/** Canonical Policy Plugin type is `policy`; `server` is a silent legacy alias. */\nexport type PluginType = 'task' | 'form' | 'task_form' | 'route' | 'policy' | 'server'\n\nexport type PluginActionResult<T> = { ok: true; value: T } | { ok: false; error: string }\n\nexport interface PluginVersionRow {\n id: string\n plugin_id: string\n version: string\n changelog: string | null\n artifact_path: string | null\n artifact_entry_path?: string | null\n artifact_sha256: string | null\n content_sha256?: string | null\n artifact_size_bytes: number | null\n created_at: string\n /** Soft-delete timestamp; when set the row is history-only (strikethrough in Admin). */\n deleted_at?: string | null\n deleted_by?: string | null\n}\n\nexport interface ParsedPluginManifest {\n pluginKey: string\n pluginName: string | null\n pluginType: PluginType | null\n /** Present in ZIP but ignored for assignment; Cervio owns release versions. */\n manifestVersion: string | null\n /** Catalog capability ids the plugin needs (e.g. calendar). */\n requiredConnectionCapabilities: string[]\n /** External Connections requirements (OAuth broker). */\n connections: PluginConnectionRequirement[]\n /** Declared Cervio entity deps (name-based; for Admin + future export). */\n entities: PluginEntityRequirement[]\n /** Declared Cervio API path prefixes (documentation / future allowlist). */\n cervioApis: PluginCervioApiRequirement[]\n /** Declared Admin-editable settings fields (drives Impostazioni dynamic form). */\n settings: PluginSettingsFieldRequirement[]\n artifactEntryPath: string\n rawManifest: Record<string, unknown>\n manifestEntryName: string\n}\n\nexport type PluginEntityAccess = 'read' | 'write'\n\nexport type PluginEntityRequirement = {\n name: string\n fields: string[]\n forms: string[]\n access: PluginEntityAccess[]\n}\n\nexport type PluginCervioApiRequirement = {\n method: string\n path_prefix: string\n}\n\nexport type PluginSettingsFieldType = 'string' | 'boolean' | 'select' | 'multiselect'\n\nexport type PluginSettingsFieldOption = {\n value: string\n label: string\n}\n\nexport type PluginSettingsFieldRequirement = {\n key: string\n type: PluginSettingsFieldType\n label: string\n hint?: string\n placeholder?: string\n default?: string | boolean | string[]\n /** Required for `select` / `multiselect`. */\n options?: PluginSettingsFieldOption[]\n}\n\nexport type ExternalConnectionHttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'\n\nexport type AllowedApiRule = {\n host: string\n methods: ExternalConnectionHttpMethod[]\n path_prefix: string\n}\n\nexport type PluginConnectionRequirement = {\n id: string\n provider: string\n consent: 'user'\n scopes: string[]\n allowed_apis: AllowedApiRule[]\n}\n\nexport type ParseManifestOptions = {\n /**\n * Capability ids a plugin may declare in `required_connection_capabilities`.\n * Host passes the live catalog; CLI uses {@link DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES}.\n */\n allowedCapabilities?: readonly string[]\n}\n\nexport const PLUGIN_ARTIFACT_BUCKET = 'plugin-artifacts'\nexport const MAX_PLUGIN_ZIP_BYTES = 50 * 1024 * 1024\nexport const MAX_PLUGIN_ZIP_ENTRIES = 5000\nexport const MAX_PLUGIN_UNCOMPRESSED_BYTES = 250 * 1024 * 1024\nexport const PLUGIN_KEY_RE = /^[a-z0-9][a-z0-9-_]{1,62}$/\n\nexport const PLUGIN_VERSION_SELECT =\n 'id, plugin_id, version, changelog, artifact_path, artifact_entry_path, artifact_sha256, content_sha256, artifact_size_bytes, created_at, deleted_at, deleted_by'\n\n/**\n * Static default for CLI validate. Generated from host `CONNECTION_CAPABILITY_CATALOG`\n * where `pluginDeclarable === true`. Guarded by drift test against the host catalog.\n */\nexport const DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES = ['calendar'] as const\n","import type JSZip from 'jszip'\nimport {\n MAX_PLUGIN_UNCOMPRESSED_BYTES,\n MAX_PLUGIN_ZIP_BYTES,\n MAX_PLUGIN_ZIP_ENTRIES,\n type PluginActionResult,\n} from './types.js'\n\n/** Max uncompressed/compressed ratio per entry (zip-bomb heuristic). */\nexport const MAX_PLUGIN_COMPRESSION_RATIO = 100\n\n/**\n * Validates a loaded plugin ZIP before any entry is inflated (CERVIO-SEC-014):\n * caps the entry count and total declared uncompressed size to stop zip bombs,\n * rejects nested archives, and rejects path-traversal / absolute entry names.\n */\nexport function assertZipSafe(zip: JSZip): PluginActionResult<true> {\n const names = Object.keys(zip.files)\n if (names.length > MAX_PLUGIN_ZIP_ENTRIES) {\n return { ok: false, error: 'zip_too_many_entries' }\n }\n let totalUncompressed = 0\n for (const name of names) {\n if (name.includes('..') || name.includes('\\\\') || name.startsWith('/')) {\n return { ok: false, error: 'zip_unsafe_entry_path' }\n }\n // Nested archives are a classic zip-bomb layering vector.\n if (/\\.zip$/i.test(name) && !zip.files[name]?.dir) {\n return { ok: false, error: 'zip_nested_archive' }\n }\n const entry = zip.files[name] as JSZip.JSZipObject & {\n _data?: { uncompressedSize?: number; compressedSize?: number }\n }\n const uncompressed = entry._data?.uncompressedSize ?? 0\n const compressed = entry._data?.compressedSize ?? 0\n totalUncompressed += uncompressed\n if (totalUncompressed > MAX_PLUGIN_UNCOMPRESSED_BYTES) {\n return { ok: false, error: 'zip_uncompressed_too_large' }\n }\n if (compressed > 0 && uncompressed > 1024 * 1024) {\n const ratio = uncompressed / compressed\n if (ratio > MAX_PLUGIN_COMPRESSION_RATIO) {\n return { ok: false, error: 'zip_compression_ratio_too_high' }\n }\n } else if (compressed === 0 && uncompressed > 1024 * 1024) {\n return { ok: false, error: 'zip_compression_ratio_too_high' }\n }\n }\n return { ok: true, value: true }\n}\n\nexport function assertZipFile(file: { name?: string; size: number }): PluginActionResult<true> {\n const name = (file.name ?? 'upload.zip').toLowerCase()\n if (!name.endsWith('.zip')) return { ok: false, error: 'invalid_file_extension' }\n if (file.size <= 0) return { ok: false, error: 'empty_file' }\n if (file.size > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: 'file_too_large' }\n return { ok: true, value: true }\n}\n"]}
@@ -1,2 +0,0 @@
1
- export { s as MAX_PLUGIN_COMPRESSION_RATIO, M as MAX_PLUGIN_UNCOMPRESSED_BYTES, h as MAX_PLUGIN_ZIP_BYTES, i as MAX_PLUGIN_ZIP_ENTRIES, d as PluginActionResult, q as assertZipFile, r as assertZipSafe } from './browser-CbWn_0Kk.cjs';
2
- import 'jszip';
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/types.ts","../src/zip-safety.ts"],"names":[],"mappings":";AAmGO,IAAM,oBAAA,GAAuB,KAAK,IAAA,GAAO;AACzC,IAAM,sBAAA,GAAyB;AAC/B,IAAM,6BAAA,GAAgC,MAAM,IAAA,GAAO;;;AC5FnD,IAAM,4BAAA,GAA+B;AAOrC,SAAS,cAAc,GAAA,EAAsC;AAClE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AACnC,EAAA,IAAI,KAAA,CAAM,SAAS,sBAAA,EAAwB;AACzC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,sBAAA,EAAuB;AAAA,EACpD;AACA,EAAA,IAAI,iBAAA,GAAoB,CAAA;AACxB,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,IAAK,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,IAAK,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AACtE,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,uBAAA,EAAwB;AAAA,IACrD;AAEA,IAAA,IAAI,SAAA,CAAU,KAAK,IAAI,CAAA,IAAK,CAAC,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,EAAG,GAAA,EAAK;AACjD,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,oBAAA,EAAqB;AAAA,IAClD;AACA,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AAG5B,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,KAAA,EAAO,gBAAA,IAAoB,CAAA;AACtD,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,KAAA,EAAO,cAAA,IAAkB,CAAA;AAClD,IAAA,iBAAA,IAAqB,YAAA;AACrB,IAAA,IAAI,oBAAoB,6BAAA,EAA+B;AACrD,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,4BAAA,EAA6B;AAAA,IAC1D;AACA,IAAA,IAAI,UAAA,GAAa,CAAA,IAAK,YAAA,GAAe,IAAA,GAAO,IAAA,EAAM;AAChD,MAAA,MAAM,QAAQ,YAAA,GAAe,UAAA;AAC7B,MAAA,IAAI,QAAQ,4BAAA,EAA8B;AACxC,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,gCAAA,EAAiC;AAAA,MAC9D;AAAA,IACF,CAAA,MAAA,IAAW,UAAA,KAAe,CAAA,IAAK,YAAA,GAAe,OAAO,IAAA,EAAM;AACzD,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,gCAAA,EAAiC;AAAA,IAC9D;AAAA,EACF;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK;AACjC;AAEO,SAAS,cAAc,IAAA,EAAiE;AAC7F,EAAA,MAAM,IAAA,GAAA,CAAQ,IAAA,CAAK,IAAA,IAAQ,YAAA,EAAc,WAAA,EAAY;AACrD,EAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA,SAAU,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,wBAAA,EAAyB;AAChF,EAAA,IAAI,IAAA,CAAK,QAAQ,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,OAAO,YAAA,EAAa;AAC5D,EAAA,IAAI,IAAA,CAAK,OAAO,oBAAA,EAAsB,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,OAAO,gBAAA,EAAiB;AAClF,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK;AACjC","file":"browser.js","sourcesContent":["/** Canonical Policy Plugin type is `policy`; `server` is a silent legacy alias. */\nexport type PluginType = 'task' | 'form' | 'task_form' | 'route' | 'policy' | 'server'\n\nexport type PluginActionResult<T> = { ok: true; value: T } | { ok: false; error: string }\n\nexport interface PluginVersionRow {\n id: string\n plugin_id: string\n version: string\n changelog: string | null\n artifact_path: string | null\n artifact_entry_path?: string | null\n artifact_sha256: string | null\n content_sha256?: string | null\n artifact_size_bytes: number | null\n created_at: string\n /** Soft-delete timestamp; when set the row is history-only (strikethrough in Admin). */\n deleted_at?: string | null\n deleted_by?: string | null\n}\n\nexport interface ParsedPluginManifest {\n pluginKey: string\n pluginName: string | null\n pluginType: PluginType | null\n /** Present in ZIP but ignored for assignment; Cervio owns release versions. */\n manifestVersion: string | null\n /** Catalog capability ids the plugin needs (e.g. calendar). */\n requiredConnectionCapabilities: string[]\n /** External Connections requirements (OAuth broker). */\n connections: PluginConnectionRequirement[]\n /** Declared Cervio entity deps (name-based; for Admin + future export). */\n entities: PluginEntityRequirement[]\n /** Declared Cervio API path prefixes (documentation / future allowlist). */\n cervioApis: PluginCervioApiRequirement[]\n /** Declared Admin-editable settings fields (drives Impostazioni dynamic form). */\n settings: PluginSettingsFieldRequirement[]\n artifactEntryPath: string\n rawManifest: Record<string, unknown>\n manifestEntryName: string\n}\n\nexport type PluginEntityAccess = 'read' | 'write'\n\nexport type PluginEntityRequirement = {\n name: string\n fields: string[]\n forms: string[]\n access: PluginEntityAccess[]\n}\n\nexport type PluginCervioApiRequirement = {\n method: string\n path_prefix: string\n}\n\nexport type PluginSettingsFieldType = 'string' | 'boolean' | 'select' | 'multiselect'\n\nexport type PluginSettingsFieldOption = {\n value: string\n label: string\n}\n\nexport type PluginSettingsFieldRequirement = {\n key: string\n type: PluginSettingsFieldType\n label: string\n hint?: string\n placeholder?: string\n default?: string | boolean | string[]\n /** Required for `select` / `multiselect`. */\n options?: PluginSettingsFieldOption[]\n}\n\nexport type ExternalConnectionHttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'\n\nexport type AllowedApiRule = {\n host: string\n methods: ExternalConnectionHttpMethod[]\n path_prefix: string\n}\n\nexport type PluginConnectionRequirement = {\n id: string\n provider: string\n consent: 'user'\n scopes: string[]\n allowed_apis: AllowedApiRule[]\n}\n\nexport type ParseManifestOptions = {\n /**\n * Capability ids a plugin may declare in `required_connection_capabilities`.\n * Host passes the live catalog; CLI uses {@link DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES}.\n */\n allowedCapabilities?: readonly string[]\n}\n\nexport const PLUGIN_ARTIFACT_BUCKET = 'plugin-artifacts'\nexport const MAX_PLUGIN_ZIP_BYTES = 50 * 1024 * 1024\nexport const MAX_PLUGIN_ZIP_ENTRIES = 5000\nexport const MAX_PLUGIN_UNCOMPRESSED_BYTES = 250 * 1024 * 1024\nexport const PLUGIN_KEY_RE = /^[a-z0-9][a-z0-9-_]{1,62}$/\n\nexport const PLUGIN_VERSION_SELECT =\n 'id, plugin_id, version, changelog, artifact_path, artifact_entry_path, artifact_sha256, content_sha256, artifact_size_bytes, created_at, deleted_at, deleted_by'\n\n/**\n * Static default for CLI validate. Generated from host `CONNECTION_CAPABILITY_CATALOG`\n * where `pluginDeclarable === true`. Guarded by drift test against the host catalog.\n */\nexport const DEFAULT_PLUGIN_DECLARABLE_CAPABILITIES = ['calendar'] as const\n","import type JSZip from 'jszip'\nimport {\n MAX_PLUGIN_UNCOMPRESSED_BYTES,\n MAX_PLUGIN_ZIP_BYTES,\n MAX_PLUGIN_ZIP_ENTRIES,\n type PluginActionResult,\n} from './types.js'\n\n/** Max uncompressed/compressed ratio per entry (zip-bomb heuristic). */\nexport const MAX_PLUGIN_COMPRESSION_RATIO = 100\n\n/**\n * Validates a loaded plugin ZIP before any entry is inflated (CERVIO-SEC-014):\n * caps the entry count and total declared uncompressed size to stop zip bombs,\n * rejects nested archives, and rejects path-traversal / absolute entry names.\n */\nexport function assertZipSafe(zip: JSZip): PluginActionResult<true> {\n const names = Object.keys(zip.files)\n if (names.length > MAX_PLUGIN_ZIP_ENTRIES) {\n return { ok: false, error: 'zip_too_many_entries' }\n }\n let totalUncompressed = 0\n for (const name of names) {\n if (name.includes('..') || name.includes('\\\\') || name.startsWith('/')) {\n return { ok: false, error: 'zip_unsafe_entry_path' }\n }\n // Nested archives are a classic zip-bomb layering vector.\n if (/\\.zip$/i.test(name) && !zip.files[name]?.dir) {\n return { ok: false, error: 'zip_nested_archive' }\n }\n const entry = zip.files[name] as JSZip.JSZipObject & {\n _data?: { uncompressedSize?: number; compressedSize?: number }\n }\n const uncompressed = entry._data?.uncompressedSize ?? 0\n const compressed = entry._data?.compressedSize ?? 0\n totalUncompressed += uncompressed\n if (totalUncompressed > MAX_PLUGIN_UNCOMPRESSED_BYTES) {\n return { ok: false, error: 'zip_uncompressed_too_large' }\n }\n if (compressed > 0 && uncompressed > 1024 * 1024) {\n const ratio = uncompressed / compressed\n if (ratio > MAX_PLUGIN_COMPRESSION_RATIO) {\n return { ok: false, error: 'zip_compression_ratio_too_high' }\n }\n } else if (compressed === 0 && uncompressed > 1024 * 1024) {\n return { ok: false, error: 'zip_compression_ratio_too_high' }\n }\n }\n return { ok: true, value: true }\n}\n\nexport function assertZipFile(file: { name?: string; size: number }): PluginActionResult<true> {\n const name = (file.name ?? 'upload.zip').toLowerCase()\n if (!name.endsWith('.zip')) return { ok: false, error: 'invalid_file_extension' }\n if (file.size <= 0) return { ok: false, error: 'empty_file' }\n if (file.size > MAX_PLUGIN_ZIP_BYTES) return { ok: false, error: 'file_too_large' }\n return { ok: true, value: true }\n}\n"]}