@kizenapps/packager 0.3.0 → 0.4.0-6ad8aed

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.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ API_NAME_PATTERN: () => API_NAME_PATTERN,
33
34
  AUTOMATION_SCRIPT_FILE: () => AUTOMATION_SCRIPT_FILE,
34
35
  AUTOMATION_STEPS_DIRECTORY_NAME: () => AUTOMATION_STEPS_DIRECTORY_NAME,
35
36
  BLOCKS_DIRECTORY_NAME: () => BLOCKS_DIRECTORY_NAME,
@@ -40,6 +41,8 @@ __export(index_exports, {
40
41
  CRYPTO_ALG: () => CRYPTO_ALG,
41
42
  CRYPTO_VERSION: () => CRYPTO_VERSION,
42
43
  DATA_ADORNMENTS_DIRECTORY_NAME: () => DATA_ADORNMENTS_DIRECTORY_NAME,
44
+ ENVIRONMENTS: () => ENVIRONMENTS,
45
+ ENVIRONMENT_ALIASES: () => ENVIRONMENT_ALIASES,
43
46
  EVENT_SCRIPTS_DIRECTORY_NAME: () => EVENT_SCRIPTS_DIRECTORY_NAME,
44
47
  FLOATING_FRAMES_DIRECTORY_NAME: () => FLOATING_FRAMES_DIRECTORY_NAME,
45
48
  JS_ACTIONS_DIRECTORY_NAME: () => JS_ACTIONS_DIRECTORY_NAME,
@@ -48,6 +51,8 @@ __export(index_exports, {
48
51
  MANIFEST_FILE_NAME: () => MANIFEST_FILE_NAME,
49
52
  MESSAGE_SCRIPT_FILE: () => MESSAGE_SCRIPT_FILE,
50
53
  OBJECT_SETTINGS_ITEMS_DIRECTORY_NAME: () => OBJECT_SETTINGS_ITEMS_DIRECTORY_NAME,
54
+ PUBLISHABLE_ENVIRONMENTS: () => PUBLISHABLE_ENVIRONMENTS,
55
+ PluginValidationError: () => PluginValidationError,
51
56
  RELEASE_NOTES_EXTENSION: () => RELEASE_NOTES_EXTENSION,
52
57
  ROUTABLE_PAGES_DIRECTORY_NAME: () => ROUTABLE_PAGES_DIRECTORY_NAME,
53
58
  SCRIPT_EXTENSION: () => SCRIPT_EXTENSION,
@@ -59,6 +64,7 @@ __export(index_exports, {
59
64
  decrypt: () => decrypt,
60
65
  deserializeEnvelope: () => deserializeEnvelope,
61
66
  encrypt: () => encrypt,
67
+ errorIssue: () => errorIssue,
62
68
  generateKeypair: () => generateKeypair,
63
69
  getAdornmentIcon: () => getAdornmentIcon,
64
70
  getMinimizedConfig: () => getMinimizedConfig,
@@ -74,6 +80,7 @@ __export(index_exports, {
74
80
  thumbnailImageExtensions: () => thumbnailImageExtensions,
75
81
  transformDeployablePlugin: () => transformDeployablePlugin,
76
82
  triggerImageExtensions: () => triggerImageExtensions,
83
+ validatePluginApp: () => validatePluginApp,
77
84
  zipToUint8Array: () => zipToUint8Array
78
85
  });
79
86
  module.exports = __toCommonJS(index_exports);
@@ -111,26 +118,54 @@ var minifyFiles = async (files) => {
111
118
 
112
119
  // src/lib/apiNames.ts
113
120
  var sanitizeToAPIName = (name) => {
114
- return name.toLowerCase().replace(/[^a-z0-9\-\s]/g, "").replace(/\s+/g, "_");
121
+ return name.toLowerCase().replace(/[-\s]+/g, "_").replace(/[^a-z0-9_]/g, "");
115
122
  };
123
+ var resolveApiName = (explicit, directoryName) => typeof explicit === "string" && explicit !== "" ? explicit : sanitizeToAPIName(directoryName);
116
124
  var scriptRuntimeToApiName = (runtime) => {
117
125
  const result = runtime.replace(/\s+/g, "-").replace(/\./g, "-");
118
126
  return result;
119
127
  };
120
128
 
121
- // src/lib/image.ts
122
- var imagetoUint8Array = (base64Image) => {
123
- const byteCharacters = atob(base64Image);
124
- const byteNumbers = new Array(byteCharacters.length);
125
- for (let i = 0; i < byteCharacters.length; i++) {
126
- byteNumbers[i] = byteCharacters.charCodeAt(i);
129
+ // src/lib/issues.ts
130
+ var errorIssue = (rule, message, path, pluginApiName) => ({
131
+ rule,
132
+ severity: "error",
133
+ message,
134
+ ...path !== void 0 && { path },
135
+ ...pluginApiName !== void 0 && { pluginApiName }
136
+ });
137
+ var warningIssue = (rule, message, path, pluginApiName) => ({
138
+ rule,
139
+ severity: "warning",
140
+ message,
141
+ ...path !== void 0 && { path },
142
+ ...pluginApiName !== void 0 && { pluginApiName }
143
+ });
144
+ var PluginValidationError = class extends Error {
145
+ issues;
146
+ constructor(issues) {
147
+ const errors = issues.filter((i) => i.severity === "error");
148
+ const details = errors.map((i) => `${i.path ? `${i.path}: ` : ""}${i.message}`).join("\n");
149
+ super(`Plugin app validation failed with ${String(errors.length)} error(s):
150
+ ${details}`);
151
+ this.name = "PluginValidationError";
152
+ this.issues = issues;
127
153
  }
128
- return new Uint8Array(byteNumbers);
129
- };
130
- var zipToUint8Array = (zip) => {
131
- return Uint8Array.from(zip);
132
154
  };
133
155
 
156
+ // src/types/manifest.ts
157
+ var PUBLISHABLE_ENVIRONMENTS = [
158
+ "go",
159
+ "fmo",
160
+ "staging",
161
+ "integration",
162
+ "e2e-integration",
163
+ "e2e-staging",
164
+ "test1"
165
+ ];
166
+ var ENVIRONMENT_ALIASES = ["prod", "dev", "testing"];
167
+ var ENVIRONMENTS = [...PUBLISHABLE_ENVIRONMENTS, ...ENVIRONMENT_ALIASES];
168
+
134
169
  // src/lib/structure.ts
135
170
  var thumbnailImageExtensions = ["png"];
136
171
  var triggerImageExtensions = [...thumbnailImageExtensions, "svg"];
@@ -148,9 +183,7 @@ var USER_SETUP_ASSISTANT_DIRECTORY_NAME = "userSetupAssistant";
148
183
  var CALENDAR_SOURCES_DIRECTORY_NAME = "calendarSources";
149
184
  var VIEWS_DIRECTORY_NAME = "views";
150
185
  var BLOCKS_DIRECTORY_NAME = "blocks";
151
- var THUMBNAIL_FILE_NAMES = thumbnailImageExtensions.map(
152
- (ext) => `thumbnail.${ext}`
153
- );
186
+ var THUMBNAIL_FILE_NAMES = thumbnailImageExtensions.map((ext) => `thumbnail.${ext}`);
154
187
  var RELEASE_NOTES_EXTENSION = "md";
155
188
  var SCRIPT_EXTENSION = "js";
156
189
  var MAIN_SCRIPT_FILE = `script.${SCRIPT_EXTENSION}`;
@@ -162,49 +195,70 @@ var CONFIG_FILE_NAME = "config.json";
162
195
  var STYLES_FILE_NAME = "styles.css";
163
196
  var MANIFEST_FILE_NAME = "kizen.json";
164
197
  var KZN_FILE_NAME = "import.kzn";
165
- var maybeParseConfig = (content, path) => {
198
+ var tryParseConfig = (content, path, pluginApiName) => {
166
199
  if (!content) {
167
- throw new Error(`No content provided for config file: ${path}`);
200
+ return {
201
+ issue: errorIssue(
202
+ "structure/config-content",
203
+ `No content provided for config file: ${path}`,
204
+ path,
205
+ pluginApiName
206
+ )
207
+ };
168
208
  }
169
209
  try {
170
- return JSON.parse(content);
210
+ return { config: JSON.parse(content) };
171
211
  } catch {
172
- throw new Error(`Failed to parse config content: ${path}`);
212
+ return {
213
+ issue: errorIssue(
214
+ "structure/config-parse",
215
+ `Failed to parse ${CONFIG_FILE_NAME} as JSON.`,
216
+ path,
217
+ pluginApiName
218
+ )
219
+ };
173
220
  }
174
221
  };
222
+ var maybeParseConfig = (content, path, pluginApiName) => {
223
+ const result = tryParseConfig(content, path, pluginApiName);
224
+ if ("issue" in result) throw new PluginValidationError([result.issue]);
225
+ return result.config;
226
+ };
175
227
  var getMinimizedConfig = (config, entry, splitPath, consideredFiles) => {
176
- const result = config["minimized_config"] ?? {};
177
- if (result["customIconFile"]) {
228
+ const result = config.minimized_config ?? {};
229
+ const customIconFile = result.customIconFile;
230
+ if (typeof customIconFile === "string" && customIconFile) {
178
231
  let basePath = splitPath.slice(0, -1).join("/");
179
232
  if (entry.endsWith("/")) {
180
233
  basePath = `${entry}${basePath}`;
181
234
  } else {
182
235
  basePath = `${entry}/${basePath}`;
183
236
  }
184
- const filePath = `${basePath}/${result["customIconFile"]}`;
237
+ const filePath = `${basePath}/${customIconFile}`;
185
238
  const file = consideredFiles.find((f) => f.path === filePath);
186
239
  if (file?.base64Image) {
187
- let extension = filePath.split(".").pop()?.toLowerCase();
240
+ let extension = filePath.split(".").pop()?.toLowerCase() ?? "";
188
241
  if (extension === "svg") {
189
242
  extension = "svg+xml";
190
243
  }
191
- result["customIcon"] = `data:image/${extension};base64,${file.base64Image}`;
244
+ result.customIcon = `data:image/${extension};base64,${file.base64Image}`;
192
245
  }
193
246
  }
194
247
  return result;
195
248
  };
196
249
  var getAdornmentIcon = (config, entry, splitPath, consideredFiles) => {
197
- if (config["customIconFile"]) {
250
+ const customIconFile = config.customIconFile;
251
+ if (typeof customIconFile === "string" && customIconFile) {
198
252
  let basePath = splitPath.slice(0, -1).join("/");
199
253
  if (entry.endsWith("/")) {
200
254
  basePath = `${entry}${basePath}`;
201
255
  } else {
202
256
  basePath = `${entry}/${basePath}`;
203
257
  }
204
- const filePath = `${basePath}/${config["customIconFile"]}`;
258
+ const filePath = `${basePath}/${customIconFile}`;
205
259
  const file = consideredFiles.find((f) => f.path === filePath);
206
260
  if (file?.base64Image) {
207
- let extension = filePath.split(".").pop()?.toLowerCase();
261
+ let extension = filePath.split(".").pop()?.toLowerCase() ?? "";
208
262
  if (extension === "svg") {
209
263
  extension = "svg+xml";
210
264
  }
@@ -214,6 +268,411 @@ var getAdornmentIcon = (config, entry, splitPath, consideredFiles) => {
214
268
  return "";
215
269
  };
216
270
 
271
+ // src/lib/validate.ts
272
+ var API_NAME_PATTERN = /^[a-z_][a-z0-9_]+$/;
273
+ var API_NAME_HINT = "must start with a letter or underscore and contain only lowercase letters, numbers, or underscores (minimum 2 characters)";
274
+ var SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
275
+ var PATH_PATTERN = /^[a-zA-Z][a-zA-Z0-9_\-/]*$/;
276
+ var SUPPORTED_ENGINES = ["1.0.0"];
277
+ var CONFIG_REQUIRED_DIRECTORIES = [
278
+ FLOATING_FRAMES_DIRECTORY_NAME,
279
+ BLOCKS_DIRECTORY_NAME,
280
+ DATA_ADORNMENTS_DIRECTORY_NAME,
281
+ TOOLBAR_ITEMS_DIRECTORY_NAME,
282
+ ROUTABLE_PAGES_DIRECTORY_NAME,
283
+ BROWSER_ROUTE_SCRIPTS_DIRECTORY_NAME,
284
+ JS_ACTIONS_DIRECTORY_NAME,
285
+ OBJECT_SETTINGS_ITEMS_DIRECTORY_NAME,
286
+ AUTOMATION_STEPS_DIRECTORY_NAME,
287
+ CALENDAR_SOURCES_DIRECTORY_NAME
288
+ ];
289
+ var API_NAMED_DIRECTORIES = /* @__PURE__ */ new Set([
290
+ FLOATING_FRAMES_DIRECTORY_NAME,
291
+ BLOCKS_DIRECTORY_NAME,
292
+ TOOLBAR_ITEMS_DIRECTORY_NAME,
293
+ ROUTABLE_PAGES_DIRECTORY_NAME,
294
+ BROWSER_ROUTE_SCRIPTS_DIRECTORY_NAME,
295
+ JS_ACTIONS_DIRECTORY_NAME,
296
+ OBJECT_SETTINGS_ITEMS_DIRECTORY_NAME,
297
+ AUTOMATION_STEPS_DIRECTORY_NAME,
298
+ CALENDAR_SOURCES_DIRECTORY_NAME,
299
+ VIEWS_DIRECTORY_NAME
300
+ ]);
301
+ var COMPONENT_DIRECTORIES = [...CONFIG_REQUIRED_DIRECTORIES, VIEWS_DIRECTORY_NAME];
302
+ var isNonEmptyString = (value) => typeof value === "string" && value !== "";
303
+ var validateManifestFields = (manifest, label, pluginApiName) => {
304
+ const issues = [];
305
+ const problem = (rule, message) => {
306
+ issues.push(errorIssue(rule, `${label}: ${message}`, MANIFEST_FILE_NAME, pluginApiName));
307
+ };
308
+ const warn = (rule, message) => {
309
+ issues.push(warningIssue(rule, `${label}: ${message}`, MANIFEST_FILE_NAME, pluginApiName));
310
+ };
311
+ const requireString = (field) => {
312
+ const value = manifest[field];
313
+ if (!isNonEmptyString(value)) {
314
+ problem("manifest/required-field", `${field} is required and must be a non-empty string.`);
315
+ return null;
316
+ }
317
+ return value;
318
+ };
319
+ const optionalString = (field) => {
320
+ const value = manifest[field];
321
+ if (value === void 0) {
322
+ return null;
323
+ }
324
+ if (!isNonEmptyString(value)) {
325
+ problem("manifest/required-field", `${field} must be a non-empty string when present.`);
326
+ return null;
327
+ }
328
+ return value;
329
+ };
330
+ const optionalStringArray = (field, rule) => {
331
+ const value = manifest[field];
332
+ if (value === void 0) {
333
+ return null;
334
+ }
335
+ if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
336
+ problem(rule, `${field} must be an array of non-empty strings when present.`);
337
+ return null;
338
+ }
339
+ return value;
340
+ };
341
+ const version = requireString("version");
342
+ if (version !== null && !SEMVER_PATTERN.test(version)) {
343
+ problem(
344
+ "manifest/version-format",
345
+ `version must be a semantic version (e.g. 1.0.0), got "${version}".`
346
+ );
347
+ }
348
+ const apiName = requireString("api_name");
349
+ if (apiName !== null && !API_NAME_PATTERN.test(apiName)) {
350
+ problem("manifest/api-name-format", `api_name "${apiName}" is invalid: ${API_NAME_HINT}.`);
351
+ }
352
+ requireString("name");
353
+ requireString("description");
354
+ const engine = requireString("engine");
355
+ if (engine !== null && !SUPPORTED_ENGINES.includes(engine)) {
356
+ problem(
357
+ "manifest/engine-version",
358
+ `engine must be one of ${SUPPORTED_ENGINES.join(", ")}, got "${engine}".`
359
+ );
360
+ }
361
+ const entry = requireString("entry");
362
+ if (entry !== null && !PATH_PATTERN.test(entry)) {
363
+ problem(
364
+ "manifest/entry-path",
365
+ `entry must be a directory path matching ${String(PATH_PATTERN)}, got "${entry}".`
366
+ );
367
+ }
368
+ const releaseNotesDirectory = optionalString("release_notes_directory");
369
+ if (releaseNotesDirectory !== null && !PATH_PATTERN.test(releaseNotesDirectory)) {
370
+ problem(
371
+ "manifest/release-notes-directory-path",
372
+ `release_notes_directory must be a directory path matching ${String(PATH_PATTERN)}, got "${releaseNotesDirectory}".`
373
+ );
374
+ }
375
+ optionalStringArray("release_branches", "manifest/release-branches");
376
+ const releaseEnvironments = optionalStringArray(
377
+ "release_environments",
378
+ "manifest/release-environments"
379
+ );
380
+ if (releaseEnvironments !== null) {
381
+ for (const env of releaseEnvironments) {
382
+ if (!ENVIRONMENTS.includes(env)) {
383
+ problem(
384
+ "manifest/release-environments",
385
+ `release_environments must only contain ${ENVIRONMENTS.join(", ")}. Instead, got "${env}".`
386
+ );
387
+ }
388
+ }
389
+ }
390
+ const developerBusinessId = manifest.developer_business_id;
391
+ if (developerBusinessId !== void 0) {
392
+ if (isNonEmptyString(developerBusinessId)) {
393
+ const distinctValidEnvs = new Set(
394
+ (releaseEnvironments ?? []).filter(
395
+ (env) => ENVIRONMENTS.includes(env)
396
+ )
397
+ );
398
+ if (distinctValidEnvs.size >= 2) {
399
+ warn(
400
+ "manifest/developer-business-id-environments",
401
+ "developer_business_id is a single id but release_environments targets multiple environments; a business id generally exists in only one environment, so publishing to the others will fail - use the per-environment object form instead."
402
+ );
403
+ }
404
+ } else if (typeof developerBusinessId === "object" && developerBusinessId !== null && !Array.isArray(developerBusinessId)) {
405
+ for (const [env, id] of Object.entries(developerBusinessId)) {
406
+ if (!PUBLISHABLE_ENVIRONMENTS.includes(env)) {
407
+ problem(
408
+ "manifest/developer-business-id",
409
+ `developer_business_id has an invalid environment key "${env}": keys must be one of ${PUBLISHABLE_ENVIRONMENTS.join(", ")}. Aliases like ${ENVIRONMENT_ALIASES.join(", ")} are not valid keys here - the publisher indexes by concrete environment, so alias keys silently resolve to nothing.`
410
+ );
411
+ }
412
+ if (!isNonEmptyString(id)) {
413
+ problem(
414
+ "manifest/developer-business-id",
415
+ `developer_business_id["${env}"] must be a non-empty string.`
416
+ );
417
+ }
418
+ }
419
+ } else {
420
+ problem(
421
+ "manifest/developer-business-id",
422
+ "developer_business_id must be a non-empty string or an object mapping publishable environments to ids."
423
+ );
424
+ }
425
+ }
426
+ return issues;
427
+ };
428
+ var collectComponents = (files, entry) => {
429
+ const entryPrefix = entry.endsWith("/") ? entry : `${entry}/`;
430
+ const components = /* @__PURE__ */ new Map();
431
+ for (const file of files) {
432
+ if (!file.path.startsWith(entryPrefix) || file.path.endsWith(".DS_Store")) {
433
+ continue;
434
+ }
435
+ const [directory, name, child] = file.path.slice(entryPrefix.length).split("/");
436
+ if (!directory || !name || !COMPONENT_DIRECTORIES.includes(directory)) {
437
+ continue;
438
+ }
439
+ const key = `${directory}/${name}`;
440
+ const existing = components.get(key) ?? {
441
+ directory,
442
+ name,
443
+ configFile: void 0
444
+ };
445
+ if (child === CONFIG_FILE_NAME) {
446
+ existing.configFile = file;
447
+ }
448
+ components.set(key, existing);
449
+ }
450
+ return [...components.values()];
451
+ };
452
+ var PAGE_LIKE_DIRECTORIES = /* @__PURE__ */ new Set([ROUTABLE_PAGES_DIRECTORY_NAME, VIEWS_DIRECTORY_NAME]);
453
+ var validateComponent = (component, entry, pluginApiName) => {
454
+ const { directory, name, configFile } = component;
455
+ const entryBase = entry.endsWith("/") ? entry.slice(0, -1) : entry;
456
+ const componentPath = `${entryBase}/${directory}/${name}`;
457
+ const issues = [];
458
+ const apiNameIssue = (value, derived2, path) => {
459
+ const derivedNote = derived2 ? ` (derived from directory name "${name}")` : "";
460
+ const derivedFix = derived2 ? ` Set an explicit api_name in ${CONFIG_FILE_NAME}.` : "";
461
+ issues.push(
462
+ errorIssue(
463
+ "structure/api-name-format",
464
+ `api_name "${value}"${derivedNote} is invalid: ${API_NAME_HINT}.${derivedFix}`,
465
+ path,
466
+ pluginApiName
467
+ )
468
+ );
469
+ };
470
+ if (!configFile) {
471
+ if (CONFIG_REQUIRED_DIRECTORIES.includes(directory)) {
472
+ issues.push(
473
+ errorIssue(
474
+ "structure/missing-config",
475
+ `Missing required ${CONFIG_FILE_NAME} for ${directory}/${name}.`,
476
+ componentPath,
477
+ pluginApiName
478
+ )
479
+ );
480
+ }
481
+ if (API_NAMED_DIRECTORIES.has(directory)) {
482
+ const derived2 = sanitizeToAPIName(name);
483
+ if (!API_NAME_PATTERN.test(derived2)) {
484
+ apiNameIssue(derived2, true, componentPath);
485
+ } else {
486
+ return {
487
+ issues,
488
+ path: componentPath,
489
+ resolved: { apiName: derived2, path: componentPath }
490
+ };
491
+ }
492
+ }
493
+ return { issues, path: componentPath };
494
+ }
495
+ const result = tryParseConfig(configFile.content, configFile.path, pluginApiName);
496
+ if ("issue" in result) {
497
+ return { issues: [result.issue], path: componentPath };
498
+ }
499
+ const config = result.config;
500
+ if (directory === FLOATING_FRAMES_DIRECTORY_NAME) {
501
+ const defaultPosition = config.default_position;
502
+ const minimizedStyle = config.minimized_style;
503
+ if ((defaultPosition === "bottom-left-fixed" || defaultPosition === "bottom-right-fixed") && minimizedStyle !== void 0 && minimizedStyle !== "circle") {
504
+ issues.push(
505
+ errorIssue(
506
+ "structure/fixed-frame-minimized-style",
507
+ `Floating frame "${name}" sets default_position "${defaultPosition}" with minimized_style ${JSON.stringify(minimizedStyle)}. Fixed-position frames require minimized_style "circle" (or omit it).`,
508
+ configFile.path,
509
+ pluginApiName
510
+ )
511
+ );
512
+ }
513
+ }
514
+ if (!API_NAMED_DIRECTORIES.has(directory)) {
515
+ return { issues, path: componentPath };
516
+ }
517
+ const explicit = config.api_name;
518
+ if (isNonEmptyString(explicit)) {
519
+ if (!API_NAME_PATTERN.test(explicit)) {
520
+ apiNameIssue(explicit, false, configFile.path);
521
+ return { issues, path: componentPath };
522
+ }
523
+ return {
524
+ issues,
525
+ path: componentPath,
526
+ resolved: { apiName: explicit, path: configFile.path }
527
+ };
528
+ }
529
+ if (explicit) {
530
+ issues.push(
531
+ errorIssue(
532
+ "structure/api-name-format",
533
+ "api_name must be a non-empty string.",
534
+ configFile.path,
535
+ pluginApiName
536
+ )
537
+ );
538
+ return { issues, path: componentPath };
539
+ }
540
+ const derived = sanitizeToAPIName(name);
541
+ if (!API_NAME_PATTERN.test(derived)) {
542
+ apiNameIssue(derived, true, configFile.path);
543
+ return { issues, path: componentPath };
544
+ }
545
+ return {
546
+ issues,
547
+ path: componentPath,
548
+ resolved: { apiName: derived, path: configFile.path }
549
+ };
550
+ };
551
+ var validateManifestList = (manifests, isMultiManifest, files) => {
552
+ const issues = [];
553
+ const seenApiNames = /* @__PURE__ */ new Map();
554
+ manifests.forEach((manifest, index) => {
555
+ const label = isMultiManifest ? `${MANIFEST_FILE_NAME} entry #${String(index + 1)}` : MANIFEST_FILE_NAME;
556
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
557
+ issues.push(errorIssue("manifest/shape", `${label}: must be a JSON object.`, MANIFEST_FILE_NAME));
558
+ return;
559
+ }
560
+ const record = manifest;
561
+ const rawApiName = record.api_name;
562
+ const pluginApiName = isNonEmptyString(rawApiName) ? rawApiName : void 0;
563
+ issues.push(...validateManifestFields(record, label, pluginApiName));
564
+ if (pluginApiName !== void 0) {
565
+ const firstIndex = seenApiNames.get(pluginApiName);
566
+ if (firstIndex !== void 0) {
567
+ issues.push(
568
+ errorIssue(
569
+ "manifest/duplicate-api-name",
570
+ `${label}: duplicate api_name "${pluginApiName}" already used by entry #${String(firstIndex + 1)}. Plugins in a multi-plugin manifest must have unique api_names.`,
571
+ MANIFEST_FILE_NAME,
572
+ pluginApiName
573
+ )
574
+ );
575
+ } else {
576
+ seenApiNames.set(pluginApiName, index);
577
+ }
578
+ }
579
+ const entry = record.entry;
580
+ if (isNonEmptyString(entry) && PATH_PATTERN.test(entry)) {
581
+ const seenResolved = /* @__PURE__ */ new Map();
582
+ const seenPageNames = /* @__PURE__ */ new Map();
583
+ for (const component of collectComponents(files, entry)) {
584
+ const {
585
+ issues: componentIssues,
586
+ path: componentPath,
587
+ resolved
588
+ } = validateComponent(component, entry, pluginApiName);
589
+ issues.push(...componentIssues);
590
+ if (PAGE_LIKE_DIRECTORIES.has(component.directory)) {
591
+ const firstPage = seenPageNames.get(component.name);
592
+ if (firstPage !== void 0) {
593
+ issues.push(
594
+ errorIssue(
595
+ "structure/duplicate-component-name",
596
+ `duplicate component name "${component.name}": ${component.directory}/${component.name} (${componentPath}) collides with ${firstPage.directory}/${component.name} (${firstPage.path}). pages and views share one routable-pages collection, so names must be unique across both directories.`,
597
+ componentPath,
598
+ pluginApiName
599
+ )
600
+ );
601
+ } else {
602
+ seenPageNames.set(component.name, {
603
+ path: componentPath,
604
+ directory: component.directory
605
+ });
606
+ }
607
+ }
608
+ if (resolved) {
609
+ const key = `${component.directory}
610
+ ${resolved.apiName}`;
611
+ const firstPath = seenResolved.get(key);
612
+ if (firstPath !== void 0) {
613
+ issues.push(
614
+ errorIssue(
615
+ "structure/duplicate-api-name",
616
+ `duplicate api_name "${resolved.apiName}" in ${component.directory}: ${resolved.path} collides with ${firstPath}. Sibling ${component.directory} components must resolve to unique api_names.`,
617
+ resolved.path,
618
+ pluginApiName
619
+ )
620
+ );
621
+ } else {
622
+ seenResolved.set(key, resolved.path);
623
+ }
624
+ }
625
+ }
626
+ }
627
+ });
628
+ return issues;
629
+ };
630
+ var validatePluginApp = (files) => {
631
+ const manifestFile = files.find((f) => f.path === MANIFEST_FILE_NAME);
632
+ if (!manifestFile) {
633
+ return [
634
+ errorIssue(
635
+ "manifest/missing",
636
+ `Missing ${MANIFEST_FILE_NAME} at the root of the plugin directory.`,
637
+ MANIFEST_FILE_NAME
638
+ )
639
+ ];
640
+ }
641
+ let parsed;
642
+ try {
643
+ parsed = JSON.parse(manifestFile.content);
644
+ } catch {
645
+ return [
646
+ errorIssue(
647
+ "manifest/parse",
648
+ `${MANIFEST_FILE_NAME} could not be parsed. Is it properly formatted JSON?`,
649
+ MANIFEST_FILE_NAME
650
+ )
651
+ ];
652
+ }
653
+ const isMultiManifest = Array.isArray(parsed);
654
+ const manifests = isMultiManifest ? parsed : [parsed];
655
+ return validateManifestList(manifests, isMultiManifest, files);
656
+ };
657
+ var validatePackagedManifests = (manifests, files) => {
658
+ const isMultiManifest = Array.isArray(manifests);
659
+ const list = isMultiManifest ? manifests : [manifests];
660
+ return validateManifestList(list, isMultiManifest, files);
661
+ };
662
+
663
+ // src/lib/image.ts
664
+ var imagetoUint8Array = (base64Image) => {
665
+ const byteCharacters = atob(base64Image);
666
+ const byteNumbers = new Array(byteCharacters.length);
667
+ for (let i = 0; i < byteCharacters.length; i++) {
668
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
669
+ }
670
+ return new Uint8Array(byteNumbers);
671
+ };
672
+ var zipToUint8Array = (zip) => {
673
+ return Uint8Array.from(zip);
674
+ };
675
+
217
676
  // src/lib/package.ts
218
677
  var replacementVar = "__kizen_state";
219
678
  var prepend = "const __kizen_utils = {};\n";
@@ -226,16 +685,13 @@ return ${inner}; })()`;
226
685
  };
227
686
  var mapFields = (fields, scriptsByKey) => {
228
687
  return fields.map((field) => {
229
- if (field["type"] === "container" && field["fields"] && Array.isArray(field["fields"])) {
688
+ if (field.type === "container" && field.fields && Array.isArray(field.fields)) {
230
689
  return {
231
690
  ...field,
232
- fields: mapFields(
233
- field["fields"],
234
- scriptsByKey
235
- )
691
+ fields: mapFields(field.fields, scriptsByKey)
236
692
  };
237
693
  }
238
- const fieldKey = field["key"];
694
+ const fieldKey = field.key;
239
695
  const scripts = scriptsByKey[fieldKey];
240
696
  if (scripts) {
241
697
  const result = { ...field };
@@ -248,12 +704,7 @@ var mapFields = (fields, scriptsByKey) => {
248
704
  ]) {
249
705
  const fn = scripts[fnName];
250
706
  if (fn) {
251
- result[fnName] = convertToSelfInvokingFunction(
252
- fn,
253
- replacementVar,
254
- {},
255
- prepend
256
- );
707
+ result[fnName] = convertToSelfInvokingFunction(fn, replacementVar, {}, prepend);
257
708
  }
258
709
  }
259
710
  return result;
@@ -265,19 +716,13 @@ var processSetupAssistantFromFiles = (assistantConfig, scripts) => {
265
716
  if (!assistantConfig) {
266
717
  return void 0;
267
718
  }
268
- const scriptsByKey = scripts.reduce(
269
- (acc, script) => {
270
- (acc[script.key] ??= {})[script.type] = script.content;
271
- return acc;
272
- },
273
- {}
274
- );
719
+ const scriptsByKey = scripts.reduce((acc, script) => {
720
+ (acc[script.key] ??= {})[script.type] = script.content;
721
+ return acc;
722
+ }, {});
275
723
  const newConfig = { ...assistantConfig };
276
- if (newConfig["fields"] && Array.isArray(newConfig["fields"])) {
277
- newConfig["fields"] = mapFields(
278
- newConfig["fields"],
279
- scriptsByKey
280
- );
724
+ if (newConfig.fields && Array.isArray(newConfig.fields)) {
725
+ newConfig.fields = mapFields(newConfig.fields, scriptsByKey);
281
726
  }
282
727
  return newConfig;
283
728
  };
@@ -285,10 +730,12 @@ var processOnePluginFromManifest = (manifest, files) => {
285
730
  const entry = manifest.entry;
286
731
  const releaseNotesDirectory = manifest.release_notes_directory;
287
732
  const apiName = manifest.api_name;
733
+ const entryPrefix = entry.endsWith("/") ? entry : `${entry}/`;
288
734
  const consideredFiles = files.filter(
289
- (f) => f.path !== MANIFEST_FILE_NAME && f.path.startsWith(entry) && !f.path.endsWith(".DS_Store")
735
+ (f) => f.path !== MANIFEST_FILE_NAME && f.path.startsWith(entryPrefix) && !f.path.endsWith(".DS_Store")
290
736
  );
291
- const releaseNotesFiles = releaseNotesDirectory ? files.filter((f) => f.path.startsWith(releaseNotesDirectory)) : [];
737
+ const releaseNotesPrefix = releaseNotesDirectory?.endsWith("/") ? releaseNotesDirectory : `${releaseNotesDirectory ?? ""}/`;
738
+ const releaseNotesFiles = releaseNotesDirectory ? files.filter((f) => f.path.startsWith(releaseNotesPrefix)) : [];
292
739
  const { version } = manifest;
293
740
  const releaseNotes = releaseNotesFiles.find(
294
741
  (file) => file.path.endsWith(`${version}.${RELEASE_NOTES_EXTENSION}`)
@@ -306,18 +753,16 @@ var processOnePluginFromManifest = (manifest, files) => {
306
753
  let thumbnail = null;
307
754
  let kznFile = null;
308
755
  let hasBlockingCondition = false;
309
- const requiredConfigFiles = {};
310
756
  let setupAssistantJSON = null;
311
757
  const setupAssistantFunctions = [];
312
758
  let userSetupAssistantJSON = null;
313
759
  const userSetupAssistantFunctions = [];
314
760
  for (const file of consideredFiles) {
315
- let strippedPath = file.path.replace(entry, "");
316
- strippedPath = strippedPath.startsWith("/") ? strippedPath.substring(1) : strippedPath;
761
+ const strippedPath = file.path.slice(entryPrefix.length);
317
762
  const splitPath = strippedPath.split("/");
318
763
  if (splitPath[0] === SETUP_ASSISTANT_DIRECTORY_NAME) {
319
764
  if (splitPath[1] === "assistant.json") {
320
- setupAssistantJSON = maybeParseConfig(file.content, file.path);
765
+ setupAssistantJSON = maybeParseConfig(file.content, file.path, apiName);
321
766
  } else {
322
767
  const assistantKey = splitPath[1];
323
768
  if (assistantKey && splitPath[2]?.endsWith(`.${SCRIPT_EXTENSION}`)) {
@@ -330,7 +775,7 @@ var processOnePluginFromManifest = (manifest, files) => {
330
775
  }
331
776
  } else if (splitPath[0] === USER_SETUP_ASSISTANT_DIRECTORY_NAME) {
332
777
  if (splitPath[1] === "assistant.json") {
333
- userSetupAssistantJSON = maybeParseConfig(file.content, file.path);
778
+ userSetupAssistantJSON = maybeParseConfig(file.content, file.path, apiName);
334
779
  } else {
335
780
  const assistantKey = splitPath[1];
336
781
  if (assistantKey && splitPath[2]?.endsWith(`.${SCRIPT_EXTENSION}`)) {
@@ -346,30 +791,27 @@ var processOnePluginFromManifest = (manifest, files) => {
346
791
  if (!floatingFrameName) {
347
792
  continue;
348
793
  }
349
- if (!floatingFrames[floatingFrameName]) {
350
- requiredConfigFiles[`frame:${floatingFrameName}`] = true;
351
- floatingFrames[floatingFrameName] = {
352
- name: "",
353
- api_name: "",
354
- title: "",
355
- type: "script",
356
- css: "",
357
- event_scripts: {},
358
- default_position: "bottom-right",
359
- header_color: "",
360
- header_text_color: "",
361
- height: 0,
362
- width: 0,
363
- ignore: [],
364
- match: [],
365
- message_handler: "",
366
- minimized_style: "circle",
367
- minimized_config: {},
368
- script: "",
369
- html: "",
370
- when: ""
371
- };
372
- }
794
+ floatingFrames[floatingFrameName] ??= {
795
+ name: "",
796
+ api_name: "",
797
+ title: "",
798
+ type: "script",
799
+ css: "",
800
+ event_scripts: {},
801
+ default_position: "bottom-right",
802
+ header_color: "",
803
+ header_text_color: "",
804
+ height: 0,
805
+ width: 0,
806
+ ignore: [],
807
+ match: [],
808
+ message_handler: "",
809
+ minimized_style: "circle",
810
+ minimized_config: {},
811
+ script: "",
812
+ html: "",
813
+ when: ""
814
+ };
373
815
  const frame = floatingFrames[floatingFrameName];
374
816
  if (splitPath[2] === EVENT_SCRIPTS_DIRECTORY_NAME) {
375
817
  if (splitPath[3]?.endsWith(`.${SCRIPT_EXTENSION}`)) {
@@ -377,27 +819,22 @@ var processOnePluginFromManifest = (manifest, files) => {
377
819
  frame.event_scripts[functionName] = file.minified;
378
820
  }
379
821
  } else if (splitPath[2] === CONFIG_FILE_NAME) {
380
- requiredConfigFiles[`frame:${floatingFrameName}`] = false;
381
- const config = maybeParseConfig(file.content, file.path);
382
- frame.name = config["name"] || "";
383
- frame.api_name = config["api_name"] || sanitizeToAPIName(floatingFrameName);
384
- frame.title = config["title"] || "";
385
- frame.default_position = config["default_position"] || "bottom-right";
386
- frame.header_color = config["header_color"] || "";
387
- frame.header_text_color = config["header_text_color"] || "";
388
- frame.height = config["height"] || 0;
389
- frame.width = config["width"] || 0;
390
- frame.ignore = config["ignore"] || [];
391
- frame.match = config["match"] || [];
392
- frame.html = config["html"] || "";
393
- frame.minimized_style = config["minimized_style"] || "circle";
394
- frame.minimized_config = getMinimizedConfig(
395
- config,
396
- entry,
397
- splitPath,
398
- consideredFiles
399
- );
400
- frame.when = config["when"] || "";
822
+ const config = maybeParseConfig(file.content, file.path, apiName);
823
+ frame.name = config.name || "";
824
+ frame.api_name = resolveApiName(config.api_name, floatingFrameName);
825
+ frame.title = config.title || "";
826
+ frame.default_position = config.default_position || "bottom-right";
827
+ frame.header_color = config.header_color || "";
828
+ frame.header_text_color = config.header_text_color || "";
829
+ frame.height = config.height || 0;
830
+ frame.width = config.width || 0;
831
+ frame.ignore = Array.isArray(config.ignore) ? config.ignore : [];
832
+ frame.match = Array.isArray(config.match) ? config.match : [];
833
+ frame.html = config.html || "";
834
+ const minimizedStyle = config.minimized_style;
835
+ frame.minimized_style = minimizedStyle === "bar" || minimizedStyle === "circle" || minimizedStyle === "none" ? minimizedStyle : "circle";
836
+ frame.minimized_config = getMinimizedConfig(config, entry, splitPath, consideredFiles);
837
+ frame.when = config.when || "";
401
838
  } else if (splitPath[2] === MESSAGE_SCRIPT_FILE) {
402
839
  frame.message_handler = file.minified;
403
840
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
@@ -410,21 +847,18 @@ var processOnePluginFromManifest = (manifest, files) => {
410
847
  if (!blockName) {
411
848
  continue;
412
849
  }
413
- if (!blocks[blockName]) {
414
- requiredConfigFiles[`block:${blockName}`] = true;
415
- blocks[blockName] = {
416
- name: "",
417
- api_name: "",
418
- min_w: 1,
419
- max_w: 12,
420
- min_h: 1,
421
- max_h: 12,
422
- event_scripts: {},
423
- script: "",
424
- styles: "",
425
- when: ""
426
- };
427
- }
850
+ blocks[blockName] ??= {
851
+ name: "",
852
+ api_name: "",
853
+ min_w: 1,
854
+ max_w: 12,
855
+ min_h: 1,
856
+ max_h: 12,
857
+ event_scripts: {},
858
+ script: "",
859
+ styles: "",
860
+ when: ""
861
+ };
428
862
  const block = blocks[blockName];
429
863
  if (splitPath[2] === EVENT_SCRIPTS_DIRECTORY_NAME) {
430
864
  if (splitPath[3]?.endsWith(`.${SCRIPT_EXTENSION}`)) {
@@ -432,22 +866,21 @@ var processOnePluginFromManifest = (manifest, files) => {
432
866
  block.event_scripts[functionName] = file.minified;
433
867
  }
434
868
  } else if (splitPath[2] === CONFIG_FILE_NAME) {
435
- requiredConfigFiles[`block:${blockName}`] = false;
436
- const config = maybeParseConfig(file.content, file.path);
437
- block.name = config["name"] || "";
438
- block.api_name = config["api_name"] || sanitizeToAPIName(blockName);
439
- block.min_w = config["min_w"] || 1;
440
- block.max_w = config["max_w"] || 12;
441
- block.min_h = config["min_h"] || 1;
442
- block.max_h = config["max_h"] || 12;
443
- if (config["recommended_height"] != null) {
444
- block.recommended_height = config["recommended_height"];
869
+ const config = maybeParseConfig(file.content, file.path, apiName);
870
+ block.name = config.name || "";
871
+ block.api_name = resolveApiName(config.api_name, blockName);
872
+ block.min_w = config.min_w || 1;
873
+ block.max_w = config.max_w || 12;
874
+ block.min_h = config.min_h || 1;
875
+ block.max_h = config.max_h || 12;
876
+ if (config.recommended_height != null) {
877
+ block.recommended_height = config.recommended_height;
445
878
  }
446
- if (config["types"]) {
447
- block.types = config["types"];
879
+ if (config.types) {
880
+ block.types = config.types;
448
881
  }
449
- block.when = config["when"] || "";
450
- if (config["when"]) {
882
+ block.when = config.when || "";
883
+ if (config.when) {
451
884
  hasBlockingCondition = true;
452
885
  }
453
886
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
@@ -460,31 +893,23 @@ var processOnePluginFromManifest = (manifest, files) => {
460
893
  if (!dataAdornmentName) {
461
894
  continue;
462
895
  }
463
- if (!dataAdornments[dataAdornmentName]) {
464
- requiredConfigFiles[`dataAdornment:${dataAdornmentName}`] = true;
465
- dataAdornments[dataAdornmentName] = {
466
- config: { icon: "", color: "", tooltip: "" },
467
- field_type: "phonenumber",
468
- script: "",
469
- when: ""
470
- };
471
- }
896
+ dataAdornments[dataAdornmentName] ??= {
897
+ config: { icon: "", color: "", tooltip: "" },
898
+ field_type: "phonenumber",
899
+ script: "",
900
+ when: ""
901
+ };
472
902
  const adornment = dataAdornments[dataAdornmentName];
473
903
  if (splitPath[2] === CONFIG_FILE_NAME) {
474
- requiredConfigFiles[`dataAdornment:${dataAdornmentName}`] = false;
475
- const config = maybeParseConfig(file.content, file.path);
476
- adornment.config.icon = config["icon"] || "";
477
- adornment.config.color = config["color"] || "";
478
- adornment.config.tooltip = config["tooltip"] || "";
479
- adornment.config.customIcon = getAdornmentIcon(
480
- config,
481
- entry,
482
- splitPath,
483
- consideredFiles
484
- );
485
- adornment.field_type = config["field_type"] || "phonenumber";
486
- adornment.when = config["when"] || "";
487
- if (config["when"]) hasBlockingCondition = true;
904
+ const config = maybeParseConfig(file.content, file.path, apiName);
905
+ adornment.config.icon = config.icon || "";
906
+ adornment.config.color = config.color || "";
907
+ adornment.config.tooltip = config.tooltip || "";
908
+ adornment.config.customIcon = getAdornmentIcon(config, entry, splitPath, consideredFiles);
909
+ const fieldType = config.field_type;
910
+ adornment.field_type = fieldType === "phonenumber" || fieldType === "date" || fieldType === "datetime" ? fieldType : "phonenumber";
911
+ adornment.when = config.when || "";
912
+ if (config.when) hasBlockingCondition = true;
488
913
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
489
914
  adornment.script = file.minified;
490
915
  }
@@ -493,27 +918,23 @@ var processOnePluginFromManifest = (manifest, files) => {
493
918
  if (!toolbarItemName) {
494
919
  continue;
495
920
  }
496
- if (!toolbarItems[toolbarItemName]) {
497
- requiredConfigFiles[`toolbarItem:${toolbarItemName}`] = true;
498
- toolbarItems[toolbarItemName] = {
499
- api_name: "",
500
- label: "",
501
- icon: "",
502
- color: "",
503
- script: "",
504
- when: ""
505
- };
506
- }
921
+ toolbarItems[toolbarItemName] ??= {
922
+ api_name: "",
923
+ label: "",
924
+ icon: "",
925
+ color: "",
926
+ script: "",
927
+ when: ""
928
+ };
507
929
  const item = toolbarItems[toolbarItemName];
508
930
  if (splitPath[2] === CONFIG_FILE_NAME) {
509
- requiredConfigFiles[`toolbarItem:${toolbarItemName}`] = false;
510
- const config = maybeParseConfig(file.content, file.path);
511
- item.api_name = config["api_name"] || sanitizeToAPIName(toolbarItemName);
512
- item.label = config["label"] || "";
513
- item.icon = config["icon"] || "";
514
- item.color = config["color"] || "";
515
- item.when = config["when"] || "";
516
- if (config["when"]) hasBlockingCondition = true;
931
+ const config = maybeParseConfig(file.content, file.path, apiName);
932
+ item.api_name = resolveApiName(config.api_name, toolbarItemName);
933
+ item.label = config.label || "";
934
+ item.icon = config.icon || "";
935
+ item.color = config.color || "";
936
+ item.when = config.when || "";
937
+ if (config.when) hasBlockingCondition = true;
517
938
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
518
939
  item.script = file.minified;
519
940
  }
@@ -522,23 +943,20 @@ var processOnePluginFromManifest = (manifest, files) => {
522
943
  if (!pageName) {
523
944
  continue;
524
945
  }
525
- if (!routablePages[pageName]) {
526
- requiredConfigFiles[`page:${pageName}`] = true;
527
- routablePages[pageName] = {
528
- name: pageName,
529
- api_name: "",
530
- event_scripts: {},
531
- script: "",
532
- callback: "",
533
- css: "",
534
- is_toolbar_item: false,
535
- toolbar_color: "",
536
- toolbar_icon: "",
537
- type: "script",
538
- html: "",
539
- iframe_url: ""
540
- };
541
- }
946
+ routablePages[pageName] ??= {
947
+ name: pageName,
948
+ api_name: "",
949
+ event_scripts: {},
950
+ script: "",
951
+ callback: "",
952
+ css: "",
953
+ is_toolbar_item: false,
954
+ toolbar_color: "",
955
+ toolbar_icon: "",
956
+ type: "script",
957
+ html: "",
958
+ iframe_url: ""
959
+ };
542
960
  const page = routablePages[pageName];
543
961
  if (splitPath[2] === EVENT_SCRIPTS_DIRECTORY_NAME) {
544
962
  if (splitPath[3]?.endsWith(SCRIPT_EXTENSION)) {
@@ -546,13 +964,12 @@ var processOnePluginFromManifest = (manifest, files) => {
546
964
  page.event_scripts[functionName] = file.minified;
547
965
  }
548
966
  } else if (splitPath[2] === CONFIG_FILE_NAME) {
549
- requiredConfigFiles[`page:${pageName}`] = false;
550
- const config = maybeParseConfig(file.content, file.path);
551
- page.api_name = config["api_name"] || sanitizeToAPIName(pageName);
552
- page.name = config["name"] || pageName;
553
- page.is_toolbar_item = config["is_toolbar_item"] || false;
554
- page.toolbar_color = config["toolbar_color"] || "";
555
- page.toolbar_icon = config["toolbar_icon"] || "";
967
+ const config = maybeParseConfig(file.content, file.path, apiName);
968
+ page.api_name = resolveApiName(config.api_name, pageName);
969
+ page.name = config.name || pageName;
970
+ page.is_toolbar_item = config.is_toolbar_item || false;
971
+ page.toolbar_color = config.toolbar_color || "";
972
+ page.toolbar_icon = config.toolbar_icon || "";
556
973
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
557
974
  page.script = file.minified;
558
975
  } else if (splitPath[2] === CALLBACK_SCRIPT_FILE) {
@@ -565,39 +982,35 @@ var processOnePluginFromManifest = (manifest, files) => {
565
982
  if (!stepName) {
566
983
  continue;
567
984
  }
568
- if (!automationSteps[stepName]) {
569
- requiredConfigFiles[`automationStep:${stepName}`] = true;
570
- automationSteps[stepName] = {
571
- name: "",
572
- action_step_api_name: "",
573
- overall_description: "",
574
- action_description: "",
575
- action_type: "",
576
- script_runtime: scriptRuntimeToApiName("python 3.12"),
577
- secrets: [],
578
- inputs: [],
579
- outputs: [],
580
- script: "",
581
- when: ""
582
- };
583
- }
985
+ automationSteps[stepName] ??= {
986
+ name: "",
987
+ action_step_api_name: "",
988
+ overall_description: "",
989
+ action_description: "",
990
+ action_type: "",
991
+ script_runtime: scriptRuntimeToApiName("python 3.12"),
992
+ secrets: [],
993
+ inputs: [],
994
+ outputs: [],
995
+ script: "",
996
+ when: ""
997
+ };
584
998
  const step = automationSteps[stepName];
585
999
  if (splitPath[2] === CONFIG_FILE_NAME) {
586
- requiredConfigFiles[`automationStep:${stepName}`] = false;
587
- const config = maybeParseConfig(file.content, file.path);
588
- step.name = config["name"] || stepName;
589
- step.action_step_api_name = config["api_name"] || sanitizeToAPIName(stepName);
590
- step.overall_description = config["plugin_description"] || "";
591
- step.action_description = config["action_description"] || "";
592
- step.action_type = config["action_type"] || "";
593
- step.secrets = config["secrets"] || [];
594
- step.inputs = config["inputs"] || [];
595
- step.outputs = config["outputs"] || [];
1000
+ const config = maybeParseConfig(file.content, file.path, apiName);
1001
+ step.name = config.name || stepName;
1002
+ step.action_step_api_name = resolveApiName(config.api_name, stepName);
1003
+ step.overall_description = config.plugin_description || "";
1004
+ step.action_description = config.action_description || "";
1005
+ step.action_type = config.action_type || "";
1006
+ step.secrets = Array.isArray(config.secrets) ? config.secrets : [];
1007
+ step.inputs = Array.isArray(config.inputs) ? config.inputs : [];
1008
+ step.outputs = Array.isArray(config.outputs) ? config.outputs : [];
596
1009
  step.script_runtime = scriptRuntimeToApiName(
597
- config["runtime"] || "python 3.12"
1010
+ config.runtime || "python 3.12"
598
1011
  );
599
- step.when = config["when"] || "";
600
- if (config["when"]) hasBlockingCondition = true;
1012
+ step.when = config.when || "";
1013
+ if (config.when) hasBlockingCondition = true;
601
1014
  } else if (splitPath[2] === AUTOMATION_SCRIPT_FILE) {
602
1015
  step.script = file.content;
603
1016
  }
@@ -606,24 +1019,20 @@ var processOnePluginFromManifest = (manifest, files) => {
606
1019
  if (!routeName) {
607
1020
  continue;
608
1021
  }
609
- if (!routeScripts[routeName]) {
610
- requiredConfigFiles[`route:${routeName}`] = true;
611
- routeScripts[routeName] = {
612
- script: "",
613
- api_name: "",
614
- hint_object_name: "",
615
- routes: [],
616
- name: routeName
617
- };
618
- }
1022
+ routeScripts[routeName] ??= {
1023
+ script: "",
1024
+ api_name: "",
1025
+ hint_object_name: "",
1026
+ routes: [],
1027
+ name: routeName
1028
+ };
619
1029
  const route = routeScripts[routeName];
620
1030
  if (splitPath[2] === CONFIG_FILE_NAME) {
621
- requiredConfigFiles[`route:${routeName}`] = false;
622
- const config = maybeParseConfig(file.content, file.path);
623
- route.api_name = config["api_name"] || sanitizeToAPIName(routeName);
624
- route.hint_object_name = config["hint_object_name"] || "";
625
- route.routes = config["routes"] || [];
626
- route.name = config["name"] || routeName;
1031
+ const config = maybeParseConfig(file.content, file.path, apiName);
1032
+ route.api_name = resolveApiName(config.api_name, routeName);
1033
+ route.hint_object_name = config.hint_object_name || "";
1034
+ route.routes = Array.isArray(config.routes) ? config.routes : [];
1035
+ route.name = config.name || routeName;
627
1036
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
628
1037
  route.script = file.minified;
629
1038
  }
@@ -632,22 +1041,18 @@ var processOnePluginFromManifest = (manifest, files) => {
632
1041
  if (!actionName) {
633
1042
  continue;
634
1043
  }
635
- if (!jsActions[actionName]) {
636
- requiredConfigFiles[`action:${actionName}`] = true;
637
- jsActions[actionName] = {
638
- script: "",
639
- name: "",
640
- hint_object_name: "",
641
- api_name: ""
642
- };
643
- }
1044
+ jsActions[actionName] ??= {
1045
+ script: "",
1046
+ name: "",
1047
+ hint_object_name: "",
1048
+ api_name: ""
1049
+ };
644
1050
  const action = jsActions[actionName];
645
1051
  if (splitPath[2] === CONFIG_FILE_NAME) {
646
- requiredConfigFiles[`action:${actionName}`] = false;
647
- const config = maybeParseConfig(file.content, file.path);
648
- action.name = config["name"] || actionName;
649
- action.hint_object_name = config["hint_object_name"] || "";
650
- action.api_name = config["api_name"] || sanitizeToAPIName(actionName);
1052
+ const config = maybeParseConfig(file.content, file.path, apiName);
1053
+ action.name = config.name || actionName;
1054
+ action.hint_object_name = config.hint_object_name || "";
1055
+ action.api_name = resolveApiName(config.api_name, actionName);
651
1056
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
652
1057
  action.script = file.minified;
653
1058
  }
@@ -656,22 +1061,18 @@ var processOnePluginFromManifest = (manifest, files) => {
656
1061
  if (!itemName) {
657
1062
  continue;
658
1063
  }
659
- if (!objectSettingsItems[itemName]) {
660
- requiredConfigFiles[`objectSettingsItem:${itemName}`] = true;
661
- objectSettingsItems[itemName] = {
662
- api_name: "",
663
- label: "",
664
- script: "",
665
- when: ""
666
- };
667
- }
1064
+ objectSettingsItems[itemName] ??= {
1065
+ api_name: "",
1066
+ label: "",
1067
+ script: "",
1068
+ when: ""
1069
+ };
668
1070
  const settingsItem = objectSettingsItems[itemName];
669
1071
  if (splitPath[2] === CONFIG_FILE_NAME) {
670
- requiredConfigFiles[`objectSettingsItem:${itemName}`] = false;
671
- const config = maybeParseConfig(file.content, file.path);
672
- settingsItem.api_name = config["api_name"] || sanitizeToAPIName(itemName);
673
- settingsItem.label = config["label"] || "";
674
- settingsItem.when = config["when"] || "";
1072
+ const config = maybeParseConfig(file.content, file.path, apiName);
1073
+ settingsItem.api_name = resolveApiName(config.api_name, itemName);
1074
+ settingsItem.label = config.label || "";
1075
+ settingsItem.when = config.when || "";
675
1076
  } else if (splitPath[2] === MAIN_SCRIPT_FILE) {
676
1077
  settingsItem.script = file.minified;
677
1078
  }
@@ -680,24 +1081,20 @@ var processOnePluginFromManifest = (manifest, files) => {
680
1081
  if (!itemName) {
681
1082
  continue;
682
1083
  }
683
- if (!calendarSources[itemName]) {
684
- requiredConfigFiles[`calendarSource:${itemName}`] = true;
685
- calendarSources[itemName] = {
686
- api_name: "",
687
- name: "",
688
- calendars_script: "",
689
- events_script: "",
690
- when: ""
691
- };
692
- }
1084
+ calendarSources[itemName] ??= {
1085
+ api_name: "",
1086
+ name: "",
1087
+ calendars_script: "",
1088
+ events_script: "",
1089
+ when: ""
1090
+ };
693
1091
  const cal = calendarSources[itemName];
694
1092
  if (splitPath[2] === CONFIG_FILE_NAME) {
695
- requiredConfigFiles[`calendarSource:${itemName}`] = false;
696
- const config = maybeParseConfig(file.content, file.path);
697
- cal.api_name = config["api_name"] || sanitizeToAPIName(itemName);
698
- cal.name = config["name"] || "";
699
- cal.when = config["when"] || "";
700
- if (config["when"]) hasBlockingCondition = true;
1093
+ const config = maybeParseConfig(file.content, file.path, apiName);
1094
+ cal.api_name = resolveApiName(config.api_name, itemName);
1095
+ cal.name = config.name || "";
1096
+ cal.when = config.when || "";
1097
+ if (config.when) hasBlockingCondition = true;
701
1098
  } else if (splitPath[2] === "calendars.js") {
702
1099
  cal.calendars_script = file.minified;
703
1100
  } else if (splitPath[2] === "events.js") {
@@ -708,22 +1105,20 @@ var processOnePluginFromManifest = (manifest, files) => {
708
1105
  if (!pageName) {
709
1106
  continue;
710
1107
  }
711
- if (!routablePages[pageName]) {
712
- routablePages[pageName] = {
713
- name: pageName,
714
- api_name: sanitizeToAPIName(pageName),
715
- event_scripts: {},
716
- script: "",
717
- callback: "",
718
- css: "",
719
- is_toolbar_item: false,
720
- toolbar_color: "",
721
- toolbar_icon: "",
722
- type: "html",
723
- html: "",
724
- iframe_url: ""
725
- };
726
- }
1108
+ routablePages[pageName] ??= {
1109
+ name: pageName,
1110
+ api_name: sanitizeToAPIName(pageName),
1111
+ event_scripts: {},
1112
+ script: "",
1113
+ callback: "",
1114
+ css: "",
1115
+ is_toolbar_item: false,
1116
+ toolbar_color: "",
1117
+ toolbar_icon: "",
1118
+ type: "html",
1119
+ html: "",
1120
+ iframe_url: ""
1121
+ };
727
1122
  const page = routablePages[pageName];
728
1123
  if (splitPath[2] === EVENT_SCRIPTS_DIRECTORY_NAME) {
729
1124
  if (splitPath[3]?.endsWith(SCRIPT_EXTENSION)) {
@@ -731,9 +1126,9 @@ var processOnePluginFromManifest = (manifest, files) => {
731
1126
  page.event_scripts[functionName] = file.minified;
732
1127
  }
733
1128
  } else if (splitPath[2] === CONFIG_FILE_NAME) {
734
- const config = maybeParseConfig(file.content, file.path);
735
- page.api_name = config["api_name"] || sanitizeToAPIName(pageName);
736
- page.name = config["name"] || pageName;
1129
+ const config = maybeParseConfig(file.content, file.path, apiName);
1130
+ page.api_name = resolveApiName(config.api_name, pageName);
1131
+ page.name = config.name || pageName;
737
1132
  } else if (splitPath[2] === VIEW_FILE) {
738
1133
  page.html = file.content;
739
1134
  page.type = "html";
@@ -745,35 +1140,42 @@ var processOnePluginFromManifest = (manifest, files) => {
745
1140
  }
746
1141
  } else if (splitPath[0] === KZN_FILE_NAME) {
747
1142
  if (!file.binaryData) {
748
- throw new Error(
749
- `KZN file ${file.path} does not contain binary data. Cannot proceed.`
750
- );
1143
+ throw new PluginValidationError([
1144
+ errorIssue(
1145
+ "structure/kzn-binary",
1146
+ `KZN file ${file.path} does not contain binary data. Cannot proceed.`,
1147
+ file.path,
1148
+ apiName
1149
+ )
1150
+ ]);
751
1151
  }
752
1152
  if (kznFile) {
753
- throw new Error(
754
- `Multiple KZN files found for plugin ${apiName}. Only one is allowed.`
755
- );
1153
+ throw new PluginValidationError([
1154
+ errorIssue(
1155
+ "structure/duplicate-kzn",
1156
+ `Multiple KZN files found for plugin ${apiName}. Only one is allowed.`,
1157
+ file.path,
1158
+ apiName
1159
+ )
1160
+ ]);
756
1161
  }
757
1162
  kznFile = zipToUint8Array(file.binaryData);
758
1163
  } else if (THUMBNAIL_FILE_NAMES.includes(splitPath[0] ?? "")) {
759
1164
  if (file.base64Image) {
760
1165
  if (thumbnail) {
761
- throw new Error(
762
- `Multiple thumbnail files found for plugin ${apiName}. Only one is allowed.`
763
- );
1166
+ throw new PluginValidationError([
1167
+ errorIssue(
1168
+ "structure/duplicate-thumbnail",
1169
+ `Multiple thumbnail files found for plugin ${apiName}. Only one is allowed.`,
1170
+ file.path,
1171
+ apiName
1172
+ )
1173
+ ]);
764
1174
  }
765
1175
  thumbnail = imagetoUint8Array(file.base64Image);
766
1176
  }
767
1177
  }
768
1178
  }
769
- const requiredFileKeys = Object.keys(requiredConfigFiles).filter(
770
- (key) => requiredConfigFiles[key] === true
771
- );
772
- if (requiredFileKeys.length > 0) {
773
- throw new Error(
774
- `The following required files are missing for plugin ${apiName}: ${requiredFileKeys.join(" config.json, ")} config.json. Please ensure these files are present in the repository.`
775
- );
776
- }
777
1179
  const setupAssistantContent = processSetupAssistantFromFiles(
778
1180
  setupAssistantJSON,
779
1181
  setupAssistantFunctions
@@ -794,26 +1196,31 @@ var processOnePluginFromManifest = (manifest, files) => {
794
1196
  if (resolvedUserSetupAssistant !== void 0) {
795
1197
  updatedManifest.user_setup_assistant = resolvedUserSetupAssistant;
796
1198
  }
797
- return {
798
- [apiName]: {
799
- manifest: updatedManifest,
800
- thumbnail,
801
- floatingFrames,
802
- blocks,
803
- dataAdornments,
804
- toolbarItems,
805
- routablePages,
806
- actions: jsActions,
807
- routeScripts,
808
- releaseNotes: releaseNotes?.content,
809
- objectSettingsMenuItems: objectSettingsItems,
810
- calendarSources,
811
- automationSteps,
812
- kznFile
813
- }
1199
+ const packagedPlugin = {
1200
+ manifest: updatedManifest,
1201
+ thumbnail,
1202
+ floatingFrames,
1203
+ blocks,
1204
+ dataAdornments,
1205
+ toolbarItems,
1206
+ routablePages,
1207
+ actions: jsActions,
1208
+ routeScripts,
1209
+ objectSettingsMenuItems: objectSettingsItems,
1210
+ calendarSources,
1211
+ automationSteps,
1212
+ kznFile
814
1213
  };
1214
+ if (releaseNotes?.content !== void 0) {
1215
+ packagedPlugin.releaseNotes = releaseNotes.content;
1216
+ }
1217
+ return { [apiName]: packagedPlugin };
815
1218
  };
816
1219
  var packagePlugin = (files, manifests) => {
1220
+ const errors = validatePackagedManifests(manifests, files).filter((i) => i.severity === "error");
1221
+ if (errors.length > 0) {
1222
+ throw new PluginValidationError(errors);
1223
+ }
817
1224
  const manifestArray = Array.isArray(manifests) ? manifests : [manifests];
818
1225
  return manifestArray.reduce((acc, manifest) => {
819
1226
  return { ...acc, ...processOnePluginFromManifest(manifest, files) };
@@ -822,12 +1229,24 @@ var packagePlugin = (files, manifests) => {
822
1229
  var parseManifestFromFiles = (files) => {
823
1230
  const manifestFile = files.find((f) => f.path === MANIFEST_FILE_NAME);
824
1231
  if (!manifestFile) {
825
- throw new Error("Missing kizen.json at the root of the plugin directory.");
1232
+ throw new PluginValidationError([
1233
+ errorIssue(
1234
+ "manifest/missing",
1235
+ `Missing ${MANIFEST_FILE_NAME} at the root of the plugin directory.`,
1236
+ MANIFEST_FILE_NAME
1237
+ )
1238
+ ]);
826
1239
  }
827
1240
  try {
828
1241
  return JSON.parse(manifestFile.content);
829
1242
  } catch {
830
- throw new Error("kizen.json could not be parsed.");
1243
+ throw new PluginValidationError([
1244
+ errorIssue(
1245
+ "manifest/parse",
1246
+ `${MANIFEST_FILE_NAME} could not be parsed. Is it properly formatted JSON?`,
1247
+ MANIFEST_FILE_NAME
1248
+ )
1249
+ ]);
831
1250
  }
832
1251
  };
833
1252
 
@@ -849,9 +1268,14 @@ var formatBaseConfig = (packagedPlugin) => {
849
1268
  actions: (assistantConfig.actions ?? []).map((apiName) => {
850
1269
  const action = actionsByApiName[apiName];
851
1270
  if (!action) {
852
- throw new Error(
853
- `Action with API name ${apiName} not found in packaged plugin for setup assistant. Allowed actions: ${Object.keys(actionsByApiName).join(", ")}`
854
- );
1271
+ throw new PluginValidationError([
1272
+ errorIssue(
1273
+ "structure/setup-assistant-action-ref",
1274
+ `Action with API name ${apiName} not found in packaged plugin for setup assistant. Allowed actions: ${Object.keys(actionsByApiName).join(", ")}`,
1275
+ void 0,
1276
+ packagedPlugin.manifest.api_name
1277
+ )
1278
+ ]);
855
1279
  }
856
1280
  return action;
857
1281
  })
@@ -877,7 +1301,7 @@ var transformDeployablePlugin = (packagedPlugin) => {
877
1301
  calendarSources,
878
1302
  kznFile
879
1303
  } = packagedPlugin;
880
- return {
1304
+ const deployablePlugin = {
881
1305
  ...manifest,
882
1306
  api_name: manifest.api_name,
883
1307
  artifacts: {
@@ -894,11 +1318,14 @@ var transformDeployablePlugin = (packagedPlugin) => {
894
1318
  },
895
1319
  thumbnail,
896
1320
  kznFile,
897
- releaseNotes,
898
1321
  base_config: formatBaseConfig(packagedPlugin),
899
1322
  setup_assistant: void 0,
900
1323
  services: manifest.services ?? []
901
1324
  };
1325
+ if (releaseNotes !== void 0) {
1326
+ deployablePlugin.releaseNotes = releaseNotes;
1327
+ }
1328
+ return deployablePlugin;
902
1329
  };
903
1330
 
904
1331
  // src/lib/crypto.ts
@@ -940,15 +1367,8 @@ var encrypt = (plaintext, publicKeyPem) => {
940
1367
  };
941
1368
  };
942
1369
  var decrypt = (envelope, privateKeyPem) => {
943
- const aesKey = (0, import_node_crypto.privateDecrypt)(
944
- { key: privateKeyPem, ...OAEP },
945
- Buffer.from(envelope.k, "base64")
946
- );
947
- const decipher = (0, import_node_crypto.createDecipheriv)(
948
- "aes-256-gcm",
949
- aesKey,
950
- Buffer.from(envelope.iv, "base64")
951
- );
1370
+ const aesKey = (0, import_node_crypto.privateDecrypt)({ key: privateKeyPem, ...OAEP }, Buffer.from(envelope.k, "base64"));
1371
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", aesKey, Buffer.from(envelope.iv, "base64"));
952
1372
  decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
953
1373
  return Buffer.concat([
954
1374
  decipher.update(Buffer.from(envelope.ct, "base64")),
@@ -989,24 +1409,23 @@ var deserializeEnvelope = (value) => {
989
1409
  const tagBytes = Buffer.from(envelope.tag, "base64");
990
1410
  const kBytes = Buffer.from(envelope.k, "base64");
991
1411
  if (ivBytes.length !== 12) {
992
- throw new Error(
993
- `Invalid encrypted value: iv must be 12 bytes (got ${ivBytes.length})`
994
- );
1412
+ throw new Error(`Invalid encrypted value: iv must be 12 bytes (got ${String(ivBytes.length)})`);
995
1413
  }
996
1414
  if (tagBytes.length !== 16) {
997
1415
  throw new Error(
998
- `Invalid encrypted value: tag must be 16 bytes (got ${tagBytes.length})`
1416
+ `Invalid encrypted value: tag must be 16 bytes (got ${String(tagBytes.length)})`
999
1417
  );
1000
1418
  }
1001
1419
  if (kBytes.length !== 384) {
1002
1420
  throw new Error(
1003
- `Invalid encrypted value: wrapped key must be 384 bytes (got ${kBytes.length})`
1421
+ `Invalid encrypted value: wrapped key must be 384 bytes (got ${String(kBytes.length)})`
1004
1422
  );
1005
1423
  }
1006
1424
  return envelope;
1007
1425
  };
1008
1426
  // Annotate the CommonJS export names for ESM import in node:
1009
1427
  0 && (module.exports = {
1428
+ API_NAME_PATTERN,
1010
1429
  AUTOMATION_SCRIPT_FILE,
1011
1430
  AUTOMATION_STEPS_DIRECTORY_NAME,
1012
1431
  BLOCKS_DIRECTORY_NAME,
@@ -1017,6 +1436,8 @@ var deserializeEnvelope = (value) => {
1017
1436
  CRYPTO_ALG,
1018
1437
  CRYPTO_VERSION,
1019
1438
  DATA_ADORNMENTS_DIRECTORY_NAME,
1439
+ ENVIRONMENTS,
1440
+ ENVIRONMENT_ALIASES,
1020
1441
  EVENT_SCRIPTS_DIRECTORY_NAME,
1021
1442
  FLOATING_FRAMES_DIRECTORY_NAME,
1022
1443
  JS_ACTIONS_DIRECTORY_NAME,
@@ -1025,6 +1446,8 @@ var deserializeEnvelope = (value) => {
1025
1446
  MANIFEST_FILE_NAME,
1026
1447
  MESSAGE_SCRIPT_FILE,
1027
1448
  OBJECT_SETTINGS_ITEMS_DIRECTORY_NAME,
1449
+ PUBLISHABLE_ENVIRONMENTS,
1450
+ PluginValidationError,
1028
1451
  RELEASE_NOTES_EXTENSION,
1029
1452
  ROUTABLE_PAGES_DIRECTORY_NAME,
1030
1453
  SCRIPT_EXTENSION,
@@ -1036,6 +1459,7 @@ var deserializeEnvelope = (value) => {
1036
1459
  decrypt,
1037
1460
  deserializeEnvelope,
1038
1461
  encrypt,
1462
+ errorIssue,
1039
1463
  generateKeypair,
1040
1464
  getAdornmentIcon,
1041
1465
  getMinimizedConfig,
@@ -1051,6 +1475,7 @@ var deserializeEnvelope = (value) => {
1051
1475
  thumbnailImageExtensions,
1052
1476
  transformDeployablePlugin,
1053
1477
  triggerImageExtensions,
1478
+ validatePluginApp,
1054
1479
  zipToUint8Array
1055
1480
  });
1056
1481
  //# sourceMappingURL=index.cjs.map