@camstack/system 1.2.69 → 1.2.70

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 (66) hide show
  1. package/dist/addon-runner.js +6 -6
  2. package/dist/addon-runner.mjs +3 -3
  3. package/dist/addon-utils.js +11 -11
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +3 -3
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +2 -2
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +3 -3
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +2 -2
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +8 -8
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +6 -6
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +3 -3
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  17. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/index.d.ts +0 -1
  26. package/dist/builtins/local-auth/index.js +0 -1
  27. package/dist/builtins/local-auth/index.mjs +2 -2
  28. package/dist/builtins/local-auth/local-auth.addon.js +129 -173
  29. package/dist/builtins/local-auth/local-auth.addon.mjs +130 -173
  30. package/dist/builtins/local-network/local-network.addon.js +1 -1
  31. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  32. package/dist/builtins/loki-logging/index.js +1 -1
  33. package/dist/builtins/loki-logging/index.mjs +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.js +5 -5
  35. package/dist/builtins/native-metrics/native-metrics.addon.mjs +4 -4
  36. package/dist/builtins/platform-probe/index.js +4 -4
  37. package/dist/builtins/platform-probe/index.mjs +3 -3
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  39. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  40. package/dist/builtins/snapshot/index.js +2 -2
  41. package/dist/builtins/snapshot/index.mjs +2 -2
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -4
  43. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +3 -3
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  45. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +3 -3
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
  48. package/dist/builtins/system-config/system-config.addon.js +1 -1
  49. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  50. package/dist/builtins/winston-logging/index.js +1 -1
  51. package/dist/builtins/winston-logging/index.mjs +1 -1
  52. package/dist/{dist-C-QzFJ8v.mjs → dist-CnQXXnK1.mjs} +60 -1
  53. package/dist/{dist-D7wC-S4p.js → dist-DCLoJf2T.js} +59 -6
  54. package/dist/{model-download-service-CTBHzORJ.mjs → file-data-plane-CuE_hBli.mjs} +848 -848
  55. package/dist/index.d.ts +73 -81
  56. package/dist/index.js +1379 -1346
  57. package/dist/index.mjs +1362 -1328
  58. package/dist/kernel/capability-registry.d.ts +26 -0
  59. package/dist/kernel/config-schema.d.ts +1 -1
  60. package/dist/{manifest-python-deps-Dz1ryHIN.mjs → manifest-python-deps-DKrHcUZj.mjs} +47 -47
  61. package/package.json +1 -4
  62. package/dist/builtins/local-auth/homeassistant-integration.d.ts +0 -7
  63. package/dist/{model-download-service-Caj1kaSi.js → file-data-plane-DUHPHa-Y.js} +847 -847
  64. package/dist/{manifest-python-deps-BE6l8Y4W.js → manifest-python-deps-DafB-1hp.js} +47 -47
  65. package/dist/{resource-monitor-BkP504Vq.mjs → resource-monitor-BWmQ5i-o.mjs} +1 -1
  66. package/dist/{resource-monitor-DNNomR-i.js → resource-monitor-CdnzxBLP.js} +1 -1
package/dist/index.js CHANGED
@@ -1,53 +1,53 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-D7wC-S4p.js");
4
- const require_model_download_service = require("./model-download-service-Caj1kaSi.js");
5
- const require_manifest_python_deps = require("./manifest-python-deps-BE6l8Y4W.js");
6
- const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
7
- const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
8
- const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
9
- const require_builtins_sqlite_storage_index = require("./builtins/sqlite-storage/index.js");
3
+ const require_dist = require("./dist-DCLoJf2T.js");
4
+ const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
5
+ require("./builtins/alerts/index.js");
10
6
  const require_formatter = require("./formatter-DqAKDlvN.js");
11
- const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
12
7
  const require_builtins_console_logging_index = require("./builtins/console-logging/index.js");
13
- const require_builtins_loki_logging_index = require("./builtins/loki-logging/index.js");
8
+ const require_builtins_core_blocks_core_blocks_addon = require("./builtins/core-blocks/core-blocks.addon.js");
9
+ require("./builtins/core-blocks/index.js");
10
+ const require_builtins_device_manager_device_manager_addon = require("./builtins/device-manager/device-manager.addon.js");
11
+ require("./builtins/device-manager/index.js");
14
12
  const require_builtins_hub_forwarder_index = require("./builtins/hub-forwarder/index.js");
15
- const require_builtins_storage_orchestrator_storage_orchestrator_addon = require("./builtins/storage-orchestrator/storage-orchestrator.addon.js");
16
- require("./builtins/storage-orchestrator/index.js");
17
- const require_builtins_native_metrics_native_metrics_addon = require("./builtins/native-metrics/native-metrics.addon.js");
18
- const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
19
- require("./builtins/alerts/index.js");
20
13
  const require_builtins_liveness_monitor_liveness_monitor_addon = require("./builtins/liveness-monitor/liveness-monitor.addon.js");
21
14
  require("./builtins/liveness-monitor/index.js");
22
- const require_builtins_system_config_system_config_addon = require("./builtins/system-config/system-config.addon.js");
23
- require("./builtins/system-config/index.js");
24
- const require_builtins_core_blocks_core_blocks_addon = require("./builtins/core-blocks/core-blocks.addon.js");
25
- require("./builtins/core-blocks/index.js");
26
15
  const require_builtins_local_auth_local_auth_addon = require("./builtins/local-auth/local-auth.addon.js");
27
16
  require("./builtins/local-auth/index.js");
28
- const require_builtins_device_manager_device_manager_addon = require("./builtins/device-manager/device-manager.addon.js");
29
- require("./builtins/device-manager/index.js");
17
+ const require_builtins_loki_logging_index = require("./builtins/loki-logging/index.js");
18
+ const require_builtins_native_metrics_native_metrics_addon = require("./builtins/native-metrics/native-metrics.addon.js");
19
+ const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
20
+ const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
21
+ const require_builtins_sqlite_storage_index = require("./builtins/sqlite-storage/index.js");
22
+ const require_builtins_storage_orchestrator_storage_orchestrator_addon = require("./builtins/storage-orchestrator/storage-orchestrator.addon.js");
23
+ require("./builtins/storage-orchestrator/index.js");
24
+ const require_builtins_system_config_system_config_addon = require("./builtins/system-config/system-config.addon.js");
25
+ require("./builtins/system-config/index.js");
26
+ const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
27
+ const require_file_data_plane = require("./file-data-plane-DUHPHa-Y.js");
28
+ const require_manifest_python_deps = require("./manifest-python-deps-DafB-1hp.js");
29
+ const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
30
30
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
31
- let node_v8 = require("node:v8");
32
- node_v8 = require_chunk.__toESM(node_v8);
33
- let node_vm = require("node:vm");
34
- node_vm = require_chunk.__toESM(node_vm);
35
- let _camstack_types_node = require("@camstack/types/node");
36
31
  let zod = require("zod");
37
32
  let node_crypto = require("node:crypto");
38
- let node_http = require("node:http");
39
- let node_fs = require("node:fs");
40
- let node_fs$1 = require_chunk.__toESM(node_fs, 1);
41
- node_fs = require_chunk.__toESM(node_fs);
33
+ let _camstack_types_node = require("@camstack/types/node");
34
+ let node_fs_promises = require("node:fs/promises");
42
35
  let node_path = require("node:path");
43
36
  node_path = require_chunk.__toESM(node_path, 1);
37
+ let node_child_process = require("node:child_process");
44
38
  let node_util = require("node:util");
45
39
  node_util = require_chunk.__toESM(node_util);
46
- let _camstack_types_addon = require("@camstack/types/addon");
47
- let node_child_process = require("node:child_process");
48
40
  let node_os = require("node:os");
49
41
  node_os = require_chunk.__toESM(node_os);
50
- let node_fs_promises = require("node:fs/promises");
42
+ let node_fs = require("node:fs");
43
+ let node_fs$1 = require_chunk.__toESM(node_fs, 1);
44
+ node_fs = require_chunk.__toESM(node_fs);
45
+ let node_http = require("node:http");
46
+ let node_v8 = require("node:v8");
47
+ node_v8 = require_chunk.__toESM(node_v8);
48
+ let node_vm = require("node:vm");
49
+ node_vm = require_chunk.__toESM(node_vm);
50
+ let _camstack_types_addon = require("@camstack/types/addon");
51
51
  let node_url = require("node:url");
52
52
  let node_events = require("node:events");
53
53
  let node_stream = require("node:stream");
@@ -58,6 +58,381 @@ fs = require_chunk.__toESM(fs, 1);
58
58
  let path = require("path");
59
59
  path = require_chunk.__toESM(path, 1);
60
60
  let module$1 = require("module");
61
+ //#region src/addon/addon-api-factory.ts
62
+ /**
63
+ * Wrap a raw tRPC caller (from `createCallerFactory`) in a proxy that
64
+ * exposes each leaf procedure as `{ query, mutate, subscribe }`, matching
65
+ * the shape of `CreateTRPCClient<AppRouter>`.
66
+ *
67
+ * The tricky part: tRPC v11 callers expose nested routers as callable
68
+ * no-op functions (e.g. `caller.greet` is `typeof === 'function'`), so
69
+ * we can't use `typeof` to tell namespace from leaf. Instead, we return
70
+ * a unified proxy that handles BOTH behaviours:
71
+ *
72
+ * - Accessing `.query` / `.mutate` treats the current node as a leaf
73
+ * procedure and invokes `caller(input)` when called.
74
+ * - Accessing `.subscribe` throws (not supported on direct caller).
75
+ * - Accessing any other property descends into `caller[prop]` and
76
+ * returns a nested proxy.
77
+ */
78
+ function wrapCallerAsClient(caller) {
79
+ return new Proxy(Object.create(null), { get(_target, prop) {
80
+ if (typeof prop !== "string") return void 0;
81
+ if (prop === "query" || prop === "mutate") {
82
+ if (typeof caller !== "function") throw new Error(`Cannot call .${prop}() — this tRPC node is not a procedure`);
83
+ const fn = caller;
84
+ return (input) => fn(input);
85
+ }
86
+ if (prop === "subscribe") return () => {
87
+ throw new Error(`subscribe() is not supported on direct caller — use the WSS client for subscriptions`);
88
+ };
89
+ const child = caller?.[prop];
90
+ if (child === void 0 || child === null) return void 0;
91
+ return wrapCallerAsClient(child);
92
+ } });
93
+ }
94
+ var AddonApiFactory = class {
95
+ /**
96
+ * Create a direct caller -- calls tRPC procedures directly in-process.
97
+ * Zero network overhead. Used for in-process addons and dev mode.
98
+ *
99
+ * Returns `AddonApi` — the same tRPC client surface that broker-routed
100
+ * callers get. Callers treat it uniformly as `context.api` regardless
101
+ * of the underlying transport.
102
+ */
103
+ async createDirectCaller(options) {
104
+ const { initTRPC } = await import("@trpc/server");
105
+ return wrapCallerAsClient(initTRPC.create().createCallerFactory(options.router)(options.context));
106
+ }
107
+ };
108
+ //#endregion
109
+ //#region src/addon-routes/addon-route-registry.ts
110
+ /**
111
+ * Registry for dynamic HTTP routes registered by addons.
112
+ * Framework-agnostic — the server HTTP layer queries this registry to dispatch requests.
113
+ */
114
+ var AddonRouteRegistry = class {
115
+ routes = /* @__PURE__ */ new Map();
116
+ /**
117
+ * Register all routes from an addon's route provider.
118
+ */
119
+ registerRoutes(addonId, provider) {
120
+ const registered = provider.getRoutes().map((route) => {
121
+ return {
122
+ addonId,
123
+ route,
124
+ fullPath: `/addon/${addonId}${route.path.startsWith("/") ? route.path : `/${route.path}`}`
125
+ };
126
+ });
127
+ this.routes.set(addonId, registered);
128
+ }
129
+ /**
130
+ * Unregister all routes for an addon.
131
+ */
132
+ unregisterRoutes(addonId) {
133
+ this.routes.delete(addonId);
134
+ }
135
+ /**
136
+ * Match an incoming request method + path to a registered route.
137
+ * Supports simple path parameters (e.g., /items/:id).
138
+ */
139
+ matchRoute(method, path) {
140
+ const normalizedMethod = method.toUpperCase();
141
+ for (const registeredRoutes of this.routes.values()) for (const entry of registeredRoutes) {
142
+ if (entry.route.method !== normalizedMethod) continue;
143
+ const params = matchPath(entry.fullPath, path);
144
+ if (params !== null) return {
145
+ route: entry.route,
146
+ addonId: entry.addonId,
147
+ params
148
+ };
149
+ }
150
+ return null;
151
+ }
152
+ /**
153
+ * List all registered routes across all addons.
154
+ */
155
+ listRoutes() {
156
+ const result = [];
157
+ for (const registeredRoutes of this.routes.values()) for (const entry of registeredRoutes) result.push({
158
+ addonId: entry.addonId,
159
+ method: entry.route.method,
160
+ path: entry.fullPath,
161
+ access: entry.route.access,
162
+ description: entry.route.description
163
+ });
164
+ return result;
165
+ }
166
+ };
167
+ /**
168
+ * Match a route pattern against an actual path.
169
+ * Supports :param segments (e.g., /addon/:addonId/items/:id).
170
+ * Returns extracted params or null if no match.
171
+ */
172
+ function matchPath(pattern, path) {
173
+ const patternParts = pattern.split("/").filter(Boolean);
174
+ const pathParts = path.split("/").filter(Boolean);
175
+ if (patternParts.length !== pathParts.length) return null;
176
+ const params = {};
177
+ for (let i = 0; i < patternParts.length; i++) {
178
+ const patternPart = patternParts[i];
179
+ const pathPart = pathParts[i];
180
+ if (patternPart.startsWith(":")) params[patternPart.slice(1)] = pathPart;
181
+ else if (patternPart !== pathPart) return null;
182
+ }
183
+ return params;
184
+ }
185
+ //#endregion
186
+ //#region src/auth/scope-matcher.ts
187
+ /**
188
+ * True if the scope set grants `access` on device-scoped capabilities.
189
+ * A `category:device` grant covers every device cap (the broad grant).
190
+ * Mirrors the OR-semantics of the existing token scope matcher.
191
+ */
192
+ function scopesAllowDeviceCap(scopes, access) {
193
+ return scopes.some((s) => s.type === "category" && s.target === "device" && s.access.includes(access));
194
+ }
195
+ //#endregion
196
+ //#region src/events/event-bus.ts
197
+ var EventBus = class {
198
+ listeners = /* @__PURE__ */ new Map();
199
+ on(event, callback) {
200
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
201
+ const set = this.listeners.get(event);
202
+ set.add(callback);
203
+ return () => set.delete(callback);
204
+ }
205
+ emit(event, data) {
206
+ const set = this.listeners.get(event);
207
+ if (!set) return;
208
+ for (const cb of set) try {
209
+ cb(data);
210
+ } catch {}
211
+ }
212
+ once(event, callback) {
213
+ const unsub = this.on(event, (data) => {
214
+ unsub();
215
+ callback(data);
216
+ });
217
+ return unsub;
218
+ }
219
+ removeAllListeners(event) {
220
+ if (event) this.listeners.delete(event);
221
+ else this.listeners.clear();
222
+ }
223
+ listenerCount(event) {
224
+ return this.listeners.get(event)?.size ?? 0;
225
+ }
226
+ };
227
+ //#endregion
228
+ //#region src/events/system-event-bus.ts
229
+ var EventRingBuffer = class {
230
+ capacity;
231
+ buffer;
232
+ head = 0;
233
+ count = 0;
234
+ constructor(capacity) {
235
+ this.capacity = capacity;
236
+ this.buffer = Array.from({ length: capacity });
237
+ }
238
+ push(event) {
239
+ this.buffer[this.head] = event;
240
+ this.head = (this.head + 1) % this.capacity;
241
+ if (this.count < this.capacity) this.count++;
242
+ }
243
+ getAll() {
244
+ const result = [];
245
+ for (let i = 0; i < this.count; i++) {
246
+ const index = (this.head - 1 - i + this.capacity) % this.capacity;
247
+ result.push(this.buffer[index]);
248
+ }
249
+ return result;
250
+ }
251
+ query(filter, limit) {
252
+ let result = [...this.getAll()];
253
+ if (filter) result = result.filter((event) => matchesFilter(event, filter));
254
+ if (limit !== void 0 && limit > 0) return result.slice(0, limit);
255
+ return result;
256
+ }
257
+ };
258
+ function matchesCategory(eventCategory, filterCategory) {
259
+ if (filterCategory.endsWith(".*")) {
260
+ const prefix = filterCategory.slice(0, -2);
261
+ return eventCategory.startsWith(prefix + ".") || eventCategory === prefix;
262
+ }
263
+ return eventCategory === filterCategory;
264
+ }
265
+ function matchesFilter(event, filter) {
266
+ if (filter.category) {
267
+ const raw = filter.category;
268
+ if (!(Array.isArray(raw) ? raw.map(String) : [String(raw)]).some((cat) => matchesCategory(event.category, cat))) return false;
269
+ }
270
+ if (filter.source) {
271
+ if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
272
+ }
273
+ if (filter.agentId) {
274
+ const eventNodeId = event.source.nodeId;
275
+ if (!eventNodeId || !eventNodeId.startsWith(filter.agentId)) return false;
276
+ }
277
+ if (filter.addonId) {
278
+ if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
279
+ }
280
+ if (filter.deviceId !== void 0) {
281
+ if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
282
+ }
283
+ if (filter.since && event.timestamp < filter.since) return false;
284
+ return true;
285
+ }
286
+ var SystemEventBus = class {
287
+ ringBuffer;
288
+ subscribers = [];
289
+ constructor(bufferSize = 1e4) {
290
+ this.ringBuffer = new EventRingBuffer(bufferSize);
291
+ }
292
+ emit(event) {
293
+ this.ringBuffer.push(event);
294
+ for (const subscriber of this.subscribers) if (matchesFilter(event, subscriber.filter)) queueMicrotask(() => {
295
+ try {
296
+ subscriber.callback(event);
297
+ } catch (err) {
298
+ console.error(`[EventBus] Subscriber error for ${event.category}:`, err);
299
+ }
300
+ });
301
+ }
302
+ /**
303
+ * Subscribe to events matching a filter.
304
+ * When called with a typed category filter, the handler receives typed event data.
305
+ */
306
+ subscribe(filter, handler) {
307
+ const subscriber = {
308
+ filter,
309
+ callback: handler
310
+ };
311
+ this.subscribers.push(subscriber);
312
+ return () => {
313
+ const index = this.subscribers.indexOf(subscriber);
314
+ if (index !== -1) this.subscribers.splice(index, 1);
315
+ };
316
+ }
317
+ getRecent(filter, limit) {
318
+ return this.ringBuffer.query(filter, limit);
319
+ }
320
+ };
321
+ //#endregion
322
+ //#region src/feature/feature-manager.ts
323
+ var FeatureManager = class {
324
+ configReader;
325
+ constructor(configReader) {
326
+ this.configReader = configReader;
327
+ }
328
+ isEnabled(flag) {
329
+ return this.configReader.features[flag];
330
+ }
331
+ getManifest() {
332
+ return { ...this.configReader.features };
333
+ }
334
+ };
335
+ //#endregion
336
+ //#region src/http/data-plane-registry.ts
337
+ function trimSlashes(s) {
338
+ return s.replace(/^\/+/, "").replace(/\/+$/, "");
339
+ }
340
+ var DataPlaneRegistry = class {
341
+ byAddon = /* @__PURE__ */ new Map();
342
+ /** Replace ALL endpoints for an addon (idempotent re-mount). */
343
+ registerAddon(addonId, endpoints) {
344
+ if (endpoints.length === 0) {
345
+ this.byAddon.delete(addonId);
346
+ return;
347
+ }
348
+ this.byAddon.set(addonId, [...endpoints]);
349
+ }
350
+ /** Drop an addon's endpoints (stop / disconnect). */
351
+ unregisterAddon(addonId) {
352
+ this.byAddon.delete(addonId);
353
+ }
354
+ /** True when the addon has at least one data-plane registered. */
355
+ has(addonId) {
356
+ return this.byAddon.has(addonId);
357
+ }
358
+ /**
359
+ * Match `subPath` (the part after `/addon/<addonId>/`, with or without a
360
+ * leading slash, NO query string) against the addon's prefixes. Longest prefix
361
+ * wins. Returns the matched endpoint + the remaining path the addon handler sees.
362
+ */
363
+ match(addonId, subPath) {
364
+ const endpoints = this.byAddon.get(addonId);
365
+ if (!endpoints) return null;
366
+ const normalized = `/${trimSlashes(subPath)}`;
367
+ let best = null;
368
+ for (const endpoint of endpoints) {
369
+ const prefix = `/${trimSlashes(endpoint.prefix)}`;
370
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
371
+ const rest = normalized.slice(prefix.length) || "/";
372
+ if (best === null || prefix.length > best.len) best = {
373
+ endpoint,
374
+ rest,
375
+ len: prefix.length
376
+ };
377
+ }
378
+ }
379
+ if (best === null) return null;
380
+ return {
381
+ addonId,
382
+ endpoint: best.endpoint,
383
+ rest: best.rest
384
+ };
385
+ }
386
+ };
387
+ //#endregion
388
+ //#region src/http/reverse-proxy.ts
389
+ /**
390
+ * Transport-agnostic reverse-proxy from the hub to an addon's `127.0.0.1`
391
+ * data-plane listener. Operates on RAW Node http objects (`request.raw` /
392
+ * `reply.raw`) so `@camstack/system` stays free of any HTTP-framework dependency.
393
+ *
394
+ * The hub authenticates first (its existing scoped-token / admin / cookie logic),
395
+ * then hands the raw request+response here. We forward method + headers (incl.
396
+ * `Range`) + body upstream — injecting the per-listener shared secret so the addon
397
+ * accepts the call — and pipe the upstream response straight back (status +
398
+ * headers + body), so `Content-Range`, backpressure and client-disconnect are all
399
+ * native. No bytes are buffered; nothing crosses the cap channel.
400
+ */
401
+ /** Reverse-proxy one request to an addon data-plane listener. Fire-and-forget:
402
+ * the caller must `reply.hijack()` (or otherwise stop the framework from also
403
+ * responding) before calling this, since we drive `clientRes` directly. */
404
+ function proxyToUpstream(opts) {
405
+ const { baseUrl, secret, upstreamPath, clientReq, clientRes } = opts;
406
+ const base = new URL(baseUrl);
407
+ const headers = { ...clientReq.headers };
408
+ delete headers.host;
409
+ delete headers.connection;
410
+ delete headers["content-length"];
411
+ headers[require_dist.DATAPLANE_SECRET_HEADER] = secret;
412
+ const upstream = (0, node_http.request)({
413
+ protocol: base.protocol,
414
+ hostname: base.hostname,
415
+ port: base.port,
416
+ method: clientReq.method ?? "GET",
417
+ path: upstreamPath,
418
+ headers
419
+ }, (upRes) => {
420
+ clientRes.writeHead(upRes.statusCode ?? 502, upRes.headers);
421
+ upRes.pipe(clientRes);
422
+ });
423
+ upstream.on("error", (err) => {
424
+ if (!clientRes.headersSent) clientRes.writeHead(502, { "content-type": "application/json" });
425
+ clientRes.end(JSON.stringify({
426
+ error: "data-plane upstream error",
427
+ detail: String(err)
428
+ }));
429
+ });
430
+ clientRes.on("close", () => {
431
+ if (!clientRes.writableFinished && !upstream.destroyed) upstream.destroy();
432
+ });
433
+ clientReq.pipe(upstream);
434
+ }
435
+ //#endregion
61
436
  //#region src/kernel/heap-watch.ts
62
437
  /**
63
438
  * heap-watch — hub-main's own memory, on the record, and returned to the OS.
@@ -323,524 +698,6 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
323
698
  };
324
699
  }
325
700
  //#endregion
326
- //#region src/events/event-bus.ts
327
- var EventBus = class {
328
- listeners = /* @__PURE__ */ new Map();
329
- on(event, callback) {
330
- if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
331
- const set = this.listeners.get(event);
332
- set.add(callback);
333
- return () => set.delete(callback);
334
- }
335
- emit(event, data) {
336
- const set = this.listeners.get(event);
337
- if (!set) return;
338
- for (const cb of set) try {
339
- cb(data);
340
- } catch {}
341
- }
342
- once(event, callback) {
343
- const unsub = this.on(event, (data) => {
344
- unsub();
345
- callback(data);
346
- });
347
- return unsub;
348
- }
349
- removeAllListeners(event) {
350
- if (event) this.listeners.delete(event);
351
- else this.listeners.clear();
352
- }
353
- listenerCount(event) {
354
- return this.listeners.get(event)?.size ?? 0;
355
- }
356
- };
357
- //#endregion
358
- //#region src/http/data-plane-registry.ts
359
- function trimSlashes(s) {
360
- return s.replace(/^\/+/, "").replace(/\/+$/, "");
361
- }
362
- var DataPlaneRegistry = class {
363
- byAddon = /* @__PURE__ */ new Map();
364
- /** Replace ALL endpoints for an addon (idempotent re-mount). */
365
- registerAddon(addonId, endpoints) {
366
- if (endpoints.length === 0) {
367
- this.byAddon.delete(addonId);
368
- return;
369
- }
370
- this.byAddon.set(addonId, [...endpoints]);
371
- }
372
- /** Drop an addon's endpoints (stop / disconnect). */
373
- unregisterAddon(addonId) {
374
- this.byAddon.delete(addonId);
375
- }
376
- /** True when the addon has at least one data-plane registered. */
377
- has(addonId) {
378
- return this.byAddon.has(addonId);
379
- }
380
- /**
381
- * Match `subPath` (the part after `/addon/<addonId>/`, with or without a
382
- * leading slash, NO query string) against the addon's prefixes. Longest prefix
383
- * wins. Returns the matched endpoint + the remaining path the addon handler sees.
384
- */
385
- match(addonId, subPath) {
386
- const endpoints = this.byAddon.get(addonId);
387
- if (!endpoints) return null;
388
- const normalized = `/${trimSlashes(subPath)}`;
389
- let best = null;
390
- for (const endpoint of endpoints) {
391
- const prefix = `/${trimSlashes(endpoint.prefix)}`;
392
- if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
393
- const rest = normalized.slice(prefix.length) || "/";
394
- if (best === null || prefix.length > best.len) best = {
395
- endpoint,
396
- rest,
397
- len: prefix.length
398
- };
399
- }
400
- }
401
- if (best === null) return null;
402
- return {
403
- addonId,
404
- endpoint: best.endpoint,
405
- rest: best.rest
406
- };
407
- }
408
- };
409
- //#endregion
410
- //#region src/http/reverse-proxy.ts
411
- /**
412
- * Transport-agnostic reverse-proxy from the hub to an addon's `127.0.0.1`
413
- * data-plane listener. Operates on RAW Node http objects (`request.raw` /
414
- * `reply.raw`) so `@camstack/system` stays free of any HTTP-framework dependency.
415
- *
416
- * The hub authenticates first (its existing scoped-token / admin / cookie logic),
417
- * then hands the raw request+response here. We forward method + headers (incl.
418
- * `Range`) + body upstream — injecting the per-listener shared secret so the addon
419
- * accepts the call — and pipe the upstream response straight back (status +
420
- * headers + body), so `Content-Range`, backpressure and client-disconnect are all
421
- * native. No bytes are buffered; nothing crosses the cap channel.
422
- */
423
- /** Reverse-proxy one request to an addon data-plane listener. Fire-and-forget:
424
- * the caller must `reply.hijack()` (or otherwise stop the framework from also
425
- * responding) before calling this, since we drive `clientRes` directly. */
426
- function proxyToUpstream(opts) {
427
- const { baseUrl, secret, upstreamPath, clientReq, clientRes } = opts;
428
- const base = new URL(baseUrl);
429
- const headers = { ...clientReq.headers };
430
- delete headers.host;
431
- delete headers.connection;
432
- delete headers["content-length"];
433
- headers[require_dist.DATAPLANE_SECRET_HEADER] = secret;
434
- const upstream = (0, node_http.request)({
435
- protocol: base.protocol,
436
- hostname: base.hostname,
437
- port: base.port,
438
- method: clientReq.method ?? "GET",
439
- path: upstreamPath,
440
- headers
441
- }, (upRes) => {
442
- clientRes.writeHead(upRes.statusCode ?? 502, upRes.headers);
443
- upRes.pipe(clientRes);
444
- });
445
- upstream.on("error", (err) => {
446
- if (!clientRes.headersSent) clientRes.writeHead(502, { "content-type": "application/json" });
447
- clientRes.end(JSON.stringify({
448
- error: "data-plane upstream error",
449
- detail: String(err)
450
- }));
451
- });
452
- clientRes.on("close", () => {
453
- if (!clientRes.writableFinished && !upstream.destroyed) upstream.destroy();
454
- });
455
- clientReq.pipe(upstream);
456
- }
457
- //#endregion
458
- //#region src/python/python-env-manager.ts
459
- var execFileAsync$4 = (0, node_util.promisify)(node_child_process.execFile);
460
- var PythonEnvManager = class {
461
- venvPath;
462
- cachedProbe = null;
463
- constructor(dataDir) {
464
- this.venvPath = node_path.join(dataDir, ".venv");
465
- }
466
- async probe() {
467
- if (this.cachedProbe) return this.cachedProbe;
468
- for (const cmd of ["python3", "python"]) try {
469
- const { stdout } = await execFileAsync$4(cmd, ["--version"]);
470
- const version = stdout.trim().replace("Python ", "");
471
- const major = parseInt(version.split(".")[0] ?? "0", 10);
472
- const minor = parseInt(version.split(".")[1] ?? "0", 10);
473
- if (major < 3 || major === 3 && minor < 10) continue;
474
- const { stdout: pathOut } = await execFileAsync$4(cmd, ["-c", "import sys; print(sys.executable)"]);
475
- this.cachedProbe = {
476
- available: true,
477
- version,
478
- path: pathOut.trim()
479
- };
480
- return this.cachedProbe;
481
- } catch {
482
- continue;
483
- }
484
- this.cachedProbe = { available: false };
485
- return this.cachedProbe;
486
- }
487
- async ensure(options) {
488
- const probe = await this.probe();
489
- if (!probe.available || !probe.path) throw new Error("Python 3.10+ is required but not found on this system");
490
- if (!node_fs.existsSync(node_path.join(this.venvPath, "bin", "python"))) await execFileAsync$4(probe.path, [
491
- "-m",
492
- "venv",
493
- this.venvPath
494
- ]);
495
- const venvPython = node_path.join(this.venvPath, "bin", "python");
496
- if (options.packages.length > 0) await execFileAsync$4(venvPython, [
497
- "-m",
498
- "pip",
499
- "install",
500
- "-q",
501
- ...options.packages
502
- ]);
503
- return {
504
- pythonPath: venvPython,
505
- venvPath: this.venvPath,
506
- spawn: (script, args) => {
507
- return (0, node_child_process.spawn)(venvPython, [script, ...args]);
508
- }
509
- };
510
- }
511
- spawn(script, args) {
512
- const venvPython = node_path.join(this.venvPath, "bin", "python");
513
- if (!node_fs.existsSync(venvPython)) throw new Error("Python venv not initialized. Call ensure() first.");
514
- return (0, node_child_process.spawn)(venvPython, [script, ...args]);
515
- }
516
- };
517
- //#endregion
518
- //#region src/pipeline/pipeline-validator.ts
519
- var PipelineValidator = class {
520
- stepExists;
521
- constructor(stepExists) {
522
- this.stepExists = stepExists;
523
- }
524
- validate(config) {
525
- const errors = [];
526
- const warnings = [];
527
- for (const node of config.video) this.validateNode(node, errors, warnings);
528
- if (config.audio) this.validateNode(config.audio, errors, warnings);
529
- return {
530
- valid: errors.length === 0,
531
- errors,
532
- warnings
533
- };
534
- }
535
- validateNode(node, errors, warnings) {
536
- if (!this.stepExists(node.addon)) {
537
- errors.push({
538
- step: node.step,
539
- addon: node.addon,
540
- message: `Step "${node.addon}" has no provider — the addon that provided it may have been removed`,
541
- severity: "error"
542
- });
543
- return;
544
- }
545
- if (node.children) for (const child of node.children) this.validateNode(child, errors, warnings);
546
- }
547
- };
548
- //#endregion
549
- //#region src/pipeline/pipeline-runner.ts
550
- var PipelineRunner = class {
551
- addonResolver;
552
- constructor(addonResolver) {
553
- this.addonResolver = addonResolver;
554
- }
555
- async run(frame, config) {
556
- const startTime = performance.now();
557
- const results = [];
558
- const timings = {};
559
- for (const rootNode of config.video) await this.executeNode(rootNode, frame, null, results, timings);
560
- if (config.audio) await this.executeNode(config.audio, frame, null, results, timings);
561
- return {
562
- results,
563
- totalMs: performance.now() - startTime,
564
- timings,
565
- frameTimestamp: frame.timestamp
566
- };
567
- }
568
- async executeNode(node, frame, parentDetection, results, timings) {
569
- const resultId = (0, node_crypto.randomUUID)();
570
- const stepStart = performance.now();
571
- try {
572
- const effectiveConfig = { ...node.configOverride };
573
- const caller = await this.addonResolver.resolve(node.addon, effectiveConfig);
574
- let output;
575
- if (parentDetection) {
576
- const cropInput = {
577
- frame,
578
- roi: parentDetection.det.bbox,
579
- parentDetection: parentDetection.det
580
- };
581
- output = await caller.process(cropInput);
582
- } else output = await caller.process(frame);
583
- const stepMs = performance.now() - stepStart;
584
- const slot = "detections" in output ? "detector" : "crops" in output ? "cropper" : "classifier";
585
- const stepResult = {
586
- addon: node.addon,
587
- slot,
588
- output,
589
- parentResultId: parentDetection?.resultId,
590
- resultId,
591
- inferenceMs: output.inferenceMs,
592
- preprocessMs: 0,
593
- totalMs: stepMs
594
- };
595
- results.push(stepResult);
596
- timings[node.step] = stepMs;
597
- if (node.children?.length) {
598
- const detections = "detections" in output ? output.detections : "crops" in output ? output.crops : [];
599
- await this.executeChildren(node.children, frame, detections, resultId, results, timings);
600
- }
601
- } catch (error) {
602
- const stepMs = performance.now() - stepStart;
603
- const message = require_dist.errMsg(error);
604
- results.push({
605
- addon: node.addon,
606
- slot: "detector",
607
- output: {
608
- detections: [],
609
- inferenceMs: 0,
610
- modelId: ""
611
- },
612
- parentResultId: parentDetection?.resultId,
613
- resultId,
614
- inferenceMs: 0,
615
- preprocessMs: 0,
616
- totalMs: stepMs,
617
- error: {
618
- code: "ADDON_ERROR",
619
- message,
620
- childrenSkipped: true
621
- }
622
- });
623
- }
624
- }
625
- async executeChildren(children, frame, parentDetections, parentResultId, results, timings) {
626
- const promises = [];
627
- for (const detection of parentDetections) {
628
- if (detection.bbox.w < 1 || detection.bbox.h < 1) continue;
629
- for (const child of children) promises.push(this.executeNode(child, frame, {
630
- det: detection,
631
- resultId: parentResultId
632
- }, results, timings));
633
- }
634
- await Promise.allSettled(promises);
635
- }
636
- };
637
- //#endregion
638
- //#region src/pipeline/engine-manager-resolver.ts
639
- /**
640
- * Adapter that wraps an IEngineManager + addonConfigs as an IAddonResolver.
641
- *
642
- * Used for backwards compatibility — the in-process AddonEngineManager
643
- * still uses the old getOrCreateEngine(addonId, globalConfig, override) API.
644
- * This adapter bridges it to the new resolve(addonId, config) interface.
645
- */
646
- var EngineManagerResolver = class {
647
- engineManager;
648
- addonConfigs;
649
- constructor(engineManager, addonConfigs) {
650
- this.engineManager = engineManager;
651
- this.addonConfigs = addonConfigs;
652
- }
653
- async resolve(addonId, config) {
654
- const globalConfig = this.addonConfigs.get(addonId) ?? {};
655
- const engine = await this.engineManager.getOrCreateEngine(addonId, globalConfig, config);
656
- return { process: async (input) => {
657
- return engine.process(input);
658
- } };
659
- }
660
- async shutdownAll() {
661
- await this.engineManager.shutdownAll();
662
- }
663
- };
664
- //#endregion
665
- //#region src/network/network-quality.ts
666
- var NetworkQualityTracker = class NetworkQualityTracker {
667
- devices = /* @__PURE__ */ new Map();
668
- static MAX_SAMPLES = 30;
669
- reportStreamStats(deviceId, streamId, bitrateKbps, packetLoss) {
670
- const device = this.getOrCreateDevice(deviceId);
671
- let stream = device.streams.get(streamId);
672
- if (!stream) {
673
- stream = {
674
- nominalBitrateKbps: bitrateKbps,
675
- samples: [],
676
- peakBitrateKbps: 0,
677
- packetLossSamples: [],
678
- lastUpdated: 0
679
- };
680
- device.streams.set(streamId, stream);
681
- }
682
- stream.samples.push(bitrateKbps);
683
- if (stream.samples.length > NetworkQualityTracker.MAX_SAMPLES) stream.samples.shift();
684
- stream.peakBitrateKbps = Math.max(stream.peakBitrateKbps, bitrateKbps);
685
- if (packetLoss !== void 0) {
686
- stream.packetLossSamples.push(packetLoss);
687
- if (stream.packetLossSamples.length > NetworkQualityTracker.MAX_SAMPLES) stream.packetLossSamples.shift();
688
- }
689
- stream.lastUpdated = Date.now();
690
- }
691
- reportClientStats(deviceId, stats) {
692
- const device = this.getOrCreateDevice(deviceId);
693
- device.client = {
694
- ...stats,
695
- lastUpdated: Date.now()
696
- };
697
- }
698
- getDeviceStats(deviceId) {
699
- const device = this.devices.get(deviceId);
700
- if (!device) return null;
701
- const streams = {};
702
- for (const [streamId, s] of device.streams) {
703
- const avg = (arr) => arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length;
704
- streams[streamId] = {
705
- nominalBitrateKbps: s.nominalBitrateKbps,
706
- observedBitrateKbps: Math.round(avg(s.samples)),
707
- peakBitrateKbps: s.peakBitrateKbps,
708
- packetLossPercent: Math.round(avg(s.packetLossSamples) * 10) / 10,
709
- lastUpdated: s.lastUpdated
710
- };
711
- }
712
- return {
713
- deviceId,
714
- streams,
715
- client: device.client ?? void 0
716
- };
717
- }
718
- getAllStats() {
719
- const results = [];
720
- for (const deviceId of this.devices.keys()) {
721
- const stats = this.getDeviceStats(deviceId);
722
- if (stats) results.push(stats);
723
- }
724
- return results;
725
- }
726
- getOrCreateDevice(deviceId) {
727
- let device = this.devices.get(deviceId);
728
- if (!device) {
729
- device = {
730
- streams: /* @__PURE__ */ new Map(),
731
- client: null
732
- };
733
- this.devices.set(deviceId, device);
734
- }
735
- return device;
736
- }
737
- };
738
- //#endregion
739
- //#region src/repl/repl-engine.ts
740
- var EXECUTION_TIMEOUT_MS = 5e3;
741
- var COMPLETION_TIMEOUT_MS = 1e3;
742
- var ReplEngine = class {
743
- contextProvider;
744
- constructor(contextProvider) {
745
- this.contextProvider = contextProvider;
746
- }
747
- async execute(code, context) {
748
- const start = Date.now();
749
- const sandbox = await this.buildSandbox(context);
750
- try {
751
- const vmContext = node_vm.createContext(sandbox);
752
- let result = new node_vm.Script(code).runInContext(vmContext, { timeout: EXECUTION_TIMEOUT_MS });
753
- if (result && typeof result === "object" && "then" in result && typeof result.then === "function") result = await Promise.race([result, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Async timeout (5s)")), EXECUTION_TIMEOUT_MS))]);
754
- const duration = Date.now() - start;
755
- if (result === void 0) return {
756
- output: "undefined",
757
- type: "void",
758
- duration
759
- };
760
- return {
761
- output: node_util.inspect(result, {
762
- depth: 4,
763
- colors: false,
764
- maxArrayLength: 50,
765
- maxStringLength: 1e3
766
- }),
767
- type: "value",
768
- duration
769
- };
770
- } catch (err) {
771
- return {
772
- output: err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : String(err),
773
- type: "error",
774
- duration: Date.now() - start
775
- };
776
- }
777
- }
778
- async getCompletions(partial, context) {
779
- const sandbox = await this.buildSandbox(context);
780
- const keys = Object.keys(sandbox);
781
- if (!partial) return keys;
782
- const lastDot = partial.lastIndexOf(".");
783
- if (lastDot === -1) return keys.filter((k) => k.startsWith(partial));
784
- const objPath = partial.substring(0, lastDot);
785
- const propPrefix = partial.substring(lastDot + 1);
786
- try {
787
- const vmContext = node_vm.createContext(sandbox);
788
- const obj = node_vm.runInContext(objPath, vmContext, { timeout: COMPLETION_TIMEOUT_MS });
789
- if (obj && typeof obj === "object") {
790
- const proto = Object.getPrototypeOf(obj);
791
- const objKeys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertyNames(proto ?? {})).filter((k) => k.startsWith(propPrefix));
792
- return [...new Set(objKeys)].map((k) => `${objPath}.${k}`);
793
- }
794
- } catch {}
795
- return [];
796
- }
797
- async buildSandbox(context) {
798
- const base = {
799
- console: {
800
- log: (...args) => node_util.inspect(args),
801
- warn: (...args) => node_util.inspect(args)
802
- },
803
- JSON,
804
- Math,
805
- Date,
806
- Array,
807
- Object,
808
- String,
809
- Number,
810
- Boolean,
811
- Map,
812
- Set,
813
- Promise,
814
- Buffer,
815
- setTimeout: void 0,
816
- setInterval: void 0,
817
- fetch: void 0,
818
- require: void 0,
819
- process: void 0,
820
- ...context.variables
821
- };
822
- switch (context.scope.type) {
823
- case "system": return {
824
- ...base,
825
- ...await this.contextProvider.getSystemSandbox()
826
- };
827
- case "device": return {
828
- ...base,
829
- ...await this.contextProvider.getDeviceSandbox(context.scope.deviceId)
830
- };
831
- case "provider": return {
832
- ...base,
833
- ...await this.contextProvider.getProviderSandbox(context.scope.providerId)
834
- };
835
- case "addon": return {
836
- ...base,
837
- ...await this.contextProvider.getAddonSandbox(context.scope.addonId)
838
- };
839
- default: return base;
840
- }
841
- }
842
- };
843
- //#endregion
844
701
  //#region src/lifecycle/lifecycle-state-machine.ts
845
702
  var VALID_TRANSITIONS = {
846
703
  stopped: ["starting", "disabled"],
@@ -852,195 +709,87 @@ var VALID_TRANSITIONS = {
852
709
  running: ["stopping", "error"],
853
710
  stopping: ["stopped", "error"],
854
711
  error: [
855
- "starting",
856
- "stopped",
857
- "disabled"
858
- ],
859
- disabled: ["stopped"]
860
- };
861
- var LifecycleStateMachine = class {
862
- elementId;
863
- elementType;
864
- eventBus;
865
- logger;
866
- _state = "stopped";
867
- _error;
868
- _startedAt;
869
- _stoppedAt;
870
- _restartCount = 0;
871
- _hasStartedOnce = false;
872
- constructor(elementId, elementType, eventBus, logger) {
873
- this.elementId = elementId;
874
- this.elementType = elementType;
875
- this.eventBus = eventBus;
876
- this.logger = logger;
877
- }
878
- get state() {
879
- return this._state;
880
- }
881
- getStatus() {
882
- return {
883
- state: this._state,
884
- error: this._error,
885
- startedAt: this._startedAt,
886
- stoppedAt: this._stoppedAt,
887
- restartCount: this._restartCount,
888
- uptime: this._state === "running" && this._startedAt ? Date.now() - this._startedAt : 0
889
- };
890
- }
891
- transition(to, error) {
892
- const from = this._state;
893
- if (!this.isValidTransition(from, to)) {
894
- this.logger.warn("Invalid state transition", { meta: {
895
- from,
896
- to
897
- } });
898
- return false;
899
- }
900
- this._state = to;
901
- this._error = error;
902
- if (to === "running") {
903
- if (this._hasStartedOnce) this._restartCount++;
904
- this._hasStartedOnce = true;
905
- this._startedAt = Date.now();
906
- this._stoppedAt = void 0;
907
- }
908
- if (to === "stopped" || to === "error" || to === "disabled") this._stoppedAt = Date.now();
909
- this.logger.info("State transition", { meta: {
910
- from,
911
- to,
912
- error
913
- } });
914
- this.eventBus.emit({
915
- id: (0, node_crypto.randomUUID)(),
916
- timestamp: /* @__PURE__ */ new Date(),
917
- source: {
918
- type: this.elementType,
919
- id: this.elementId
920
- },
921
- category: `${this.elementType}.state.${to}`,
922
- data: {
923
- from,
924
- to,
925
- error,
926
- elementId: this.elementId
927
- }
928
- });
929
- return true;
930
- }
931
- incrementRestartCount() {
932
- this._restartCount++;
933
- }
934
- isValidTransition(from, to) {
935
- return VALID_TRANSITIONS[from]?.includes(to) ?? false;
936
- }
937
- };
938
- //#endregion
939
- //#region src/feature/feature-manager.ts
940
- var FeatureManager = class {
941
- configReader;
942
- constructor(configReader) {
943
- this.configReader = configReader;
944
- }
945
- isEnabled(flag) {
946
- return this.configReader.features[flag];
947
- }
948
- getManifest() {
949
- return { ...this.configReader.features };
950
- }
951
- };
952
- //#endregion
953
- //#region src/events/system-event-bus.ts
954
- var EventRingBuffer = class {
955
- capacity;
956
- buffer;
957
- head = 0;
958
- count = 0;
959
- constructor(capacity) {
960
- this.capacity = capacity;
961
- this.buffer = Array.from({ length: capacity });
962
- }
963
- push(event) {
964
- this.buffer[this.head] = event;
965
- this.head = (this.head + 1) % this.capacity;
966
- if (this.count < this.capacity) this.count++;
967
- }
968
- getAll() {
969
- const result = [];
970
- for (let i = 0; i < this.count; i++) {
971
- const index = (this.head - 1 - i + this.capacity) % this.capacity;
972
- result.push(this.buffer[index]);
973
- }
974
- return result;
975
- }
976
- query(filter, limit) {
977
- let result = [...this.getAll()];
978
- if (filter) result = result.filter((event) => matchesFilter(event, filter));
979
- if (limit !== void 0 && limit > 0) return result.slice(0, limit);
980
- return result;
981
- }
982
- };
983
- function matchesCategory(eventCategory, filterCategory) {
984
- if (filterCategory.endsWith(".*")) {
985
- const prefix = filterCategory.slice(0, -2);
986
- return eventCategory.startsWith(prefix + ".") || eventCategory === prefix;
987
- }
988
- return eventCategory === filterCategory;
989
- }
990
- function matchesFilter(event, filter) {
991
- if (filter.category) {
992
- const raw = filter.category;
993
- if (!(Array.isArray(raw) ? raw.map(String) : [String(raw)]).some((cat) => matchesCategory(event.category, cat))) return false;
994
- }
995
- if (filter.source) {
996
- if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
997
- }
998
- if (filter.agentId) {
999
- const eventNodeId = event.source.nodeId;
1000
- if (!eventNodeId || !eventNodeId.startsWith(filter.agentId)) return false;
1001
- }
1002
- if (filter.addonId) {
1003
- if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
712
+ "starting",
713
+ "stopped",
714
+ "disabled"
715
+ ],
716
+ disabled: ["stopped"]
717
+ };
718
+ var LifecycleStateMachine = class {
719
+ elementId;
720
+ elementType;
721
+ eventBus;
722
+ logger;
723
+ _state = "stopped";
724
+ _error;
725
+ _startedAt;
726
+ _stoppedAt;
727
+ _restartCount = 0;
728
+ _hasStartedOnce = false;
729
+ constructor(elementId, elementType, eventBus, logger) {
730
+ this.elementId = elementId;
731
+ this.elementType = elementType;
732
+ this.eventBus = eventBus;
733
+ this.logger = logger;
1004
734
  }
1005
- if (filter.deviceId !== void 0) {
1006
- if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
735
+ get state() {
736
+ return this._state;
1007
737
  }
1008
- if (filter.since && event.timestamp < filter.since) return false;
1009
- return true;
1010
- }
1011
- var SystemEventBus = class {
1012
- ringBuffer;
1013
- subscribers = [];
1014
- constructor(bufferSize = 1e4) {
1015
- this.ringBuffer = new EventRingBuffer(bufferSize);
738
+ getStatus() {
739
+ return {
740
+ state: this._state,
741
+ error: this._error,
742
+ startedAt: this._startedAt,
743
+ stoppedAt: this._stoppedAt,
744
+ restartCount: this._restartCount,
745
+ uptime: this._state === "running" && this._startedAt ? Date.now() - this._startedAt : 0
746
+ };
1016
747
  }
1017
- emit(event) {
1018
- this.ringBuffer.push(event);
1019
- for (const subscriber of this.subscribers) if (matchesFilter(event, subscriber.filter)) queueMicrotask(() => {
1020
- try {
1021
- subscriber.callback(event);
1022
- } catch (err) {
1023
- console.error(`[EventBus] Subscriber error for ${event.category}:`, err);
748
+ transition(to, error) {
749
+ const from = this._state;
750
+ if (!this.isValidTransition(from, to)) {
751
+ this.logger.warn("Invalid state transition", { meta: {
752
+ from,
753
+ to
754
+ } });
755
+ return false;
756
+ }
757
+ this._state = to;
758
+ this._error = error;
759
+ if (to === "running") {
760
+ if (this._hasStartedOnce) this._restartCount++;
761
+ this._hasStartedOnce = true;
762
+ this._startedAt = Date.now();
763
+ this._stoppedAt = void 0;
764
+ }
765
+ if (to === "stopped" || to === "error" || to === "disabled") this._stoppedAt = Date.now();
766
+ this.logger.info("State transition", { meta: {
767
+ from,
768
+ to,
769
+ error
770
+ } });
771
+ this.eventBus.emit({
772
+ id: (0, node_crypto.randomUUID)(),
773
+ timestamp: /* @__PURE__ */ new Date(),
774
+ source: {
775
+ type: this.elementType,
776
+ id: this.elementId
777
+ },
778
+ category: `${this.elementType}.state.${to}`,
779
+ data: {
780
+ from,
781
+ to,
782
+ error,
783
+ elementId: this.elementId
1024
784
  }
1025
785
  });
786
+ return true;
1026
787
  }
1027
- /**
1028
- * Subscribe to events matching a filter.
1029
- * When called with a typed category filter, the handler receives typed event data.
1030
- */
1031
- subscribe(filter, handler) {
1032
- const subscriber = {
1033
- filter,
1034
- callback: handler
1035
- };
1036
- this.subscribers.push(subscriber);
1037
- return () => {
1038
- const index = this.subscribers.indexOf(subscriber);
1039
- if (index !== -1) this.subscribers.splice(index, 1);
1040
- };
788
+ incrementRestartCount() {
789
+ this._restartCount++;
1041
790
  }
1042
- getRecent(filter, limit) {
1043
- return this.ringBuffer.query(filter, limit);
791
+ isValidTransition(from, to) {
792
+ return VALID_TRANSITIONS[from]?.includes(to) ?? false;
1044
793
  }
1045
794
  };
1046
795
  //#endregion
@@ -1475,92 +1224,601 @@ var LogManager = class {
1475
1224
  } catch {}
1476
1225
  });
1477
1226
  }
1478
- enrichDeviceName(entry) {
1479
- if (!this.deviceNameLookup) return entry;
1480
- const tags = entry.tags;
1481
- if (!tags) return entry;
1482
- const deviceId = tags.deviceId;
1483
- if (typeof deviceId !== "number" || !Number.isFinite(deviceId)) return entry;
1484
- if (typeof tags.deviceName === "string") return entry;
1485
- const name = this.deviceNameLookup(deviceId);
1486
- if (!name) return entry;
1227
+ enrichDeviceName(entry) {
1228
+ if (!this.deviceNameLookup) return entry;
1229
+ const tags = entry.tags;
1230
+ if (!tags) return entry;
1231
+ const deviceId = tags.deviceId;
1232
+ if (typeof deviceId !== "number" || !Number.isFinite(deviceId)) return entry;
1233
+ if (typeof tags.deviceName === "string") return entry;
1234
+ const name = this.deviceNameLookup(deviceId);
1235
+ if (!name) return entry;
1236
+ return {
1237
+ ...entry,
1238
+ tags: {
1239
+ ...tags,
1240
+ deviceName: name
1241
+ }
1242
+ };
1243
+ }
1244
+ /** Subscribe to live logs matching a filter. Returns an unsubscribe function. */
1245
+ subscribe(filter, callback) {
1246
+ const id = Math.random().toString(36).slice(2);
1247
+ this.subscribers.set(id, {
1248
+ filter,
1249
+ callback
1250
+ });
1251
+ return () => {
1252
+ this.subscribers.delete(id);
1253
+ };
1254
+ }
1255
+ matchesFilter(entry, filter) {
1256
+ if (filter.level) {
1257
+ const levels = [
1258
+ "debug",
1259
+ "info",
1260
+ "warn",
1261
+ "error"
1262
+ ];
1263
+ const minIdx = levels.indexOf(filter.level);
1264
+ if (levels.indexOf(entry.level) < minIdx) return false;
1265
+ }
1266
+ if (filter.tags) {
1267
+ if (!matchesLogTags(entry.tags, filter.tags)) return false;
1268
+ }
1269
+ return true;
1270
+ }
1271
+ /**
1272
+ * Register a log destination.
1273
+ *
1274
+ * @param opts.replay If true (default), replay every entry currently in
1275
+ * the ring buffer to the new destination before live
1276
+ * traffic starts. Lets a destination added mid-boot
1277
+ * still receive boot-time log entries.
1278
+ */
1279
+ /** Apply new in-memory buffer limits at runtime (see PartitionedLogBuffer). */
1280
+ reconfigureBuffer(options) {
1281
+ this.ringBuffer.reconfigure(options);
1282
+ }
1283
+ /** Set the per-destination enablement check (see {@link isDestinationEnabled}). */
1284
+ setDestinationEnabledCheck(check) {
1285
+ this.isDestinationEnabled = check ?? void 0;
1286
+ }
1287
+ addDestination(dest, opts = {}) {
1288
+ const replay = opts.replay ?? true;
1289
+ this.destinations.push(opts.addonId === void 0 ? { dest } : {
1290
+ dest,
1291
+ addonId: opts.addonId
1292
+ });
1293
+ if (replay) for (const entry of this.ringBuffer.query({})) try {
1294
+ dest.write(entry);
1295
+ } catch {}
1296
+ }
1297
+ removeDestination(dest) {
1298
+ const index = this.destinations.findIndex((d) => d.dest === dest);
1299
+ if (index !== -1) this.destinations.splice(index, 1);
1300
+ }
1301
+ query(filter) {
1302
+ return this.ringBuffer.query(filter);
1303
+ }
1304
+ /**
1305
+ * Drop ring-buffer entries matching `filter`. Returns the count
1306
+ * removed. Backs the UI's "Clear logs" admin action so an operator
1307
+ * can wipe the historical window for a scope (per-device, per-addon,
1308
+ * agent-level) without restarting the server. External destinations
1309
+ * (file, forwarder) are NOT touched — only the in-memory ring.
1310
+ */
1311
+ clear(filter = {}) {
1312
+ return this.ringBuffer.clear(filter);
1313
+ }
1314
+ };
1315
+ //#endregion
1316
+ //#region src/network/network-quality.ts
1317
+ var NetworkQualityTracker = class NetworkQualityTracker {
1318
+ devices = /* @__PURE__ */ new Map();
1319
+ static MAX_SAMPLES = 30;
1320
+ reportStreamStats(deviceId, streamId, bitrateKbps, packetLoss) {
1321
+ const device = this.getOrCreateDevice(deviceId);
1322
+ let stream = device.streams.get(streamId);
1323
+ if (!stream) {
1324
+ stream = {
1325
+ nominalBitrateKbps: bitrateKbps,
1326
+ samples: [],
1327
+ peakBitrateKbps: 0,
1328
+ packetLossSamples: [],
1329
+ lastUpdated: 0
1330
+ };
1331
+ device.streams.set(streamId, stream);
1332
+ }
1333
+ stream.samples.push(bitrateKbps);
1334
+ if (stream.samples.length > NetworkQualityTracker.MAX_SAMPLES) stream.samples.shift();
1335
+ stream.peakBitrateKbps = Math.max(stream.peakBitrateKbps, bitrateKbps);
1336
+ if (packetLoss !== void 0) {
1337
+ stream.packetLossSamples.push(packetLoss);
1338
+ if (stream.packetLossSamples.length > NetworkQualityTracker.MAX_SAMPLES) stream.packetLossSamples.shift();
1339
+ }
1340
+ stream.lastUpdated = Date.now();
1341
+ }
1342
+ reportClientStats(deviceId, stats) {
1343
+ const device = this.getOrCreateDevice(deviceId);
1344
+ device.client = {
1345
+ ...stats,
1346
+ lastUpdated: Date.now()
1347
+ };
1348
+ }
1349
+ getDeviceStats(deviceId) {
1350
+ const device = this.devices.get(deviceId);
1351
+ if (!device) return null;
1352
+ const streams = {};
1353
+ for (const [streamId, s] of device.streams) {
1354
+ const avg = (arr) => arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length;
1355
+ streams[streamId] = {
1356
+ nominalBitrateKbps: s.nominalBitrateKbps,
1357
+ observedBitrateKbps: Math.round(avg(s.samples)),
1358
+ peakBitrateKbps: s.peakBitrateKbps,
1359
+ packetLossPercent: Math.round(avg(s.packetLossSamples) * 10) / 10,
1360
+ lastUpdated: s.lastUpdated
1361
+ };
1362
+ }
1363
+ return {
1364
+ deviceId,
1365
+ streams,
1366
+ client: device.client ?? void 0
1367
+ };
1368
+ }
1369
+ getAllStats() {
1370
+ const results = [];
1371
+ for (const deviceId of this.devices.keys()) {
1372
+ const stats = this.getDeviceStats(deviceId);
1373
+ if (stats) results.push(stats);
1374
+ }
1375
+ return results;
1376
+ }
1377
+ getOrCreateDevice(deviceId) {
1378
+ let device = this.devices.get(deviceId);
1379
+ if (!device) {
1380
+ device = {
1381
+ streams: /* @__PURE__ */ new Map(),
1382
+ client: null
1383
+ };
1384
+ this.devices.set(deviceId, device);
1385
+ }
1386
+ return device;
1387
+ }
1388
+ };
1389
+ //#endregion
1390
+ //#region src/notification/notification-service.ts
1391
+ /**
1392
+ * Central notification service that routes canonical notifications to
1393
+ * configured `(addonId, targetId)` targets. Framework-agnostic —
1394
+ * dependencies injected via constructor.
1395
+ *
1396
+ * Providers are resolved from the ICapabilityRegistry by `addonId` on each
1397
+ * call (proxy pattern). Falls back to a local map when no registry is
1398
+ * provided (tests / backward compat).
1399
+ */
1400
+ var NotificationService = class {
1401
+ logger;
1402
+ localProviders = /* @__PURE__ */ new Map();
1403
+ routing = /* @__PURE__ */ new Map();
1404
+ rateLimits = /* @__PURE__ */ new Map();
1405
+ lastSent = /* @__PURE__ */ new Map();
1406
+ registry = null;
1407
+ constructor(logger) {
1408
+ this.logger = logger;
1409
+ }
1410
+ /** Set the registry for live provider lookup. Called once during boot. */
1411
+ setRegistry(registry) {
1412
+ this.registry = registry;
1413
+ }
1414
+ /** Resolve the collection provider for an addon — registry first, then local map. */
1415
+ resolveProvider(addonId) {
1416
+ if (this.registry) return this.registry.getProviderByAddon("notification-output", addonId) ?? void 0;
1417
+ return this.localProviders.get(addonId);
1418
+ }
1419
+ /** Register a provider in the local fallback map (used when no registry is set). */
1420
+ registerLocalProvider(addonId, provider) {
1421
+ this.localProviders.set(addonId, provider);
1422
+ this.logger.info("Notification provider added", { meta: { addonId } });
1423
+ }
1424
+ /** Remove a provider from the local fallback map. */
1425
+ unregisterLocalProvider(addonId) {
1426
+ this.localProviders.delete(addonId);
1427
+ this.logger.info("Notification provider removed", { meta: { addonId } });
1428
+ }
1429
+ /** Route a category to a set of `(addonId, targetId)` targets. */
1430
+ setRouting(category, targets) {
1431
+ this.routing.set(category, [...targets]);
1432
+ }
1433
+ setRateLimit(category, minIntervalMs) {
1434
+ this.rateLimits.set(category, minIntervalMs);
1435
+ }
1436
+ async notify(notification) {
1437
+ const { category, deviceId } = notification;
1438
+ const rateLimitKey = deviceId ? `${category}:${deviceId}` : category;
1439
+ const minInterval = this.rateLimits.get(category) ?? 0;
1440
+ if (minInterval > 0) {
1441
+ const last = this.lastSent.get(rateLimitKey) ?? 0;
1442
+ if (notification.timestamp - last < minInterval) {
1443
+ this.logger.debug("Rate limited", { meta: { rateLimitKey } });
1444
+ return;
1445
+ }
1446
+ }
1447
+ const targets = this.routing.get(category) ?? this.routing.get("*") ?? [];
1448
+ if (targets.length === 0) {
1449
+ this.logger.debug("No routing configured for category", { meta: { category } });
1450
+ return;
1451
+ }
1452
+ this.lastSent.set(rateLimitKey, notification.timestamp);
1453
+ const { category: _category, timestamp: _timestamp, ...canonical } = notification;
1454
+ await Promise.allSettled(targets.map(async (ref) => {
1455
+ const provider = this.resolveProvider(ref.addonId);
1456
+ if (!provider) {
1457
+ this.logger.debug("No provider for target", { meta: { addonId: ref.addonId } });
1458
+ return;
1459
+ }
1460
+ try {
1461
+ await provider.send({
1462
+ targetId: ref.targetId,
1463
+ notification: canonical
1464
+ });
1465
+ } catch (err) {
1466
+ this.logger.error("Notification target failed", { meta: {
1467
+ addonId: ref.addonId,
1468
+ targetId: ref.targetId,
1469
+ error: require_dist.errMsg(err)
1470
+ } });
1471
+ }
1472
+ }));
1473
+ }
1474
+ getRouting() {
1475
+ return this.routing;
1476
+ }
1477
+ };
1478
+ //#endregion
1479
+ //#region src/notification/toast-service.ts
1480
+ /**
1481
+ * Service for broadcasting toast notifications to connected UI clients.
1482
+ * Framework-agnostic — integrates with tRPC subscriptions via subscribe().
1483
+ */
1484
+ var ToastService = class {
1485
+ listeners = /* @__PURE__ */ new Map();
1486
+ /**
1487
+ * Subscribe to toast events for a specific user.
1488
+ * Returns an unsubscribe function.
1489
+ */
1490
+ subscribe(connectionId, userId, callback) {
1491
+ this.listeners.set(connectionId, {
1492
+ userId,
1493
+ callback
1494
+ });
1495
+ return () => {
1496
+ this.listeners.delete(connectionId);
1497
+ };
1498
+ }
1499
+ /**
1500
+ * Broadcast a toast to all connected clients.
1501
+ */
1502
+ broadcast(toast) {
1503
+ for (const entry of this.listeners.values()) entry.callback(toast);
1504
+ }
1505
+ /**
1506
+ * Send a toast to a specific user's connections only.
1507
+ */
1508
+ sendToUser(userId, toast) {
1509
+ for (const entry of this.listeners.values()) if (entry.userId === userId) entry.callback(toast);
1510
+ }
1511
+ };
1512
+ //#endregion
1513
+ //#region src/pipeline/engine-manager-resolver.ts
1514
+ /**
1515
+ * Adapter that wraps an IEngineManager + addonConfigs as an IAddonResolver.
1516
+ *
1517
+ * Used for backwards compatibility — the in-process AddonEngineManager
1518
+ * still uses the old getOrCreateEngine(addonId, globalConfig, override) API.
1519
+ * This adapter bridges it to the new resolve(addonId, config) interface.
1520
+ */
1521
+ var EngineManagerResolver = class {
1522
+ engineManager;
1523
+ addonConfigs;
1524
+ constructor(engineManager, addonConfigs) {
1525
+ this.engineManager = engineManager;
1526
+ this.addonConfigs = addonConfigs;
1527
+ }
1528
+ async resolve(addonId, config) {
1529
+ const globalConfig = this.addonConfigs.get(addonId) ?? {};
1530
+ const engine = await this.engineManager.getOrCreateEngine(addonId, globalConfig, config);
1531
+ return { process: async (input) => {
1532
+ return engine.process(input);
1533
+ } };
1534
+ }
1535
+ async shutdownAll() {
1536
+ await this.engineManager.shutdownAll();
1537
+ }
1538
+ };
1539
+ //#endregion
1540
+ //#region src/pipeline/pipeline-runner.ts
1541
+ var PipelineRunner = class {
1542
+ addonResolver;
1543
+ constructor(addonResolver) {
1544
+ this.addonResolver = addonResolver;
1545
+ }
1546
+ async run(frame, config) {
1547
+ const startTime = performance.now();
1548
+ const results = [];
1549
+ const timings = {};
1550
+ for (const rootNode of config.video) await this.executeNode(rootNode, frame, null, results, timings);
1551
+ if (config.audio) await this.executeNode(config.audio, frame, null, results, timings);
1552
+ return {
1553
+ results,
1554
+ totalMs: performance.now() - startTime,
1555
+ timings,
1556
+ frameTimestamp: frame.timestamp
1557
+ };
1558
+ }
1559
+ async executeNode(node, frame, parentDetection, results, timings) {
1560
+ const resultId = (0, node_crypto.randomUUID)();
1561
+ const stepStart = performance.now();
1562
+ try {
1563
+ const effectiveConfig = { ...node.configOverride };
1564
+ const caller = await this.addonResolver.resolve(node.addon, effectiveConfig);
1565
+ let output;
1566
+ if (parentDetection) {
1567
+ const cropInput = {
1568
+ frame,
1569
+ roi: parentDetection.det.bbox,
1570
+ parentDetection: parentDetection.det
1571
+ };
1572
+ output = await caller.process(cropInput);
1573
+ } else output = await caller.process(frame);
1574
+ const stepMs = performance.now() - stepStart;
1575
+ const slot = "detections" in output ? "detector" : "crops" in output ? "cropper" : "classifier";
1576
+ const stepResult = {
1577
+ addon: node.addon,
1578
+ slot,
1579
+ output,
1580
+ parentResultId: parentDetection?.resultId,
1581
+ resultId,
1582
+ inferenceMs: output.inferenceMs,
1583
+ preprocessMs: 0,
1584
+ totalMs: stepMs
1585
+ };
1586
+ results.push(stepResult);
1587
+ timings[node.step] = stepMs;
1588
+ if (node.children?.length) {
1589
+ const detections = "detections" in output ? output.detections : "crops" in output ? output.crops : [];
1590
+ await this.executeChildren(node.children, frame, detections, resultId, results, timings);
1591
+ }
1592
+ } catch (error) {
1593
+ const stepMs = performance.now() - stepStart;
1594
+ const message = require_dist.errMsg(error);
1595
+ results.push({
1596
+ addon: node.addon,
1597
+ slot: "detector",
1598
+ output: {
1599
+ detections: [],
1600
+ inferenceMs: 0,
1601
+ modelId: ""
1602
+ },
1603
+ parentResultId: parentDetection?.resultId,
1604
+ resultId,
1605
+ inferenceMs: 0,
1606
+ preprocessMs: 0,
1607
+ totalMs: stepMs,
1608
+ error: {
1609
+ code: "ADDON_ERROR",
1610
+ message,
1611
+ childrenSkipped: true
1612
+ }
1613
+ });
1614
+ }
1615
+ }
1616
+ async executeChildren(children, frame, parentDetections, parentResultId, results, timings) {
1617
+ const promises = [];
1618
+ for (const detection of parentDetections) {
1619
+ if (detection.bbox.w < 1 || detection.bbox.h < 1) continue;
1620
+ for (const child of children) promises.push(this.executeNode(child, frame, {
1621
+ det: detection,
1622
+ resultId: parentResultId
1623
+ }, results, timings));
1624
+ }
1625
+ await Promise.allSettled(promises);
1626
+ }
1627
+ };
1628
+ //#endregion
1629
+ //#region src/pipeline/pipeline-validator.ts
1630
+ var PipelineValidator = class {
1631
+ stepExists;
1632
+ constructor(stepExists) {
1633
+ this.stepExists = stepExists;
1634
+ }
1635
+ validate(config) {
1636
+ const errors = [];
1637
+ const warnings = [];
1638
+ for (const node of config.video) this.validateNode(node, errors, warnings);
1639
+ if (config.audio) this.validateNode(config.audio, errors, warnings);
1487
1640
  return {
1488
- ...entry,
1489
- tags: {
1490
- ...tags,
1491
- deviceName: name
1492
- }
1493
- };
1494
- }
1495
- /** Subscribe to live logs matching a filter. Returns an unsubscribe function. */
1496
- subscribe(filter, callback) {
1497
- const id = Math.random().toString(36).slice(2);
1498
- this.subscribers.set(id, {
1499
- filter,
1500
- callback
1501
- });
1502
- return () => {
1503
- this.subscribers.delete(id);
1641
+ valid: errors.length === 0,
1642
+ errors,
1643
+ warnings
1504
1644
  };
1505
1645
  }
1506
- matchesFilter(entry, filter) {
1507
- if (filter.level) {
1508
- const levels = [
1509
- "debug",
1510
- "info",
1511
- "warn",
1512
- "error"
1513
- ];
1514
- const minIdx = levels.indexOf(filter.level);
1515
- if (levels.indexOf(entry.level) < minIdx) return false;
1646
+ validateNode(node, errors, warnings) {
1647
+ if (!this.stepExists(node.addon)) {
1648
+ errors.push({
1649
+ step: node.step,
1650
+ addon: node.addon,
1651
+ message: `Step "${node.addon}" has no provider — the addon that provided it may have been removed`,
1652
+ severity: "error"
1653
+ });
1654
+ return;
1516
1655
  }
1517
- if (filter.tags) {
1518
- if (!matchesLogTags(entry.tags, filter.tags)) return false;
1656
+ if (node.children) for (const child of node.children) this.validateNode(child, errors, warnings);
1657
+ }
1658
+ };
1659
+ //#endregion
1660
+ //#region src/python/python-env-manager.ts
1661
+ var execFileAsync$4 = (0, node_util.promisify)(node_child_process.execFile);
1662
+ var PythonEnvManager = class {
1663
+ venvPath;
1664
+ cachedProbe = null;
1665
+ constructor(dataDir) {
1666
+ this.venvPath = node_path.join(dataDir, ".venv");
1667
+ }
1668
+ async probe() {
1669
+ if (this.cachedProbe) return this.cachedProbe;
1670
+ for (const cmd of ["python3", "python"]) try {
1671
+ const { stdout } = await execFileAsync$4(cmd, ["--version"]);
1672
+ const version = stdout.trim().replace("Python ", "");
1673
+ const major = parseInt(version.split(".")[0] ?? "0", 10);
1674
+ const minor = parseInt(version.split(".")[1] ?? "0", 10);
1675
+ if (major < 3 || major === 3 && minor < 10) continue;
1676
+ const { stdout: pathOut } = await execFileAsync$4(cmd, ["-c", "import sys; print(sys.executable)"]);
1677
+ this.cachedProbe = {
1678
+ available: true,
1679
+ version,
1680
+ path: pathOut.trim()
1681
+ };
1682
+ return this.cachedProbe;
1683
+ } catch {
1684
+ continue;
1519
1685
  }
1520
- return true;
1686
+ this.cachedProbe = { available: false };
1687
+ return this.cachedProbe;
1521
1688
  }
1522
- /**
1523
- * Register a log destination.
1524
- *
1525
- * @param opts.replay If true (default), replay every entry currently in
1526
- * the ring buffer to the new destination before live
1527
- * traffic starts. Lets a destination added mid-boot
1528
- * still receive boot-time log entries.
1529
- */
1530
- /** Apply new in-memory buffer limits at runtime (see PartitionedLogBuffer). */
1531
- reconfigureBuffer(options) {
1532
- this.ringBuffer.reconfigure(options);
1689
+ async ensure(options) {
1690
+ const probe = await this.probe();
1691
+ if (!probe.available || !probe.path) throw new Error("Python 3.10+ is required but not found on this system");
1692
+ if (!node_fs.existsSync(node_path.join(this.venvPath, "bin", "python"))) await execFileAsync$4(probe.path, [
1693
+ "-m",
1694
+ "venv",
1695
+ this.venvPath
1696
+ ]);
1697
+ const venvPython = node_path.join(this.venvPath, "bin", "python");
1698
+ if (options.packages.length > 0) await execFileAsync$4(venvPython, [
1699
+ "-m",
1700
+ "pip",
1701
+ "install",
1702
+ "-q",
1703
+ ...options.packages
1704
+ ]);
1705
+ return {
1706
+ pythonPath: venvPython,
1707
+ venvPath: this.venvPath,
1708
+ spawn: (script, args) => {
1709
+ return (0, node_child_process.spawn)(venvPython, [script, ...args]);
1710
+ }
1711
+ };
1533
1712
  }
1534
- /** Set the per-destination enablement check (see {@link isDestinationEnabled}). */
1535
- setDestinationEnabledCheck(check) {
1536
- this.isDestinationEnabled = check ?? void 0;
1713
+ spawn(script, args) {
1714
+ const venvPython = node_path.join(this.venvPath, "bin", "python");
1715
+ if (!node_fs.existsSync(venvPython)) throw new Error("Python venv not initialized. Call ensure() first.");
1716
+ return (0, node_child_process.spawn)(venvPython, [script, ...args]);
1537
1717
  }
1538
- addDestination(dest, opts = {}) {
1539
- const replay = opts.replay ?? true;
1540
- this.destinations.push(opts.addonId === void 0 ? { dest } : {
1541
- dest,
1542
- addonId: opts.addonId
1543
- });
1544
- if (replay) for (const entry of this.ringBuffer.query({})) try {
1545
- dest.write(entry);
1546
- } catch {}
1718
+ };
1719
+ //#endregion
1720
+ //#region src/repl/repl-engine.ts
1721
+ var EXECUTION_TIMEOUT_MS = 5e3;
1722
+ var COMPLETION_TIMEOUT_MS = 1e3;
1723
+ var ReplEngine = class {
1724
+ contextProvider;
1725
+ constructor(contextProvider) {
1726
+ this.contextProvider = contextProvider;
1547
1727
  }
1548
- removeDestination(dest) {
1549
- const index = this.destinations.findIndex((d) => d.dest === dest);
1550
- if (index !== -1) this.destinations.splice(index, 1);
1728
+ async execute(code, context) {
1729
+ const start = Date.now();
1730
+ const sandbox = await this.buildSandbox(context);
1731
+ try {
1732
+ const vmContext = node_vm.createContext(sandbox);
1733
+ let result = new node_vm.Script(code).runInContext(vmContext, { timeout: EXECUTION_TIMEOUT_MS });
1734
+ if (result && typeof result === "object" && "then" in result && typeof result.then === "function") result = await Promise.race([result, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Async timeout (5s)")), EXECUTION_TIMEOUT_MS))]);
1735
+ const duration = Date.now() - start;
1736
+ if (result === void 0) return {
1737
+ output: "undefined",
1738
+ type: "void",
1739
+ duration
1740
+ };
1741
+ return {
1742
+ output: node_util.inspect(result, {
1743
+ depth: 4,
1744
+ colors: false,
1745
+ maxArrayLength: 50,
1746
+ maxStringLength: 1e3
1747
+ }),
1748
+ type: "value",
1749
+ duration
1750
+ };
1751
+ } catch (err) {
1752
+ return {
1753
+ output: err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : String(err),
1754
+ type: "error",
1755
+ duration: Date.now() - start
1756
+ };
1757
+ }
1551
1758
  }
1552
- query(filter) {
1553
- return this.ringBuffer.query(filter);
1759
+ async getCompletions(partial, context) {
1760
+ const sandbox = await this.buildSandbox(context);
1761
+ const keys = Object.keys(sandbox);
1762
+ if (!partial) return keys;
1763
+ const lastDot = partial.lastIndexOf(".");
1764
+ if (lastDot === -1) return keys.filter((k) => k.startsWith(partial));
1765
+ const objPath = partial.substring(0, lastDot);
1766
+ const propPrefix = partial.substring(lastDot + 1);
1767
+ try {
1768
+ const vmContext = node_vm.createContext(sandbox);
1769
+ const obj = node_vm.runInContext(objPath, vmContext, { timeout: COMPLETION_TIMEOUT_MS });
1770
+ if (obj && typeof obj === "object") {
1771
+ const proto = Object.getPrototypeOf(obj);
1772
+ const objKeys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertyNames(proto ?? {})).filter((k) => k.startsWith(propPrefix));
1773
+ return [...new Set(objKeys)].map((k) => `${objPath}.${k}`);
1774
+ }
1775
+ } catch {}
1776
+ return [];
1554
1777
  }
1555
- /**
1556
- * Drop ring-buffer entries matching `filter`. Returns the count
1557
- * removed. Backs the UI's "Clear logs" admin action so an operator
1558
- * can wipe the historical window for a scope (per-device, per-addon,
1559
- * agent-level) without restarting the server. External destinations
1560
- * (file, forwarder) are NOT touched — only the in-memory ring.
1561
- */
1562
- clear(filter = {}) {
1563
- return this.ringBuffer.clear(filter);
1778
+ async buildSandbox(context) {
1779
+ const base = {
1780
+ console: {
1781
+ log: (...args) => node_util.inspect(args),
1782
+ warn: (...args) => node_util.inspect(args)
1783
+ },
1784
+ JSON,
1785
+ Math,
1786
+ Date,
1787
+ Array,
1788
+ Object,
1789
+ String,
1790
+ Number,
1791
+ Boolean,
1792
+ Map,
1793
+ Set,
1794
+ Promise,
1795
+ Buffer,
1796
+ setTimeout: void 0,
1797
+ setInterval: void 0,
1798
+ fetch: void 0,
1799
+ require: void 0,
1800
+ process: void 0,
1801
+ ...context.variables
1802
+ };
1803
+ switch (context.scope.type) {
1804
+ case "system": return {
1805
+ ...base,
1806
+ ...await this.contextProvider.getSystemSandbox()
1807
+ };
1808
+ case "device": return {
1809
+ ...base,
1810
+ ...await this.contextProvider.getDeviceSandbox(context.scope.deviceId)
1811
+ };
1812
+ case "provider": return {
1813
+ ...base,
1814
+ ...await this.contextProvider.getProviderSandbox(context.scope.providerId)
1815
+ };
1816
+ case "addon": return {
1817
+ ...base,
1818
+ ...await this.contextProvider.getAddonSandbox(context.scope.addonId)
1819
+ };
1820
+ default: return base;
1821
+ }
1564
1822
  }
1565
1823
  };
1566
1824
  //#endregion
@@ -1902,216 +2160,6 @@ var StorageManager = class {
1902
2160
  }
1903
2161
  };
1904
2162
  //#endregion
1905
- //#region src/auth/scope-matcher.ts
1906
- /**
1907
- * True if the scope set grants `access` on device-scoped capabilities.
1908
- * A `category:device` grant covers every device cap (the broad grant).
1909
- * Mirrors the OR-semantics of the existing token scope matcher.
1910
- */
1911
- function scopesAllowDeviceCap(scopes, access) {
1912
- return scopes.some((s) => s.type === "category" && s.target === "device" && s.access.includes(access));
1913
- }
1914
- //#endregion
1915
- //#region src/notification/notification-service.ts
1916
- /**
1917
- * Central notification service that routes canonical notifications to
1918
- * configured `(addonId, targetId)` targets. Framework-agnostic —
1919
- * dependencies injected via constructor.
1920
- *
1921
- * Providers are resolved from the ICapabilityRegistry by `addonId` on each
1922
- * call (proxy pattern). Falls back to a local map when no registry is
1923
- * provided (tests / backward compat).
1924
- */
1925
- var NotificationService = class {
1926
- logger;
1927
- localProviders = /* @__PURE__ */ new Map();
1928
- routing = /* @__PURE__ */ new Map();
1929
- rateLimits = /* @__PURE__ */ new Map();
1930
- lastSent = /* @__PURE__ */ new Map();
1931
- registry = null;
1932
- constructor(logger) {
1933
- this.logger = logger;
1934
- }
1935
- /** Set the registry for live provider lookup. Called once during boot. */
1936
- setRegistry(registry) {
1937
- this.registry = registry;
1938
- }
1939
- /** Resolve the collection provider for an addon — registry first, then local map. */
1940
- resolveProvider(addonId) {
1941
- if (this.registry) return this.registry.getProviderByAddon("notification-output", addonId) ?? void 0;
1942
- return this.localProviders.get(addonId);
1943
- }
1944
- /** Register a provider in the local fallback map (used when no registry is set). */
1945
- registerLocalProvider(addonId, provider) {
1946
- this.localProviders.set(addonId, provider);
1947
- this.logger.info("Notification provider added", { meta: { addonId } });
1948
- }
1949
- /** Remove a provider from the local fallback map. */
1950
- unregisterLocalProvider(addonId) {
1951
- this.localProviders.delete(addonId);
1952
- this.logger.info("Notification provider removed", { meta: { addonId } });
1953
- }
1954
- /** Route a category to a set of `(addonId, targetId)` targets. */
1955
- setRouting(category, targets) {
1956
- this.routing.set(category, [...targets]);
1957
- }
1958
- setRateLimit(category, minIntervalMs) {
1959
- this.rateLimits.set(category, minIntervalMs);
1960
- }
1961
- async notify(notification) {
1962
- const { category, deviceId } = notification;
1963
- const rateLimitKey = deviceId ? `${category}:${deviceId}` : category;
1964
- const minInterval = this.rateLimits.get(category) ?? 0;
1965
- if (minInterval > 0) {
1966
- const last = this.lastSent.get(rateLimitKey) ?? 0;
1967
- if (notification.timestamp - last < minInterval) {
1968
- this.logger.debug("Rate limited", { meta: { rateLimitKey } });
1969
- return;
1970
- }
1971
- }
1972
- const targets = this.routing.get(category) ?? this.routing.get("*") ?? [];
1973
- if (targets.length === 0) {
1974
- this.logger.debug("No routing configured for category", { meta: { category } });
1975
- return;
1976
- }
1977
- this.lastSent.set(rateLimitKey, notification.timestamp);
1978
- const { category: _category, timestamp: _timestamp, ...canonical } = notification;
1979
- await Promise.allSettled(targets.map(async (ref) => {
1980
- const provider = this.resolveProvider(ref.addonId);
1981
- if (!provider) {
1982
- this.logger.debug("No provider for target", { meta: { addonId: ref.addonId } });
1983
- return;
1984
- }
1985
- try {
1986
- await provider.send({
1987
- targetId: ref.targetId,
1988
- notification: canonical
1989
- });
1990
- } catch (err) {
1991
- this.logger.error("Notification target failed", { meta: {
1992
- addonId: ref.addonId,
1993
- targetId: ref.targetId,
1994
- error: require_dist.errMsg(err)
1995
- } });
1996
- }
1997
- }));
1998
- }
1999
- getRouting() {
2000
- return this.routing;
2001
- }
2002
- };
2003
- //#endregion
2004
- //#region src/notification/toast-service.ts
2005
- /**
2006
- * Service for broadcasting toast notifications to connected UI clients.
2007
- * Framework-agnostic — integrates with tRPC subscriptions via subscribe().
2008
- */
2009
- var ToastService = class {
2010
- listeners = /* @__PURE__ */ new Map();
2011
- /**
2012
- * Subscribe to toast events for a specific user.
2013
- * Returns an unsubscribe function.
2014
- */
2015
- subscribe(connectionId, userId, callback) {
2016
- this.listeners.set(connectionId, {
2017
- userId,
2018
- callback
2019
- });
2020
- return () => {
2021
- this.listeners.delete(connectionId);
2022
- };
2023
- }
2024
- /**
2025
- * Broadcast a toast to all connected clients.
2026
- */
2027
- broadcast(toast) {
2028
- for (const entry of this.listeners.values()) entry.callback(toast);
2029
- }
2030
- /**
2031
- * Send a toast to a specific user's connections only.
2032
- */
2033
- sendToUser(userId, toast) {
2034
- for (const entry of this.listeners.values()) if (entry.userId === userId) entry.callback(toast);
2035
- }
2036
- };
2037
- //#endregion
2038
- //#region src/addon-routes/addon-route-registry.ts
2039
- /**
2040
- * Registry for dynamic HTTP routes registered by addons.
2041
- * Framework-agnostic — the server HTTP layer queries this registry to dispatch requests.
2042
- */
2043
- var AddonRouteRegistry = class {
2044
- routes = /* @__PURE__ */ new Map();
2045
- /**
2046
- * Register all routes from an addon's route provider.
2047
- */
2048
- registerRoutes(addonId, provider) {
2049
- const registered = provider.getRoutes().map((route) => {
2050
- return {
2051
- addonId,
2052
- route,
2053
- fullPath: `/addon/${addonId}${route.path.startsWith("/") ? route.path : `/${route.path}`}`
2054
- };
2055
- });
2056
- this.routes.set(addonId, registered);
2057
- }
2058
- /**
2059
- * Unregister all routes for an addon.
2060
- */
2061
- unregisterRoutes(addonId) {
2062
- this.routes.delete(addonId);
2063
- }
2064
- /**
2065
- * Match an incoming request method + path to a registered route.
2066
- * Supports simple path parameters (e.g., /items/:id).
2067
- */
2068
- matchRoute(method, path) {
2069
- const normalizedMethod = method.toUpperCase();
2070
- for (const registeredRoutes of this.routes.values()) for (const entry of registeredRoutes) {
2071
- if (entry.route.method !== normalizedMethod) continue;
2072
- const params = matchPath(entry.fullPath, path);
2073
- if (params !== null) return {
2074
- route: entry.route,
2075
- addonId: entry.addonId,
2076
- params
2077
- };
2078
- }
2079
- return null;
2080
- }
2081
- /**
2082
- * List all registered routes across all addons.
2083
- */
2084
- listRoutes() {
2085
- const result = [];
2086
- for (const registeredRoutes of this.routes.values()) for (const entry of registeredRoutes) result.push({
2087
- addonId: entry.addonId,
2088
- method: entry.route.method,
2089
- path: entry.fullPath,
2090
- access: entry.route.access,
2091
- description: entry.route.description
2092
- });
2093
- return result;
2094
- }
2095
- };
2096
- /**
2097
- * Match a route pattern against an actual path.
2098
- * Supports :param segments (e.g., /addon/:addonId/items/:id).
2099
- * Returns extracted params or null if no match.
2100
- */
2101
- function matchPath(pattern, path) {
2102
- const patternParts = pattern.split("/").filter(Boolean);
2103
- const pathParts = path.split("/").filter(Boolean);
2104
- if (patternParts.length !== pathParts.length) return null;
2105
- const params = {};
2106
- for (let i = 0; i < patternParts.length; i++) {
2107
- const patternPart = patternParts[i];
2108
- const pathPart = pathParts[i];
2109
- if (patternPart.startsWith(":")) params[patternPart.slice(1)] = pathPart;
2110
- else if (patternPart !== pathPart) return null;
2111
- }
2112
- return params;
2113
- }
2114
- //#endregion
2115
2163
  //#region src/tls/cert-manager.ts
2116
2164
  var execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
2117
2165
  /**
@@ -2185,54 +2233,6 @@ function loadTlsCert(certPath, keyPath) {
2185
2233
  };
2186
2234
  }
2187
2235
  //#endregion
2188
- //#region src/addon/addon-api-factory.ts
2189
- /**
2190
- * Wrap a raw tRPC caller (from `createCallerFactory`) in a proxy that
2191
- * exposes each leaf procedure as `{ query, mutate, subscribe }`, matching
2192
- * the shape of `CreateTRPCClient<AppRouter>`.
2193
- *
2194
- * The tricky part: tRPC v11 callers expose nested routers as callable
2195
- * no-op functions (e.g. `caller.greet` is `typeof === 'function'`), so
2196
- * we can't use `typeof` to tell namespace from leaf. Instead, we return
2197
- * a unified proxy that handles BOTH behaviours:
2198
- *
2199
- * - Accessing `.query` / `.mutate` treats the current node as a leaf
2200
- * procedure and invokes `caller(input)` when called.
2201
- * - Accessing `.subscribe` throws (not supported on direct caller).
2202
- * - Accessing any other property descends into `caller[prop]` and
2203
- * returns a nested proxy.
2204
- */
2205
- function wrapCallerAsClient(caller) {
2206
- return new Proxy(Object.create(null), { get(_target, prop) {
2207
- if (typeof prop !== "string") return void 0;
2208
- if (prop === "query" || prop === "mutate") {
2209
- if (typeof caller !== "function") throw new Error(`Cannot call .${prop}() — this tRPC node is not a procedure`);
2210
- const fn = caller;
2211
- return (input) => fn(input);
2212
- }
2213
- if (prop === "subscribe") return () => {
2214
- throw new Error(`subscribe() is not supported on direct caller — use the WSS client for subscriptions`);
2215
- };
2216
- const child = caller?.[prop];
2217
- if (child === void 0 || child === null) return void 0;
2218
- return wrapCallerAsClient(child);
2219
- } });
2220
- }
2221
- var AddonApiFactory = class {
2222
- /**
2223
- * Create a direct caller -- calls tRPC procedures directly in-process.
2224
- * Zero network overhead. Used for in-process addons and dev mode.
2225
- *
2226
- * Returns `AddonApi` — the same tRPC client surface that broker-routed
2227
- * callers get. Callers treat it uniformly as `context.api` regardless
2228
- * of the underlying transport.
2229
- */
2230
- async createDirectCaller(options) {
2231
- const { initTRPC } = await import("@trpc/server");
2232
- return wrapCallerAsClient(initTRPC.create().createCallerFactory(options.router)(options.context));
2233
- }
2234
- };
2235
- //#endregion
2236
2236
  //#region src/builtins/sqlite-storage/integration-registry.ts
2237
2237
  function serializeSetting(value) {
2238
2238
  if (typeof value === "number") return {
@@ -2807,6 +2807,268 @@ var IntegrationRegistry = class IntegrationRegistry {
2807
2807
  }
2808
2808
  };
2809
2809
  //#endregion
2810
+ //#region src/kernel/deps/ensure-native-prebuilds.ts
2811
+ /**
2812
+ * `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
2813
+ * shared `RootUpdateService` runs while staging a `@camstack/server` closure,
2814
+ * BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
2815
+ * decides whether to arm the swap.
2816
+ *
2817
+ * Why this exists (Mac desktop incident, 2026-07-21): a runtime
2818
+ * `applyServerUpdate` on a packaged desktop stages the closure with
2819
+ * `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
2820
+ * TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
2821
+ * its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
2822
+ * with no build toolchain (and where the auto `prebuild-install` does not
2823
+ * target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
2824
+ * `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
2825
+ * that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
2826
+ * `.node` → the validator refuses the swap ("staged closure missing native
2827
+ * prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
2828
+ * has a published prebuild the auto path fetches cleanly, and it carries a
2829
+ * build toolchain fallback.
2830
+ *
2831
+ * This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
2832
+ * into the runtime staging path: for each traditional native that is present
2833
+ * in the closure but missing its compiled `.node`, fetch the prebuild for the
2834
+ * RUNNING Node ABI/arch (the same Node that will load the swapped closure).
2835
+ *
2836
+ * N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
2837
+ * platform SIBLING packages that npm fetches reliably with no compile step, so
2838
+ * they are not in scope here — a genuine miss there is a broken install the
2839
+ * validator should still reject, not something a prebuild re-fetch can heal.
2840
+ *
2841
+ * Injected into the update service from the construction site so the zero-dep
2842
+ * `@camstack/node-root` layer never depends on npm/@camstack/system, and the
2843
+ * fetch is unit-testable with a fake fetcher.
2844
+ */
2845
+ var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
2846
+ /** Bounded recursive `.node` scan depth inside a module dir. */
2847
+ var NATIVE_SCAN_DEPTH = 4;
2848
+ /** Timeout for a single `prebuild-install` fetch. */
2849
+ var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
2850
+ /**
2851
+ * Traditional (compile-or-prebuild) natives an installed closure must carry a
2852
+ * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
2853
+ * are excluded (see file header).
2854
+ */
2855
+ var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
2856
+ /** Bounded recursive scan for ANY `*.node` file under `dir`. */
2857
+ function hasDotNode(dir, maxDepth) {
2858
+ let entries;
2859
+ try {
2860
+ entries = node_fs.readdirSync(dir, { withFileTypes: true });
2861
+ } catch {
2862
+ return false;
2863
+ }
2864
+ for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
2865
+ if (maxDepth <= 0) return false;
2866
+ for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
2867
+ return false;
2868
+ }
2869
+ /** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
2870
+ function resolvePrebuildInstallBin(closureDir, moduleDir) {
2871
+ return [node_path.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), node_path.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => node_fs.existsSync(c)) ?? null;
2872
+ }
2873
+ /**
2874
+ * Default fetcher: run the closure's own `prebuild-install` under the CURRENT
2875
+ * Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
2876
+ * app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
2877
+ * `scripts/stage-deps.mjs`.
2878
+ */
2879
+ function makeDefaultFetch(closureDir, logger) {
2880
+ return async (moduleDir, target) => {
2881
+ const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
2882
+ if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
2883
+ logger.info("fetching native prebuild via prebuild-install", { meta: {
2884
+ moduleDir,
2885
+ bin,
2886
+ nodeVersion: target.nodeVersion,
2887
+ arch: target.arch
2888
+ } });
2889
+ await execFileAsync$2(process.execPath, [
2890
+ bin,
2891
+ "--runtime",
2892
+ "node",
2893
+ "--target",
2894
+ target.nodeVersion,
2895
+ "--arch",
2896
+ target.arch
2897
+ ], {
2898
+ cwd: moduleDir,
2899
+ timeout: PREBUILD_FETCH_TIMEOUT_MS
2900
+ });
2901
+ };
2902
+ }
2903
+ /**
2904
+ * Ensure every traditional native in `<closureDir>/node_modules` carries its
2905
+ * compiled `.node`, fetching the prebuild for the running ABI when missing.
2906
+ *
2907
+ * A NO-OP when nothing is missing (docker / tests) and when a native package is
2908
+ * absent from the closure. Never throws for a single package's fetch failure —
2909
+ * it logs and continues so the downstream `findMissingNativePrebuilds`
2910
+ * validator makes the final swap/abort decision. Returns the packages whose
2911
+ * prebuild was successfully fetched (for logging/tests).
2912
+ */
2913
+ async function ensureNativePrebuilds(closureDir, options) {
2914
+ const nm = node_path.join(closureDir, "node_modules");
2915
+ const target = options.target ?? {
2916
+ nodeVersion: process.versions.node,
2917
+ arch: process.arch
2918
+ };
2919
+ const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
2920
+ const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
2921
+ const fetched = [];
2922
+ for (const pkg of packages) {
2923
+ const moduleDir = node_path.join(nm, pkg);
2924
+ if (!node_fs.existsSync(moduleDir)) continue;
2925
+ if (hasDotNode(moduleDir, 4)) continue;
2926
+ options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
2927
+ pkg,
2928
+ moduleDir,
2929
+ nodeVersion: target.nodeVersion,
2930
+ arch: target.arch
2931
+ } });
2932
+ try {
2933
+ await fetchPrebuild(moduleDir, target);
2934
+ } catch (err) {
2935
+ options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
2936
+ pkg,
2937
+ error: (0, _camstack_types_addon.errMsg)(err)
2938
+ } });
2939
+ continue;
2940
+ }
2941
+ if (hasDotNode(moduleDir, 4)) {
2942
+ fetched.push(pkg);
2943
+ options.logger.info("native prebuild fetched", { meta: { pkg } });
2944
+ } else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
2945
+ }
2946
+ return fetched;
2947
+ }
2948
+ //#endregion
2949
+ //#region src/kernel/deps/ensure-addon-natives.ts
2950
+ /**
2951
+ * `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
2952
+ * addon install runs on the INSTALLED addon dir AFTER `npm install`.
2953
+ *
2954
+ * Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
2955
+ * copies `dist/`, plants prebuilt natives from the bundled addon copy
2956
+ * (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
2957
+ * On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
2958
+ * `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
2959
+ * copy just planted — and the runner later dies at addon init with a cryptic
2960
+ * "Could not locate the bindings file". The copy-BEFORE-install fix was
2961
+ * empirically insufficient: npm's own install in the copied dir replaces the
2962
+ * satisfied dep without its binary.
2963
+ *
2964
+ * This pass runs LAST, so it always sees the final on-disk state npm left, and
2965
+ * per referenced traditional native still lacking its compiled `.node`:
2966
+ * 1. bundled-copy first — if the bundled addon copy carries the matching
2967
+ * `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
2968
+ * Cheapest + offline-safe; runs BEFORE any network fetch.
2969
+ * 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
2970
+ * shared `ensureNativePrebuilds` pointed at the addon dir.
2971
+ * 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
2972
+ * error naming module + addon and THROW so the install fails at install
2973
+ * time rather than at addon init.
2974
+ *
2975
+ * N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
2976
+ * SIBLING packages npm fetches without a compile step, so they are excluded from
2977
+ * the default set — a miss there is a broken install, not something a prebuild
2978
+ * re-fetch heals (same rationale as `ensureNativePrebuilds`).
2979
+ */
2980
+ /**
2981
+ * Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
2982
+ * Returns whether the destination ends up with a compiled binary.
2983
+ */
2984
+ function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
2985
+ if (!hasNative(srcModuleDir)) return false;
2986
+ try {
2987
+ node_fs.rmSync(dstModuleDir, {
2988
+ recursive: true,
2989
+ force: true
2990
+ });
2991
+ node_fs.mkdirSync(node_path.dirname(dstModuleDir), { recursive: true });
2992
+ node_fs.cpSync(srcModuleDir, dstModuleDir, {
2993
+ recursive: true,
2994
+ dereference: true
2995
+ });
2996
+ } catch {
2997
+ return false;
2998
+ }
2999
+ return hasNative(dstModuleDir);
3000
+ }
3001
+ /**
3002
+ * Ensure every referenced traditional native in `<addonDir>/node_modules`
3003
+ * carries its compiled `.node` after the addon's `npm install`. Bundled-copy
3004
+ * first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
3005
+ *
3006
+ * SKIPS a native absent from the addon's own `node_modules` (it resolves via a
3007
+ * hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
3008
+ * npm produced a working `.node` (already-present). Returns the per-native
3009
+ * outcomes for logging/tests.
3010
+ *
3011
+ * @throws if any required native is still source-only after copy + fetch.
3012
+ */
3013
+ async function ensureAddonNativePrebuilds(addonDir, options) {
3014
+ const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3015
+ const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
3016
+ const nm = node_path.join(addonDir, "node_modules");
3017
+ const results = [];
3018
+ const missing = [];
3019
+ for (const pkg of packages) {
3020
+ const moduleDir = node_path.join(nm, pkg);
3021
+ if (!node_fs.existsSync(moduleDir)) continue;
3022
+ if (hasNative(moduleDir)) {
3023
+ results.push({
3024
+ pkg,
3025
+ outcome: "already-present"
3026
+ });
3027
+ continue;
3028
+ }
3029
+ if (options.bundledSourceDir) {
3030
+ const src = node_path.join(options.bundledSourceDir, "node_modules", pkg);
3031
+ if (replantFromBundled(src, moduleDir, hasNative)) {
3032
+ options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
3033
+ addon: options.addonName,
3034
+ pkg,
3035
+ src,
3036
+ moduleDir
3037
+ } });
3038
+ results.push({
3039
+ pkg,
3040
+ outcome: "copied"
3041
+ });
3042
+ continue;
3043
+ }
3044
+ }
3045
+ if ((await ensureNativePrebuilds(addonDir, {
3046
+ logger: options.logger,
3047
+ packages: [pkg],
3048
+ ...options.target ? { target: options.target } : {},
3049
+ ...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
3050
+ })).includes(pkg)) {
3051
+ results.push({
3052
+ pkg,
3053
+ outcome: "fetched"
3054
+ });
3055
+ continue;
3056
+ }
3057
+ options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
3058
+ addon: options.addonName,
3059
+ pkg,
3060
+ moduleDir
3061
+ } });
3062
+ results.push({
3063
+ pkg,
3064
+ outcome: "missing"
3065
+ });
3066
+ missing.push(pkg);
3067
+ }
3068
+ if (missing.length > 0) throw new Error(`${options.addonName} — required native module(s) missing a compiled .node after install (bundled-copy + prebuild-fetch both failed): ${missing.join(", ")}`);
3069
+ return results;
3070
+ }
3071
+ //#endregion
2810
3072
  //#region src/kernel/addon-loader.ts
2811
3073
  function isRecord$1(value) {
2812
3074
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -3168,7 +3430,7 @@ var AddonEngineManager = class {
3168
3430
  };
3169
3431
  //#endregion
3170
3432
  //#region src/kernel/fs-utils.ts
3171
- var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
3433
+ var execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
3172
3434
  /**
3173
3435
  * Ensure a directory exists (recursive).
3174
3436
  * Single source of truth — replaces scattered mkdirSync calls.
@@ -3357,7 +3619,7 @@ function ensureLibraryBuilt(packageName, packagesDir) {
3357
3619
  async function installPackageFromNpm(packageName, targetDir) {
3358
3620
  const tmpDir = await node_fs.promises.mkdtemp(node_path.join(node_os.tmpdir(), "camstack-install-"));
3359
3621
  try {
3360
- const { stdout } = await execFileAsync$2("npm", [
3622
+ const { stdout } = await execFileAsync$1("npm", [
3361
3623
  "pack",
3362
3624
  packageName,
3363
3625
  "--pack-destination",
@@ -3371,7 +3633,7 @@ async function installPackageFromNpm(packageName, targetDir) {
3371
3633
  const tgzPath = node_path.join(tmpDir, tgzFilename);
3372
3634
  const extractDir = node_path.join(tmpDir, "extracted");
3373
3635
  ensureDir(extractDir);
3374
- await execFileAsync$2("tar", [
3636
+ await execFileAsync$1("tar", [
3375
3637
  "-xzf",
3376
3638
  tgzPath,
3377
3639
  "-C",
@@ -3675,268 +3937,6 @@ var AddonManifest = class {
3675
3937
  }
3676
3938
  };
3677
3939
  //#endregion
3678
- //#region src/kernel/deps/ensure-native-prebuilds.ts
3679
- /**
3680
- * `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
3681
- * shared `RootUpdateService` runs while staging a `@camstack/server` closure,
3682
- * BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
3683
- * decides whether to arm the swap.
3684
- *
3685
- * Why this exists (Mac desktop incident, 2026-07-21): a runtime
3686
- * `applyServerUpdate` on a packaged desktop stages the closure with
3687
- * `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
3688
- * TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
3689
- * its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
3690
- * with no build toolchain (and where the auto `prebuild-install` does not
3691
- * target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
3692
- * `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
3693
- * that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
3694
- * `.node` → the validator refuses the swap ("staged closure missing native
3695
- * prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
3696
- * has a published prebuild the auto path fetches cleanly, and it carries a
3697
- * build toolchain fallback.
3698
- *
3699
- * This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
3700
- * into the runtime staging path: for each traditional native that is present
3701
- * in the closure but missing its compiled `.node`, fetch the prebuild for the
3702
- * RUNNING Node ABI/arch (the same Node that will load the swapped closure).
3703
- *
3704
- * N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
3705
- * platform SIBLING packages that npm fetches reliably with no compile step, so
3706
- * they are not in scope here — a genuine miss there is a broken install the
3707
- * validator should still reject, not something a prebuild re-fetch can heal.
3708
- *
3709
- * Injected into the update service from the construction site so the zero-dep
3710
- * `@camstack/node-root` layer never depends on npm/@camstack/system, and the
3711
- * fetch is unit-testable with a fake fetcher.
3712
- */
3713
- var execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
3714
- /** Bounded recursive `.node` scan depth inside a module dir. */
3715
- var NATIVE_SCAN_DEPTH = 4;
3716
- /** Timeout for a single `prebuild-install` fetch. */
3717
- var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
3718
- /**
3719
- * Traditional (compile-or-prebuild) natives an installed closure must carry a
3720
- * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
3721
- * are excluded (see file header).
3722
- */
3723
- var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
3724
- /** Bounded recursive scan for ANY `*.node` file under `dir`. */
3725
- function hasDotNode(dir, maxDepth) {
3726
- let entries;
3727
- try {
3728
- entries = node_fs.readdirSync(dir, { withFileTypes: true });
3729
- } catch {
3730
- return false;
3731
- }
3732
- for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
3733
- if (maxDepth <= 0) return false;
3734
- for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
3735
- return false;
3736
- }
3737
- /** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
3738
- function resolvePrebuildInstallBin(closureDir, moduleDir) {
3739
- return [node_path.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), node_path.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => node_fs.existsSync(c)) ?? null;
3740
- }
3741
- /**
3742
- * Default fetcher: run the closure's own `prebuild-install` under the CURRENT
3743
- * Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
3744
- * app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
3745
- * `scripts/stage-deps.mjs`.
3746
- */
3747
- function makeDefaultFetch(closureDir, logger) {
3748
- return async (moduleDir, target) => {
3749
- const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
3750
- if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
3751
- logger.info("fetching native prebuild via prebuild-install", { meta: {
3752
- moduleDir,
3753
- bin,
3754
- nodeVersion: target.nodeVersion,
3755
- arch: target.arch
3756
- } });
3757
- await execFileAsync$1(process.execPath, [
3758
- bin,
3759
- "--runtime",
3760
- "node",
3761
- "--target",
3762
- target.nodeVersion,
3763
- "--arch",
3764
- target.arch
3765
- ], {
3766
- cwd: moduleDir,
3767
- timeout: PREBUILD_FETCH_TIMEOUT_MS
3768
- });
3769
- };
3770
- }
3771
- /**
3772
- * Ensure every traditional native in `<closureDir>/node_modules` carries its
3773
- * compiled `.node`, fetching the prebuild for the running ABI when missing.
3774
- *
3775
- * A NO-OP when nothing is missing (docker / tests) and when a native package is
3776
- * absent from the closure. Never throws for a single package's fetch failure —
3777
- * it logs and continues so the downstream `findMissingNativePrebuilds`
3778
- * validator makes the final swap/abort decision. Returns the packages whose
3779
- * prebuild was successfully fetched (for logging/tests).
3780
- */
3781
- async function ensureNativePrebuilds(closureDir, options) {
3782
- const nm = node_path.join(closureDir, "node_modules");
3783
- const target = options.target ?? {
3784
- nodeVersion: process.versions.node,
3785
- arch: process.arch
3786
- };
3787
- const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3788
- const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
3789
- const fetched = [];
3790
- for (const pkg of packages) {
3791
- const moduleDir = node_path.join(nm, pkg);
3792
- if (!node_fs.existsSync(moduleDir)) continue;
3793
- if (hasDotNode(moduleDir, 4)) continue;
3794
- options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
3795
- pkg,
3796
- moduleDir,
3797
- nodeVersion: target.nodeVersion,
3798
- arch: target.arch
3799
- } });
3800
- try {
3801
- await fetchPrebuild(moduleDir, target);
3802
- } catch (err) {
3803
- options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
3804
- pkg,
3805
- error: (0, _camstack_types_addon.errMsg)(err)
3806
- } });
3807
- continue;
3808
- }
3809
- if (hasDotNode(moduleDir, 4)) {
3810
- fetched.push(pkg);
3811
- options.logger.info("native prebuild fetched", { meta: { pkg } });
3812
- } else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
3813
- }
3814
- return fetched;
3815
- }
3816
- //#endregion
3817
- //#region src/kernel/deps/ensure-addon-natives.ts
3818
- /**
3819
- * `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
3820
- * addon install runs on the INSTALLED addon dir AFTER `npm install`.
3821
- *
3822
- * Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
3823
- * copies `dist/`, plants prebuilt natives from the bundled addon copy
3824
- * (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
3825
- * On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
3826
- * `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
3827
- * copy just planted — and the runner later dies at addon init with a cryptic
3828
- * "Could not locate the bindings file". The copy-BEFORE-install fix was
3829
- * empirically insufficient: npm's own install in the copied dir replaces the
3830
- * satisfied dep without its binary.
3831
- *
3832
- * This pass runs LAST, so it always sees the final on-disk state npm left, and
3833
- * per referenced traditional native still lacking its compiled `.node`:
3834
- * 1. bundled-copy first — if the bundled addon copy carries the matching
3835
- * `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
3836
- * Cheapest + offline-safe; runs BEFORE any network fetch.
3837
- * 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
3838
- * shared `ensureNativePrebuilds` pointed at the addon dir.
3839
- * 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
3840
- * error naming module + addon and THROW so the install fails at install
3841
- * time rather than at addon init.
3842
- *
3843
- * N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
3844
- * SIBLING packages npm fetches without a compile step, so they are excluded from
3845
- * the default set — a miss there is a broken install, not something a prebuild
3846
- * re-fetch heals (same rationale as `ensureNativePrebuilds`).
3847
- */
3848
- /**
3849
- * Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
3850
- * Returns whether the destination ends up with a compiled binary.
3851
- */
3852
- function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
3853
- if (!hasNative(srcModuleDir)) return false;
3854
- try {
3855
- node_fs.rmSync(dstModuleDir, {
3856
- recursive: true,
3857
- force: true
3858
- });
3859
- node_fs.mkdirSync(node_path.dirname(dstModuleDir), { recursive: true });
3860
- node_fs.cpSync(srcModuleDir, dstModuleDir, {
3861
- recursive: true,
3862
- dereference: true
3863
- });
3864
- } catch {
3865
- return false;
3866
- }
3867
- return hasNative(dstModuleDir);
3868
- }
3869
- /**
3870
- * Ensure every referenced traditional native in `<addonDir>/node_modules`
3871
- * carries its compiled `.node` after the addon's `npm install`. Bundled-copy
3872
- * first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
3873
- *
3874
- * SKIPS a native absent from the addon's own `node_modules` (it resolves via a
3875
- * hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
3876
- * npm produced a working `.node` (already-present). Returns the per-native
3877
- * outcomes for logging/tests.
3878
- *
3879
- * @throws if any required native is still source-only after copy + fetch.
3880
- */
3881
- async function ensureAddonNativePrebuilds(addonDir, options) {
3882
- const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3883
- const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
3884
- const nm = node_path.join(addonDir, "node_modules");
3885
- const results = [];
3886
- const missing = [];
3887
- for (const pkg of packages) {
3888
- const moduleDir = node_path.join(nm, pkg);
3889
- if (!node_fs.existsSync(moduleDir)) continue;
3890
- if (hasNative(moduleDir)) {
3891
- results.push({
3892
- pkg,
3893
- outcome: "already-present"
3894
- });
3895
- continue;
3896
- }
3897
- if (options.bundledSourceDir) {
3898
- const src = node_path.join(options.bundledSourceDir, "node_modules", pkg);
3899
- if (replantFromBundled(src, moduleDir, hasNative)) {
3900
- options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
3901
- addon: options.addonName,
3902
- pkg,
3903
- src,
3904
- moduleDir
3905
- } });
3906
- results.push({
3907
- pkg,
3908
- outcome: "copied"
3909
- });
3910
- continue;
3911
- }
3912
- }
3913
- if ((await ensureNativePrebuilds(addonDir, {
3914
- logger: options.logger,
3915
- packages: [pkg],
3916
- ...options.target ? { target: options.target } : {},
3917
- ...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
3918
- })).includes(pkg)) {
3919
- results.push({
3920
- pkg,
3921
- outcome: "fetched"
3922
- });
3923
- continue;
3924
- }
3925
- options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
3926
- addon: options.addonName,
3927
- pkg,
3928
- moduleDir
3929
- } });
3930
- results.push({
3931
- pkg,
3932
- outcome: "missing"
3933
- });
3934
- missing.push(pkg);
3935
- }
3936
- if (missing.length > 0) throw new Error(`${options.addonName} — required native module(s) missing a compiled .node after install (bundled-copy + prebuild-fetch both failed): ${missing.join(", ")}`);
3937
- return results;
3938
- }
3939
- //#endregion
3940
3940
  //#region src/kernel/addon-installer.ts
3941
3941
  var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
3942
3942
  function parseInstallSource(value) {
@@ -5453,6 +5453,19 @@ var CapabilityRegistry = class CapabilityRegistry {
5453
5453
  _ready = false;
5454
5454
  /** Boot-time dependency graph: capability → dependsOn names (from CapabilityDeclaration) */
5455
5455
  capabilityDeps = /* @__PURE__ */ new Map();
5456
+ /**
5457
+ * capability → addon ids whose MANIFEST declares it.
5458
+ *
5459
+ * Populated by {@link declareFromManifest}, which runs at addon-LOAD time —
5460
+ * before a forked addon's runner has spawned, let alone registered anything.
5461
+ * So this map answers a question `providers` cannot: **which addons still owe
5462
+ * this capability a provider**, i.e. is an absence final or merely early.
5463
+ *
5464
+ * It is not a second registry and nothing waits on it: it stores an argument
5465
+ * `declareFromManifest` was already being handed and dropping, and it is read
5466
+ * synchronously at the moment a caller needs the answer ([D3](../../../../docs/decisions/adr-0003.md)).
5467
+ */
5468
+ manifestDeclarers = /* @__PURE__ */ new Map();
5456
5469
  /** Per-device singleton overrides: deviceId → (capability → addonId) */
5457
5470
  deviceOverrides = /* @__PURE__ */ new Map();
5458
5471
  /** capName → (nodeId → bare addonId) per-node singleton overrides. */
@@ -5578,6 +5591,11 @@ var CapabilityRegistry = class CapabilityRegistry {
5578
5591
  */
5579
5592
  declareFromManifest(declaration, addonId) {
5580
5593
  if (declaration.dependsOn?.length) this.capabilityDeps.set(declaration.name, declaration.dependsOn);
5594
+ if (addonId !== void 0) {
5595
+ const declarers = this.manifestDeclarers.get(declaration.name);
5596
+ if (declarers) declarers.add(addonId);
5597
+ else this.manifestDeclarers.set(declaration.name, new Set([addonId]));
5598
+ }
5581
5599
  if (addonId !== void 0 && declaration.autoActivate !== void 0) this.autoActivateFlags.set(`${declaration.name}/${addonId}`, declaration.autoActivate);
5582
5600
  const state = this.capabilities.get(declaration.name);
5583
5601
  if (!state) {
@@ -5700,6 +5718,22 @@ var CapabilityRegistry = class CapabilityRegistry {
5700
5718
  hasProvider(capabilityName, addonId) {
5701
5719
  return this.capabilities.get(capabilityName)?.providers.has(addonId) ?? false;
5702
5720
  }
5721
+ /**
5722
+ * Addon ids whose manifest declares `capabilityName`, whether or not they
5723
+ * have registered a provider yet.
5724
+ *
5725
+ * Pair it with {@link hasProvider} to separate "this capability's collection
5726
+ * is still filling up" from "this is everything there will ever be" — the
5727
+ * difference between a 503 a client should retry and a 400 it should not.
5728
+ * Every entry is an addon INSTALLED on this node; an addon that is installed
5729
+ * but disabled therefore stays listed, which is the honest answer (the
5730
+ * capability is owed by something that is not going to supply it, and naming
5731
+ * it is the diagnosis).
5732
+ */
5733
+ getManifestDeclarers(capabilityName) {
5734
+ const declarers = this.manifestDeclarers.get(capabilityName);
5735
+ return declarers ? [...declarers] : [];
5736
+ }
5703
5737
  /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
5704
5738
  unregisterProvider(capabilityName, addonId) {
5705
5739
  const state = this.capabilities.get(capabilityName);
@@ -94964,7 +94998,6 @@ exports.HEAP_RECLAIM_MIN_INTERVAL_MS = HEAP_RECLAIM_MIN_INTERVAL_MS;
94964
94998
  exports.HEAP_RECLAIM_TRIGGER_MB = HEAP_RECLAIM_TRIGGER_MB;
94965
94999
  exports.HEAP_WATCH_INTERVAL_MS = HEAP_WATCH_INTERVAL_MS;
94966
95000
  exports.HEAP_WATCH_WARN_RATIO = HEAP_WATCH_WARN_RATIO;
94967
- exports.HOME_ASSISTANT_OAUTH_INTEGRATION = require_builtins_local_auth_local_auth_addon.HOME_ASSISTANT_OAUTH_INTEGRATION;
94968
95001
  exports.HUB_CAP_FWD_ACTION = require_manifest_python_deps.HUB_CAP_FWD_ACTION;
94969
95002
  exports.HUB_CAP_FWD_SERVICE = require_manifest_python_deps.HUB_CAP_FWD_SERVICE;
94970
95003
  exports.HubForwarderAddon = require_builtins_hub_forwarder_index.HubForwarderAddon$1;
@@ -94985,7 +95018,7 @@ exports.LogRingBuffer = LogRingBuffer;
94985
95018
  exports.LokiDestination = require_builtins_loki_logging_index.LokiDestination$1;
94986
95019
  exports.LokiLoggingAddon = require_builtins_loki_logging_index.LokiLoggingAddon$1;
94987
95020
  exports.METHOD_ACCESS_MAP = require_dist.METHOD_ACCESS_MAP;
94988
- exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
95021
+ exports.ModelDownloadService = require_file_data_plane.ModelDownloadService;
94989
95022
  exports.NATIVE_PROVIDER_SERVICE_INFIX = require_manifest_python_deps.NATIVE_PROVIDER_SERVICE_INFIX;
94990
95023
  exports.NATIVE_SCAN_DEPTH = NATIVE_SCAN_DEPTH;
94991
95024
  exports.NativeMetricsAddon = require_builtins_native_metrics_native_metrics_addon.default;
@@ -95059,18 +95092,18 @@ exports.classifyCapRoute = require_manifest_python_deps.classifyCapRoute;
95059
95092
  exports.clearPendingRestart = clearPendingRestart;
95060
95093
  exports.clusterEventTopic = require_manifest_python_deps.clusterEventTopic;
95061
95094
  exports.clusterSecretMatches = clusterSecretMatches;
95062
- exports.collectModelFiles = require_model_download_service.collectModelFiles;
95063
- exports.contentTypeFor = require_model_download_service.contentTypeFor;
95095
+ exports.collectModelFiles = require_file_data_plane.collectModelFiles;
95096
+ exports.contentTypeFor = require_file_data_plane.contentTypeFor;
95064
95097
  exports.copyDirRecursive = copyDirRecursive;
95065
95098
  exports.copyExtraFileDirs = copyExtraFileDirs;
95066
95099
  exports.createAddonContext = require_manifest_python_deps.createAddonContext;
95067
95100
  exports.createAddonDataPlaneFacility = require_manifest_python_deps.createAddonDataPlaneFacility;
95068
95101
  exports.createAddonService = require_manifest_python_deps.createAddonService;
95069
- exports.createAuthenticatedFileServer = require_model_download_service.createAuthenticatedFileServer;
95102
+ exports.createAuthenticatedFileServer = require_file_data_plane.createAuthenticatedFileServer;
95070
95103
  exports.createBroker = createBroker;
95071
95104
  exports.createBrokerDeviceManagerApi = require_manifest_python_deps.createBrokerDeviceManagerApi;
95072
95105
  exports.createCoreCapService = createCoreCapService;
95073
- exports.createFileDataPlaneHandler = require_model_download_service.createFileDataPlaneHandler;
95106
+ exports.createFileDataPlaneHandler = require_file_data_plane.createFileDataPlaneHandler;
95074
95107
  exports.createHubCapForwardService = require_manifest_python_deps.createHubCapForwardService;
95075
95108
  exports.createHubService = createHubService;
95076
95109
  exports.createKernelHwAccel = require_manifest_python_deps.createKernelHwAccel;
@@ -95087,7 +95120,7 @@ exports.createUdsEventBus = require_manifest_python_deps.createUdsEventBus;
95087
95120
  exports.createUdsLogger = require_manifest_python_deps.createUdsLogger;
95088
95121
  exports.createUdsLoggerWithControl = require_manifest_python_deps.createUdsLoggerWithControl;
95089
95122
  exports.createV8Reclaimer = createV8Reclaimer;
95090
- exports.deleteModelFromDisk = require_model_download_service.deleteModelFromDisk;
95123
+ exports.deleteModelFromDisk = require_file_data_plane.deleteModelFromDisk;
95091
95124
  exports.deriveAgentListenPort = deriveAgentListenPort;
95092
95125
  exports.describeProviderKindDrift = describeProviderKindDrift;
95093
95126
  exports.detectWorkspacePackagesDir = detectWorkspacePackagesDir;
@@ -95097,8 +95130,8 @@ Object.defineProperty(exports, "downloadBinary", {
95097
95130
  return _camstack_types_node.downloadBinary;
95098
95131
  }
95099
95132
  });
95100
- exports.downloadFile = require_model_download_service.downloadFile;
95101
- exports.downloadModel = require_model_download_service.downloadModel;
95133
+ exports.downloadFile = require_file_data_plane.downloadFile;
95134
+ exports.downloadModel = require_file_data_plane.downloadModel;
95102
95135
  exports.emitDownForOwnedCaps = require_dist.emitDownForOwnedCaps;
95103
95136
  exports.encodeFrame = require_manifest_python_deps.encodeFrame;
95104
95137
  exports.ensureAddonNativePrebuilds = ensureAddonNativePrebuilds;
@@ -95116,7 +95149,7 @@ Object.defineProperty(exports, "ensureFfmpeg", {
95116
95149
  }
95117
95150
  });
95118
95151
  exports.ensureLibraryBuilt = ensureLibraryBuilt;
95119
- exports.ensureModel = require_model_download_service.ensureModel;
95152
+ exports.ensureModel = require_file_data_plane.ensureModel;
95120
95153
  exports.ensureNativePrebuilds = ensureNativePrebuilds;
95121
95154
  Object.defineProperty(exports, "ensurePython", {
95122
95155
  enumerable: true,
@@ -95126,7 +95159,7 @@ Object.defineProperty(exports, "ensurePython", {
95126
95159
  });
95127
95160
  exports.ensureTlsCert = ensureTlsCert;
95128
95161
  exports.expandCapMethods = require_dist.expandCapMethods;
95129
- exports.fetchJson = require_model_download_service.fetchJson;
95162
+ exports.fetchJson = require_file_data_plane.fetchJson;
95130
95163
  Object.defineProperty(exports, "findInPath", {
95131
95164
  enumerable: true,
95132
95165
  get: function() {
@@ -95142,7 +95175,7 @@ Object.defineProperty(exports, "getFfmpegDownloadUrl", {
95142
95175
  return _camstack_types_node.getFfmpegDownloadUrl;
95143
95176
  }
95144
95177
  });
95145
- exports.getModelFilePath = require_model_download_service.getModelFilePath;
95178
+ exports.getModelFilePath = require_file_data_plane.getModelFilePath;
95146
95179
  exports.getMoleculerEventStats = require_manifest_python_deps.getMoleculerEventStats;
95147
95180
  exports.getOrInitReadinessRegistry = require_manifest_python_deps.getOrInitReadinessRegistry;
95148
95181
  exports.getOrInitReadinessRegistryForClient = require_manifest_python_deps.getOrInitReadinessRegistryForClient;
@@ -95186,21 +95219,21 @@ exports.isArrayOutputSchema = require_dist.isArrayOutputSchema;
95186
95219
  exports.isClusterSecretMismatchError = isClusterSecretMismatchError;
95187
95220
  exports.isCollectionArrayMethod = require_dist.isCollectionArrayMethod;
95188
95221
  exports.isInfraCapability = isInfraCapability;
95189
- exports.isModelDownloaded = require_model_download_service.isModelDownloaded;
95222
+ exports.isModelDownloaded = require_file_data_plane.isModelDownloaded;
95190
95223
  exports.isSourceNewer = isSourceNewer;
95191
95224
  exports.loadTlsCert = loadTlsCert;
95192
95225
  exports.localEndpointPath = require_manifest_python_deps.localEndpointPath;
95193
95226
  exports.localProviderLink = require_manifest_python_deps.localProviderLink;
95194
95227
  exports.mountNativeCapService = require_manifest_python_deps.mountNativeCapService;
95195
95228
  exports.parseCapAction = require_manifest_python_deps.parseCapAction;
95196
- exports.parseRangeHeader = require_model_download_service.parseRangeHeader;
95197
- exports.parseTokenizedUrl = require_model_download_service.parseTokenizedUrl;
95229
+ exports.parseRangeHeader = require_file_data_plane.parseRangeHeader;
95230
+ exports.parseTokenizedUrl = require_file_data_plane.parseTokenizedUrl;
95198
95231
  exports.proxyToUpstream = proxyToUpstream;
95199
95232
  exports.quarantineAddonResidue = quarantineAddonResidue;
95200
95233
  exports.readPendingRestart = readPendingRestart;
95201
95234
  exports.readinessKey = require_dist.readinessKey;
95202
95235
  exports.registerEventBusService = require_manifest_python_deps.registerEventBusService;
95203
- exports.resolveFilePath = require_model_download_service.resolveFilePath;
95236
+ exports.resolveFilePath = require_file_data_plane.resolveFilePath;
95204
95237
  exports.resolveHwAccel = require_manifest_python_deps.resolveHwAccel;
95205
95238
  exports.resolveNpmInvocation = require_manifest_python_deps.resolveNpmInvocation;
95206
95239
  exports.runNpm = require_manifest_python_deps.runNpm;