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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultravisor",
3
- "version": "1.0.25",
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.4",
33
- "orator-authentication": "^1.0.0",
32
+ "orator": "^6.1.0",
33
+ "orator-authentication": "^1.0.1",
34
34
  "orator-serviceserver-restify": "^2.0.10",
35
- "pict": "^1.0.363",
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.11",
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.0"
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;