@aaqu/fromcubes-react 0.0.0-stage → 0.1.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/LICENSE +190 -0
- package/README.md +405 -2
- package/THIRD-PARTY-NOTICES.md +164 -0
- package/examples/D3 Poland Map.json +90 -0
- package/examples/Live Chart.json +101 -0
- package/examples/PixiJS Sprites.json +114 -0
- package/examples/Sensor Portal.json +93 -0
- package/examples/Shared Components.json +68 -0
- package/examples/Three.js Scene.json +118 -0
- package/examples/Utility Hooks.json +94 -0
- package/examples/WebGPU Shader.json +110 -0
- package/nodes/editor/ark-pixel.LICENSE.txt +96 -0
- package/nodes/editor/ark-pixel.woff2 +0 -0
- package/nodes/editor/file-tree.js +403 -0
- package/nodes/editor/format.js +182 -0
- package/nodes/editor/icons.svg +46 -0
- package/nodes/editor/mark.svg +5 -0
- package/nodes/editor/provider-settings.js +192 -0
- package/nodes/editor/workspace.css +653 -0
- package/nodes/editor/workspace.js +1565 -0
- package/nodes/icons/fromcubes-component.svg +27 -0
- package/nodes/icons/fromcubes-react.svg +25 -0
- package/nodes/icons/fromcubes-utility.svg +21 -0
- package/nodes/lib/admin-api.js +238 -0
- package/nodes/lib/admin-security.js +161 -0
- package/nodes/lib/api.js +29 -0
- package/nodes/lib/helpers.js +990 -0
- package/nodes/lib/hooks.js +145 -0
- package/nodes/lib/page-builder.js +499 -0
- package/nodes/lib/portal-page-route.js +131 -0
- package/nodes/lib/providers.js +536 -0
- package/nodes/lib/registry-nodes.js +331 -0
- package/nodes/lib/router.js +103 -0
- package/nodes/lib/ws-heartbeat.js +58 -0
- package/nodes/portal-react.html +2539 -0
- package/nodes/portal-react.js +1652 -0
- package/package.json +76 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="60" viewBox="0 0 44 60">
|
|
2
|
+
<!-- The fromcubes mark, traced from the project artwork by tools/build-icon.mjs.
|
|
3
|
+
The badge is cut out of the mark with a mask rather than hidden behind a
|
|
4
|
+
halo painted in the background colour: an icon cannot know what is behind
|
|
5
|
+
it, and masking keeps the traced edges exact instead of re-rasterising them.
|
|
6
|
+
The badge is the "component" icon from Lucide (https://lucide.dev), ISC —
|
|
7
|
+
see THIRD-PARTY-NOTICES.md. Worn by fc-portal-component. React's own logo is a
|
|
8
|
+
Meta trademark and is deliberately not used. -->
|
|
9
|
+
<mask id="fc-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="60">
|
|
10
|
+
<rect width="44" height="60" fill="#ffffff"/>
|
|
11
|
+
<g transform="translate(25.216003874368653 34.26600387436865) scale(0.7903)" fill="none" stroke="#000000"
|
|
12
|
+
stroke-width="6.83" stroke-linecap="round" stroke-linejoin="round">
|
|
13
|
+
<path d="M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
|
|
14
|
+
<path d="M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"/>
|
|
15
|
+
<path d="M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"/>
|
|
16
|
+
<path d="M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
|
|
17
|
+
</g>
|
|
18
|
+
</mask>
|
|
19
|
+
<path fill="#10181d" fill-rule="evenodd" mask="url(#fc-cut)" d="M20.79 6.25L22.01 6.25L23.23 6.86L32.17 12.34L37.65 15.19L38.46 16.00L38.46 35.50L37.65 36.31L32.17 39.16L23.23 44.64L22.01 45.25L20.38 45.05L12.06 39.97L6.57 37.13L4.34 35.50L4.34 16.00L9.42 12.75L20.79 6.45ZM21.20 8.08L6.98 16.20L7.18 16.61L7.99 16.61L10.84 14.78L14.29 13.97L15.92 13.97L22.42 17.63L24.65 19.05L24.85 20.67L24.04 21.48L23.43 22.91L20.99 24.13L21.20 24.73L35.82 16.41L35.01 16.00L31.96 18.03L27.09 18.84L20.59 15.19L19.37 14.17L18.15 13.77L17.95 12.14L19.57 9.70L22.01 8.48L21.40 8.08ZM23.63 9.50L20.99 10.92L19.98 12.75L27.49 17.02L30.95 16.41L33.38 14.78L23.84 9.50ZM14.49 15.80L11.45 16.41L9.42 17.83L15.71 21.28L16.93 22.30L18.56 22.91L18.76 23.31L22.21 21.48L22.82 20.06L17.13 16.61L15.51 15.80L14.70 15.80ZM6.17 18.03L5.96 18.84L9.21 20.47L12.46 24.13L12.67 34.69L11.85 35.50L8.81 35.50L6.57 34.08L5.96 34.08L5.96 34.69L9.62 36.52L20.38 43.02L20.38 41.80L17.34 40.38L14.29 37.13L13.88 26.16L14.70 25.34L17.95 25.34L20.18 26.77L20.59 26.77L20.59 26.16L6.37 18.03ZM36.63 18.03L35.82 18.44L35.82 22.70L34.40 26.77L26.07 31.84L25.06 32.05L24.04 31.64L22.82 29.41L22.62 26.16L22.21 26.36L22.21 42.81L23.02 42.61L22.82 38.95L24.24 34.48L33.18 29.00L34.81 29.61L36.02 31.84L36.02 34.89L36.84 34.69L36.84 18.03ZM33.99 19.45L24.45 24.94L24.65 29.00L25.06 29.81L25.67 30.02L27.49 28.59L32.98 25.55L33.99 22.50L33.99 19.66ZM6.17 20.88L5.96 31.84L8.81 33.27L9.01 33.67L10.84 33.67L10.63 24.53L8.81 22.91L8.40 22.09L6.17 21.08ZM15.92 27.17L15.92 36.11L18.56 38.95L20.38 39.97L20.59 29.00L17.54 27.17L16.12 27.17ZM33.18 31.23L25.67 35.70L24.65 38.95L24.65 41.59L34.20 36.11L34.20 32.25L33.38 31.23Z"/>
|
|
20
|
+
<g transform="translate(25.216003874368653 34.26600387436865) scale(0.7903)" fill="none" stroke="#0f6f8c"
|
|
21
|
+
stroke-width="3.04" stroke-linecap="round" stroke-linejoin="round">
|
|
22
|
+
<path d="M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
|
|
23
|
+
<path d="M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"/>
|
|
24
|
+
<path d="M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"/>
|
|
25
|
+
<path d="M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
|
|
26
|
+
</g>
|
|
27
|
+
</svg>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="60" viewBox="0 0 44 60">
|
|
2
|
+
<!-- The fromcubes mark, traced from the project artwork by tools/build-icon.mjs.
|
|
3
|
+
The badge is cut out of the mark with a mask rather than hidden behind a
|
|
4
|
+
halo painted in the background colour: an icon cannot know what is behind
|
|
5
|
+
it, and masking keeps the traced edges exact instead of re-rasterising them.
|
|
6
|
+
The badge is the "atom" icon from Lucide (https://lucide.dev), ISC —
|
|
7
|
+
see THIRD-PARTY-NOTICES.md. Worn by portal-react. React's own logo is a
|
|
8
|
+
Meta trademark and is deliberately not used. -->
|
|
9
|
+
<mask id="fc-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="60">
|
|
10
|
+
<rect width="44" height="60" fill="#ffffff"/>
|
|
11
|
+
<g transform="translate(24.158614219195222 33.20861421919522) scale(0.8784)" fill="none" stroke="#000000"
|
|
12
|
+
stroke-width="6.15" stroke-linecap="round" stroke-linejoin="round">
|
|
13
|
+
<circle cx="12" cy="12" r="1"/>
|
|
14
|
+
<path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/>
|
|
15
|
+
<path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>
|
|
16
|
+
</g>
|
|
17
|
+
</mask>
|
|
18
|
+
<path fill="#10181d" fill-rule="evenodd" mask="url(#fc-cut)" d="M20.79 6.25L22.01 6.25L23.23 6.86L32.17 12.34L37.65 15.19L38.46 16.00L38.46 35.50L37.65 36.31L32.17 39.16L23.23 44.64L22.01 45.25L20.38 45.05L12.06 39.97L6.57 37.13L4.34 35.50L4.34 16.00L9.42 12.75L20.79 6.45ZM21.20 8.08L6.98 16.20L7.18 16.61L7.99 16.61L10.84 14.78L14.29 13.97L15.92 13.97L22.42 17.63L24.65 19.05L24.85 20.67L24.04 21.48L23.43 22.91L20.99 24.13L21.20 24.73L35.82 16.41L35.01 16.00L31.96 18.03L27.09 18.84L20.59 15.19L19.37 14.17L18.15 13.77L17.95 12.14L19.57 9.70L22.01 8.48L21.40 8.08ZM23.63 9.50L20.99 10.92L19.98 12.75L27.49 17.02L30.95 16.41L33.38 14.78L23.84 9.50ZM14.49 15.80L11.45 16.41L9.42 17.83L15.71 21.28L16.93 22.30L18.56 22.91L18.76 23.31L22.21 21.48L22.82 20.06L17.13 16.61L15.51 15.80L14.70 15.80ZM6.17 18.03L5.96 18.84L9.21 20.47L12.46 24.13L12.67 34.69L11.85 35.50L8.81 35.50L6.57 34.08L5.96 34.08L5.96 34.69L9.62 36.52L20.38 43.02L20.38 41.80L17.34 40.38L14.29 37.13L13.88 26.16L14.70 25.34L17.95 25.34L20.18 26.77L20.59 26.77L20.59 26.16L6.37 18.03ZM36.63 18.03L35.82 18.44L35.82 22.70L34.40 26.77L26.07 31.84L25.06 32.05L24.04 31.64L22.82 29.41L22.62 26.16L22.21 26.36L22.21 42.81L23.02 42.61L22.82 38.95L24.24 34.48L33.18 29.00L34.81 29.61L36.02 31.84L36.02 34.89L36.84 34.69L36.84 18.03ZM33.99 19.45L24.45 24.94L24.65 29.00L25.06 29.81L25.67 30.02L27.49 28.59L32.98 25.55L33.99 22.50L33.99 19.66ZM6.17 20.88L5.96 31.84L8.81 33.27L9.01 33.67L10.84 33.67L10.63 24.53L8.81 22.91L8.40 22.09L6.17 21.08ZM15.92 27.17L15.92 36.11L18.56 38.95L20.38 39.97L20.59 29.00L17.54 27.17L16.12 27.17ZM33.18 31.23L25.67 35.70L24.65 38.95L24.65 41.59L34.20 36.11L34.20 32.25L33.38 31.23Z"/>
|
|
19
|
+
<g transform="translate(24.158614219195222 33.20861421919522) scale(0.8784)" fill="none" stroke="#0f6f8c"
|
|
20
|
+
stroke-width="2.73" stroke-linecap="round" stroke-linejoin="round">
|
|
21
|
+
<circle cx="12" cy="12" r="1"/>
|
|
22
|
+
<path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/>
|
|
23
|
+
<path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>
|
|
24
|
+
</g>
|
|
25
|
+
</svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="60" viewBox="0 0 44 60">
|
|
2
|
+
<!-- The fromcubes mark, traced from the project artwork by tools/build-icon.mjs.
|
|
3
|
+
The badge is cut out of the mark with a mask rather than hidden behind a
|
|
4
|
+
halo painted in the background colour: an icon cannot know what is behind
|
|
5
|
+
it, and masking keeps the traced edges exact instead of re-rasterising them.
|
|
6
|
+
The badge is the "wrench" icon from Lucide (https://lucide.dev), ISC —
|
|
7
|
+
see THIRD-PARTY-NOTICES.md. Worn by fc-portal-utility. React's own logo is a
|
|
8
|
+
Meta trademark and is deliberately not used. -->
|
|
9
|
+
<mask id="fc-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="60">
|
|
10
|
+
<rect width="44" height="60" fill="#ffffff"/>
|
|
11
|
+
<g transform="translate(25.219431250695457 34.25674619515118) scale(0.7906)" fill="none" stroke="#000000"
|
|
12
|
+
stroke-width="6.83" stroke-linecap="round" stroke-linejoin="round">
|
|
13
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>
|
|
14
|
+
</g>
|
|
15
|
+
</mask>
|
|
16
|
+
<path fill="#10181d" fill-rule="evenodd" mask="url(#fc-cut)" d="M20.79 6.25L22.01 6.25L23.23 6.86L32.17 12.34L37.65 15.19L38.46 16.00L38.46 35.50L37.65 36.31L32.17 39.16L23.23 44.64L22.01 45.25L20.38 45.05L12.06 39.97L6.57 37.13L4.34 35.50L4.34 16.00L9.42 12.75L20.79 6.45ZM21.20 8.08L6.98 16.20L7.18 16.61L7.99 16.61L10.84 14.78L14.29 13.97L15.92 13.97L22.42 17.63L24.65 19.05L24.85 20.67L24.04 21.48L23.43 22.91L20.99 24.13L21.20 24.73L35.82 16.41L35.01 16.00L31.96 18.03L27.09 18.84L20.59 15.19L19.37 14.17L18.15 13.77L17.95 12.14L19.57 9.70L22.01 8.48L21.40 8.08ZM23.63 9.50L20.99 10.92L19.98 12.75L27.49 17.02L30.95 16.41L33.38 14.78L23.84 9.50ZM14.49 15.80L11.45 16.41L9.42 17.83L15.71 21.28L16.93 22.30L18.56 22.91L18.76 23.31L22.21 21.48L22.82 20.06L17.13 16.61L15.51 15.80L14.70 15.80ZM6.17 18.03L5.96 18.84L9.21 20.47L12.46 24.13L12.67 34.69L11.85 35.50L8.81 35.50L6.57 34.08L5.96 34.08L5.96 34.69L9.62 36.52L20.38 43.02L20.38 41.80L17.34 40.38L14.29 37.13L13.88 26.16L14.70 25.34L17.95 25.34L20.18 26.77L20.59 26.77L20.59 26.16L6.37 18.03ZM36.63 18.03L35.82 18.44L35.82 22.70L34.40 26.77L26.07 31.84L25.06 32.05L24.04 31.64L22.82 29.41L22.62 26.16L22.21 26.36L22.21 42.81L23.02 42.61L22.82 38.95L24.24 34.48L33.18 29.00L34.81 29.61L36.02 31.84L36.02 34.89L36.84 34.69L36.84 18.03ZM33.99 19.45L24.45 24.94L24.65 29.00L25.06 29.81L25.67 30.02L27.49 28.59L32.98 25.55L33.99 22.50L33.99 19.66ZM6.17 20.88L5.96 31.84L8.81 33.27L9.01 33.67L10.84 33.67L10.63 24.53L8.81 22.91L8.40 22.09L6.17 21.08ZM15.92 27.17L15.92 36.11L18.56 38.95L20.38 39.97L20.59 29.00L17.54 27.17L16.12 27.17ZM33.18 31.23L25.67 35.70L24.65 38.95L24.65 41.59L34.20 36.11L34.20 32.25L33.38 31.23Z"/>
|
|
17
|
+
<g transform="translate(25.219431250695457 34.25674619515118) scale(0.7906)" fill="none" stroke="#0f6f8c"
|
|
18
|
+
stroke-width="3.04" stroke-linecap="round" stroke-linejoin="round">
|
|
19
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register editor/admin/public support routes for portal-react.
|
|
5
|
+
*
|
|
6
|
+
* This module intentionally owns only HTTP route registration. Runtime page
|
|
7
|
+
* state and registries remain owned by portal-react.js and are passed in so
|
|
8
|
+
* deploy/rebuild lifecycle stays in one place.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} RED
|
|
11
|
+
* @param {Object} deps
|
|
12
|
+
* @param {import("express")} deps.express
|
|
13
|
+
* @param {Function} deps.permRead
|
|
14
|
+
* @param {Function} deps.permWrite
|
|
15
|
+
* @param {Function} deps.csrfGuard
|
|
16
|
+
* @param {Function} deps.rateLimit
|
|
17
|
+
* @param {Object<string, Object>} deps.pageState
|
|
18
|
+
* @param {Object<string, Object>} deps.registry
|
|
19
|
+
* @param {Object<string, Object>} deps.utilities
|
|
20
|
+
* @param {(code: string) => Set<string>} deps.extractUtilitySymbols
|
|
21
|
+
* @param {Object} [deps.providers] lib/providers registry (describe/select).
|
|
22
|
+
* @param {Function} [deps.onProviderChange] Called with the kind after a choice changed.
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
function registerAdminApi(RED, deps) {
|
|
26
|
+
const {
|
|
27
|
+
express,
|
|
28
|
+
permRead,
|
|
29
|
+
permWrite,
|
|
30
|
+
csrfGuard,
|
|
31
|
+
rateLimit,
|
|
32
|
+
pageState,
|
|
33
|
+
registry,
|
|
34
|
+
utilities,
|
|
35
|
+
extractUtilitySymbols,
|
|
36
|
+
providers,
|
|
37
|
+
onProviderChange,
|
|
38
|
+
} = deps;
|
|
39
|
+
|
|
40
|
+
// Since 0.56 monaco-editor has an `exports` map that hides package.json;
|
|
41
|
+
// its CommonJS entry is min/vs/index.js, so the AMD build (loader.js and
|
|
42
|
+
// editor/editor.main.js) sits in the same directory.
|
|
43
|
+
const monacoVsPath = path.dirname(require.resolve("monaco-editor"));
|
|
44
|
+
RED.httpAdmin.use(
|
|
45
|
+
"/fromcubes-react/vs",
|
|
46
|
+
permRead,
|
|
47
|
+
express.static(monacoVsPath),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Editor-side workspace plugin (tree + full-screen Monaco overlay). Served
|
|
51
|
+
// as static files instead of being inlined into portal-react.html so the
|
|
52
|
+
// node's editor template stays readable.
|
|
53
|
+
RED.httpAdmin.use(
|
|
54
|
+
"/fromcubes-react/editor",
|
|
55
|
+
permRead,
|
|
56
|
+
express.static(path.join(__dirname, "..", "editor")),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// The node icon doubles as the editor's status-bar mark. Serving the icons
|
|
60
|
+
// directory here keeps one copy on disk and spares the client from having to
|
|
61
|
+
// know the package name that Node-RED's own /icons route is keyed by.
|
|
62
|
+
RED.httpAdmin.use(
|
|
63
|
+
"/fromcubes-react/icons",
|
|
64
|
+
permRead,
|
|
65
|
+
express.static(path.join(__dirname, "..", "icons")),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Prettier's browser builds, straight from node_modules — same arrangement
|
|
69
|
+
// as Monaco at /fromcubes-react/vs. Nothing is fetched until someone formats.
|
|
70
|
+
RED.httpAdmin.use(
|
|
71
|
+
"/fromcubes-react/prettier",
|
|
72
|
+
permRead,
|
|
73
|
+
express.static(path.dirname(require.resolve("prettier/package.json"))),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const CSS_HASH_RE = /^[a-f0-9]{1,64}$/;
|
|
77
|
+
function findCssByHash(reqHash) {
|
|
78
|
+
for (const ep in pageState) {
|
|
79
|
+
if (pageState[ep]?.cssHash === reqHash) return pageState[ep].css;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function serveCss(req, res) {
|
|
84
|
+
const reqHash = req.params.hash;
|
|
85
|
+
if (!CSS_HASH_RE.test(reqHash)) {
|
|
86
|
+
res.status(400).send("Bad request");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const css = findCssByHash(reqHash);
|
|
90
|
+
if (!css) {
|
|
91
|
+
res.status(404).send("Not found");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
res.set({
|
|
95
|
+
"Content-Type": "text/css",
|
|
96
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
97
|
+
});
|
|
98
|
+
res.send(css);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
RED.httpNode.get("/fromcubes/css/:hash.css", serveCss);
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
RED.httpAdmin.get("/fromcubes-react/registry", permRead, (_req, res) => {
|
|
105
|
+
res.json(registry);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
RED.httpAdmin.post(
|
|
109
|
+
"/fromcubes-react/registry",
|
|
110
|
+
permWrite,
|
|
111
|
+
csrfGuard,
|
|
112
|
+
rateLimit,
|
|
113
|
+
(_req, res) => {
|
|
114
|
+
res.status(410).json({
|
|
115
|
+
error: "registry writes are deprecated; use fc-portal-component nodes",
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
RED.httpAdmin.delete(
|
|
121
|
+
"/fromcubes-react/registry/:name",
|
|
122
|
+
permWrite,
|
|
123
|
+
csrfGuard,
|
|
124
|
+
rateLimit,
|
|
125
|
+
(_req, res) => {
|
|
126
|
+
res.status(410).json({
|
|
127
|
+
error: "registry writes are deprecated; use fc-portal-component nodes",
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
RED.httpAdmin.get("/fromcubes-react/utilities", permRead, (_req, res) => {
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const [name, u] of Object.entries(utilities)) {
|
|
135
|
+
out[name] = {
|
|
136
|
+
code: u.code,
|
|
137
|
+
error: u.error || null,
|
|
138
|
+
symbols: [...extractUtilitySymbols(u.code || "")],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
res.json(out);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
RED.httpAdmin.post(
|
|
145
|
+
"/fromcubes-react/utilities",
|
|
146
|
+
permWrite,
|
|
147
|
+
csrfGuard,
|
|
148
|
+
rateLimit,
|
|
149
|
+
(_req, res) => {
|
|
150
|
+
res.status(410).json({
|
|
151
|
+
error: "utility writes are deprecated; use fc-portal-utility nodes",
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
RED.httpAdmin.delete(
|
|
157
|
+
"/fromcubes-react/utilities/:name",
|
|
158
|
+
permWrite,
|
|
159
|
+
csrfGuard,
|
|
160
|
+
rateLimit,
|
|
161
|
+
(_req, res) => {
|
|
162
|
+
res.status(410).json({
|
|
163
|
+
error: "utility writes are deprecated; use fc-portal-utility nodes",
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (providers) registerProviderRoutes(RED, deps);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// select() error code → HTTP status
|
|
172
|
+
const SELECT_STATUS = { "bad-kind": 400, "bad-id": 400, unknown: 400, locked: 409 };
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Settings → Fromcubes: read and change the identity provider and CSS engine.
|
|
176
|
+
* Changing identity decides who counts as logged in, so the write sits
|
|
177
|
+
* behind the same permission, CSRF and rate-limit gates as every other
|
|
178
|
+
* admin write, and is logged with the editor user who made it.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} RED
|
|
181
|
+
* @param {Object} deps See registerAdminApi.
|
|
182
|
+
* @returns {void}
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
function registerProviderRoutes(RED, deps) {
|
|
186
|
+
const { express, permRead, permWrite, csrfGuard, rateLimit, providers, onProviderChange } = deps;
|
|
187
|
+
// Node-RED's admin app parses JSON already (capped by apiMaxLength); parse
|
|
188
|
+
// only where nothing upstream did — the same arrangement as lib/assets.js.
|
|
189
|
+
const parseJson = express.json({ limit: (RED.settings && RED.settings.apiMaxLength) || "5mb" });
|
|
190
|
+
const jsonBody = (req, res, next) => (req._body ? next() : parseJson(req, res, next));
|
|
191
|
+
|
|
192
|
+
RED.httpAdmin.get("/fromcubes-react/providers", permRead, (_req, res) => {
|
|
193
|
+
res.json(providers.describe());
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Completion list for className="…", from whichever CSS engine is in force.
|
|
197
|
+
// `engine` lets the editor tell a stale list from a fresh one.
|
|
198
|
+
RED.httpAdmin.get("/fromcubes-react/css-classes", permRead, async (_req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
res.json(await providers.editorClasses());
|
|
201
|
+
} catch (e) {
|
|
202
|
+
RED.log.error("[portal-react] css-classes: " + e.message);
|
|
203
|
+
res.status(500).json({ error: "could not list classes" });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
RED.httpAdmin.put(
|
|
208
|
+
"/fromcubes-react/providers",
|
|
209
|
+
permWrite,
|
|
210
|
+
csrfGuard,
|
|
211
|
+
rateLimit,
|
|
212
|
+
jsonBody,
|
|
213
|
+
(req, res) => {
|
|
214
|
+
const { kind, id } = req.body || {};
|
|
215
|
+
let changed;
|
|
216
|
+
try {
|
|
217
|
+
changed = providers.select(kind, id);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
const status = SELECT_STATUS[e.code];
|
|
220
|
+
if (!status) {
|
|
221
|
+
RED.log.error("[portal-react] saving provider choice failed: " + e.message);
|
|
222
|
+
res.status(500).json({ error: "could not save the provider choice" });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
res.status(status).json({ error: e.message, code: e.code });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (changed) {
|
|
229
|
+
const who = (req.user && req.user.username) || "anonymous editor";
|
|
230
|
+
RED.log.warn(`[portal-react] ${kind} provider set to "${id}" by ${who}`);
|
|
231
|
+
if (onProviderChange) onProviderChange(kind);
|
|
232
|
+
}
|
|
233
|
+
res.json(providers.describe());
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { registerAdminApi };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** @module nodes/lib/admin-security */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The gates every fromcubes admin route goes through — permission scopes,
|
|
5
|
+
* the CSRF header check and the per-IP rate limit — plus the express that
|
|
6
|
+
* Node-RED itself ships. Shared by the core and by the add-on packages
|
|
7
|
+
* (through the `api` export, lib/api.js), so an add-on's routes get exactly
|
|
8
|
+
* the same protection and the same rate-limit buckets as the core's.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Borrow the express that the running Node-RED ships with instead of
|
|
13
|
+
* declaring our own copy. We only need its static/json/raw middleware; the
|
|
14
|
+
* server itself is RED.httpAdmin/RED.httpNode. This package lives in
|
|
15
|
+
* `<userDir>/node_modules`, from where a plain require cannot see Node-RED's
|
|
16
|
+
* install directory, so resolve from the entry script that started the
|
|
17
|
+
* process (node-red's red.js). Embedded Node-RED (an app that requires
|
|
18
|
+
* node-red itself) falls back to a plain require — such an app has express.
|
|
19
|
+
* @returns {typeof import("express")}
|
|
20
|
+
*/
|
|
21
|
+
function loadExpress() {
|
|
22
|
+
const entry = require.main && require.main.filename;
|
|
23
|
+
if (entry) {
|
|
24
|
+
try {
|
|
25
|
+
return require("module").createRequire(entry)("express");
|
|
26
|
+
} catch (_) {
|
|
27
|
+
// not launched through node-red's own entry script
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return require("express");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the admin gates bound to `RED`. Safe to call more than once (core
|
|
35
|
+
* and each add-on do): the rate-limit buckets and their prune timer live on
|
|
36
|
+
* `RED.settings`, so every caller shares one bucket per client IP.
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} RED
|
|
39
|
+
* @returns {{
|
|
40
|
+
* permRead: import("express").RequestHandler,
|
|
41
|
+
* permWrite: import("express").RequestHandler,
|
|
42
|
+
* csrfGuard: import("express").RequestHandler,
|
|
43
|
+
* rateLimit: import("express").RequestHandler,
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
function createAdminSecurity(RED) {
|
|
47
|
+
// ── Admin auth gate (Node-RED 4.x adminAuth) ─────────────────
|
|
48
|
+
// When the user configures `adminAuth` in settings.js, RED.auth.needsPermission
|
|
49
|
+
// returns an Express middleware that enforces the named permission scope.
|
|
50
|
+
// Without this gate, any endpoint mounted on RED.httpAdmin is reachable
|
|
51
|
+
// without auth and can read/modify the registry, utilities, and assets.
|
|
52
|
+
// Fallback no-op for runtimes that do not expose RED.auth.
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a Node-RED permission middleware. Returns a no-op when the runtime
|
|
55
|
+
* does not expose `RED.auth.needsPermission` (test harnesses, Node-RED <1.0)
|
|
56
|
+
* so unit tests don't have to mock auth.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} scope Permission scope (e.g. `"portal-react.write"`).
|
|
59
|
+
* @returns {import("express").RequestHandler}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
function needsPerm(scope) {
|
|
63
|
+
if (RED.auth && typeof RED.auth.needsPermission === "function") {
|
|
64
|
+
return RED.auth.needsPermission(scope);
|
|
65
|
+
}
|
|
66
|
+
return function (_req, _res, next) { next(); };
|
|
67
|
+
}
|
|
68
|
+
const PERM_READ = needsPerm("portal-react.read");
|
|
69
|
+
const PERM_WRITE = needsPerm("portal-react.write");
|
|
70
|
+
|
|
71
|
+
// ── CSRF guard for admin write endpoints ──────────────────────
|
|
72
|
+
// The Node-RED editor sends `Node-RED-API-Version` on every XHR. Browsers
|
|
73
|
+
// refuse to attach custom headers on cross-origin form/fetch submissions
|
|
74
|
+
// without a CORS preflight, so requiring this header on write endpoints is
|
|
75
|
+
// sufficient CSRF protection without per-session tokens. Same trick the
|
|
76
|
+
// Node-RED core admin API uses for its own POST/PUT/DELETE routes.
|
|
77
|
+
/**
|
|
78
|
+
* Express middleware enforcing the `Node-RED-API-Version` header on write
|
|
79
|
+
* endpoints. The header is sent by the Node-RED editor XHR layer on every
|
|
80
|
+
* request; browser cross-origin form/fetch POSTs cannot attach custom
|
|
81
|
+
* headers without a CORS preflight, so requiring this header gives CSRF
|
|
82
|
+
* protection without per-session tokens.
|
|
83
|
+
*
|
|
84
|
+
* @param {import("express").Request} req
|
|
85
|
+
* @param {import("express").Response} res
|
|
86
|
+
* @param {Function} next
|
|
87
|
+
* @returns {void}
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
function csrfGuard(req, res, next) {
|
|
91
|
+
if (!req.get("Node-RED-API-Version")) {
|
|
92
|
+
return res.status(403).json({ error: "missing Node-RED-API-Version header" });
|
|
93
|
+
}
|
|
94
|
+
next();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Token-bucket rate limiter keyed by req.ip ─────────────────
|
|
98
|
+
// Steady-state: RATE_LIMIT_TOKENS tokens refilled over RATE_LIMIT_WINDOW_MS.
|
|
99
|
+
// Default = 60 tokens / 60 s → 1 req/s sustained, 60 burst. Tunable via
|
|
100
|
+
// RED.settings.portalReact.rateLimit = { tokens, windowMs }. The buckets
|
|
101
|
+
// are pruned every 5 minutes to bound memory usage.
|
|
102
|
+
const rateLimitCfg = (RED.settings.portalReact &&
|
|
103
|
+
RED.settings.portalReact.rateLimit) || {};
|
|
104
|
+
const RATE_LIMIT_TOKENS = Number.isFinite(rateLimitCfg.tokens)
|
|
105
|
+
? rateLimitCfg.tokens
|
|
106
|
+
: 60;
|
|
107
|
+
const RATE_LIMIT_WINDOW_MS = Number.isFinite(rateLimitCfg.windowMs)
|
|
108
|
+
? rateLimitCfg.windowMs
|
|
109
|
+
: 60_000;
|
|
110
|
+
if (!RED.settings.portalReactRateBuckets) RED.settings.portalReactRateBuckets = new Map();
|
|
111
|
+
const rateBuckets = RED.settings.portalReactRateBuckets;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Token-bucket rate limit middleware keyed by `req.ip`. Each bucket refills
|
|
115
|
+
* `RATE_LIMIT_TOKENS` over `RATE_LIMIT_WINDOW_MS`. Steady-state default is
|
|
116
|
+
* 1 request/second with a 60-token burst. On exhaustion: HTTP 429 with
|
|
117
|
+
* `Retry-After`. Buckets idle > 10 minutes get pruned by a separate
|
|
118
|
+
* interval (see below).
|
|
119
|
+
*
|
|
120
|
+
* @param {import("express").Request} req
|
|
121
|
+
* @param {import("express").Response} res
|
|
122
|
+
* @param {Function} next
|
|
123
|
+
* @returns {void}
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
function rateLimit(req, res, next) {
|
|
127
|
+
const ip = req.ip || req.socket?.remoteAddress || "unknown";
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
let b = rateBuckets.get(ip);
|
|
130
|
+
if (!b) {
|
|
131
|
+
b = { tokens: RATE_LIMIT_TOKENS, last: now };
|
|
132
|
+
rateBuckets.set(ip, b);
|
|
133
|
+
}
|
|
134
|
+
const elapsed = now - b.last;
|
|
135
|
+
b.tokens = Math.min(
|
|
136
|
+
RATE_LIMIT_TOKENS,
|
|
137
|
+
b.tokens + (elapsed * RATE_LIMIT_TOKENS) / RATE_LIMIT_WINDOW_MS,
|
|
138
|
+
);
|
|
139
|
+
b.last = now;
|
|
140
|
+
if (b.tokens < 1) {
|
|
141
|
+
res.set("Retry-After", Math.ceil(RATE_LIMIT_WINDOW_MS / 1000));
|
|
142
|
+
return res.status(429).json({ error: "rate limit exceeded" });
|
|
143
|
+
}
|
|
144
|
+
b.tokens -= 1;
|
|
145
|
+
next();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!RED.settings.portalReactRateBucketPruneIv) {
|
|
149
|
+
RED.settings.portalReactRateBucketPruneIv = setInterval(() => {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
for (const [ip, b] of rateBuckets) {
|
|
152
|
+
if (now - b.last > 10 * 60_000) rateBuckets.delete(ip);
|
|
153
|
+
}
|
|
154
|
+
}, 5 * 60_000);
|
|
155
|
+
RED.settings.portalReactRateBucketPruneIv.unref?.();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { permRead: PERM_READ, permWrite: PERM_WRITE, csrfGuard, rateLimit };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { createAdminSecurity, loadExpress };
|
package/nodes/lib/api.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** @module nodes/lib/api */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The core's public surface for add-ons. Add-on packages
|
|
5
|
+
* (@aaqu/fromcubes-auth-headers, -tailwind, -assets, and third-party ones)
|
|
6
|
+
* require `@aaqu/fromcubes-react/api` — package.json `exports` maps it here —
|
|
7
|
+
* and nothing else from the core (tests/addon-boundary.test.js).
|
|
8
|
+
*
|
|
9
|
+
* Keep it small and stable — anything exported here is a promise to every
|
|
10
|
+
* add-on, ours and third-party.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { createAdminSecurity, loadExpress } = require("./admin-security");
|
|
14
|
+
const {
|
|
15
|
+
IDENTITY_TYPE,
|
|
16
|
+
CSS_TYPE,
|
|
17
|
+
API_VERSION,
|
|
18
|
+
} = require("./providers");
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
// Plugin contract (see lib/providers.js)
|
|
22
|
+
IDENTITY_TYPE,
|
|
23
|
+
CSS_TYPE,
|
|
24
|
+
API_VERSION,
|
|
25
|
+
// Admin route gates shared with the core: permission, CSRF, rate limit
|
|
26
|
+
createAdminSecurity,
|
|
27
|
+
// The express Node-RED itself ships (static/json/raw middleware)
|
|
28
|
+
loadExpress,
|
|
29
|
+
};
|