ultravisor 1.0.25 → 1.0.26
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/docs/_sidebar.md +1 -0
- package/docs/features/persistence-via-databeacon.md +1211 -0
- package/package.json +6 -6
- package/source/cli/Ultravisor-CLIProgram.cjs +62 -0
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +9 -1
- package/source/persistence/UltravisorPersistenceSchema.json +240 -0
- package/source/services/Ultravisor-AuthBeaconBridge.cjs +271 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +242 -149
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +65 -29
- package/source/services/Ultravisor-ExecutionManifest.cjs +99 -4
- package/source/services/Ultravisor-FleetManager.cjs +19 -1
- package/source/services/Ultravisor-ManifestStoreBridge.cjs +1134 -0
- package/source/services/Ultravisor-QueuePersistenceBridge.cjs +1336 -0
- package/source/web_server/Ultravisor-API-Server.cjs +951 -90
- package/webinterface/package.json +1 -0
- package/webinterface/source/Pict-Application-Ultravisor.js +57 -2
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +8 -0
- package/webinterface/source/views/PictView-Ultravisor-Login.js +74 -0
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +25 -0
- package/webinterface/source/views/PictView-Ultravisor-UserManagement.js +159 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultravisor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"description": "Cyclic process execution with ai integration.",
|
|
5
5
|
"main": "source/Ultravisor.cjs",
|
|
6
6
|
"bin": {
|
|
@@ -29,20 +29,20 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"better-sqlite3": "^11.10.0",
|
|
31
31
|
"cron": "^4.4.0",
|
|
32
|
-
"orator": "^6.0
|
|
33
|
-
"orator-authentication": "^1.0.
|
|
32
|
+
"orator": "^6.1.0",
|
|
33
|
+
"orator-authentication": "^1.0.1",
|
|
34
34
|
"orator-serviceserver-restify": "^2.0.10",
|
|
35
|
-
"pict": "^1.0.
|
|
35
|
+
"pict": "^1.0.365",
|
|
36
36
|
"pict-service-commandlineutility": "^1.0.19",
|
|
37
37
|
"pict-serviceproviderbase": "^1.0.4",
|
|
38
|
-
"ultravisor-beacon": "^0.0.
|
|
38
|
+
"ultravisor-beacon": "^0.0.13",
|
|
39
39
|
"ultravisor-file-stream": "^0.0.1",
|
|
40
40
|
"ws": "^8.20.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"pict-docuserve": "^0.1.5",
|
|
44
44
|
"puppeteer": "^24.40.0",
|
|
45
|
-
"quackage": "^1.1.
|
|
45
|
+
"quackage": "^1.1.2"
|
|
46
46
|
},
|
|
47
47
|
"mocha": {
|
|
48
48
|
"diff": true,
|
|
@@ -18,7 +18,12 @@ const libServiceExecutionManifest = require('../services/Ultravisor-ExecutionMan
|
|
|
18
18
|
const libServiceBeaconCoordinator = require('../services/Ultravisor-Beacon-Coordinator.cjs');
|
|
19
19
|
const libServiceBeaconReachability = require('../services/Ultravisor-Beacon-Reachability.cjs');
|
|
20
20
|
const libServiceBeaconQueueJournal = require('../services/persistence/Ultravisor-Beacon-QueueJournal.cjs');
|
|
21
|
+
const libServiceBeaconQueueStore = require('../services/persistence/Ultravisor-Beacon-QueueStore.cjs');
|
|
22
|
+
const libServiceBeaconScheduler = require('../services/Ultravisor-Beacon-Scheduler.cjs');
|
|
21
23
|
const libServiceBeaconFleetStore = require('../services/persistence/Ultravisor-Beacon-FleetStore.cjs');
|
|
24
|
+
const libServiceAuthBeaconBridge = require('../services/Ultravisor-AuthBeaconBridge.cjs');
|
|
25
|
+
const libServiceQueuePersistenceBridge = require('../services/Ultravisor-QueuePersistenceBridge.cjs');
|
|
26
|
+
const libServiceManifestStoreBridge = require('../services/Ultravisor-ManifestStoreBridge.cjs');
|
|
22
27
|
const libServiceDirectoryDistributor = require('../services/Ultravisor-DirectoryDistributor.cjs');
|
|
23
28
|
const libServiceFleetManager = require('../services/Ultravisor-FleetManager.cjs');
|
|
24
29
|
|
|
@@ -210,6 +215,19 @@ if (tmpQueueJournal)
|
|
|
210
215
|
tmpQueueJournal.initialize(_Ultravisor_Pict.fable.settings.UltravisorFileStorePath);
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
// --- Beacon queue store (SQLite-backed history + per-item events) ---
|
|
219
|
+
// Distinct from the journal above: the journal is a write-ahead log for
|
|
220
|
+
// crash recovery of the in-flight queue; the store is a long-lived
|
|
221
|
+
// SQLite database that backs the /queue UI's history list and the
|
|
222
|
+
// /Beacon/Work/:hash/Events endpoint. Both can coexist — they were
|
|
223
|
+
// added in separate generations of the queue work.
|
|
224
|
+
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists('UltravisorBeaconQueueStore', libServiceBeaconQueueStore);
|
|
225
|
+
let tmpQueueStore = Object.values(_Ultravisor_Pict.fable.servicesMap['UltravisorBeaconQueueStore'])[0];
|
|
226
|
+
if (tmpQueueStore)
|
|
227
|
+
{
|
|
228
|
+
tmpQueueStore.initialize(_Ultravisor_Pict.fable.settings.UltravisorFileStorePath);
|
|
229
|
+
}
|
|
230
|
+
|
|
213
231
|
// Restore persisted work queue from journal (if any)
|
|
214
232
|
let tmpCoordinator = Object.values(_Ultravisor_Pict.fable.servicesMap['UltravisorBeaconCoordinator'])[0];
|
|
215
233
|
if (tmpCoordinator)
|
|
@@ -233,8 +251,52 @@ _Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
|
|
|
233
251
|
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
|
|
234
252
|
'UltravisorFleetManager', libServiceFleetManager);
|
|
235
253
|
|
|
254
|
+
// --- Beacon scheduler (queue.* topic broadcasts + dispatch tick) ---
|
|
255
|
+
// The scheduler must exist before the APIServer wires its broadcast
|
|
256
|
+
// handler — see Ultravisor-API-Server._initializeWebSocket where it
|
|
257
|
+
// calls scheduler.setBroadcastHandler(...). After APIServer is up,
|
|
258
|
+
// we kick off the dispatch/health/summary timers via .start().
|
|
259
|
+
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
|
|
260
|
+
'UltravisorBeaconScheduler', libServiceBeaconScheduler);
|
|
261
|
+
|
|
262
|
+
// --- Auth beacon bridge (consults the optional Authentication-capable
|
|
263
|
+
// beacon for session validation + non-promiscuous mode admission). The
|
|
264
|
+
// bridge is always installed; it just resolves to "not available" when
|
|
265
|
+
// no auth beacon is connected, so the rest of the hub keeps working
|
|
266
|
+
// without it. See source/services/Ultravisor-AuthBeaconBridge.cjs and
|
|
267
|
+
// the matching ultravisor-auth-beacon module under modules/apps/.
|
|
268
|
+
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
|
|
269
|
+
'UltravisorAuthBeaconBridge', libServiceAuthBeaconBridge);
|
|
270
|
+
|
|
271
|
+
// --- Queue persistence bridge (consults the optional QueuePersistence
|
|
272
|
+
// beacon for durable queue + event log storage). Like the auth bridge,
|
|
273
|
+
// it's always installed and falls back to the in-process
|
|
274
|
+
// UltravisorBeaconQueueStore when no beacon is connected. The
|
|
275
|
+
// coordinator + scheduler call into the bridge for every persistence
|
|
276
|
+
// op; switching to a beacon-backed backend is a runtime decision.
|
|
277
|
+
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
|
|
278
|
+
'UltravisorQueuePersistenceBridge', libServiceQueuePersistenceBridge);
|
|
279
|
+
|
|
280
|
+
// --- Manifest store bridge (consults the optional ManifestStore
|
|
281
|
+
// beacon for durable execution-manifest storage). Same shape as the
|
|
282
|
+
// queue bridge: always installed, falls back to the in-process
|
|
283
|
+
// UltravisorExecutionManifest service when no beacon is connected.
|
|
284
|
+
// Persistence calls (finalizeExecution, abandonRun) go through this
|
|
285
|
+
// bridge instead of directly writing JSON files.
|
|
286
|
+
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
|
|
287
|
+
'UltravisorManifestStoreBridge', libServiceManifestStoreBridge);
|
|
288
|
+
|
|
236
289
|
_Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists('UltravisorAPIServer', libWebServerAPIServer);
|
|
237
290
|
|
|
291
|
+
// Kick the scheduler timers AFTER the API server has wired
|
|
292
|
+
// setBroadcastHandler — otherwise the first summary tick has nowhere
|
|
293
|
+
// to fan out to and is silently dropped.
|
|
294
|
+
let tmpScheduler = Object.values(_Ultravisor_Pict.fable.servicesMap['UltravisorBeaconScheduler'])[0];
|
|
295
|
+
if (tmpScheduler && typeof tmpScheduler.start === 'function')
|
|
296
|
+
{
|
|
297
|
+
tmpScheduler.start();
|
|
298
|
+
}
|
|
299
|
+
|
|
238
300
|
// ── Service name aliases ────────────────────────────────
|
|
239
301
|
// Some CLI commands access services by hyphenated names via this.fable['Name'].
|
|
240
302
|
// Bridge the camelCase registration to hyphenated access.
|
|
@@ -16,5 +16,13 @@ module.exports = (
|
|
|
16
16
|
"UltravisorBeaconWorkItemTimeoutMs": 300000,
|
|
17
17
|
"UltravisorBeaconAffinityTTLMs": 3600000,
|
|
18
18
|
"UltravisorBeaconPollIntervalMs": 5000,
|
|
19
|
-
"UltravisorBeaconJournalCompactThreshold": 500
|
|
19
|
+
"UltravisorBeaconJournalCompactThreshold": 500,
|
|
20
|
+
|
|
21
|
+
// Optional non-promiscuous mode. When true, every BeaconRegister
|
|
22
|
+
// must present a JoinSecret that either (a) matches the bootstrap
|
|
23
|
+
// secret below, for the auth beacon's own admission, or (b) is
|
|
24
|
+
// validated by the auth beacon's AUTH_ValidateBeaconJoin action.
|
|
25
|
+
// Default false → behavior identical to pre-auth-beacon ultravisor.
|
|
26
|
+
"UltravisorNonPromiscuous": false,
|
|
27
|
+
"UltravisorBootstrapAuthSecret": ""
|
|
20
28
|
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Ultravisor persistence schema — source of truth for the four tables used when ultravisor's QueuePersistenceBridge / ManifestStoreBridge route through retold-databeacon's MeadowProxy. See modules/apps/ultravisor/docs/features/persistence-via-databeacon.md for the architectural context.",
|
|
3
|
+
|
|
4
|
+
"SchemaName": "ultravisor",
|
|
5
|
+
"Version": 1,
|
|
6
|
+
|
|
7
|
+
"Tables":
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
"Scope": "UVQueueWorkItem",
|
|
11
|
+
"DefaultIdentifier": "IDUVQueueWorkItem",
|
|
12
|
+
"Domain": "Ultravisor",
|
|
13
|
+
"Schema":
|
|
14
|
+
[
|
|
15
|
+
{ "Column": "IDUVQueueWorkItem", "Type": "AutoIdentity", "Size": "Default" },
|
|
16
|
+
{ "Column": "WorkItemHash", "Type": "String", "Size": "128" },
|
|
17
|
+
{ "Column": "RunID", "Type": "String", "Size": "128" },
|
|
18
|
+
{ "Column": "RunHash", "Type": "String", "Size": "128" },
|
|
19
|
+
{ "Column": "NodeHash", "Type": "String", "Size": "128" },
|
|
20
|
+
{ "Column": "OperationHash", "Type": "String", "Size": "128" },
|
|
21
|
+
{ "Column": "Capability", "Type": "String", "Size": "128" },
|
|
22
|
+
{ "Column": "Action", "Type": "String", "Size": "128" },
|
|
23
|
+
{ "Column": "Settings", "Type": "String", "Size": "Default" },
|
|
24
|
+
{ "Column": "AffinityKey", "Type": "String", "Size": "128" },
|
|
25
|
+
{ "Column": "AssignedBeaconID", "Type": "String", "Size": "128" },
|
|
26
|
+
{ "Column": "Status", "Type": "String", "Size": "32" },
|
|
27
|
+
{ "Column": "Priority", "Type": "Integer", "Size": "int" },
|
|
28
|
+
{ "Column": "EnqueuedAt", "Type": "String", "Size": "50" },
|
|
29
|
+
{ "Column": "DispatchedAt", "Type": "String", "Size": "50" },
|
|
30
|
+
{ "Column": "StartedAt", "Type": "String", "Size": "50" },
|
|
31
|
+
{ "Column": "ClaimedAt", "Type": "String", "Size": "50" },
|
|
32
|
+
{ "Column": "CompletedAt", "Type": "String", "Size": "50" },
|
|
33
|
+
{ "Column": "CanceledAt", "Type": "String", "Size": "50" },
|
|
34
|
+
{ "Column": "AssignedAt", "Type": "String", "Size": "50" },
|
|
35
|
+
{ "Column": "LastEventAt", "Type": "String", "Size": "50" },
|
|
36
|
+
{ "Column": "QueueWaitMs", "Type": "Integer", "Size": "int" },
|
|
37
|
+
{ "Column": "TimeoutMs", "Type": "Integer", "Size": "int" },
|
|
38
|
+
{ "Column": "Health", "Type": "Float", "Size": "Default" },
|
|
39
|
+
{ "Column": "HealthLabel", "Type": "String", "Size": "32" },
|
|
40
|
+
{ "Column": "HealthReason", "Type": "String", "Size": "512" },
|
|
41
|
+
{ "Column": "HealthComputedAt", "Type": "String", "Size": "50" },
|
|
42
|
+
{ "Column": "AttemptNumber", "Type": "Integer", "Size": "int" },
|
|
43
|
+
{ "Column": "MaxAttempts", "Type": "Integer", "Size": "int" },
|
|
44
|
+
{ "Column": "RetryBackoffMs", "Type": "Integer", "Size": "int" },
|
|
45
|
+
{ "Column": "RetryAfter", "Type": "String", "Size": "50" },
|
|
46
|
+
{ "Column": "LastError", "Type": "String", "Size": "Default" },
|
|
47
|
+
{ "Column": "Result", "Type": "String", "Size": "Default" },
|
|
48
|
+
{ "Column": "CancelRequested", "Type": "Boolean", "Size": "Default" },
|
|
49
|
+
{ "Column": "CancelReason", "Type": "String", "Size": "512" },
|
|
50
|
+
{ "Column": "CreateDate", "Type": "CreateDate", "Size": "Default" },
|
|
51
|
+
{ "Column": "UpdateDate", "Type": "UpdateDate", "Size": "Default" },
|
|
52
|
+
{ "Column": "Deleted", "Type": "Deleted", "Size": "Default" }
|
|
53
|
+
],
|
|
54
|
+
"DefaultObject":
|
|
55
|
+
{
|
|
56
|
+
"IDUVQueueWorkItem": 0,
|
|
57
|
+
"WorkItemHash": "",
|
|
58
|
+
"RunID": "",
|
|
59
|
+
"RunHash": "",
|
|
60
|
+
"NodeHash": "",
|
|
61
|
+
"OperationHash": "",
|
|
62
|
+
"Capability": "",
|
|
63
|
+
"Action": "",
|
|
64
|
+
"Settings": "{}",
|
|
65
|
+
"AffinityKey": "",
|
|
66
|
+
"AssignedBeaconID": "",
|
|
67
|
+
"Status": "Pending",
|
|
68
|
+
"Priority": 0,
|
|
69
|
+
"EnqueuedAt": "",
|
|
70
|
+
"DispatchedAt": "",
|
|
71
|
+
"StartedAt": "",
|
|
72
|
+
"ClaimedAt": "",
|
|
73
|
+
"CompletedAt": "",
|
|
74
|
+
"CanceledAt": "",
|
|
75
|
+
"AssignedAt": "",
|
|
76
|
+
"LastEventAt": "",
|
|
77
|
+
"QueueWaitMs": 0,
|
|
78
|
+
"TimeoutMs": 0,
|
|
79
|
+
"Health": 0,
|
|
80
|
+
"HealthLabel": "Unknown",
|
|
81
|
+
"HealthReason": "",
|
|
82
|
+
"HealthComputedAt": "",
|
|
83
|
+
"AttemptNumber": 0,
|
|
84
|
+
"MaxAttempts": 1,
|
|
85
|
+
"RetryBackoffMs": 0,
|
|
86
|
+
"RetryAfter": "",
|
|
87
|
+
"LastError": "",
|
|
88
|
+
"Result": "",
|
|
89
|
+
"CancelRequested": false,
|
|
90
|
+
"CancelReason": "",
|
|
91
|
+
"Deleted": false
|
|
92
|
+
},
|
|
93
|
+
"Indexes":
|
|
94
|
+
[
|
|
95
|
+
{ "Name": "IX_UVQueueWorkItem_Hash", "Columns": ["WorkItemHash"], "Unique": true },
|
|
96
|
+
{ "Name": "IX_UVQueueWorkItem_Status", "Columns": ["Status"] },
|
|
97
|
+
{ "Name": "IX_UVQueueWorkItem_RunID", "Columns": ["RunID"] },
|
|
98
|
+
{ "Name": "IX_UVQueueWorkItem_Assigned", "Columns": ["AssignedBeaconID", "Status"] },
|
|
99
|
+
{ "Name": "IX_UVQueueWorkItem_Dispatch", "Columns": ["Status", "Priority", "EnqueuedAt"] }
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
"Scope": "UVQueueWorkItemEvent",
|
|
105
|
+
"DefaultIdentifier": "IDUVQueueWorkItemEvent",
|
|
106
|
+
"Domain": "Ultravisor",
|
|
107
|
+
"_comment": "Append-only event log per work item. EventGUID is unique for idempotency on bootstrap-flush replay.",
|
|
108
|
+
"Schema":
|
|
109
|
+
[
|
|
110
|
+
{ "Column": "IDUVQueueWorkItemEvent", "Type": "AutoIdentity", "Size": "Default" },
|
|
111
|
+
{ "Column": "EventGUID", "Type": "String", "Size": "36" },
|
|
112
|
+
{ "Column": "WorkItemHash", "Type": "String", "Size": "128" },
|
|
113
|
+
{ "Column": "EventType", "Type": "String", "Size": "64" },
|
|
114
|
+
{ "Column": "Payload", "Type": "String", "Size": "Default" },
|
|
115
|
+
{ "Column": "EmittedAt", "Type": "String", "Size": "50" },
|
|
116
|
+
{ "Column": "Seq", "Type": "Integer", "Size": "int" },
|
|
117
|
+
{ "Column": "FromStatus", "Type": "String", "Size": "32" },
|
|
118
|
+
{ "Column": "ToStatus", "Type": "String", "Size": "32" },
|
|
119
|
+
{ "Column": "BeaconID", "Type": "String", "Size": "128" },
|
|
120
|
+
{ "Column": "CreateDate", "Type": "CreateDate", "Size": "Default" },
|
|
121
|
+
{ "Column": "Deleted", "Type": "Deleted", "Size": "Default" }
|
|
122
|
+
],
|
|
123
|
+
"DefaultObject":
|
|
124
|
+
{
|
|
125
|
+
"IDUVQueueWorkItemEvent": 0,
|
|
126
|
+
"EventGUID": "",
|
|
127
|
+
"WorkItemHash": "",
|
|
128
|
+
"EventType": "",
|
|
129
|
+
"Payload": "{}",
|
|
130
|
+
"EmittedAt": "",
|
|
131
|
+
"Seq": 0,
|
|
132
|
+
"FromStatus": "",
|
|
133
|
+
"ToStatus": "",
|
|
134
|
+
"BeaconID": "",
|
|
135
|
+
"Deleted": false
|
|
136
|
+
},
|
|
137
|
+
"Indexes":
|
|
138
|
+
[
|
|
139
|
+
{ "Name": "IX_UVQueueEvent_GUID", "Columns": ["EventGUID"], "Unique": true },
|
|
140
|
+
{ "Name": "IX_UVQueueEvent_Hash", "Columns": ["WorkItemHash"] }
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
"Scope": "UVQueueWorkItemAttempt",
|
|
146
|
+
"DefaultIdentifier": "IDUVQueueWorkItemAttempt",
|
|
147
|
+
"Domain": "Ultravisor",
|
|
148
|
+
"_comment": "One row per dispatch attempt. (WorkItemHash, AttemptNumber) is unique.",
|
|
149
|
+
"Schema":
|
|
150
|
+
[
|
|
151
|
+
{ "Column": "IDUVQueueWorkItemAttempt", "Type": "AutoIdentity", "Size": "Default" },
|
|
152
|
+
{ "Column": "WorkItemHash", "Type": "String", "Size": "128" },
|
|
153
|
+
{ "Column": "AttemptNumber", "Type": "Integer", "Size": "int" },
|
|
154
|
+
{ "Column": "BeaconID", "Type": "String", "Size": "128" },
|
|
155
|
+
{ "Column": "StartedAt", "Type": "String", "Size": "50" },
|
|
156
|
+
{ "Column": "EndedAt", "Type": "String", "Size": "50" },
|
|
157
|
+
{ "Column": "Outcome", "Type": "String", "Size": "32" },
|
|
158
|
+
{ "Column": "Error", "Type": "String", "Size": "Default" },
|
|
159
|
+
{ "Column": "CreateDate", "Type": "CreateDate", "Size": "Default" },
|
|
160
|
+
{ "Column": "Deleted", "Type": "Deleted", "Size": "Default" }
|
|
161
|
+
],
|
|
162
|
+
"DefaultObject":
|
|
163
|
+
{
|
|
164
|
+
"IDUVQueueWorkItemAttempt": 0,
|
|
165
|
+
"WorkItemHash": "",
|
|
166
|
+
"AttemptNumber": 0,
|
|
167
|
+
"BeaconID": "",
|
|
168
|
+
"StartedAt": "",
|
|
169
|
+
"EndedAt": "",
|
|
170
|
+
"Outcome": "",
|
|
171
|
+
"Error": "",
|
|
172
|
+
"Deleted": false
|
|
173
|
+
},
|
|
174
|
+
"Indexes":
|
|
175
|
+
[
|
|
176
|
+
{ "Name": "IX_UVQueueAttempt_HashNum", "Columns": ["WorkItemHash", "AttemptNumber"], "Unique": true }
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
"Scope": "UVManifest",
|
|
182
|
+
"DefaultIdentifier": "IDUVManifest",
|
|
183
|
+
"Domain": "Ultravisor",
|
|
184
|
+
"_comment": "Execution manifests, one row per run. ManifestJSON is the full wire-safe blob produced by _cleanManifestForWire.",
|
|
185
|
+
"Schema":
|
|
186
|
+
[
|
|
187
|
+
{ "Column": "IDUVManifest", "Type": "AutoIdentity", "Size": "Default" },
|
|
188
|
+
{ "Column": "Hash", "Type": "String", "Size": "128" },
|
|
189
|
+
{ "Column": "OperationHash", "Type": "String", "Size": "128" },
|
|
190
|
+
{ "Column": "OperationName", "Type": "String", "Size": "500" },
|
|
191
|
+
{ "Column": "Status", "Type": "String", "Size": "32" },
|
|
192
|
+
{ "Column": "RunMode", "Type": "String", "Size": "32" },
|
|
193
|
+
{ "Column": "Live", "Type": "Boolean", "Size": "Default" },
|
|
194
|
+
{ "Column": "StartTime", "Type": "String", "Size": "50" },
|
|
195
|
+
{ "Column": "StopTime", "Type": "String", "Size": "50" },
|
|
196
|
+
{ "Column": "ElapsedMs", "Type": "Integer", "Size": "int" },
|
|
197
|
+
{ "Column": "ManifestJSON", "Type": "String", "Size": "Default" },
|
|
198
|
+
{ "Column": "StagingPath", "Type": "String", "Size": "1024" },
|
|
199
|
+
{ "Column": "CreateDate", "Type": "CreateDate", "Size": "Default" },
|
|
200
|
+
{ "Column": "UpdateDate", "Type": "UpdateDate", "Size": "Default" },
|
|
201
|
+
{ "Column": "Deleted", "Type": "Deleted", "Size": "Default" }
|
|
202
|
+
],
|
|
203
|
+
"DefaultObject":
|
|
204
|
+
{
|
|
205
|
+
"IDUVManifest": 0,
|
|
206
|
+
"Hash": "",
|
|
207
|
+
"OperationHash": "",
|
|
208
|
+
"OperationName": "",
|
|
209
|
+
"Status": "",
|
|
210
|
+
"RunMode": "",
|
|
211
|
+
"Live": false,
|
|
212
|
+
"StartTime": "",
|
|
213
|
+
"StopTime": "",
|
|
214
|
+
"ElapsedMs": 0,
|
|
215
|
+
"ManifestJSON": "{}",
|
|
216
|
+
"StagingPath": "",
|
|
217
|
+
"Deleted": false
|
|
218
|
+
},
|
|
219
|
+
"Indexes":
|
|
220
|
+
[
|
|
221
|
+
{ "Name": "IX_UVManifest_Hash", "Columns": ["Hash"], "Unique": true },
|
|
222
|
+
{ "Name": "IX_UVManifest_StatusStop","Columns": ["Status", "StopTime"] },
|
|
223
|
+
{ "Name": "IX_UVManifest_OpHash", "Columns": ["OperationHash"] }
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
|
|
228
|
+
"_notes":
|
|
229
|
+
{
|
|
230
|
+
"meadowConventions": "Type values follow meadow column conventions. AutoIdentity → engine PK; CreateDate / UpdateDate / Deleted → meadow's auto-managed audit columns. The per-engine Meadow-Schema-<engine>.js files translate these to engine-specific DDL.",
|
|
231
|
+
|
|
232
|
+
"indexes": "Indexes here are NOT part of meadow's standard table descriptor. retold-databeacon's EnsureSchema action will issue them as CREATE INDEX IF NOT EXISTS statements after the meadow table create. Per-engine syntax differences (e.g. SQL Server's CREATE INDEX vs Postgres's CREATE INDEX IF NOT EXISTS) handled in the engine-specific path.",
|
|
233
|
+
|
|
234
|
+
"jsonColumns": "Settings (work item), Payload (event), Result, LastError, ManifestJSON are stored as opaque strings (JSON-encoded by callers). We don't use a native JSON column type because not all meadow connectors support it uniformly (mssql JSON support varies; sqlite has none). Callers are responsible for parse/stringify on the boundary.",
|
|
235
|
+
|
|
236
|
+
"floatColumns": "Health is stored as Float (0..1 score). Connector translation: REAL on sqlite, DOUBLE on mysql/postgres, FLOAT on mssql.",
|
|
237
|
+
|
|
238
|
+
"softDelete": "Deleted column matches meadow's standard soft-delete convention. The bridges' `removeManifest` etc. flip Deleted=1 rather than DROP. Hard-delete deferred to a separate retention sweep."
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor-AuthBeaconBridge
|
|
3
|
+
*
|
|
4
|
+
* Talks to whichever beacon advertises the `Authentication` capability
|
|
5
|
+
* via dispatchAndWait, presents a clean async surface to the rest of
|
|
6
|
+
* the hub. The bridge is the only thing that knows the auth beacon
|
|
7
|
+
* exists — every other call site (API server, coordinator non-
|
|
8
|
+
* promiscuous mode) just asks the bridge "is this session valid?"
|
|
9
|
+
* or "is this beacon allowed to join?" and gets a Promise back.
|
|
10
|
+
*
|
|
11
|
+
* Design notes
|
|
12
|
+
* ============
|
|
13
|
+
* - The bridge has NO local cache. Sessions are short-lived and
|
|
14
|
+
* security-sensitive; caching invalidation here is more dangerous
|
|
15
|
+
* than the extra dispatch cost. If the auth beacon becomes a
|
|
16
|
+
* bottleneck, that's where caching should land — it owns the
|
|
17
|
+
* lifecycle.
|
|
18
|
+
*
|
|
19
|
+
* - The bridge is OPTIONAL — when the coordinator can't find an auth
|
|
20
|
+
* beacon, methods resolve with `{Available:false, ...}` and the
|
|
21
|
+
* caller decides how to proceed (typically: fail-closed in non-
|
|
22
|
+
* promiscuous mode, fall back to legacy auth in promiscuous mode).
|
|
23
|
+
*
|
|
24
|
+
* - Bridge methods all return Promises (not callbacks) because they're
|
|
25
|
+
* meant to be awaited in async route handlers and middleware. The
|
|
26
|
+
* underlying coordinator.dispatchAndWait is callback-based, so we
|
|
27
|
+
* wrap it once here.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const libPictService = require('pict-serviceproviderbase');
|
|
31
|
+
|
|
32
|
+
// Default bridge dispatch timeout. Authentication should be FAST —
|
|
33
|
+
// password hashing + DB lookup measured in single-digit ms, session
|
|
34
|
+
// validation usually a Map.get(). 5s is generous.
|
|
35
|
+
const DEFAULT_BRIDGE_TIMEOUT_MS = 5000;
|
|
36
|
+
|
|
37
|
+
class UltravisorAuthBeaconBridge extends libPictService
|
|
38
|
+
{
|
|
39
|
+
constructor(pPict, pOptions, pServiceHash)
|
|
40
|
+
{
|
|
41
|
+
super(pPict, pOptions, pServiceHash);
|
|
42
|
+
this.serviceType = 'UltravisorAuthBeaconBridge';
|
|
43
|
+
this._TimeoutMs = (pOptions && pOptions.TimeoutMs) || DEFAULT_BRIDGE_TIMEOUT_MS;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Look up the BeaconID currently advertising the Authentication
|
|
48
|
+
* capability. Returns null when no auth beacon is connected.
|
|
49
|
+
*
|
|
50
|
+
* If multiple beacons advertise Authentication, the FIRST one is
|
|
51
|
+
* returned — for now we don't elect a primary. A future enhancement
|
|
52
|
+
* could weight by tags (e.g., Tags.Role==='auth' wins).
|
|
53
|
+
*/
|
|
54
|
+
getAuthBeaconID()
|
|
55
|
+
{
|
|
56
|
+
let tmpCoord = this._coord();
|
|
57
|
+
if (!tmpCoord) return null;
|
|
58
|
+
let tmpBeacons = tmpCoord.listBeacons() || [];
|
|
59
|
+
for (let i = 0; i < tmpBeacons.length; i++)
|
|
60
|
+
{
|
|
61
|
+
let tmpCaps = tmpBeacons[i].Capabilities || [];
|
|
62
|
+
if (tmpCaps.indexOf('Authentication') >= 0)
|
|
63
|
+
{
|
|
64
|
+
return tmpBeacons[i].BeaconID;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @returns {boolean} true iff some beacon claims Authentication
|
|
72
|
+
*/
|
|
73
|
+
isAvailable()
|
|
74
|
+
{
|
|
75
|
+
return this.getAuthBeaconID() !== null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run a Login on the auth beacon. Returns the auth beacon's
|
|
80
|
+
* Outputs unchanged so callers can read SessionToken / UserContext
|
|
81
|
+
* / ExpiresAt directly.
|
|
82
|
+
*/
|
|
83
|
+
login(pUsername, pPassword, pMethod)
|
|
84
|
+
{
|
|
85
|
+
return this._dispatchAuthAction('AUTH_Login',
|
|
86
|
+
{
|
|
87
|
+
Username: pUsername,
|
|
88
|
+
Password: pPassword,
|
|
89
|
+
Method: pMethod || 'password'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
validateSession(pSessionToken)
|
|
94
|
+
{
|
|
95
|
+
return this._dispatchAuthAction('AUTH_ValidateSession',
|
|
96
|
+
{
|
|
97
|
+
SessionToken: pSessionToken || ''
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logout(pSessionToken)
|
|
102
|
+
{
|
|
103
|
+
return this._dispatchAuthAction('AUTH_Logout',
|
|
104
|
+
{
|
|
105
|
+
SessionToken: pSessionToken || ''
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
authorizeAction(pSessionToken, pCapability, pAction)
|
|
110
|
+
{
|
|
111
|
+
return this._dispatchAuthAction('AUTH_AuthorizeAction',
|
|
112
|
+
{
|
|
113
|
+
SessionToken: pSessionToken || '',
|
|
114
|
+
Capability: pCapability || '',
|
|
115
|
+
Action: pAction || ''
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
validateBeaconJoin(pBeaconName, pJoinSecret, pBeaconCapabilities)
|
|
120
|
+
{
|
|
121
|
+
return this._dispatchAuthAction('AUTH_ValidateBeaconJoin',
|
|
122
|
+
{
|
|
123
|
+
BeaconName: pBeaconName || '',
|
|
124
|
+
JoinSecret: pJoinSecret || '',
|
|
125
|
+
Capabilities: Array.isArray(pBeaconCapabilities) ? pBeaconCapabilities : []
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generic dispatch — for callers (e.g. the orator-authentication
|
|
131
|
+
* Beacon provider) that want to forward an arbitrary AUTH_* action
|
|
132
|
+
* without one of the named convenience methods above. Same return
|
|
133
|
+
* shape as the rest of the bridge: a Promise resolving to
|
|
134
|
+
* `{Available, ...Outputs}` (or `{Available:false, Reason}` if the
|
|
135
|
+
* auth beacon isn't reachable).
|
|
136
|
+
*/
|
|
137
|
+
dispatchAction(pAction, pSettings)
|
|
138
|
+
{
|
|
139
|
+
return this._dispatchAuthAction(pAction, pSettings || {});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============== User management ==============
|
|
143
|
+
//
|
|
144
|
+
// Convenience wrappers around the AUTH_*User actions. Authorization
|
|
145
|
+
// is the caller's job — these dispatch unconditionally; protect the
|
|
146
|
+
// HTTP routes (or whatever surface invokes them) with a session +
|
|
147
|
+
// role check before letting them through.
|
|
148
|
+
|
|
149
|
+
listUsers(pSelector)
|
|
150
|
+
{
|
|
151
|
+
return this._dispatchAuthAction('AUTH_ListUsers', { Selector: pSelector || null });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getUser(pUserID)
|
|
155
|
+
{
|
|
156
|
+
return this._dispatchAuthAction('AUTH_GetUser', { UserID: pUserID });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
createUser(pUserSpec)
|
|
160
|
+
{
|
|
161
|
+
return this._dispatchAuthAction('AUTH_CreateUser', { UserSpec: pUserSpec || {} });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateUser(pUserID, pUpdates)
|
|
165
|
+
{
|
|
166
|
+
return this._dispatchAuthAction('AUTH_UpdateUser',
|
|
167
|
+
{ UserID: pUserID, Updates: pUpdates || {} });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
deleteUser(pUserID)
|
|
171
|
+
{
|
|
172
|
+
return this._dispatchAuthAction('AUTH_DeleteUser', { UserID: pUserID });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setUserPassword(pUserID, pNewPassword)
|
|
176
|
+
{
|
|
177
|
+
return this._dispatchAuthAction('AUTH_SetUserPassword',
|
|
178
|
+
{ UserID: pUserID, NewPassword: pNewPassword });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
changePassword(pUserID, pCurrentPassword, pNewPassword)
|
|
182
|
+
{
|
|
183
|
+
return this._dispatchAuthAction('AUTH_ChangePassword',
|
|
184
|
+
{
|
|
185
|
+
UserID: pUserID,
|
|
186
|
+
CurrentPassword: pCurrentPassword,
|
|
187
|
+
NewPassword: pNewPassword
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* One-time admin bootstrap. Dispatches AUTH_BootstrapAdmin to the
|
|
193
|
+
* auth beacon, which validates the token and creates the admin user
|
|
194
|
+
* atomically. Intentionally NOT gated by an admin session — that
|
|
195
|
+
* would be a chicken-and-egg problem (no admin exists yet to
|
|
196
|
+
* authenticate). The auth beacon's bootstrap token IS the auth.
|
|
197
|
+
*/
|
|
198
|
+
bootstrapAdmin(pToken, pUserSpec)
|
|
199
|
+
{
|
|
200
|
+
return this._dispatchAuthAction('AUTH_BootstrapAdmin',
|
|
201
|
+
{
|
|
202
|
+
Token: pToken,
|
|
203
|
+
UserSpec: pUserSpec || {}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============== Internals ==============
|
|
208
|
+
|
|
209
|
+
_coord()
|
|
210
|
+
{
|
|
211
|
+
// Resolve lazily — the coordinator can be added/replaced after
|
|
212
|
+
// the bridge is constructed.
|
|
213
|
+
let tmpMap = this.fable && this.fable.servicesMap
|
|
214
|
+
&& this.fable.servicesMap['UltravisorBeaconCoordinator'];
|
|
215
|
+
return tmpMap ? Object.values(tmpMap)[0] : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_dispatchAuthAction(pActionName, pSettings)
|
|
219
|
+
{
|
|
220
|
+
return new Promise((fResolve) =>
|
|
221
|
+
{
|
|
222
|
+
let tmpCoord = this._coord();
|
|
223
|
+
if (!tmpCoord)
|
|
224
|
+
{
|
|
225
|
+
return fResolve(
|
|
226
|
+
{
|
|
227
|
+
Available: false,
|
|
228
|
+
Reason: 'BeaconCoordinator not available'
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
let tmpAuthID = this.getAuthBeaconID();
|
|
232
|
+
if (!tmpAuthID)
|
|
233
|
+
{
|
|
234
|
+
return fResolve(
|
|
235
|
+
{
|
|
236
|
+
Available: false,
|
|
237
|
+
Reason: 'No beacon currently advertises Authentication'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
tmpCoord.dispatchAndWait(
|
|
241
|
+
{
|
|
242
|
+
Capability: 'Authentication',
|
|
243
|
+
Action: pActionName,
|
|
244
|
+
Settings: pSettings,
|
|
245
|
+
AffinityKey: 'auth', // single auth beacon at a time
|
|
246
|
+
TimeoutMs: this._TimeoutMs
|
|
247
|
+
},
|
|
248
|
+
(pError, pResult) =>
|
|
249
|
+
{
|
|
250
|
+
if (pError)
|
|
251
|
+
{
|
|
252
|
+
return fResolve(
|
|
253
|
+
{
|
|
254
|
+
Available: true,
|
|
255
|
+
Error: pError.message || String(pError),
|
|
256
|
+
// Surface a sane default — most callers want a
|
|
257
|
+
// boolean Allowed/Valid/Success they can branch on.
|
|
258
|
+
Allowed: false, Valid: false, Success: false
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// pResult.Outputs is the action's response payload.
|
|
262
|
+
// Tag Available:true so callers can distinguish "auth
|
|
263
|
+
// beacon answered no" from "auth beacon not reachable."
|
|
264
|
+
let tmpOut = (pResult && pResult.Outputs) || {};
|
|
265
|
+
return fResolve(Object.assign({ Available: true }, tmpOut));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = UltravisorAuthBeaconBridge;
|