@cyprnet/node-red-contrib-uibuilder-formgen 0.4.11

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +22 -0
  3. package/README.md +58 -0
  4. package/docs/user-guide.html +565 -0
  5. package/examples/formgen-builder/src/index.html +921 -0
  6. package/examples/formgen-builder/src/index.js +1338 -0
  7. package/examples/portalsmith-formgen-example.json +531 -0
  8. package/examples/schema-builder-integration.json +109 -0
  9. package/examples/schemas/Banking/banking_fraud_report.json +102 -0
  10. package/examples/schemas/Banking/banking_kyc_update.json +59 -0
  11. package/examples/schemas/Banking/banking_loan_application.json +113 -0
  12. package/examples/schemas/Banking/banking_new_account.json +98 -0
  13. package/examples/schemas/Banking/banking_wire_transfer_request.json +94 -0
  14. package/examples/schemas/HR/hr_employee_change_form.json +65 -0
  15. package/examples/schemas/HR/hr_exit_interview.json +105 -0
  16. package/examples/schemas/HR/hr_job_application.json +166 -0
  17. package/examples/schemas/HR/hr_onboarding_request.json +140 -0
  18. package/examples/schemas/HR/hr_time_off_request.json +95 -0
  19. package/examples/schemas/HR/hr_training_request.json +70 -0
  20. package/examples/schemas/Healthcare/health_appointment_request.json +103 -0
  21. package/examples/schemas/Healthcare/health_incident_report.json +82 -0
  22. package/examples/schemas/Healthcare/health_lab_order_request.json +72 -0
  23. package/examples/schemas/Healthcare/health_medication_refill.json +72 -0
  24. package/examples/schemas/Healthcare/health_patient_intake.json +113 -0
  25. package/examples/schemas/IT/it_access_request.json +145 -0
  26. package/examples/schemas/IT/it_dhcp_reservation.json +175 -0
  27. package/examples/schemas/IT/it_dns_domain_external.json +192 -0
  28. package/examples/schemas/IT/it_dns_domain_internal.json +171 -0
  29. package/examples/schemas/IT/it_network_change_request.json +126 -0
  30. package/examples/schemas/IT/it_network_request-form.json +299 -0
  31. package/examples/schemas/IT/it_new_hardware_request.json +155 -0
  32. package/examples/schemas/IT/it_password_reset.json +133 -0
  33. package/examples/schemas/IT/it_software_license_request.json +93 -0
  34. package/examples/schemas/IT/it_static_ip_request.json +199 -0
  35. package/examples/schemas/IT/it_subnet_request_form.json +216 -0
  36. package/examples/schemas/Maintenance/maint_checklist.json +176 -0
  37. package/examples/schemas/Maintenance/maint_facility_issue_report.json +127 -0
  38. package/examples/schemas/Maintenance/maint_incident_intake.json +174 -0
  39. package/examples/schemas/Maintenance/maint_inventory_restock.json +79 -0
  40. package/examples/schemas/Maintenance/maint_safety_audit.json +92 -0
  41. package/examples/schemas/Maintenance/maint_vehicle_inspection.json +112 -0
  42. package/examples/schemas/Maintenance/maint_work_order.json +134 -0
  43. package/index.js +12 -0
  44. package/lib/licensing.js +254 -0
  45. package/nodes/portalsmith-license.html +40 -0
  46. package/nodes/portalsmith-license.js +23 -0
  47. package/nodes/uibuilder-formgen.html +261 -0
  48. package/nodes/uibuilder-formgen.js +598 -0
  49. package/package.json +47 -0
  50. package/scripts/normalize_schema_titles.py +77 -0
  51. package/templates/index.html.mustache +541 -0
  52. package/templates/index.js.mustache +1135 -0
@@ -0,0 +1,134 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "formId": "maint_work_order",
4
+ "title": "Maintenance - Work Order",
5
+ "description": "Create a work order for facility/equipment maintenance.",
6
+ "sections": [
7
+ {
8
+ "id": "request",
9
+ "title": "Request",
10
+ "fields": [
11
+ {
12
+ "id": "requestor_name",
13
+ "type": "text",
14
+ "label": "Requester Name",
15
+ "required": true
16
+ },
17
+ {
18
+ "id": "requestor_phone",
19
+ "type": "text",
20
+ "label": "Phone",
21
+ "validate": "phone"
22
+ },
23
+ {
24
+ "id": "requested_date",
25
+ "type": "date",
26
+ "label": "Requested Date",
27
+ "required": true
28
+ },
29
+ {
30
+ "id": "priority",
31
+ "type": "radio",
32
+ "label": "Priority",
33
+ "required": true,
34
+ "options": [
35
+ {
36
+ "value": "low",
37
+ "text": "Low"
38
+ },
39
+ {
40
+ "value": "normal",
41
+ "text": "Normal"
42
+ },
43
+ {
44
+ "value": "high",
45
+ "text": "High"
46
+ },
47
+ {
48
+ "value": "emergency",
49
+ "text": "Emergency"
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ "id": "location",
57
+ "title": "Location",
58
+ "fields": [
59
+ {
60
+ "id": "site",
61
+ "type": "text",
62
+ "label": "Site/Building",
63
+ "required": true
64
+ },
65
+ {
66
+ "id": "room",
67
+ "type": "text",
68
+ "label": "Room/Area",
69
+ "required": true
70
+ },
71
+ {
72
+ "id": "asset_tag",
73
+ "type": "text",
74
+ "label": "Asset Tag (if applicable)"
75
+ }
76
+ ]
77
+ },
78
+ {
79
+ "id": "details",
80
+ "title": "Details",
81
+ "fields": [
82
+ {
83
+ "id": "category",
84
+ "type": "select",
85
+ "label": "Category",
86
+ "required": true,
87
+ "options": [
88
+ {
89
+ "value": "",
90
+ "text": "Select..."
91
+ },
92
+ {
93
+ "value": "plumbing",
94
+ "text": "Plumbing"
95
+ },
96
+ {
97
+ "value": "electrical",
98
+ "text": "Electrical"
99
+ },
100
+ {
101
+ "value": "hvac",
102
+ "text": "HVAC"
103
+ },
104
+ {
105
+ "value": "general",
106
+ "text": "General"
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "id": "description",
112
+ "type": "textarea",
113
+ "label": "Issue Description",
114
+ "rows": 4,
115
+ "required": true
116
+ },
117
+ {
118
+ "id": "estimated_cost",
119
+ "type": "number",
120
+ "label": "Estimated Cost (USD)",
121
+ "min": 0,
122
+ "step": 0.01
123
+ },
124
+ {
125
+ "id": "requires_shutdown",
126
+ "type": "checkbox",
127
+ "label": "Requires shutdown",
128
+ "defaultValue": false
129
+ }
130
+ ]
131
+ }
132
+ ],
133
+ "actions": []
134
+ }
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PortalSmith FormGen - Node-RED module entry
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Copyright (c) 2026 CyprNet Solutions, LLC
6
+ */
7
+
8
+ module.exports = function(RED) {
9
+ require("./nodes/portalsmith-license")(RED);
10
+ require("./nodes/uibuilder-formgen")(RED);
11
+ };
12
+
@@ -0,0 +1,254 @@
1
+ /**
2
+ * PortalSmith FormGen Licensing (offline-first)
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Copyright (c) 2026 CyprNet Solutions, LLC
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const crypto = require("crypto");
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+
14
+ const LICENSE_PREFIX = "PSLF1";
15
+ const PRODUCT_ID = "portalsmith-formgen";
16
+
17
+ // NOTE: This is a PUBLIC key (verification only). No private keys are stored in this repo.
18
+ // Replace this PEM with your real PortalSmith FormGen license signing public key.
19
+ // You may also override this at runtime using PORTALSMITH_LICENSE_PUBLIC_KEY_PEM (public key only).
20
+ const DEFAULT_PUBLIC_KEY_PEM = [
21
+ "-----BEGIN PUBLIC KEY-----",
22
+ // PortalSmith FormGen Ed25519 public key (global signing key; verification only)
23
+ "MCowBQYDK2VwAyEAg0GNHxcE/kUWFQgcAeMJPYbI2w5JepQQmqLKJrhjAE4=",
24
+ "-----END PUBLIC KEY-----",
25
+ ].join("\n");
26
+
27
+ function getVerifierPublicKey() {
28
+ const envPem = String(process.env.PORTALSMITH_LICENSE_PUBLIC_KEY_PEM || "").trim();
29
+ if (envPem) {
30
+ return { pem: envPem, source: "env" };
31
+ }
32
+ const envFile = String(process.env.PORTALSMITH_LICENSE_PUBLIC_KEY_PEM_FILE || "").trim();
33
+ if (envFile) {
34
+ try {
35
+ const p = path.isAbsolute(envFile) ? envFile : path.join(process.cwd(), envFile);
36
+ const pem = fs.readFileSync(p, "utf8");
37
+ if (String(pem || "").trim()) return { pem, source: "file" };
38
+ } catch (e) {
39
+ // fall through to embedded
40
+ }
41
+ }
42
+ return { pem: DEFAULT_PUBLIC_KEY_PEM, source: "embedded" };
43
+ }
44
+
45
+ function publicKeyFingerprint(pem) {
46
+ try {
47
+ const norm = String(pem || "").replace(/\r\n/g, "\n").trim();
48
+ return crypto.createHash("sha256").update(norm, "utf8").digest("hex").slice(0, 16);
49
+ } catch (e) {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function base64urlToBuffer(s) {
55
+ const str = String(s || "").trim();
56
+ if (!str) return Buffer.alloc(0);
57
+ const pad = str.length % 4 === 2 ? "==" : str.length % 4 === 3 ? "=" : "";
58
+ const b64 = str.replace(/-/g, "+").replace(/_/g, "/") + pad;
59
+ return Buffer.from(b64, "base64");
60
+ }
61
+
62
+ function stableSortObjectKeys(obj) {
63
+ if (obj === null || obj === undefined) return obj;
64
+ if (Array.isArray(obj)) return obj.map(stableSortObjectKeys);
65
+ if (typeof obj !== "object") return obj;
66
+ const out = {};
67
+ Object.keys(obj)
68
+ .sort()
69
+ .forEach((k) => {
70
+ out[k] = stableSortObjectKeys(obj[k]);
71
+ });
72
+ return out;
73
+ }
74
+
75
+ function canonicalJson(obj) {
76
+ return JSON.stringify(stableSortObjectKeys(obj));
77
+ }
78
+
79
+ function parseLicenseKey(licenseString) {
80
+ const raw = String(licenseString || "").trim();
81
+ if (!raw) {
82
+ return { ok: false, reason: "missing" };
83
+ }
84
+
85
+ const parts = raw.split(".");
86
+ if (parts.length === 3 && parts[0] === LICENSE_PREFIX) {
87
+ parts.shift();
88
+ }
89
+ if (parts.length !== 2) {
90
+ return { ok: false, reason: "format_invalid", detail: "Expected PSLF1.<payload>.<sig> or <payload>.<sig>" };
91
+ }
92
+
93
+ let payload;
94
+ try {
95
+ const payloadBuf = base64urlToBuffer(parts[0]);
96
+ payload = JSON.parse(payloadBuf.toString("utf8"));
97
+ } catch (e) {
98
+ return { ok: false, reason: "payload_invalid" };
99
+ }
100
+
101
+ const signature = base64urlToBuffer(parts[1]);
102
+ if (!signature || signature.length < 32) {
103
+ return { ok: false, reason: "signature_invalid" };
104
+ }
105
+
106
+ return { ok: true, payload, signature };
107
+ }
108
+
109
+ function verifySignature(payload, signature, publicKeyPem) {
110
+ try {
111
+ const data = Buffer.from(canonicalJson(payload), "utf8");
112
+ // For Ed25519 in Node.js, algorithm is null.
113
+ return crypto.verify(null, data, publicKeyPem, signature);
114
+ } catch (e) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function isLicenseValid(licenseObj, now = new Date()) {
120
+ if (!licenseObj || !licenseObj.ok) return { ok: false, reason: licenseObj?.reason || "invalid" };
121
+ const payload = licenseObj.payload || {};
122
+
123
+ if (payload.product !== PRODUCT_ID) return { ok: false, reason: "product_mismatch" };
124
+
125
+ if (payload.issuedAt) {
126
+ const issued = new Date(payload.issuedAt);
127
+ if (Number.isNaN(issued.getTime())) return { ok: false, reason: "issuedAt_invalid" };
128
+ }
129
+
130
+ if (payload.expiresAt) {
131
+ const exp = new Date(payload.expiresAt);
132
+ if (Number.isNaN(exp.getTime())) return { ok: false, reason: "expiresAt_invalid" };
133
+ if (now.getTime() > exp.getTime()) return { ok: false, reason: "expired" };
134
+ }
135
+
136
+ return { ok: true };
137
+ }
138
+
139
+ function normalizeFeatures(payloadFeatures) {
140
+ const f = payloadFeatures && typeof payloadFeatures === "object" ? payloadFeatures : {};
141
+ return {
142
+ removeWatermark: Boolean(f.removeWatermark),
143
+ customBranding: Boolean(f.customBranding),
144
+ emailExport: Boolean(f.emailExport),
145
+ multiServerProfiles: Boolean(f.multiServerProfiles),
146
+ };
147
+ }
148
+
149
+ function makeFreeResult(reason = "missing") {
150
+ return {
151
+ licensed: false,
152
+ tier: "FREE",
153
+ features: {
154
+ removeWatermark: false,
155
+ customBranding: false,
156
+ emailExport: false,
157
+ multiServerProfiles: false,
158
+ },
159
+ reason,
160
+ display: {
161
+ displayName: "Free (Watermarked)",
162
+ licensedTo: "",
163
+ expiresAt: "",
164
+ },
165
+ policy: {
166
+ watermark: true,
167
+ brandingLocked: true,
168
+ },
169
+ };
170
+ }
171
+
172
+ function makeLicensedResult(payload, features) {
173
+ const licensedTo = String(payload.licensedTo || "").trim();
174
+ const expiresAt = payload.expiresAt ? String(payload.expiresAt) : "";
175
+ const tier = String(payload.edition || "PRO").toUpperCase() === "PRO" ? "PRO" : "PRO";
176
+ const removeWatermark = Boolean(features.removeWatermark);
177
+ const customBranding = Boolean(features.customBranding);
178
+ return {
179
+ licensed: true,
180
+ tier,
181
+ features: {
182
+ removeWatermark,
183
+ customBranding,
184
+ emailExport: Boolean(features.emailExport),
185
+ multiServerProfiles: Boolean(features.multiServerProfiles),
186
+ },
187
+ reason: "ok",
188
+ display: {
189
+ displayName: licensedTo ? `Licensed: ${licensedTo}` : "Licensed",
190
+ licensedTo,
191
+ expiresAt,
192
+ },
193
+ policy: {
194
+ watermark: !removeWatermark,
195
+ brandingLocked: !customBranding,
196
+ },
197
+ };
198
+ }
199
+
200
+ // Basic cache so we don't re-verify on every msg
201
+ let _cache = {
202
+ key: "",
203
+ result: makeFreeResult("missing"),
204
+ checkedAt: 0,
205
+ };
206
+
207
+ function resolveLicenseString(licenseString, now = new Date()) {
208
+ const key = String(licenseString || "").trim();
209
+ const parsed = parseLicenseKey(key);
210
+ if (!parsed.ok) return makeFreeResult(parsed.reason);
211
+
212
+ const publicKeyPem = getVerifierPublicKey().pem;
213
+ const sigOk = verifySignature(parsed.payload, parsed.signature, publicKeyPem);
214
+ if (!sigOk) return makeFreeResult("signature_invalid");
215
+
216
+ const valid = isLicenseValid(parsed, now);
217
+ if (!valid.ok) return makeFreeResult(valid.reason);
218
+
219
+ const features = normalizeFeatures(parsed.payload.features);
220
+ return makeLicensedResult(parsed.payload, features);
221
+ }
222
+
223
+ function resolveLicense({ node, RED, licenseConfigNode, cacheTtlMs = 5 * 60 * 1000 } = {}) {
224
+ const now = Date.now();
225
+
226
+ const fromNodeCred = String(node?.credentials?.licenseKey || "").trim();
227
+ const fromConfigCred = String(licenseConfigNode?.credentials?.licenseKey || "").trim();
228
+ const fromEnv = String(process.env.PORTALSMITH_LICENSE || "").trim();
229
+
230
+ const key = fromNodeCred || fromConfigCred || fromEnv || "";
231
+
232
+ if (_cache.key === key && now - _cache.checkedAt < cacheTtlMs) {
233
+ return _cache.result;
234
+ }
235
+
236
+ const result = resolveLicenseString(key, new Date(now));
237
+ _cache = { key, result, checkedAt: now };
238
+ return result;
239
+ }
240
+
241
+ module.exports = {
242
+ LICENSE_PREFIX,
243
+ PRODUCT_ID,
244
+ DEFAULT_PUBLIC_KEY_PEM,
245
+ getVerifierPublicKey,
246
+ publicKeyFingerprint,
247
+ parseLicenseKey,
248
+ verifySignature,
249
+ isLicenseValid,
250
+ resolveLicenseString,
251
+ resolveLicense,
252
+ };
253
+
254
+
@@ -0,0 +1,40 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('portalsmith-license', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: "PortalSmith License" }
6
+ },
7
+ credentials: {
8
+ licenseKey: { type: "password" }
9
+ },
10
+ label: function () {
11
+ return this.name || "PortalSmith License";
12
+ }
13
+ });
14
+ </script>
15
+
16
+ <script type="text/html" data-template-name="portalsmith-license">
17
+ <div class="form-row">
18
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
19
+ <input type="text" id="node-config-input-name" placeholder="PortalSmith License" />
20
+ </div>
21
+
22
+ <div class="form-row">
23
+ <label for="node-config-input-licenseKey"><i class="fa fa-key"></i> License Key</label>
24
+ <input type="password" id="node-config-input-licenseKey" placeholder="PSLF1.<payload>.<signature>" />
25
+ </div>
26
+
27
+ <div class="form-tips">
28
+ Paste the license string provided by PortalSmith/CyprNet. Leave blank for Free (watermarked) mode.
29
+ Keys are stored in the Node-RED credentials store (encrypted at rest when credentialsSecret is set).
30
+ </div>
31
+ </script>
32
+
33
+ <script type="text/markdown" data-help-name="portalsmith-license">
34
+ Stores a PortalSmith FormGen license key in the Node-RED credentials store for reuse across multiple `uibuilder-formgen` nodes.
35
+
36
+ - If no key is provided, nodes operate in **Free (watermarked)** mode.
37
+ - Licensing is **offline-first**: no phone-home and no external services.
38
+ </script>
39
+
40
+
@@ -0,0 +1,23 @@
1
+ /**
2
+ * PortalSmith FormGen - License Config Node
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Copyright (c) 2026 CyprNet Solutions, LLC
6
+ */
7
+
8
+ "use strict";
9
+
10
+ module.exports = function (RED) {
11
+ function PortalSmithLicenseNode(config) {
12
+ RED.nodes.createNode(this, config);
13
+ this.name = config.name || "PortalSmith License";
14
+ }
15
+
16
+ RED.nodes.registerType("portalsmith-license", PortalSmithLicenseNode, {
17
+ credentials: {
18
+ licenseKey: { type: "password" },
19
+ },
20
+ });
21
+ };
22
+
23
+