@mmmbuto/nexuscrew 0.9.28 → 0.9.30

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 (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/docs/LIVE_PROMPT.md +1 -1
  3. package/docs/NOTIFICATIONS.md +37 -0
  4. package/docs/OPERATIONS.md +20 -0
  5. package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
  6. package/frontend/dist/assets/index-DqCfZFGi.css +32 -0
  7. package/frontend/dist/index.html +2 -2
  8. package/frontend/dist/sw.js +91 -10
  9. package/frontend/dist/version.json +1 -1
  10. package/lib/audio/groups.js +1 -1
  11. package/lib/auth/token.js +4 -4
  12. package/lib/cli/commands.js +70 -15
  13. package/lib/cli/doctor.js +5 -5
  14. package/lib/cli/fleet-service.js +7 -7
  15. package/lib/cli/init.js +55 -31
  16. package/lib/cli/pidfile.js +1 -1
  17. package/lib/cli/service.js +124 -8
  18. package/lib/cli/stable-alias.js +4 -4
  19. package/lib/cli/url.js +1 -1
  20. package/lib/config.js +36 -6
  21. package/lib/files/routes.js +1 -1
  22. package/lib/fleet/boot.js +2 -2
  23. package/lib/fleet/builtin.js +15 -15
  24. package/lib/fleet/causes.js +1 -1
  25. package/lib/fleet/cell-exec.js +23 -23
  26. package/lib/fleet/cell-hold.js +6 -6
  27. package/lib/fleet/cell-lease-server.js +106 -106
  28. package/lib/fleet/cell-lease.js +21 -21
  29. package/lib/fleet/definitions.js +10 -10
  30. package/lib/fleet/identity-authority.js +1 -1
  31. package/lib/fleet/launch-broker.js +13 -13
  32. package/lib/fleet/launch.js +15 -15
  33. package/lib/fleet/lease-client.js +23 -23
  34. package/lib/fleet/lease-routes.js +3 -3
  35. package/lib/fleet/lease-verifier.js +22 -22
  36. package/lib/fleet/managed.js +35 -35
  37. package/lib/fleet/prompt-delivery.js +13 -13
  38. package/lib/fleet/routes.js +15 -10
  39. package/lib/fleet/runtime.js +24 -24
  40. package/lib/live-host/bridge.js +53 -43
  41. package/lib/live-host/federation.js +97 -0
  42. package/lib/live-host/routes.js +166 -8
  43. package/lib/live-host/store.js +44 -6
  44. package/lib/mcp/server.js +14 -14
  45. package/lib/mcp/tools.js +5 -5
  46. package/lib/nodes/access-presets.js +258 -0
  47. package/lib/nodes/commands.js +198 -4
  48. package/lib/nodes/store.js +152 -9
  49. package/lib/nodes/tunnel.js +7 -7
  50. package/lib/notify/ask-answer-service.js +184 -0
  51. package/lib/notify/ask-receipts.js +197 -0
  52. package/lib/notify/ask-relay.js +152 -0
  53. package/lib/notify/asks.js +28 -3
  54. package/lib/notify/event-feed-acl.js +95 -0
  55. package/lib/notify/event-feed-asks-routes.js +144 -0
  56. package/lib/notify/event-feed-client.js +434 -0
  57. package/lib/notify/event-feed-history.js +115 -0
  58. package/lib/notify/event-feed-producers.js +175 -0
  59. package/lib/notify/event-feed-routes.js +254 -0
  60. package/lib/notify/event-feed.js +328 -0
  61. package/lib/notify/events.js +1 -1
  62. package/lib/notify/notifier.js +3 -3
  63. package/lib/notify/push-relay.js +240 -0
  64. package/lib/notify/push.js +8 -8
  65. package/lib/notify/routes.js +62 -23
  66. package/lib/proxy/federation.js +132 -12
  67. package/lib/proxy/node-proxy.js +13 -13
  68. package/lib/proxy/panel-auth.js +4 -4
  69. package/lib/proxy/panel-proxy.js +2 -2
  70. package/lib/proxy/resource-acl.js +145 -0
  71. package/lib/server.js +396 -40
  72. package/lib/settings/pairing-coordinator.js +1 -1
  73. package/lib/settings/routes.js +45 -32
  74. package/lib/update/manager.js +1 -1
  75. package/lib/update/runner.js +1 -1
  76. package/lib/voice/transcribe.js +1 -1
  77. package/lib/ws/bridge.js +3 -3
  78. package/package.json +1 -1
  79. package/skills/live/SKILL.md +12 -0
  80. package/frontend/dist/assets/index-CWra6B79.js +0 -167
  81. package/frontend/dist/assets/index-D6CIfCjv.css +0 -32
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ // Classification of the resources reachable through the federation proxy.
4
+ //
5
+ // Every federated resource belongs to exactly one class, and each class demands
6
+ // explicit grants from the node that OWNS the resource. The class is decided on
7
+ // the resource and the method, BEFORE the request is dispatched: a route that is
8
+ // not classified is denied, so a new endpoint cannot become reachable by a
9
+ // limited peer just because nobody classified it. The gate reads grants — never
10
+ // a role string — and an owner-side request (no peer ingress) is unaffected.
11
+
12
+ const CLASSES = Object.freeze({
13
+ // Events of cells and of the node itself.
14
+ 'event-feed': Object.freeze({ grants: ['eventsAccess'] }),
15
+ 'node-event-feed': Object.freeze({ grants: ['eventsAccess', 'nodeEventsAccess'] }),
16
+ // Answering, dismissing or polling a single ask. Seeing the card does not
17
+ // grant the action.
18
+ 'ask-action': Object.freeze({ grants: ['eventsAccess', 'askReplyAccess'] }),
19
+ // Inventories: which cells, which sessions, the status of the fleet. Only the
20
+ // authorized inventory is projected, and a peer with no visible cell sees none
21
+ // of it.
22
+ inventory: Object.freeze({ grants: [], needsVisibility: true }),
23
+ // Reading shared files: list and download, filtered by the owning cell.
24
+ 'files-read': Object.freeze({ grants: ['filesReadAccess'] }),
25
+ // Choosing which cell of this node hosts the Live session.
26
+ 'live-host': Object.freeze({ grants: ['liveHostAccess'] }),
27
+ // Opening a panel: a browser with already-authenticated sessions behind it.
28
+ panel: Object.freeze({ grants: ['panelAccess', 'peerOperatorAccess'], needsVisibility: true }),
29
+ // Everything else that mutates: configuration, filesystem browsing,
30
+ // diagnostics, credentials, fleet topology, decks, terminals, uploads and
31
+ // deletions. Operator-level by definition.
32
+ operator: Object.freeze({ grants: ['peerOperatorAccess'] }),
33
+ });
34
+
35
+ const CLASS_NAMES = Object.freeze(Object.keys(CLASSES));
36
+
37
+ const CELL_ID = '[A-Za-z0-9._-]{1,32}';
38
+ const NODE_ID = '[a-f0-9]{32}';
39
+
40
+ // Resource -> class. Keep this table aligned with the federation allowlist: the
41
+ // coverage test reads both and fails when one has a resource the other ignores.
42
+ function classifyResource(resource, method = 'GET') {
43
+ if (typeof resource !== 'string' || resource === '') return null;
44
+ const m = String(method || 'GET').toUpperCase();
45
+
46
+ if (resource === '/event-feed' || resource === '/event-feed/snapshot') {
47
+ return m === 'GET' ? 'event-feed' : null;
48
+ }
49
+ if (new RegExp(`^/event-feed/${CELL_ID}$`).test(resource)) return m === 'GET' ? 'event-feed' : null;
50
+ if (new RegExp(`^/event-feed/node/${NODE_ID}$`).test(resource)) return m === 'GET' ? 'node-event-feed' : null;
51
+ if (new RegExp(`^/asks/${CELL_ID}/(answer|dismiss|status)$`).test(resource)) {
52
+ return m === 'POST' || m === 'GET' ? 'ask-action' : null;
53
+ }
54
+ // Federated ask surface: the ask id is the 8-hex local id, the
55
+ // attempt id a UUID. The routes live UNDER /event-feed because they are the
56
+ // feed's own write-back path; the class is the same ask-action.
57
+ if (/^\/event-feed\/asks\/[a-f0-9]{8}\/answer$/.test(resource)) return m === 'POST' ? 'ask-action' : null;
58
+ if (/^\/event-feed\/asks\/[a-f0-9]{8}$/.test(resource)) return m === 'DELETE' ? 'ask-action' : null;
59
+ if (/^\/event-feed\/asks\/[a-f0-9]{8}\/requests\/[0-9a-f-]{16,64}$/.test(resource)) return m === 'GET' ? 'ask-action' : null;
60
+
61
+ if (resource === '/cells') return m === 'GET' ? 'inventory' : null;
62
+ if (resource === '/sessions') return m === 'GET' ? 'inventory' : (m === 'POST' ? 'operator' : null);
63
+ if (/^\/sessions\/[\w.@%:+-]{1,128}$/.test(resource)) return m === 'DELETE' ? 'operator' : null;
64
+ if (/^\/sessions\/[\w.@%:+-]{1,128}\/visibility$/.test(resource)) return m === 'PATCH' ? 'operator' : null;
65
+ if (resource === '/vl-nodes') return m === 'GET' ? 'inventory' : null;
66
+ if (new RegExp(`^/vl-nodes/${NODE_ID}/events$`).test(resource)) return m === 'GET' ? 'inventory' : null;
67
+
68
+ if (resource === '/files') return m === 'GET' ? 'files-read' : (m === 'DELETE' ? 'operator' : null);
69
+ if (resource === '/files/download') return m === 'GET' ? 'files-read' : null;
70
+ if (resource === '/files/upload') return m === 'POST' ? 'operator' : null;
71
+
72
+ if (resource === '/live-host') return m === 'GET' ? 'live-host' : null;
73
+ if (resource === '/live-host/designate' || resource === '/live-host/clear' || resource === '/live-host/bridge') {
74
+ return m === 'POST' ? 'live-host' : null;
75
+ }
76
+ if (new RegExp(`^/panel/${CELL_ID}(?:/.*)?$`).test(resource)) {
77
+ return m === 'GET' || m === 'POST' ? 'panel' : null;
78
+ }
79
+
80
+ // Operator-level mutations and diagnostics, one by one.
81
+ if (resource === '/config') return m === 'GET' ? 'operator' : null;
82
+ if (resource === '/fs/dirs') return m === 'GET' ? 'operator' : null;
83
+ if (resource === '/cells/send') return m === 'POST' ? 'operator' : null;
84
+ if (resource === '/vl-nodes/invite') return m === 'POST' ? 'operator' : null;
85
+ if (new RegExp(`^/vl-nodes/${NODE_ID}/commands$`).test(resource)) return m === 'POST' ? 'operator' : null;
86
+ if (new RegExp(`^/vl-nodes/${NODE_ID}$`).test(resource)) return m === 'DELETE' ? 'operator' : null;
87
+ // Fleet: the allowlist splits the family in two — the read alternatives are
88
+ // GET, the rest are POST. A peer that is not an operator is refused by the
89
+ // class gate; an operator no longer gets "resource-not-classified".
90
+ if (/^\/fleet\/(status|schema|definitions|credentials\/status)$/.test(resource)) {
91
+ return m === 'GET' ? 'operator' : null;
92
+ }
93
+ if (/^\/fleet\/(credentials\/(?:set|remove)|up|down|restart|engine|boot|define-engine|edit-engine|remove-engine|define-model|remove-model|model-test|define-cell|edit-cell|remove-cell|restore-cells|restore-engines)$/.test(resource)) {
94
+ return m === 'POST' ? 'operator' : null;
95
+ }
96
+ if (resource === '/decks') return m === 'GET' || m === 'POST' ? 'operator' : null;
97
+ if (/^\/decks\/[a-z0-9-]{1,32}$/.test(resource)) {
98
+ return ['PUT', 'PATCH', 'DELETE'].includes(m) ? 'operator' : null;
99
+ }
100
+
101
+ // Topology, diagnostics, terminals and the notify/audio surface of this node:
102
+ // all of them act ON the owner's node, so they belong to the operator class.
103
+ if (resource === '/topology') return m === 'GET' ? 'operator' : null;
104
+ // Diagnostics: the allowlist accepts GET on status and logs, DELETE on logs
105
+ // and PATCH on verbose — the class follows those methods, not a wider one.
106
+ if (resource === '/diagnostics/status') return m === 'GET' ? 'operator' : null;
107
+ if (resource === '/diagnostics/logs') return m === 'GET' || m === 'DELETE' ? 'operator' : null;
108
+ if (resource === '/diagnostics/verbose') return m === 'PATCH' ? 'operator' : null;
109
+ if (resource === '/ws') return m === 'GET' ? 'operator' : null;
110
+ if (resource === '/notify') return m === 'POST' ? 'operator' : null;
111
+ if (resource === '/audio/capability') return m === 'GET' ? 'operator' : null;
112
+ if (resource === '/audio/speak') return m === 'POST' ? 'operator' : null;
113
+ if (resource === '/audio/speak/status') return m === 'POST' ? 'operator' : null;
114
+ if (resource === '/audio/stop') return m === 'POST' ? 'operator' : null;
115
+
116
+ // Not classified: denied until it is classified on purpose.
117
+ return null;
118
+ }
119
+
120
+ // True when the grants of the peer satisfy the class. `ingress` null is the
121
+ // owner of this node, who is not limited by these grants.
122
+ function allowedByClass(className, ingress) {
123
+ if (!ingress) return true;
124
+ const cls = CLASSES[className];
125
+ if (!cls) return false; // unknown class: never a reason to allow
126
+ for (const grant of cls.grants) {
127
+ if (ingress[grant] !== true) return false;
128
+ }
129
+ if (cls.needsVisibility === true && ingress.cellVisibility === 'none') return false;
130
+ return true;
131
+ }
132
+
133
+ // The grants a class demands, for diagnostics and for the coverage test.
134
+ function grantsForClass(className) {
135
+ const cls = CLASSES[className];
136
+ return cls ? [...cls.grants] : null;
137
+ }
138
+
139
+ module.exports = {
140
+ CLASSES,
141
+ CLASS_NAMES,
142
+ allowedByClass,
143
+ classifyResource,
144
+ grantsForClass,
145
+ };