@mmmbuto/nexuscrew 0.9.65 → 0.9.67
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/CHANGELOG.md +24 -0
- package/docs/NODES.md +55 -0
- package/docs/SECURITY.md +40 -0
- package/frontend/dist/assets/{index-DkYRC1vV.js → index-D__76yWR.js} +42 -42
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/audio/dispatch.js +14 -1
- package/lib/audio/routes.js +2 -2
- package/lib/cli/commands.js +31 -0
- package/lib/live-host/federation.js +10 -0
- package/lib/live-host/routes.js +12 -2
- package/lib/nodes/access-presets.js +27 -7
- package/lib/nodes/commands.js +63 -4
- package/lib/nodes/reverse-slot-listeners.js +4 -1
- package/lib/nodes/store.js +205 -12
- package/lib/notify/ask-cell.js +53 -0
- package/lib/notify/event-feed-client.js +40 -2
- package/lib/notify/event-feed-routes.js +8 -2
- package/lib/proxy/federation.js +308 -17
- package/lib/proxy/node-proxy.js +86 -3
- package/lib/server.js +126 -12
- package/lib/settings/routes.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NexusCrew are tracked here.
|
|
4
4
|
|
|
5
|
+
## 0.9.67 — 2026-10-10
|
|
6
|
+
|
|
7
|
+
- **A host-only peer is closed to every legacy door.** The audience gate of 0.9.66 covered the peer-facing protocol only; the older paths that re-enter the local API with the node's own authority did not look at it. They now do, and the owner's view and the trusted peers keep their behaviour.
|
|
8
|
+
- **No unauthenticated federated panel route towards, or through, a host-only peer.** The panel route that lets a browser frame load a remote panel without a header is no longer opened for a route that names a host-only peer at any hop, for a request that carries any Authorization header, or for a request that arrived on a reverse (peer-bound) listener; those go through the normal token check. A host-only peer has no panel and no Live host: the panel, its ticket and every Live-host resource towards it are refused, over HTTP and WebSocket.
|
|
9
|
+
- **Audience is checked on every hop of the request, not only on the last.** A peer outside the audience can no longer reach a host-only peer through an audience member, and the topology served to an audience member on behalf of an outsider does not list it; the topology answer to a peer is decided again after the fan-out wait, so a grant, an audience or a publication that ended meanwhile removes the leaf, and a reader that was not entitled to a host-only peer when it asked is not handed it because the publication ended during the wait (the same holds for the local topology, the Live host and the audio dispatcher).
|
|
10
|
+
- **What a host-only peer answers cannot act on the origin of the app.** HTTP answers (the routes and the legacy `/node/<name>` proxy) lose their cookies, `Clear-Site-Data`, CSP and the headers that redirect (`Location`, `Content-Location`, `Refresh`), and are served with `Content-Security-Policy: sandbox; default-src 'none'` and `nosniff`; any 3xx is refused with a 502, so a browser can no longer repeat the owner's call somewhere else with the owner's Authorization, and the app itself no longer follows redirects on a call that goes through a peer (remote API calls from the web app, `/api/route/*` and `/node/*`, to any peer, trusted or not): no API of this server redirects, so nothing changes in normal operation, while a peer that answers with a redirect gets an error instead of having the owner's Authorization replayed. WebSocket upgrade answers (the federated route and `/node/<name>/ws`) are passed along only when the first status is a `101`; the head is read the way the most lenient parser would read it (bare line feeds, folded lines, odd header names), cookies, `Clear-Site-Data` and CSP are dropped from it, anything malformed, interim or not a `101` is refused, and the frames that follow are untouched. No CSP or `nosniff` is added to a WebSocket head (a `101` has no body). A frame whose route passes through a host-only peer is also sandboxed in an opaque origin (second lock: its panel is refused anyway); the frames of trusted remote cells are unchanged.
|
|
11
|
+
- **Open streams towards a host-only peer end on revocation.** Server-sent events, WebSockets and held (detached) terminal attaches are re-checked about once a second against the publication they were opened under (the mode still on, the same audience and generation), the operator grant, the credentials and identity of both peers and the store; a stream whose authorization changed in any of these ways is closed, and a held attach cannot be resumed. Streams already open through a peer when its publication is turned on are closed too (the event streams and terminals that were opened under plain sharing).
|
|
12
|
+
- **A federated `/audio/speak` or `/audio/stop` that terminates here is not re-sent to a host-only peer.** The target actually resolved (instance id and every hop of the route) is checked just before the request leaves. Audience members keep their direct route.
|
|
13
|
+
- **Live host never designates, queries or bridges to a host-only peer.** A designation recorded before the mode was turned on is unusable and is not shown to a reader outside the audience.
|
|
14
|
+
- **Topology, configuration, diagnostics and decks do not name a host-only peer to a federated reader outside its audience.** The reader is identified only by the verified hop proof (HMAC over method, path and chain); a header that does not verify hides the peer. The decision is taken on the final result, after any wait, and covers cached and stale entries, whole diagnostic records and the tiles and windows of a deck that point at the peer; a reader that cannot see those references cannot change or delete the deck either, so it cannot save back the filtered copy it was served.
|
|
15
|
+
- **Two opt-ins in the same second no longer collide.** The protected backup taken before each opt-in gets a distinct name when another backup of the same bytes exists, instead of failing the command.
|
|
16
|
+
- **Behaviours worth knowing.** An upgrade answer from a host-only peer that is refused shows up as a WebSocket closed with code 4402 on the federated route and as a 502 on `/node/<name>/ws`; a 304 from a host-only peer is answered with a 502 like any 3xx; a refused change to a deck that holds references the reader cannot see says only that the deck cannot be changed from this node; the `Link` header of a host-only peer is not stripped.
|
|
17
|
+
- **The 0.9.66 note about older owners is corrected.** An ASK that arrived with an origin cell attested on this node stays dismissible even with an older owner; any other card cannot be dismissed locally and answers `cell-unresolved`.
|
|
18
|
+
|
|
19
|
+
## 0.9.66 — 2026-10-10
|
|
20
|
+
|
|
21
|
+
- **An imported ASK from a cell with a custom tmux session can be dismissed on this node.** The local dismissal looked the cell up by decoding the session name, which only works for `cloud-*` names, so an ASK from a session such as `lab-Dev` answered 404 "unknown or invisible ask" although the node holds the grants and the owner publishes the ASK. The owner now publishes, with each ASK of its snapshot, the cell it resolved through its own definitions; this node keeps it (from the snapshot, from the cell-scoped live envelope, or from the attested origin of a pushed ASK), validates it at the authenticated ingress, binds it to the owner and to the generation of the ASK, and uses it before the name. A cell is never guessed from a name, never read from this node's own definitions and never accepted from the request. A card persisted by an older release with no authenticated cell is refused with `cell-unresolved` instead of a generic 404, and an answer in flight or of uncertain delivery still blocks the dismissal. Non-canonical names resolve when the owner runs this release. With an older owner, canonical names decode as before, and an ASK that arrived with an origin cell attested on this node (a legacy card, even with the owner offline) stays dismissible too; any other card cannot be dismissed locally (the node answers `cell-unresolved`).
|
|
22
|
+
- **A remote peer can be published one way to your own admin nodes.** `nexuscrew nodes publish-remote <peer> on --audience <instanceId,...>` (and `off`, `status`) makes a peer you reach over your private `-L` tunnel visible and operable to an explicit audience of your operator nodes, while the peer reaches nothing of yours: no peer, no route, no topology, no protocol endpoint but the health probe. It is separate from Share and never restarts SSH, opens a reverse channel or touches the reverse pool.
|
|
23
|
+
- **Turning it on zeroes every grant towards the peer in the same save, and widening them is refused while it is on.** Cell scope none; events, node events, files, ASK replies, operator, panel and live host off; `eventsReceive` off. `off` ends the publication and leaves the grants at zero.
|
|
24
|
+
- **The topology announces the peer as a leaf, only to the audience.** What the peer reports about its own children is never requested, relayed or cached, and entries cached before the mode was turned on are purged.
|
|
25
|
+
- **The store moves to schema 4 only at the opt-in, after a full protected backup.** Older stores keep loading unchanged; a release before 0.9.66 cannot read a schema 4 store, so going back means restoring the backup taken at the opt-in.
|
|
26
|
+
- **`nodes inspect` shows the real grants.** It read the peer from a redacted view that carried no access fields, so it printed every grant as off and the peer as unconfigured while `nodes access` and the authorization code saw them granted. The redacted record now carries the grants and the configured marker.
|
|
27
|
+
- **The access views show the grants the resource gate applies, also for records from before the grants were configured.** `nodes inspect`, `nodes access` and the node lists of Settings printed an all-denied vector for any record without the `accessConfigured` marker, while the gate reads the stored values and ignores the marker: a legacy record with `eventsAccess` and no marker read "unconfigured, off" and was granted. The views now show the real vector (seven flags and the cell scope) and keep the label and the configured flag as provenance; `nodes inspect` adds a note for a record that is not configured. What is granted does not change, and the event stream still requires a configured vector.
|
|
28
|
+
|
|
5
29
|
## 0.9.65 — 2026-10-09
|
|
6
30
|
|
|
7
31
|
- **Notifications from a remote node can be cleared locally, one voice or all at once.** Every imported notice offers the same close action asks already have: one request dismisses a single notice or the whole visible set of an owner. The dismissal travels to the owner over the authorized routes and is judged by the origin's view; the owner records it and publishes one `notify-closed` closure per cleared notice, so every receiver empties, including the ones that were not part of the clear. When the owner cannot take it — unreachable, or without the notices surface — the request still succeeds: the node records the intent durably, as pending with a bounded retry or as blocked until the pairing changes, and the record, not the browser, is what keeps the card down. A reload or a restart of the receiving node does not bring a cleared notice back, and once the owner takes the dismissed intent he stops serving those notices himself. "Clear" means the whole visible set of that owner on this node: every notice currently shown for him, cleared in one request inside the owner's per-request budget.
|
package/docs/NODES.md
CHANGED
|
@@ -85,6 +85,60 @@ Temporary loss of reachability does not revoke consent. An authorized node
|
|
|
85
85
|
remains visible as stale/offline until an authoritative refresh restores it or
|
|
86
86
|
confirms withdrawal.
|
|
87
87
|
|
|
88
|
+
## Publish a remote node, one way (host-only)
|
|
89
|
+
|
|
90
|
+
Share publishes **this** installation on a peer's hub. The opposite need is
|
|
91
|
+
real too: a remote node you reach over your own private `-L` tunnel (a sandboxed
|
|
92
|
+
box, say) that your own admin nodes must be able to see and operate, while the
|
|
93
|
+
remote node must see nothing of yours. That is the **host-only publication**:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
nexuscrew nodes publish-remote <name|nodeId> on --audience <instanceId>,<instanceId>
|
|
97
|
+
nexuscrew nodes publish-remote <name|nodeId> off
|
|
98
|
+
nexuscrew nodes publish-remote <name|nodeId> status [--json]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- It is **not Share**. It never calls the Share path, restarts SSH, opens a
|
|
102
|
+
reverse (`-R`) channel or touches the reverse pool: the tunnel stays the
|
|
103
|
+
private `-L` it was.
|
|
104
|
+
- The **audience** is an explicit list of `instanceId`s of your own direct peers
|
|
105
|
+
that already hold the operator grant. Unknown ids, ids of peers without that
|
|
106
|
+
grant and the published peer itself are refused. There is no wildcard.
|
|
107
|
+
- The boundary is one-way and is decided here, on the installation that owns
|
|
108
|
+
the tunnel. An audience member (operator grant re-read on every call) reaches
|
|
109
|
+
the peer; **the peer reaches nothing** — no other peer, no route, no topology,
|
|
110
|
+
nothing of this installation — whatever grants its record carries or gains.
|
|
111
|
+
Everyone else, including your other admin nodes, does not see it at all.
|
|
112
|
+
- The topology announces the peer as a **leaf**, only to the audience: what the
|
|
113
|
+
peer says about its own children is never requested or relayed.
|
|
114
|
+
- Turning it on zeroes, in the same single save, every grant towards the peer
|
|
115
|
+
(cell scope none; events, node events, files, ASK replies, operator, panel and
|
|
116
|
+
live host off; `eventsReceive` off), and any later edit that would widen them
|
|
117
|
+
is refused while the mode is on. `off` ends the publication at once and leaves
|
|
118
|
+
the grants at zero: reopening anything is an explicit act.
|
|
119
|
+
- Turning it on moves the store to **schema 4**, and only then, after taking a
|
|
120
|
+
full protected backup next to `nodes.json`
|
|
121
|
+
(`nodes.json.pre-publish-remote-<time>-<sha>.bak`, mode 0600). Older stores
|
|
122
|
+
keep loading unchanged. A release older than 0.9.66 cannot read a schema 4
|
|
123
|
+
store (the schema is closed): going back means restoring that backup.
|
|
124
|
+
- Streams opened towards the peer by a reader (event streams, terminals, held
|
|
125
|
+
terminal attaches) are re-checked about once a second against the audience, the
|
|
126
|
+
operator grant, the publication state, the identity of both nodes and the store:
|
|
127
|
+
after `off`, an audience change or a revoked grant they are closed within a couple
|
|
128
|
+
of seconds, and a held attach cannot be resumed. Read it as a short, bounded
|
|
129
|
+
delay, not as an instant cut. (Before 0.9.67 they stayed open.)
|
|
130
|
+
- The peer has **no panel and no Live host**: the panel, its ticket and every
|
|
131
|
+
Live-host resource towards it are refused, and a Live designation recorded
|
|
132
|
+
before the mode was turned on stops being used. What the peer answers is served
|
|
133
|
+
without its cookies, `Clear-Site-Data` and CSP, sandboxed and with `nosniff`.
|
|
134
|
+
A federated audio command that terminates here is not re-sent to it, and
|
|
135
|
+
topology, configuration and diagnostics served to a federated reader outside
|
|
136
|
+
the audience do not name it. The audience is checked on every hop of a request,
|
|
137
|
+
not only on the last one.
|
|
138
|
+
- A reader's topology cache is emptied by the **next authoritative answer** from
|
|
139
|
+
this installation. A reader that cannot reach it keeps showing the peer, marked
|
|
140
|
+
stale, until then; nothing is pushed to it.
|
|
141
|
+
|
|
88
142
|
## Which cells a node may see
|
|
89
143
|
|
|
90
144
|
Sharing decides whether a node is reachable. A **cell scope** decides how much
|
|
@@ -200,6 +254,7 @@ nexuscrew nodes test # every peer, plus what is only claimed
|
|
|
200
254
|
nexuscrew nodes up|down|connect|disconnect <name|nodeId>
|
|
201
255
|
nexuscrew nodes restart|reconnect <name|nodeId>
|
|
202
256
|
nexuscrew nodes share <name|nodeId> on|off
|
|
257
|
+
nexuscrew nodes publish-remote <name|nodeId> on --audience <id,...>|off|status
|
|
203
258
|
nexuscrew nodes cells <name|nodeId> all|none|Cell1,Cell2
|
|
204
259
|
nexuscrew nodes remove <name|nodeId> --yes
|
|
205
260
|
```
|
package/docs/SECURITY.md
CHANGED
|
@@ -107,6 +107,46 @@ Federated ASK creation and closure go only to configured `admin` peers, hop by
|
|
|
107
107
|
hop. Each hub checks its own next peer before forwarding. This rule does not
|
|
108
108
|
change generic notifications or the relay of ASK replies.
|
|
109
109
|
|
|
110
|
+
### Host-only publication
|
|
111
|
+
|
|
112
|
+
`nexuscrew nodes publish-remote <peer> on --audience <ids>` publishes a remote
|
|
113
|
+
peer to an explicit audience of your own operator nodes and nothing else (see
|
|
114
|
+
[Connect nodes](NODES.md)). The security properties, each covered by a test:
|
|
115
|
+
|
|
116
|
+
- **One way.** The check is on the authenticated peer record found by its token,
|
|
117
|
+
never on a header, a query or the visited chain. Audience → peer is allowed;
|
|
118
|
+
peer → anything is refused in the route relay, in the WebSocket upgrade, in
|
|
119
|
+
the topology collector and on every protocol endpoint except the health
|
|
120
|
+
probe.
|
|
121
|
+
- **Two locks on the peer's own access.** All grants towards it are zeroed in
|
|
122
|
+
the same save that turns the mode on, and the class gate refuses a host-only
|
|
123
|
+
peer regardless of the grants its record shows. The `user` preset with no
|
|
124
|
+
visible cell does **not** give this: it keeps events, node events and file
|
|
125
|
+
reads granted. Do not use it as an isolation boundary.
|
|
126
|
+
- **Re-read, not remembered.** Audience membership and the operator grant are
|
|
127
|
+
read from the store on every call; revoking either, or turning the mode off,
|
|
128
|
+
takes effect on the next request.
|
|
129
|
+
- **No Share, no reverse channel.** The mode cannot coexist with Share on the
|
|
130
|
+
same peer, and enabling Share on a published peer is refused before any SSH
|
|
131
|
+
restart is attempted.
|
|
132
|
+
- **Not covered:** a stream opened before `off` stays open until it closes (the
|
|
133
|
+
next upgrade is refused), a reader that cannot reach this installation keeps
|
|
134
|
+
its cached view of the peer until its next authoritative answer, and an
|
|
135
|
+
audience member is itself trusted to forward further according to its own
|
|
136
|
+
access rules.
|
|
137
|
+
|
|
138
|
+
### What the access views show
|
|
139
|
+
|
|
140
|
+
`nodes inspect`, `nodes access` and the node lists of Settings show, for each
|
|
141
|
+
peer, the grants **the resource gate applies** — the stored values, read the way
|
|
142
|
+
the gate reads them — together with a label and a *configured* flag. The label
|
|
143
|
+
and the flag say how the vector was set (a preset, or a record from before the
|
|
144
|
+
grants existed that was never configured); they do not change what is granted.
|
|
145
|
+
A record that is not configured but carries a grant is shown with that grant on
|
|
146
|
+
and the label `unconfigured`. The event stream is stricter than the resource
|
|
147
|
+
gate: it also requires a configured vector. Apply a preset to make a legacy
|
|
148
|
+
record explicit.
|
|
149
|
+
|
|
110
150
|
### Cell scope
|
|
111
151
|
|
|
112
152
|
A per-node cell scope narrows what one paired node sees and can act on. It is
|