@aaqu/fromcubes-react 0.0.0-stage → 0.2.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 +435 -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 +660 -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 +581 -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 +73 -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,436 @@
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
+ [![Sponsor](https://img.shields.io/badge/Sponsor-GitHub-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/Aaqu)
10
+
11
+ **📖 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.
12
+
13
+ [![Documentation walkthrough](https://aaqu.github.io/fromcubes-modules/img/docs-scroll.gif)](https://aaqu.github.io/fromcubes-modules/)
14
+
15
+ 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.
16
+
17
+ For internals, plugin authoring, and the deploy pipeline see [README-DEV.md](./README-DEV.md).
18
+
19
+ **💬 Questions, feedback, showcase?** Join the [fromcubes Discord](https://discord.gg/KYzsfKkYxX).
20
+
21
+ **❤️ Support the project:** [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/Aaqu)
22
+
23
+ ## Prerequisites
24
+
25
+ - **Node-RED ≥ 4.0** — declared in `package.json`'s `node-red.version` and
26
+ enforced by the editor's Palette Manager.
27
+ - **Node.js ≥ 18.5** — Node-RED 4.x's effective minimum.
28
+ - **npm** — only required when you use the `libs` config field to install
29
+ user packages at deploy time. The portal itself does not need `npm`
30
+ available at runtime.
31
+ - A WebSocket-capable reverse proxy or direct connection — the portal
32
+ upgrades `/fromcubes/<sub-path>/_ws`. If you front Node-RED with nginx /
33
+ Traefik / Caddy, enable `Upgrade: websocket` headers on that path.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ cd ~/.node-red
39
+ npm install @aaqu/fromcubes-react
40
+ # restart Node-RED
41
+ ```
42
+
43
+ That's it — this is the whole core: the nodes, deploy-time bundling, the WebSocket bridge and the React Editor. No build step. Any npm packages you list in the node config (e.g. `d3, three`) are installed automatically on deploy.
44
+
45
+ ### Add-ons
46
+
47
+ Everything else is an add-on you install only if you need it:
48
+
49
+ ```bash
50
+ cd ~/.node-red
51
+ npm install @aaqu/fromcubes-tailwind # and/or -assets, -auth-headers
52
+ # restart Node-RED
53
+ ```
54
+
55
+ | Package | What it adds |
56
+ |---|---|
57
+ | [`@aaqu/fromcubes-tailwind`](https://www.npmjs.com/package/@aaqu/fromcubes-tailwind) | Tailwind CSS 4 for portals, plus class completion in the editor |
58
+ | [`@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) |
59
+ | [`@aaqu/fromcubes-assets`](https://www.npmjs.com/package/@aaqu/fromcubes-assets) | Static files for portals, managed from a sidebar tab |
60
+
61
+ Without an add-on its feature is simply off, and the editor says so once in
62
+ the log: no Tailwind means portals get no generated stylesheet (style them
63
+ with a `<link>`/`<style>` in **Head HTML**, or inline), no auth-headers means
64
+ Portal Auth sees every session as anonymous, no assets means no sidebar and no
65
+ `/fromcubes/public/`. Install an add-on later and it switches on by itself —
66
+ **Menu → Settings → Fromcubes** shows what is in use.
67
+
68
+ The shipped examples are styled with Tailwind; install
69
+ `@aaqu/fromcubes-tailwind` to see them as intended.
70
+
71
+ Each add-on is a Node-RED plugin built on the same API anyone can use — see
72
+ [Plugins](#plugins-identity-providers-and-css-engines).
73
+
74
+ **Coming from `@aaqu/fromcubes-portal-react`?** It contained all of the above.
75
+ Uninstall it first (both register the same nodes), then install the core
76
+ **and the add-ons you used** — for the same features as before:
77
+
78
+ ```bash
79
+ npm uninstall @aaqu/fromcubes-portal-react
80
+ npm install @aaqu/fromcubes-react @aaqu/fromcubes-tailwind @aaqu/fromcubes-assets @aaqu/fromcubes-auth-headers
81
+ ```
82
+
83
+ Your flows keep working unchanged.
84
+
85
+ ## Your first portal
86
+
87
+ 1. Drop a **portal-react** node onto a flow.
88
+ 2. Set **Sub-path** to e.g. `hello` (the node will serve at `/fromcubes/hello`; the `/fromcubes/` prefix is fixed).
89
+ 3. Open the code editor and paste:
90
+ ```jsx
91
+ function App() {
92
+ const { data, send } = useNodeRed();
93
+ return (
94
+ <div className="p-4">
95
+ <p>From flow: {JSON.stringify(data)}</p>
96
+ <button
97
+ className="px-3 py-1 bg-blue-500 text-white rounded"
98
+ onClick={() => send({ clicked: Date.now() })}
99
+ >
100
+ click me
101
+ </button>
102
+ </div>
103
+ );
104
+ }
105
+ ```
106
+ 4. Deploy. Open `http://localhost:1880/fromcubes/hello`.
107
+ 5. Wire an **inject** node into the portal-react input → see `data` update live. Wire its output → see button clicks arrive in your flow.
108
+
109
+ ## The `useNodeRed()` hook
110
+
111
+ Everything your component needs from the flow lives in one hook:
112
+
113
+ ```jsx
114
+ const {
115
+ data, // last broadcast msg.payload from input wire (reactive)
116
+ send, // send(payload, topic?) — emit msg on output wire
117
+ user, // portal user object (when Portal Auth is enabled), or null
118
+ portalClient, // unique session/tab ID assigned by server on connect
119
+ authRequired, // true when this session is anonymous on an authenticated-only portal
120
+ } = useNodeRed();
121
+ ```
122
+
123
+ ### Initial data on connect
124
+
125
+ 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:
126
+
127
+ ```jsx
128
+ const { data, send } = useNodeRed();
129
+ useEffect(() => { send({}, "init"); }, []);
130
+ ```
131
+
132
+ 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.
133
+
134
+ ## Node configuration
135
+
136
+ | Field | Purpose |
137
+ |---|---|
138
+ | Sub-path | Part after `/fromcubes/`, e.g. `page1` → served at `/fromcubes/page1`. Required. Nesting allowed (`team/alpha`). Reserved: `public`, `_ws`. |
139
+ | Page Title | Browser tab title |
140
+ | npm Packages | Comma-separated, e.g. `d3, three, @react-three/fiber` |
141
+ | Portal Auth | Enable portal user header extraction (see Multi-user) |
142
+ | 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. |
143
+ | Show WS status | Small "fromcubes • connected/disconnected" badge in the page's bottom-right corner (off by default) |
144
+ | Head HTML | Extra trusted-author `<head>` tags (CDN, fonts, CSS, scripts). Runs in the public portal page. |
145
+ | Code Editor | Monaco with JSX — must define `<App />` |
146
+
147
+ 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.
148
+
149
+ 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.
150
+
151
+ ```jsx
152
+ // fc-portal-utility, Module name = mathHelpers
153
+ const PI2 = Math.PI * 2;
154
+ function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
155
+ function useDebounce(value, ms = 300) {
156
+ const [v, setV] = React.useState(value);
157
+ React.useEffect(() => {
158
+ const t = setTimeout(() => setV(value), ms);
159
+ return () => clearTimeout(t);
160
+ }, [value, ms]);
161
+ return v;
162
+ }
163
+
164
+ // portal-react, JSX tab
165
+ function App() {
166
+ const { data } = useNodeRed();
167
+ const slow = useDebounce(data?.value);
168
+ return <div>{clamp(slow ?? 0, 0, 100)}</div>;
169
+ }
170
+ ```
171
+
172
+ ## Editor features
173
+
174
+ - **Monaco** with full JSX support and `useNodeRed()` type declarations
175
+ - **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.
176
+ - **JSX tag completion** — type tag name, Tab to expand
177
+ - **Self-close collapse** — type `/` inside empty `<tag></tag>` to convert to `<tag />`
178
+ - **Ctrl+D / Cmd+D** — duplicate the current line or selection (in every Monaco editor of the package)
179
+ - **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)
180
+ - **Component completion** — registry components + any PascalCase word
181
+ - **Utility-symbol completion** — top-level identifiers from any `fc-portal-utility` node, suggested in JS context
182
+ - **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
183
+ - **Portal Assets sidebar** — file manager for static assets (GLB, textures, fonts…)
184
+
185
+ ## Multi-user / Multi-tenancy
186
+
187
+ 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.
188
+
189
+ ### Identity
190
+
191
+ | Identifier | Scope | Persistence | Source |
192
+ |---|---|---|---|
193
+ | `portalClient` | Single WS session (one tab) | Lost on reconnect — server assigns a new UUID | Generated server-side on connect |
194
+ | `userId` / `login` | All sessions of a user | Survives reconnect and new tabs | `x-portal-user-*` headers (when **Portal Auth** is enabled) |
195
+
196
+ **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`:
197
+
198
+ | Header | Field |
199
+ |---|---|
200
+ | `x-portal-user-id` | `userId` — required; without it the session is anonymous |
201
+ | `x-portal-user-login` | `login` |
202
+ | `x-portal-user-display-name` | `displayName` |
203
+ | `x-portal-user-email` | `email` |
204
+ | `x-portal-user-role` | `role` (single value) |
205
+ | `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 |
206
+
207
+ 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.
208
+
209
+ **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.
210
+
211
+ 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.
212
+
213
+ ### Routing modes
214
+
215
+ 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`:
216
+
217
+ ```javascript
218
+ // BROADCAST — everyone connected to this endpoint
219
+ delete msg._client;
220
+ return msg;
221
+
222
+ // UNICAST — only the tab that sent the original msg (echo)
223
+ // Keep msg._client as-is; it already carries portalClient.
224
+ return msg;
225
+
226
+ // UNICAST — a specific tab whose ID you know
227
+ msg._client = { portalClient: "a1b2c3d4-..." };
228
+ return msg;
229
+
230
+ // USER-CAST — every tab of a specific user (even ones that just opened)
231
+ msg._client = { userId: "alice" };
232
+ return msg;
233
+
234
+ // AUTH-CAST — every session that arrived with x-portal-user-* identity;
235
+ // anonymous sessions (no proxy headers) are skipped
236
+ msg._client = { authenticated: true };
237
+ return msg;
238
+ ```
239
+
240
+ **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.
241
+
242
+ ### Authenticated-only delivery (fail-safe default)
243
+
244
+ 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:
245
+
246
+ ```javascript
247
+ msg._client = { authenticated: false }; // deliberate broadcast, anonymous included
248
+ return msg;
249
+ ```
250
+
251
+ 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:
252
+
253
+ ```jsx
254
+ const { data, authRequired } = useNodeRed();
255
+ if (authRequired) return <p className="p-4 text-zinc-500">Log in to see this data.</p>;
256
+ ```
257
+
258
+ **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.
259
+
260
+ **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.
261
+
262
+ ### Without a user (anonymous mode)
263
+
264
+ If **Portal Auth** is off (or no proxy headers arrive), everything still works:
265
+
266
+ - `broadcast` and `portalClient` unicast: unchanged
267
+ - `user`-cast: gracefully skipped (no `userId` to target)
268
+ - `auth`-cast: delivers to nobody (no session has identity headers)
269
+ - `useNodeRed().user` is `null`
270
+
271
+ Same model as dashboard 2 — no auth required to use the node.
272
+
273
+ ## Plugins: identity providers and CSS engines
274
+
275
+ Who the user is and how CSS is generated are both pluggable. The defaults,
276
+ `headers` (the contract above) and `tailwind`, are themselves plugins shipped
277
+ in this package and use exactly this API. A separate npm package can register its own, e.g. for a different
278
+ auth proxy or a different CSS tool:
279
+
280
+ ```js
281
+ // package.json: "node-red": { "plugins": { "my-providers": "plugin.js" } }
282
+ module.exports = function (RED) {
283
+ RED.plugins.registerPlugin("my-sso", {
284
+ type: "fromcubes-identity",
285
+ apiVersion: 1,
286
+ async resolveUser(req) { // HTTP request or WS upgrade request
287
+ const claims = await verify(req.headers.authorization);
288
+ return claims ? { userId: claims.sub, login: claims.preferred_username, groups: claims.groups } : null;
289
+ },
290
+ });
291
+ RED.plugins.registerPlugin("my-css", {
292
+ type: "fromcubes-css",
293
+ apiVersion: 1,
294
+ async generate(source) { // the portal's full source, components included
295
+ return { css: await compileMyCss(source) };
296
+ },
297
+ editor: { // optional: className="…" completion
298
+ classes() { return ["card", "btn", "btn-primary"]; },
299
+ },
300
+ });
301
+ };
302
+ ```
303
+
304
+ Installing a plugin does not activate it. Choose one per kind in the editor,
305
+ **Menu → Settings → Fromcubes**: every installed provider is listed with the
306
+ package it comes from, and exactly one is selected. The choice is saved in
307
+ `<userDir>/fromcubes/providers.json` and applies at once — a new CSS engine
308
+ rebuilds every portal; a new identity provider asks for confirmation, then
309
+ reloads every open portal tab so page and connection both use it.
310
+
311
+ An administrator can pin either choice in `settings.js`; the editor then shows
312
+ it locked:
313
+
314
+ ```js
315
+ portalReact: {
316
+ identityProvider: "my-sso", // default "headers"; "none" = everyone anonymous
317
+ cssEngine: "my-css", // default "tailwind"; "none" serves no stylesheet
318
+ },
319
+ ```
320
+
321
+ - `resolveUser` returns a user or `null`. Only the known fields
322
+ (`userId`, `login`, `displayName`, `email`, `role`, `groups`) are kept; a
323
+ throw, a rejection or 5 s without an answer means anonymous.
324
+ - `generate` returns `{ css }`. A failure shows the yellow `css-fail` status
325
+ and the page is served unstyled.
326
+ - `editor.classes()` (optional, may be async) feeds the class completion in
327
+ every code editor; it is asked once per installed plugin. Without it the
328
+ engine just gets no completion. Strings only, no whitespace, ≤ 200 chars,
329
+ at most 50 000.
330
+ - An unknown provider name is logged once; for identity every session is then
331
+ anonymous. The settings pane flags a selected provider whose package is gone.
332
+ - Optional `label: "…"` on the plugin is shown next to its id in the settings
333
+ pane.
334
+ - `require("@aaqu/fromcubes-react/api")` gives the type names and
335
+ `API_VERSION`, plus `createAdminSecurity(RED)` — the permission, CSRF and
336
+ rate-limit gates the core puts on its own admin routes — for a plugin that
337
+ adds routes. Declare `@aaqu/fromcubes-react` as a peer dependency. The
338
+ three add-ons above are complete examples.
339
+
340
+ ## Portal Assets
341
+
342
+ Static files (3D models, textures, fonts, etc.) can be uploaded and served from a public endpoint.
343
+
344
+ - Open the **Portal Assets** tab in the Node-RED sidebar
345
+ - Upload files via button or drag & drop
346
+ - Organize in folders (create, rename, move between folders)
347
+ - Copy public path with one click — use in JSX: `/fromcubes/public/models/scene.glb`
348
+ - Download and delete files from the context menu
349
+
350
+ All uploads require Node-RED admin authentication. Files are served publicly at `/fromcubes/public/`.
351
+
352
+ Limits: 100 MB per file, 500 MB total, 1000 files max.
353
+
354
+ ## Examples
355
+
356
+ Available via **Menu → Import → Examples → @aaqu/fromcubes-react**.
357
+ Each flow includes a comment node with what to expect and the URL to open.
358
+
359
+ Import **Shared Components** first — it provides the UI building blocks (Page, Header, Stat, Button, ValueBadge) referenced by the others.
360
+
361
+ | Example | npm packages | Description |
362
+ |---|---|---|
363
+ | **Shared Components** | — | Reusable components: Page, Header, Stat, Button, ValueBadge |
364
+ | **Sensor Portal** | — | Sensor gauge with live WebSocket data |
365
+ | **Live Chart** | `chart.js/auto` | Live updating Chart.js charts |
366
+ | **D3 Poland Map** | `d3` | Interactive SVG map of Poland (simulated data) |
367
+ | **Three.js Scene** | `three`, `@react-three/fiber`, `@react-three/drei` | 3D scene with Three.js |
368
+ | **PixiJS Sprites** | `pixi.js`, `@pixi/react` | Clickable bunny sprites with PixiJS |
369
+ | **WebGPU Shader** | `three` | WebGPU renderer + TSL animated shaders |
370
+ | **Utility Hooks** | — | `fc-portal-utility` demo: `useDebounce` custom hook + `clamp` helper |
371
+
372
+ ## Troubleshooting
373
+
374
+ | Symptom | Likely cause |
375
+ |---|---|
376
+ | Red status "transpile error" + error page on the endpoint | JSX syntax error — fix code, redeploy (cache invalidates automatically) |
377
+ | 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. |
378
+ | 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). |
379
+ | 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. |
380
+ | 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`). |
381
+ | `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. |
382
+ | 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. |
383
+ | 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. |
384
+ | `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. |
385
+ | 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) |
386
+ | `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 |
387
+ | Page reloads on every deploy | Expected for code changes; clients soft-reconnect with exponential backoff |
388
+
389
+ ## Architecture: source AND sink
390
+
391
+ The Node-RED guidelines suggest a node should "sit at the beginning, middle
392
+ or end of a flow — not all at once". `portal-react` intentionally violates
393
+ this: every node is both a **sink** (`msg → render`) and a **source** (UI
394
+ event → outbound `msg`). Trade-offs:
395
+
396
+ - **Pro**: one wire per portal is conceptually simpler — the same node owns
397
+ the page lifecycle, its WebSocket, and the routing rules.
398
+ - **Con**: flow-error tracking only follows the `input → output` path.
399
+ Errors originating in the browser (e.g. a runtime exception inside a UI
400
+ handler) cross the WebSocket as a source-event and are **not** routed to
401
+ a Catch node on the same tab — they surface as a red node status and a
402
+ log line. This matches the Node-RED guidance for source nodes: "produces
403
+ messages in response to external events".
404
+
405
+ If you need explicit Catch-node visibility for UI-originated errors, wire
406
+ the portal's output through a Switch / Catch chain — a UI event landing on
407
+ `msg.payload` is then a regular wire-level message that Catch can observe.
408
+
409
+ ## Security
410
+
411
+ This module is meant to run behind a trusted Node-RED instance, ideally
412
+ behind a reverse proxy. Highlights:
413
+
414
+ - Admin write endpoints require the `Node-RED-API-Version` header (CSRF
415
+ protection) and a `portal-react.write` permission when `adminAuth` is
416
+ configured.
417
+ - Token-bucket rate-limit (default 60 burst / 1 req/s steady) on every
418
+ write endpoint, tunable via `RED.settings.portalReact.rateLimit`.
419
+ - JSON bodies capped by Node-RED's own `apiMaxLength` (settings.js, default
420
+ 5 MB), 100 MB asset-upload cap, 500 MB / 1000 files total
421
+ assets quota, 1 MB WebSocket frame cap.
422
+ - `x-portal-user-*` identity headers are trusted unconditionally — production
423
+ deployments **must** terminate auth at a reverse proxy that strips all six
424
+ inbound identity headers before injecting verified ones, and Node-RED must
425
+ not be reachable except through that proxy.
426
+
427
+ ## Support
428
+
429
+ fromcubes is built and maintained by one person. If it saves you time,
430
+ consider sponsoring its development:
431
+
432
+ [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor_on-GitHub-ea4aaa?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/Aaqu)
433
+
434
+ ## License
435
+
436
+ Apache-2.0