@colixsystems/widget-sdk 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manifest.cjs CHANGED
@@ -5,11 +5,78 @@
5
5
  // implementation.
6
6
 
7
7
  const VALID_CATEGORIES = new Set([
8
- "input", "display", "layout", "data", "media", "communication", "custom",
9
- "INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
8
+ "input", "display", "layout", "data", "media", "communication", "administration", "custom",
9
+ "INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
10
10
  ]);
11
11
  const VALID_PLATFORMS = new Set(["web", "native"]);
12
12
 
13
+ // REQ-WIDGET-ACTION — structural validation for manifest-declared server
14
+ // actions. Mirrors the backend action.service caps; the runner re-validates
15
+ // the cron expression (node-cron) and binds the tenant-local API key + table
16
+ // at enable time, so we only check shape + size here (cross-env: no Buffer).
17
+ const VALID_ACTION_TRIGGERS = new Set([
18
+ "schedule",
19
+ "record_created",
20
+ "record_updated",
21
+ "record_deleted",
22
+ ]);
23
+ const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
24
+ const ACTION_TIMEOUT_MIN_MS = 100;
25
+ const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
26
+
27
+ function utf8ByteLength(s) {
28
+ return new TextEncoder().encode(s).length;
29
+ }
30
+
31
+ function validateManifestActions(actions, errors) {
32
+ if (!Array.isArray(actions)) {
33
+ errors.push("manifest.actions must be an array (omit it or use [] for none)");
34
+ return;
35
+ }
36
+ const seenKeys = new Set();
37
+ for (const a of actions) {
38
+ if (a === null || typeof a !== "object") {
39
+ errors.push("manifest.actions entries must be objects");
40
+ break;
41
+ }
42
+ if (!isNonEmptyString(a.key)) {
43
+ errors.push("manifest.actions[].key must be a non-empty string");
44
+ } else if (seenKeys.has(a.key)) {
45
+ errors.push(`manifest.actions[].key "${a.key}" is duplicated`);
46
+ } else {
47
+ seenKeys.add(a.key);
48
+ }
49
+ pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
50
+ if (!VALID_ACTION_TRIGGERS.has(a.triggerType)) {
51
+ errors.push(
52
+ `manifest.actions[].triggerType must be one of ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
53
+ );
54
+ } else if (a.triggerType === "schedule") {
55
+ pushIf(
56
+ errors,
57
+ isNonEmptyString(a.scheduleCron),
58
+ "manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
59
+ );
60
+ }
61
+ if (!isNonEmptyString(a.scriptSource)) {
62
+ errors.push("manifest.actions[].scriptSource must be a non-empty string");
63
+ } else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
64
+ errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
65
+ }
66
+ if (a.timeoutMs !== undefined) {
67
+ const t = Number(a.timeoutMs);
68
+ if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
69
+ errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
70
+ }
71
+ }
72
+ if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
73
+ errors.push(
74
+ "manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
75
+ );
76
+ }
77
+ }
78
+ }
79
+
13
80
  function canonicalCategory(c) {
14
81
  if (typeof c !== "string") return null;
15
82
  const upper = c.toUpperCase();
@@ -20,6 +87,7 @@ function canonicalCategory(c) {
20
87
  "DATA",
21
88
  "MEDIA",
22
89
  "COMMUNICATION",
90
+ "ADMINISTRATION",
23
91
  "CUSTOM",
24
92
  ].includes(upper)
25
93
  ? upper
@@ -135,6 +203,11 @@ function validateManifest(m) {
135
203
  }
136
204
  }
137
205
 
206
+ // `actions` is optional (additive in SDK 1.8.0) — only validate when present.
207
+ if (manifest.actions !== undefined) {
208
+ validateManifestActions(manifest.actions, errors);
209
+ }
210
+
138
211
  return errors.length === 0 ? { ok: true } : { ok: false, errors };
139
212
  }
140
213
 
package/dist/manifest.js CHANGED
@@ -5,11 +5,78 @@
5
5
  // implementation.
6
6
 
7
7
  const VALID_CATEGORIES = new Set([
8
- "input", "display", "layout", "data", "media", "communication", "custom",
9
- "INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
8
+ "input", "display", "layout", "data", "media", "communication", "administration", "custom",
9
+ "INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
10
10
  ]);
11
11
  const VALID_PLATFORMS = new Set(["web", "native"]);
12
12
 
13
+ // REQ-WIDGET-ACTION — structural validation for manifest-declared server
14
+ // actions. Mirrors the backend action.service caps; the runner re-validates
15
+ // the cron expression (node-cron) and binds the tenant-local API key + table
16
+ // at enable time, so we only check shape + size here (cross-env: no Buffer).
17
+ const VALID_ACTION_TRIGGERS = new Set([
18
+ "schedule",
19
+ "record_created",
20
+ "record_updated",
21
+ "record_deleted",
22
+ ]);
23
+ const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
24
+ const ACTION_TIMEOUT_MIN_MS = 100;
25
+ const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
26
+
27
+ function utf8ByteLength(s) {
28
+ return new TextEncoder().encode(s).length;
29
+ }
30
+
31
+ function validateManifestActions(actions, errors) {
32
+ if (!Array.isArray(actions)) {
33
+ errors.push("manifest.actions must be an array (omit it or use [] for none)");
34
+ return;
35
+ }
36
+ const seenKeys = new Set();
37
+ for (const a of actions) {
38
+ if (a === null || typeof a !== "object") {
39
+ errors.push("manifest.actions entries must be objects");
40
+ break;
41
+ }
42
+ if (!isNonEmptyString(a.key)) {
43
+ errors.push("manifest.actions[].key must be a non-empty string");
44
+ } else if (seenKeys.has(a.key)) {
45
+ errors.push(`manifest.actions[].key "${a.key}" is duplicated`);
46
+ } else {
47
+ seenKeys.add(a.key);
48
+ }
49
+ pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
50
+ if (!VALID_ACTION_TRIGGERS.has(a.triggerType)) {
51
+ errors.push(
52
+ `manifest.actions[].triggerType must be one of ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
53
+ );
54
+ } else if (a.triggerType === "schedule") {
55
+ pushIf(
56
+ errors,
57
+ isNonEmptyString(a.scheduleCron),
58
+ "manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
59
+ );
60
+ }
61
+ if (!isNonEmptyString(a.scriptSource)) {
62
+ errors.push("manifest.actions[].scriptSource must be a non-empty string");
63
+ } else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
64
+ errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
65
+ }
66
+ if (a.timeoutMs !== undefined) {
67
+ const t = Number(a.timeoutMs);
68
+ if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
69
+ errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
70
+ }
71
+ }
72
+ if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
73
+ errors.push(
74
+ "manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
75
+ );
76
+ }
77
+ }
78
+ }
79
+
13
80
  function canonicalCategory(c) {
14
81
  if (typeof c !== "string") return null;
15
82
  const upper = c.toUpperCase();
@@ -20,6 +87,7 @@ function canonicalCategory(c) {
20
87
  "DATA",
21
88
  "MEDIA",
22
89
  "COMMUNICATION",
90
+ "ADMINISTRATION",
23
91
  "CUSTOM",
24
92
  ].includes(upper)
25
93
  ? upper
@@ -135,6 +203,11 @@ function validateManifest(m) {
135
203
  }
136
204
  }
137
205
 
206
+ // `actions` is optional (additive in SDK 1.8.0) — only validate when present.
207
+ if (manifest.actions !== undefined) {
208
+ validateManifestActions(manifest.actions, errors);
209
+ }
210
+
138
211
  return errors.length === 0 ? { ok: true } : { ok: false, errors };
139
212
  }
140
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "build": "node scripts/build.js",
38
- "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js"
38
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=18"