@mmmbuto/nexuscrew 0.9.64 → 0.9.66

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 CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to NexusCrew are tracked here.
4
4
 
5
+ ## 0.9.66 — 2026-10-10
6
+
7
+ - **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 only when the owner runs this release; with an older owner only canonical names decode.
8
+ - **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.
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+ - **`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.
13
+ - **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.
14
+
15
+ ## 0.9.65 — 2026-10-09
16
+
17
+ - **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.
18
+ - **A dismissal queue that cannot persist says so.** The queue still honours a cleared notice in memory when the disk refuses the write, but the operator's answer no longer looks like the durable success: queued answers (the single dismiss and the pending/blocked outcomes of clear-all) carry `durable: false` and a `degraded` reason (`queue-write-failed`, `queue-oversize`, `queue-read-failed`, `queue-schema`) when the queue cannot be written or read. The loss is real and now visible: after a restart the cleared card can come back, and the answer warns about it before that happens. A healthy write clears the flag again, and the answer returns to its normal shape.
19
+ - **A queue file that cannot be read is never overwritten with a partial list.** When the queue file is unreadable, foreign or oversize at the first read, the node retries the read before any write, merges what the disk holds with the dismissals accepted in the meantime, and replays them in order on top of the disk. The queue keeps its bounds (200 records, 64 KiB, 15 minutes): when it is full the oldest records leave, never the dismissal just accepted. While the disk stays unreadable the answers keep declaring `durable: false` and the reason.
20
+ - **A missed closure heals at the next read.** A browser that misses a notice's live closure frame kept the card on screen even after a fresh, complete feed-state read no longer listed the notice. For every owner whose view is fresh and authoritative, the notice panel now reconciles against that read: what the owner no longer serves goes, and a card that arrived while the read was in flight stays. Stale, incomplete or resync-flagged views keep their cards as before.
21
+ - **An owner with a full window is no longer refused as incomplete.** A snapshot page cut exactly at its cap (50 notifications, 100 asks) was marked as incomplete on every full page, so a receiving node refused the snapshot of an owner with a full window, imported nothing and never opened the stream. The marker now means that the source had more entries than the page holds; a page exactly at the cap is whole and accepted, and a page over the cap or flagged for resync is still refused.
22
+
5
23
  ## 0.9.64 — 2026-10-07
6
24
 
7
25
  - **The active quick list hides stopped cells.** The active mode keeps only live cells and active degraded rows, regardless of each node's saved all or pinned preference; ordering and pins remain intact.
package/docs/NODES.md CHANGED
@@ -85,6 +85,49 @@ 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
+ - Already-open streams (a terminal attached before `off`) are not torn down by
125
+ `off`; new calls are refused. Read `off` as "no new request", not as an
126
+ instant cut of a channel that is already open.
127
+ - A reader's topology cache is emptied by the **next authoritative answer** from
128
+ this installation. A reader that cannot reach it keeps showing the peer, marked
129
+ stale, until then; nothing is pushed to it.
130
+
88
131
  ## Which cells a node may see
89
132
 
90
133
  Sharing decides whether a node is reachable. A **cell scope** decides how much
@@ -200,6 +243,7 @@ nexuscrew nodes test # every peer, plus what is only claimed
200
243
  nexuscrew nodes up|down|connect|disconnect <name|nodeId>
201
244
  nexuscrew nodes restart|reconnect <name|nodeId>
202
245
  nexuscrew nodes share <name|nodeId> on|off
246
+ nexuscrew nodes publish-remote <name|nodeId> on --audience <id,...>|off|status
203
247
  nexuscrew nodes cells <name|nodeId> all|none|Cell1,Cell2
204
248
  nexuscrew nodes remove <name|nodeId> --yes
205
249
  ```
@@ -119,6 +119,40 @@ browser, keeps the app fully usable, and never changes the other side's push
119
119
  setup. Turning it back on requires the same button, so a device that was asked
120
120
  to stay silent does not re-subscribe itself.
121
121
 
122
+ ## Clearing imported notifications
123
+
124
+ An imported notice can be closed like a local one, for a single notice or for
125
+ the whole visible set of one owner with a single request. The closure is not a
126
+ browser-side hide; the protocol keeps every view of that owner honest:
127
+
128
+ - **The owner records it.** The dismissal travels the authorized routes to the
129
+ owner, is judged by the origin's view, and is answered by one `notify-closed`
130
+ closure frame per cleared notice. Closures enter the owner's feed history, so
131
+ a receiver that was offline recovers them from the backlog; the receiving
132
+ side never turns a closure into a card.
133
+ - **Every receiver empties.** Once the owner has recorded the dismissal he
134
+ stops serving those notices, so no node that holds a view of him keeps
135
+ showing them, whether it was live during the clear or reloads afterwards.
136
+ - **The intent survives when the owner cannot take it.** If the owner is
137
+ unreachable the clear still succeeds: the node records the intent durably
138
+ (a bounded local queue, retries with backoff, one delivery attempt per owner
139
+ at a time) and hands it over when the owner is back. If the owner has no
140
+ notices surface at all the record stays blocked until the pairing changes.
141
+ In both cases the record — not the browser — is what keeps the card down: a
142
+ reload or a restart of the node does not bring it back. If the queue cannot
143
+ write (disk refusal), the intent is still honoured in memory but it cannot
144
+ survive a restart: the answer declares it with `durable: false` and the
145
+ degradation reason, instead of looking like the durable success.
146
+ - **A browser that misses a closure heals at the next read.** The closure
147
+ frame travels live; a browser that never received it (offline, tab asleep)
148
+ catches up at the next fresh feed-state read, which reconciles the panel
149
+ with what the owner still serves. Views that are stale, incomplete or
150
+ flagged for resync never reconcile, so a degraded feed cannot silently
151
+ empty the panel.
152
+ - **"Clear" is the whole visible set of that owner.** It closes every notice
153
+ currently shown for him on this node, in one request inside the owner's
154
+ per-request budget — never one request per card.
155
+
122
156
  ## Platform notes
123
157
 
124
158
  | Platform | Expected behavior |
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