@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.
Files changed (37) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +405 -2
  3. package/THIRD-PARTY-NOTICES.md +164 -0
  4. package/examples/D3 Poland Map.json +90 -0
  5. package/examples/Live Chart.json +101 -0
  6. package/examples/PixiJS Sprites.json +114 -0
  7. package/examples/Sensor Portal.json +93 -0
  8. package/examples/Shared Components.json +68 -0
  9. package/examples/Three.js Scene.json +118 -0
  10. package/examples/Utility Hooks.json +94 -0
  11. package/examples/WebGPU Shader.json +110 -0
  12. package/nodes/editor/ark-pixel.LICENSE.txt +96 -0
  13. package/nodes/editor/ark-pixel.woff2 +0 -0
  14. package/nodes/editor/file-tree.js +403 -0
  15. package/nodes/editor/format.js +182 -0
  16. package/nodes/editor/icons.svg +46 -0
  17. package/nodes/editor/mark.svg +5 -0
  18. package/nodes/editor/provider-settings.js +192 -0
  19. package/nodes/editor/workspace.css +653 -0
  20. package/nodes/editor/workspace.js +1565 -0
  21. package/nodes/icons/fromcubes-component.svg +27 -0
  22. package/nodes/icons/fromcubes-react.svg +25 -0
  23. package/nodes/icons/fromcubes-utility.svg +21 -0
  24. package/nodes/lib/admin-api.js +238 -0
  25. package/nodes/lib/admin-security.js +161 -0
  26. package/nodes/lib/api.js +29 -0
  27. package/nodes/lib/helpers.js +990 -0
  28. package/nodes/lib/hooks.js +145 -0
  29. package/nodes/lib/page-builder.js +499 -0
  30. package/nodes/lib/portal-page-route.js +131 -0
  31. package/nodes/lib/providers.js +536 -0
  32. package/nodes/lib/registry-nodes.js +331 -0
  33. package/nodes/lib/router.js +103 -0
  34. package/nodes/lib/ws-heartbeat.js +58 -0
  35. package/nodes/portal-react.html +2539 -0
  36. package/nodes/portal-react.js +1652 -0
  37. package/package.json +76 -4
package/LICENSE ADDED
@@ -0,0 +1,190 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to the Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by the Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding any notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2026 Mazur Albert Fromcubes
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,406 @@
1
- # Temporary Holding Version
1
+ # @aaqu/fromcubes-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/%40aaqu%2Ffromcubes-react.svg)](https://www.npmjs.com/package/@aaqu/fromcubes-react)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40aaqu%2Ffromcubes-react.svg)](https://www.npmjs.com/package/@aaqu/fromcubes-react)
5
+ [![node](https://img.shields.io/node/v/%40aaqu%2Ffromcubes-react.svg)](https://www.npmjs.com/package/@aaqu/fromcubes-react)
6
+ [![Node-RED](https://img.shields.io/badge/Node--RED-%E2%89%A5%204.0-8f0000.svg)](https://nodered.org)
7
+ [![license](https://img.shields.io/npm/l/%40aaqu%2Ffromcubes-react.svg)](./LICENSE)
8
+ [![Discord](https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/KYzsfKkYxX)
9
+
10
+ **📖 Documentation — step-by-step guide with screenshots:** [aaqu.github.io/fromcubes-modules](https://aaqu.github.io/fromcubes-modules/) — install → import → edit JSX → live dashboard, on one example.
11
+
12
+ [![Documentation walkthrough](https://aaqu.github.io/fromcubes-modules/img/docs-scroll.gif)](https://aaqu.github.io/fromcubes-modules/)
13
+
14
+ > **⚠️ Alpha Module** — This project is in early development. Expect breaking changes. Test on a clean Node-RED instance.
15
+
16
+ A Node-RED node that turns any `/fromcubes/<sub-path>` URL into a React page. Write JSX in the editor, deploy, open the URL — your component talks to the flow over WebSocket. No build step, no browser compiler. All portal pages are served under the hardcoded `/fromcubes/` prefix so every node cleanly coexists under one URL tree.
17
+
18
+ For internals, plugin authoring, and the deploy pipeline see [README-DEV.md](./README-DEV.md).
19
+
20
+ **💬 Questions, feedback, showcase?** Join the [fromcubes Discord](https://discord.gg/KYzsfKkYxX).
21
+
22
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L4L01UOFRG)
23
+
24
+ ## Prerequisites
25
+
26
+ - **Node-RED ≥ 4.0** — declared in `package.json`'s `node-red.version` and
27
+ enforced by the editor's Palette Manager.
28
+ - **Node.js ≥ 18.5** — Node-RED 4.x's effective minimum.
29
+ - **npm** — only required when you use the `libs` config field to install
30
+ user packages at deploy time. The portal itself does not need `npm`
31
+ available at runtime.
32
+ - A WebSocket-capable reverse proxy or direct connection — the portal
33
+ upgrades `/fromcubes/<sub-path>/_ws`. If you front Node-RED with nginx /
34
+ Traefik / Caddy, enable `Upgrade: websocket` headers on that path.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ cd ~/.node-red
40
+ npm install @aaqu/fromcubes-react
41
+ # restart Node-RED
42
+ ```
43
+
44
+ That's it. No build step. Any npm packages you list in the node config (e.g. `d3, three`) are installed automatically on deploy.
45
+
46
+ ### Add-ons
47
+
48
+ Three parts ship as their own packages, and for now installing the core
49
+ installs them too:
50
+
51
+ | Package | What it adds |
52
+ |---|---|
53
+ | [`@aaqu/fromcubes-tailwind`](https://www.npmjs.com/package/@aaqu/fromcubes-tailwind) | Tailwind CSS 4 for portals, plus class completion in the editor |
54
+ | [`@aaqu/fromcubes-auth-headers`](https://www.npmjs.com/package/@aaqu/fromcubes-auth-headers) | Users from `x-portal-user-*` headers set by a reverse proxy (Portal Auth) |
55
+ | [`@aaqu/fromcubes-assets`](https://www.npmjs.com/package/@aaqu/fromcubes-assets) | Static files for portals, managed from a sidebar tab |
56
+
57
+ Each is a Node-RED plugin built on the same API anyone can use — see
58
+ [Plugins](#plugins-identity-providers-and-css-engines). Coming from
59
+ `@aaqu/fromcubes-portal-react`? Uninstall it first, then install this
60
+ package: both register the same nodes. Your flows keep working unchanged.
61
+
62
+ ## Your first portal
63
+
64
+ 1. Drop a **portal-react** node onto a flow.
65
+ 2. Set **Sub-path** to e.g. `hello` (the node will serve at `/fromcubes/hello`; the `/fromcubes/` prefix is fixed).
66
+ 3. Open the code editor and paste:
67
+ ```jsx
68
+ function App() {
69
+ const { data, send } = useNodeRed();
70
+ return (
71
+ <div className="p-4">
72
+ <p>From flow: {JSON.stringify(data)}</p>
73
+ <button
74
+ className="px-3 py-1 bg-blue-500 text-white rounded"
75
+ onClick={() => send({ clicked: Date.now() })}
76
+ >
77
+ click me
78
+ </button>
79
+ </div>
80
+ );
81
+ }
82
+ ```
83
+ 4. Deploy. Open `http://localhost:1880/fromcubes/hello`.
84
+ 5. Wire an **inject** node into the portal-react input → see `data` update live. Wire its output → see button clicks arrive in your flow.
85
+
86
+ ## The `useNodeRed()` hook
87
+
88
+ Everything your component needs from the flow lives in one hook:
89
+
90
+ ```jsx
91
+ const {
92
+ data, // last broadcast msg.payload from input wire (reactive)
93
+ send, // send(payload, topic?) — emit msg on output wire
94
+ user, // portal user object (when Portal Auth is enabled), or null
95
+ portalClient, // unique session/tab ID assigned by server on connect
96
+ authRequired, // true when this session is anonymous on an authenticated-only portal
97
+ } = useNodeRed();
98
+ ```
99
+
100
+ ### Initial data on connect
101
+
102
+ The server does **not** cache payloads — a freshly-loaded page has `data === null` until the flow pushes the next message. For periodic streams that's at most one interval of blankness. For event-driven or per-user data, request the initial state from the component:
103
+
104
+ ```jsx
105
+ const { data, send } = useNodeRed();
106
+ useEffect(() => { send({}, "init"); }, []);
107
+ ```
108
+
109
+ In the flow, catch `topic === "init"`, look up the state (per-user via `msg._client.userId` if needed) and reply — `msg._client` is already set on the inbound message, so the response returns as a unicast to exactly that tab.
110
+
111
+ ## Node configuration
112
+
113
+ | Field | Purpose |
114
+ |---|---|
115
+ | Sub-path | Part after `/fromcubes/`, e.g. `page1` → served at `/fromcubes/page1`. Required. Nesting allowed (`team/alpha`). Reserved: `public`, `_ws`. |
116
+ | Page Title | Browser tab title |
117
+ | npm Packages | Comma-separated, e.g. `d3, three, @react-three/fiber` |
118
+ | Portal Auth | Enable portal user header extraction (see Multi-user) |
119
+ | Authenticated-only delivery | Untargeted msgs (no `msg._client`) go only to sessions with portal identity headers; anonymous sessions get an `auth_required` frame on connect (see Multi-user). Off by default. |
120
+ | Show WS status | Small "fromcubes • connected/disconnected" badge in the page's bottom-right corner (off by default) |
121
+ | Head HTML | Extra trusted-author `<head>` tags (CDN, fonts, CSS, scripts). Runs in the public portal page. |
122
+ | Code Editor | Monaco with JSX — must define `<App />` |
123
+
124
+ There is also a config node, **fc-portal-component**, that lets you define reusable React components once and reference them by name from any portal-react node. Referenced components (and their transitive dependencies) are injected at transpile time, so unused ones add nothing to the bundle.
125
+
126
+ For shared **non-component** code (helpers, custom hooks, constants), use **fc-portal-utility** — a sibling config node. Unlike component nodes (which export exactly one symbol via an IIFE wrapper), a utility node is injected raw at top level so it can declare any number of `function` / `const` / `let` / `class` symbols. Selective inclusion: a utility node lands in a portal's bundle only when the portal's JSX or any of its referenced library components mentions at least one of the symbols declared in that utility.
127
+
128
+ ```jsx
129
+ // fc-portal-utility, Module name = mathHelpers
130
+ const PI2 = Math.PI * 2;
131
+ function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
132
+ function useDebounce(value, ms = 300) {
133
+ const [v, setV] = React.useState(value);
134
+ React.useEffect(() => {
135
+ const t = setTimeout(() => setV(value), ms);
136
+ return () => clearTimeout(t);
137
+ }, [value, ms]);
138
+ return v;
139
+ }
140
+
141
+ // portal-react, JSX tab
142
+ function App() {
143
+ const { data } = useNodeRed();
144
+ const slow = useDebounce(data?.value);
145
+ return <div>{clamp(slow ?? 0, 0, 100)}</div>;
146
+ }
147
+ ```
148
+
149
+ ## Editor features
150
+
151
+ - **Monaco** with full JSX support and `useNodeRed()` type declarations
152
+ - **Tailwind CSS autocompletion** inside `className="..."` (~19k utility classes). Classes must be complete literals in a string — dynamic `bg-${tone}-500` generates no CSS (use a map of full classes). Arbitrary values incl. `w-[calc(100%-2rem)]`, `grid-cols-[repeat(2,1fr)]`, `mt-0!` and `@md:flex` are supported.
153
+ - **JSX tag completion** — type tag name, Tab to expand
154
+ - **Self-close collapse** — type `/` inside empty `<tag></tag>` to convert to `<tag />`
155
+ - **Ctrl+D / Cmd+D** — duplicate the current line or selection (in every Monaco editor of the package)
156
+ - **Ctrl+/ / Cmd+/** — JSX-aware comment toggle. Style follows the first selected line (VS Code behavior): selecting a whole `return ( … )` block gives plain `//` on every line, while lines between live JSX tags are wrapped in `{/* … */}` (a plain `//` there is literal text — JSX renders it on the page)
157
+ - **Component completion** — registry components + any PascalCase word
158
+ - **Utility-symbol completion** — top-level identifiers from any `fc-portal-utility` node, suggested in JS context
159
+ - **Components / Utilities dialogs** — buttons in the JSX tab; Components inserts `<Tag></Tag>`, Utilities expands to the symbols declared in each node and inserts the bare identifier on click
160
+ - **Portal Assets sidebar** — file manager for static assets (GLB, textures, fonts…)
161
+
162
+ ## Multi-user / Multi-tenancy
163
+
164
+ Portal-react has four routing modes — broadcast, auth-cast (every authenticated session), user-cast (every tab of one user), and unicast (one specific tab). Everything works without authentication too — user-cast and auth-cast just degrade gracefully when there is no user.
165
+
166
+ ### Identity
167
+
168
+ | Identifier | Scope | Persistence | Source |
169
+ |---|---|---|---|
170
+ | `portalClient` | Single WS session (one tab) | Lost on reconnect — server assigns a new UUID | Generated server-side on connect |
171
+ | `userId` / `login` | All sessions of a user | Survives reconnect and new tabs | `x-portal-user-*` headers (when **Portal Auth** is enabled) |
172
+
173
+ **Portal Auth header contract** — injected by an upstream reverse proxy (the [fromcubes-portal](https://github.com/Aaqu/fromcubes-portal) platform sets them from Traefik forward-auth). Every value is percent-encoded UTF-8 (`%C5%81ukasz` → `Łukasz`); the same field names appear in `useNodeRed().user` and `msg._client`:
174
+
175
+ | Header | Field |
176
+ |---|---|
177
+ | `x-portal-user-id` | `userId` — required; without it the session is anonymous |
178
+ | `x-portal-user-login` | `login` |
179
+ | `x-portal-user-display-name` | `displayName` |
180
+ | `x-portal-user-email` | `email` |
181
+ | `x-portal-user-role` | `role` (single value) |
182
+ | `x-portal-user-groups` | `groups` — comma-separated, each element encoded on its own (`a%2Cb` is one group `a,b`); always an array, `[]` when absent |
183
+
184
+ A value with a malformed escape is skipped (in `groups`, only that element). Without `x-portal-user-id` the session is anonymous and `user` is `null`, whatever other headers arrive. The pre-contract `x-portal-user-username` / `x-portal-user-name` headers are ignored.
185
+
186
+ **The proxy must strip all six headers from the client's request** before injecting its own. The package trusts them as they arrive; one header let through is enough to impersonate any user.
187
+
188
+ If Portal Auth is disabled or headers are absent, `user` is `null` and user-scoped features are silently skipped — broadcast and per-session features still work.
189
+
190
+ ### Routing modes
191
+
192
+ Every inbound `msg` arrives in your flow with `msg._client` already filled in by the server. The flow then decides where the response should go by setting (or clearing) `msg._client` on the outgoing `msg`:
193
+
194
+ ```javascript
195
+ // BROADCAST — everyone connected to this endpoint
196
+ delete msg._client;
197
+ return msg;
198
+
199
+ // UNICAST — only the tab that sent the original msg (echo)
200
+ // Keep msg._client as-is; it already carries portalClient.
201
+ return msg;
202
+
203
+ // UNICAST — a specific tab whose ID you know
204
+ msg._client = { portalClient: "a1b2c3d4-..." };
205
+ return msg;
206
+
207
+ // USER-CAST — every tab of a specific user (even ones that just opened)
208
+ msg._client = { userId: "alice" };
209
+ return msg;
210
+
211
+ // AUTH-CAST — every session that arrived with x-portal-user-* identity;
212
+ // anonymous sessions (no proxy headers) are skipped
213
+ msg._client = { authenticated: true };
214
+ return msg;
215
+ ```
216
+
217
+ **Auth-cast vs broadcast.** Broadcast reaches every connected client, including anonymous ones. Auth-cast reaches only sessions that arrived with `x-portal-user-*` identity headers. Use auth-cast for data that any logged-in user may see; use broadcast only for data that is genuinely public. Nothing is cached server-side — each mode delivers to the clients connected at send time.
218
+
219
+ ### Authenticated-only delivery (fail-safe default)
220
+
221
+ With the **Authenticated-only delivery** checkbox enabled on the node, an untargeted `msg` (no `msg._client`) is delivered as an **auth-cast** instead of a broadcast — a forgotten `_client` then narrows delivery to logged-in sessions rather than leaking to anonymous ones. A true broadcast stays available explicitly:
222
+
223
+ ```javascript
224
+ msg._client = { authenticated: false }; // deliberate broadcast, anonymous included
225
+ return msg;
226
+ ```
227
+
228
+ Anonymous sessions are told up front: right after `hello` the server sends a single `auth_required` frame (once per connection — never per skipped message, so unauthenticated clients learn nothing about traffic volume or timing). The hook exposes it so the page can render a login hint instead of staying silently empty:
229
+
230
+ ```jsx
231
+ const { data, authRequired } = useNodeRed();
232
+ if (authRequired) return <p className="p-4 text-zinc-500">Log in to see this data.</p>;
233
+ ```
234
+
235
+ **Anti-spoof guarantee.** On every inbound message the server overwrites `msg._client` from scratch using the socket's own `portalClient` and the user data captured at connect. A browser cannot forge `_client` — whatever it puts there is discarded.
236
+
237
+ **User-cast uses an O(1) index** so a message to `{userId: "alice"}` reaches every tab of Alice with a single lookup, not a scan.
238
+
239
+ ### Without a user (anonymous mode)
240
+
241
+ If **Portal Auth** is off (or no proxy headers arrive), everything still works:
242
+
243
+ - `broadcast` and `portalClient` unicast: unchanged
244
+ - `user`-cast: gracefully skipped (no `userId` to target)
245
+ - `auth`-cast: delivers to nobody (no session has identity headers)
246
+ - `useNodeRed().user` is `null`
247
+
248
+ Same model as dashboard 2 — no auth required to use the node.
249
+
250
+ ## Plugins: identity providers and CSS engines
251
+
252
+ Who the user is and how CSS is generated are both pluggable. The defaults,
253
+ `headers` (the contract above) and `tailwind`, are themselves plugins shipped
254
+ in this package and use exactly this API. A separate npm package can register its own, e.g. for a different
255
+ auth proxy or a different CSS tool:
256
+
257
+ ```js
258
+ // package.json: "node-red": { "plugins": { "my-providers": "plugin.js" } }
259
+ module.exports = function (RED) {
260
+ RED.plugins.registerPlugin("my-sso", {
261
+ type: "fromcubes-identity",
262
+ apiVersion: 1,
263
+ async resolveUser(req) { // HTTP request or WS upgrade request
264
+ const claims = await verify(req.headers.authorization);
265
+ return claims ? { userId: claims.sub, login: claims.preferred_username, groups: claims.groups } : null;
266
+ },
267
+ });
268
+ RED.plugins.registerPlugin("my-css", {
269
+ type: "fromcubes-css",
270
+ apiVersion: 1,
271
+ async generate(source) { // the portal's full source, components included
272
+ return { css: await compileMyCss(source) };
273
+ },
274
+ editor: { // optional: className="…" completion
275
+ classes() { return ["card", "btn", "btn-primary"]; },
276
+ },
277
+ });
278
+ };
279
+ ```
280
+
281
+ Installing a plugin does not activate it. Choose one per kind in the editor,
282
+ **Menu → Settings → Fromcubes**: every installed provider is listed with the
283
+ package it comes from, and exactly one is selected. The choice is saved in
284
+ `<userDir>/fromcubes/providers.json` and applies at once — a new CSS engine
285
+ rebuilds every portal; a new identity provider asks for confirmation, then
286
+ reloads every open portal tab so page and connection both use it.
287
+
288
+ An administrator can pin either choice in `settings.js`; the editor then shows
289
+ it locked:
290
+
291
+ ```js
292
+ portalReact: {
293
+ identityProvider: "my-sso", // default "headers"; "none" = everyone anonymous
294
+ cssEngine: "my-css", // default "tailwind"; "none" serves no stylesheet
295
+ },
296
+ ```
297
+
298
+ - `resolveUser` returns a user or `null`. Only the known fields
299
+ (`userId`, `login`, `displayName`, `email`, `role`, `groups`) are kept; a
300
+ throw, a rejection or 5 s without an answer means anonymous.
301
+ - `generate` returns `{ css }`. A failure shows the yellow `css-fail` status
302
+ and the page is served unstyled.
303
+ - `editor.classes()` (optional, may be async) feeds the class completion in
304
+ every code editor; it is asked once per installed plugin. Without it the
305
+ engine just gets no completion. Strings only, no whitespace, ≤ 200 chars,
306
+ at most 50 000.
307
+ - An unknown provider name is logged once; for identity every session is then
308
+ anonymous. The settings pane flags a selected provider whose package is gone.
309
+ - Optional `label: "…"` on the plugin is shown next to its id in the settings
310
+ pane.
311
+ - `require("@aaqu/fromcubes-react/api")` gives the type names and
312
+ `API_VERSION`, plus `createAdminSecurity(RED)` — the permission, CSRF and
313
+ rate-limit gates the core puts on its own admin routes — for a plugin that
314
+ adds routes. Declare `@aaqu/fromcubes-react` as a peer dependency. The
315
+ three add-ons above are complete examples.
316
+
317
+ ## Portal Assets
318
+
319
+ Static files (3D models, textures, fonts, etc.) can be uploaded and served from a public endpoint.
320
+
321
+ - Open the **Portal Assets** tab in the Node-RED sidebar
322
+ - Upload files via button or drag & drop
323
+ - Organize in folders (create, rename, move between folders)
324
+ - Copy public path with one click — use in JSX: `/fromcubes/public/models/scene.glb`
325
+ - Download and delete files from the context menu
326
+
327
+ All uploads require Node-RED admin authentication. Files are served publicly at `/fromcubes/public/`.
328
+
329
+ Limits: 100 MB per file, 500 MB total, 1000 files max.
330
+
331
+ ## Examples
332
+
333
+ Available via **Menu → Import → Examples → @aaqu/fromcubes-react**.
334
+ Each flow includes a comment node with what to expect and the URL to open.
335
+
336
+ Import **Shared Components** first — it provides the UI building blocks (Page, Header, Stat, Button, ValueBadge) referenced by the others.
337
+
338
+ | Example | npm packages | Description |
339
+ |---|---|---|
340
+ | **Shared Components** | — | Reusable components: Page, Header, Stat, Button, ValueBadge |
341
+ | **Sensor Portal** | — | Sensor gauge with live WebSocket data |
342
+ | **Live Chart** | `chart.js/auto` | Live updating Chart.js charts |
343
+ | **D3 Poland Map** | `d3` | Interactive SVG map of Poland (simulated data) |
344
+ | **Three.js Scene** | `three`, `@react-three/fiber`, `@react-three/drei` | 3D scene with Three.js |
345
+ | **PixiJS Sprites** | `pixi.js`, `@pixi/react` | Clickable bunny sprites with PixiJS |
346
+ | **WebGPU Shader** | `three` | WebGPU renderer + TSL animated shaders |
347
+ | **Utility Hooks** | — | `fc-portal-utility` demo: `useDebounce` custom hook + `clamp` helper |
348
+
349
+ ## Troubleshooting
350
+
351
+ | Symptom | Likely cause |
352
+ |---|---|
353
+ | Red status "transpile error" + error page on the endpoint | JSX syntax error — fix code, redeploy (cache invalidates automatically) |
354
+ | Red status "legacy endpoint" on deploy | Flow was saved before the `/fromcubes/` prefix became hardcoded. Open the node, set **Sub-path**, redeploy. Automatic migration is disabled to avoid silent URL changes. |
355
+ | Red status "bad sub-path" | Sub-path is empty or violates the rules (no leading `/`, no whitespace, no `..`, segments must start alphanumerically, `public`/`_ws` reserved). |
356
+ | Yellow status "css-fail" | Tailwind generation failed (usually an invalid class in JSX). Page still loads but unstyled. Fix the class, redeploy — the status clears on the next successful build. |
357
+ | Code editor stays blank / `fromcubes-react/vs/loader.js` 404s | Monaco is served live from the package's own `node_modules` at `/fromcubes-react/vs` (no postinstall copy step). The 404 means `RED.httpAdmin` is mounted under a non-root `httpAdminRoot` the editor didn't account for, or `monaco-editor` failed to install. Reinstall the package and hard-refresh the editor (`Cmd/Ctrl-Shift-R`). |
358
+ | `npm install @aaqu/fromcubes-react` ends with `EACCES` | The Node-RED `userDir/node_modules` install path is not writable by the user running Node-RED. Re-run `npm install` from a shell with the right ownership. |
359
+ | Browser request `/fromcubes/css/<hash>.css` returns 404 | The portal's deploy hasn't produced a CSS bundle yet — open the editor, redeploy. If the URL is bookmarked from before a deploy, the hash is stale; reload the portal page itself. With a custom `httpNodeRoot`, prepend it to this path. |
360
+ | WebSocket reconnects in an endless loop | Reverse-proxy is not forwarding `Upgrade: websocket` on `/fromcubes/<sub-path>/_ws`. Check the proxy config — nginx needs `proxy_set_header Upgrade $http_upgrade`, Traefik needs the `websocket` middleware. |
361
+ | `libs` packages fail to install on deploy | The user-installed npm packages declared in **Libs** install via Node-RED's `dynamicModuleList` mechanism, which needs network access from `userDir` and a writable `node_modules`. Behind a corporate proxy set `npm config set proxy …` for the Node-RED user. |
362
+ | Page loads but `data` stays `null` | Nothing has been pushed since the page connected — payloads are not cached server-side. Push periodically, or request initial state from the component (`send({}, "init")` → flow replies) |
363
+ | `user` is `null` even with Portal Auth on | Upstream proxy is not injecting `x-portal-user-id` (required — the other headers alone count as anonymous), or sends a malformed percent-escape in it |
364
+ | Page reloads on every deploy | Expected for code changes; clients soft-reconnect with exponential backoff |
365
+
366
+ ## Architecture: source AND sink
367
+
368
+ The Node-RED guidelines suggest a node should "sit at the beginning, middle
369
+ or end of a flow — not all at once". `portal-react` intentionally violates
370
+ this: every node is both a **sink** (`msg → render`) and a **source** (UI
371
+ event → outbound `msg`). Trade-offs:
372
+
373
+ - **Pro**: one wire per portal is conceptually simpler — the same node owns
374
+ the page lifecycle, its WebSocket, and the routing rules.
375
+ - **Con**: flow-error tracking only follows the `input → output` path.
376
+ Errors originating in the browser (e.g. a runtime exception inside a UI
377
+ handler) cross the WebSocket as a source-event and are **not** routed to
378
+ a Catch node on the same tab — they surface as a red node status and a
379
+ log line. This matches the Node-RED guidance for source nodes: "produces
380
+ messages in response to external events".
381
+
382
+ If you need explicit Catch-node visibility for UI-originated errors, wire
383
+ the portal's output through a Switch / Catch chain — a UI event landing on
384
+ `msg.payload` is then a regular wire-level message that Catch can observe.
385
+
386
+ ## Security
387
+
388
+ This module is meant to run behind a trusted Node-RED instance, ideally
389
+ behind a reverse proxy. Highlights:
390
+
391
+ - Admin write endpoints require the `Node-RED-API-Version` header (CSRF
392
+ protection) and a `portal-react.write` permission when `adminAuth` is
393
+ configured.
394
+ - Token-bucket rate-limit (default 60 burst / 1 req/s steady) on every
395
+ write endpoint, tunable via `RED.settings.portalReact.rateLimit`.
396
+ - JSON bodies capped by Node-RED's own `apiMaxLength` (settings.js, default
397
+ 5 MB), 100 MB asset-upload cap, 500 MB / 1000 files total
398
+ assets quota, 1 MB WebSocket frame cap.
399
+ - `x-portal-user-*` identity headers are trusted unconditionally — production
400
+ deployments **must** terminate auth at a reverse proxy that strips all six
401
+ inbound identity headers before injecting verified ones, and Node-RED must
402
+ not be reachable except through that proxy.
403
+
404
+ ## License
405
+
406
+ Apache-2.0