@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.
- package/CHANGELOG.md +16 -0
- package/docs/LIVE_PROMPT.md +1 -1
- package/docs/NOTIFICATIONS.md +37 -0
- package/docs/OPERATIONS.md +20 -0
- package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
- package/frontend/dist/assets/index-DqCfZFGi.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +91 -10
- package/frontend/dist/version.json +1 -1
- package/lib/audio/groups.js +1 -1
- package/lib/auth/token.js +4 -4
- package/lib/cli/commands.js +70 -15
- package/lib/cli/doctor.js +5 -5
- package/lib/cli/fleet-service.js +7 -7
- package/lib/cli/init.js +55 -31
- package/lib/cli/pidfile.js +1 -1
- package/lib/cli/service.js +124 -8
- package/lib/cli/stable-alias.js +4 -4
- package/lib/cli/url.js +1 -1
- package/lib/config.js +36 -6
- package/lib/files/routes.js +1 -1
- package/lib/fleet/boot.js +2 -2
- package/lib/fleet/builtin.js +15 -15
- package/lib/fleet/causes.js +1 -1
- package/lib/fleet/cell-exec.js +23 -23
- package/lib/fleet/cell-hold.js +6 -6
- package/lib/fleet/cell-lease-server.js +106 -106
- package/lib/fleet/cell-lease.js +21 -21
- package/lib/fleet/definitions.js +10 -10
- package/lib/fleet/identity-authority.js +1 -1
- package/lib/fleet/launch-broker.js +13 -13
- package/lib/fleet/launch.js +15 -15
- package/lib/fleet/lease-client.js +23 -23
- package/lib/fleet/lease-routes.js +3 -3
- package/lib/fleet/lease-verifier.js +22 -22
- package/lib/fleet/managed.js +35 -35
- package/lib/fleet/prompt-delivery.js +13 -13
- package/lib/fleet/routes.js +15 -10
- package/lib/fleet/runtime.js +24 -24
- package/lib/live-host/bridge.js +53 -43
- package/lib/live-host/federation.js +97 -0
- package/lib/live-host/routes.js +166 -8
- package/lib/live-host/store.js +44 -6
- package/lib/mcp/server.js +14 -14
- package/lib/mcp/tools.js +5 -5
- package/lib/nodes/access-presets.js +258 -0
- package/lib/nodes/commands.js +198 -4
- package/lib/nodes/store.js +152 -9
- package/lib/nodes/tunnel.js +7 -7
- package/lib/notify/ask-answer-service.js +184 -0
- package/lib/notify/ask-receipts.js +197 -0
- package/lib/notify/ask-relay.js +152 -0
- package/lib/notify/asks.js +28 -3
- package/lib/notify/event-feed-acl.js +95 -0
- package/lib/notify/event-feed-asks-routes.js +144 -0
- package/lib/notify/event-feed-client.js +434 -0
- package/lib/notify/event-feed-history.js +115 -0
- package/lib/notify/event-feed-producers.js +175 -0
- package/lib/notify/event-feed-routes.js +254 -0
- package/lib/notify/event-feed.js +328 -0
- package/lib/notify/events.js +1 -1
- package/lib/notify/notifier.js +3 -3
- package/lib/notify/push-relay.js +240 -0
- package/lib/notify/push.js +8 -8
- package/lib/notify/routes.js +62 -23
- package/lib/proxy/federation.js +132 -12
- package/lib/proxy/node-proxy.js +13 -13
- package/lib/proxy/panel-auth.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/proxy/resource-acl.js +145 -0
- package/lib/server.js +396 -40
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +45 -32
- package/lib/update/manager.js +1 -1
- package/lib/update/runner.js +1 -1
- package/lib/voice/transcribe.js +1 -1
- package/lib/ws/bridge.js +3 -3
- package/package.json +1 -1
- package/skills/live/SKILL.md +12 -0
- package/frontend/dist/assets/index-CWra6B79.js +0 -167
- 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
|
+
};
|