@camstack/system 1.2.69 → 1.2.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +6 -6
- package/dist/addon-runner.mjs +3 -3
- package/dist/addon-utils.js +11 -11
- package/dist/addon-utils.mjs +1 -1
- package/dist/auth/auth-manager.d.ts +13 -29
- package/dist/auth/scope-matcher.d.ts +6 -5
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +3 -3
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +2 -2
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +3 -3
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +2 -2
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +8 -8
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +6 -6
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +3 -3
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/index.d.ts +0 -1
- package/dist/builtins/local-auth/index.js +0 -1
- package/dist/builtins/local-auth/index.mjs +2 -2
- package/dist/builtins/local-auth/local-auth.addon.js +4878 -4883
- package/dist/builtins/local-auth/local-auth.addon.mjs +4879 -4883
- package/dist/builtins/local-auth/oauth-grants.d.ts +15 -0
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +5 -5
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +4 -4
- package/dist/builtins/platform-probe/index.js +4 -4
- package/dist/builtins/platform-probe/index.mjs +3 -3
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +2 -2
- package/dist/builtins/snapshot/index.mjs +2 -2
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -4
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +3 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-C-QzFJ8v.mjs → dist-Daxx9CB0.mjs} +139 -5
- package/dist/{dist-D7wC-S4p.js → dist-I0WgqQQ-.js} +150 -10
- package/dist/{model-download-service-CTBHzORJ.mjs → file-data-plane-CuE_hBli.mjs} +848 -848
- package/dist/index.d.ts +73 -81
- package/dist/index.js +1302 -1278
- package/dist/index.mjs +1283 -1259
- package/dist/kernel/capability-registry.d.ts +26 -0
- package/dist/kernel/config-schema.d.ts +1 -1
- package/dist/kernel/transport/parent-unowned-call.d.ts +27 -0
- package/dist/{manifest-python-deps-Dz1ryHIN.mjs → manifest-python-deps-BdWxKNvu.mjs} +49 -47
- package/dist/{manifest-python-deps-BE6l8Y4W.js → manifest-python-deps-BqIpg0C7.js} +49 -47
- package/package.json +1 -4
- package/dist/builtins/local-auth/homeassistant-integration.d.ts +0 -7
- package/dist/{model-download-service-Caj1kaSi.js → file-data-plane-DUHPHa-Y.js} +847 -847
- package/dist/{resource-monitor-BkP504Vq.mjs → resource-monitor-BWmQ5i-o.mjs} +1 -1
- 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-
|
|
4
|
-
const
|
|
5
|
-
|
|
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-I0WgqQQ-.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
|
|
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
|
|
29
|
-
require("./builtins/
|
|
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-BqIpg0C7.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
|
|
39
|
-
let
|
|
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
|
|
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,371 @@ 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/events/event-bus.ts
|
|
187
|
+
var EventBus = class {
|
|
188
|
+
listeners = /* @__PURE__ */ new Map();
|
|
189
|
+
on(event, callback) {
|
|
190
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
191
|
+
const set = this.listeners.get(event);
|
|
192
|
+
set.add(callback);
|
|
193
|
+
return () => set.delete(callback);
|
|
194
|
+
}
|
|
195
|
+
emit(event, data) {
|
|
196
|
+
const set = this.listeners.get(event);
|
|
197
|
+
if (!set) return;
|
|
198
|
+
for (const cb of set) try {
|
|
199
|
+
cb(data);
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
once(event, callback) {
|
|
203
|
+
const unsub = this.on(event, (data) => {
|
|
204
|
+
unsub();
|
|
205
|
+
callback(data);
|
|
206
|
+
});
|
|
207
|
+
return unsub;
|
|
208
|
+
}
|
|
209
|
+
removeAllListeners(event) {
|
|
210
|
+
if (event) this.listeners.delete(event);
|
|
211
|
+
else this.listeners.clear();
|
|
212
|
+
}
|
|
213
|
+
listenerCount(event) {
|
|
214
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/events/system-event-bus.ts
|
|
219
|
+
var EventRingBuffer = class {
|
|
220
|
+
capacity;
|
|
221
|
+
buffer;
|
|
222
|
+
head = 0;
|
|
223
|
+
count = 0;
|
|
224
|
+
constructor(capacity) {
|
|
225
|
+
this.capacity = capacity;
|
|
226
|
+
this.buffer = Array.from({ length: capacity });
|
|
227
|
+
}
|
|
228
|
+
push(event) {
|
|
229
|
+
this.buffer[this.head] = event;
|
|
230
|
+
this.head = (this.head + 1) % this.capacity;
|
|
231
|
+
if (this.count < this.capacity) this.count++;
|
|
232
|
+
}
|
|
233
|
+
getAll() {
|
|
234
|
+
const result = [];
|
|
235
|
+
for (let i = 0; i < this.count; i++) {
|
|
236
|
+
const index = (this.head - 1 - i + this.capacity) % this.capacity;
|
|
237
|
+
result.push(this.buffer[index]);
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
query(filter, limit) {
|
|
242
|
+
let result = [...this.getAll()];
|
|
243
|
+
if (filter) result = result.filter((event) => matchesFilter(event, filter));
|
|
244
|
+
if (limit !== void 0 && limit > 0) return result.slice(0, limit);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
function matchesCategory(eventCategory, filterCategory) {
|
|
249
|
+
if (filterCategory.endsWith(".*")) {
|
|
250
|
+
const prefix = filterCategory.slice(0, -2);
|
|
251
|
+
return eventCategory.startsWith(prefix + ".") || eventCategory === prefix;
|
|
252
|
+
}
|
|
253
|
+
return eventCategory === filterCategory;
|
|
254
|
+
}
|
|
255
|
+
function matchesFilter(event, filter) {
|
|
256
|
+
if (filter.category) {
|
|
257
|
+
const raw = filter.category;
|
|
258
|
+
if (!(Array.isArray(raw) ? raw.map(String) : [String(raw)]).some((cat) => matchesCategory(event.category, cat))) return false;
|
|
259
|
+
}
|
|
260
|
+
if (filter.source) {
|
|
261
|
+
if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
|
|
262
|
+
}
|
|
263
|
+
if (filter.agentId) {
|
|
264
|
+
const eventNodeId = event.source.nodeId;
|
|
265
|
+
if (!eventNodeId || !eventNodeId.startsWith(filter.agentId)) return false;
|
|
266
|
+
}
|
|
267
|
+
if (filter.addonId) {
|
|
268
|
+
if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
|
|
269
|
+
}
|
|
270
|
+
if (filter.deviceId !== void 0) {
|
|
271
|
+
if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
|
|
272
|
+
}
|
|
273
|
+
if (filter.since && event.timestamp < filter.since) return false;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
var SystemEventBus = class {
|
|
277
|
+
ringBuffer;
|
|
278
|
+
subscribers = [];
|
|
279
|
+
constructor(bufferSize = 1e4) {
|
|
280
|
+
this.ringBuffer = new EventRingBuffer(bufferSize);
|
|
281
|
+
}
|
|
282
|
+
emit(event) {
|
|
283
|
+
this.ringBuffer.push(event);
|
|
284
|
+
for (const subscriber of this.subscribers) if (matchesFilter(event, subscriber.filter)) queueMicrotask(() => {
|
|
285
|
+
try {
|
|
286
|
+
subscriber.callback(event);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error(`[EventBus] Subscriber error for ${event.category}:`, err);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Subscribe to events matching a filter.
|
|
294
|
+
* When called with a typed category filter, the handler receives typed event data.
|
|
295
|
+
*/
|
|
296
|
+
subscribe(filter, handler) {
|
|
297
|
+
const subscriber = {
|
|
298
|
+
filter,
|
|
299
|
+
callback: handler
|
|
300
|
+
};
|
|
301
|
+
this.subscribers.push(subscriber);
|
|
302
|
+
return () => {
|
|
303
|
+
const index = this.subscribers.indexOf(subscriber);
|
|
304
|
+
if (index !== -1) this.subscribers.splice(index, 1);
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
getRecent(filter, limit) {
|
|
308
|
+
return this.ringBuffer.query(filter, limit);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/feature/feature-manager.ts
|
|
313
|
+
var FeatureManager = class {
|
|
314
|
+
configReader;
|
|
315
|
+
constructor(configReader) {
|
|
316
|
+
this.configReader = configReader;
|
|
317
|
+
}
|
|
318
|
+
isEnabled(flag) {
|
|
319
|
+
return this.configReader.features[flag];
|
|
320
|
+
}
|
|
321
|
+
getManifest() {
|
|
322
|
+
return { ...this.configReader.features };
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/http/data-plane-registry.ts
|
|
327
|
+
function trimSlashes(s) {
|
|
328
|
+
return s.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
329
|
+
}
|
|
330
|
+
var DataPlaneRegistry = class {
|
|
331
|
+
byAddon = /* @__PURE__ */ new Map();
|
|
332
|
+
/** Replace ALL endpoints for an addon (idempotent re-mount). */
|
|
333
|
+
registerAddon(addonId, endpoints) {
|
|
334
|
+
if (endpoints.length === 0) {
|
|
335
|
+
this.byAddon.delete(addonId);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.byAddon.set(addonId, [...endpoints]);
|
|
339
|
+
}
|
|
340
|
+
/** Drop an addon's endpoints (stop / disconnect). */
|
|
341
|
+
unregisterAddon(addonId) {
|
|
342
|
+
this.byAddon.delete(addonId);
|
|
343
|
+
}
|
|
344
|
+
/** True when the addon has at least one data-plane registered. */
|
|
345
|
+
has(addonId) {
|
|
346
|
+
return this.byAddon.has(addonId);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Match `subPath` (the part after `/addon/<addonId>/`, with or without a
|
|
350
|
+
* leading slash, NO query string) against the addon's prefixes. Longest prefix
|
|
351
|
+
* wins. Returns the matched endpoint + the remaining path the addon handler sees.
|
|
352
|
+
*/
|
|
353
|
+
match(addonId, subPath) {
|
|
354
|
+
const endpoints = this.byAddon.get(addonId);
|
|
355
|
+
if (!endpoints) return null;
|
|
356
|
+
const normalized = `/${trimSlashes(subPath)}`;
|
|
357
|
+
let best = null;
|
|
358
|
+
for (const endpoint of endpoints) {
|
|
359
|
+
const prefix = `/${trimSlashes(endpoint.prefix)}`;
|
|
360
|
+
if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
|
|
361
|
+
const rest = normalized.slice(prefix.length) || "/";
|
|
362
|
+
if (best === null || prefix.length > best.len) best = {
|
|
363
|
+
endpoint,
|
|
364
|
+
rest,
|
|
365
|
+
len: prefix.length
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (best === null) return null;
|
|
370
|
+
return {
|
|
371
|
+
addonId,
|
|
372
|
+
endpoint: best.endpoint,
|
|
373
|
+
rest: best.rest
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/http/reverse-proxy.ts
|
|
379
|
+
/**
|
|
380
|
+
* Transport-agnostic reverse-proxy from the hub to an addon's `127.0.0.1`
|
|
381
|
+
* data-plane listener. Operates on RAW Node http objects (`request.raw` /
|
|
382
|
+
* `reply.raw`) so `@camstack/system` stays free of any HTTP-framework dependency.
|
|
383
|
+
*
|
|
384
|
+
* The hub authenticates first (its existing scoped-token / admin / cookie logic),
|
|
385
|
+
* then hands the raw request+response here. We forward method + headers (incl.
|
|
386
|
+
* `Range`) + body upstream — injecting the per-listener shared secret so the addon
|
|
387
|
+
* accepts the call — and pipe the upstream response straight back (status +
|
|
388
|
+
* headers + body), so `Content-Range`, backpressure and client-disconnect are all
|
|
389
|
+
* native. No bytes are buffered; nothing crosses the cap channel.
|
|
390
|
+
*/
|
|
391
|
+
/** Reverse-proxy one request to an addon data-plane listener. Fire-and-forget:
|
|
392
|
+
* the caller must `reply.hijack()` (or otherwise stop the framework from also
|
|
393
|
+
* responding) before calling this, since we drive `clientRes` directly. */
|
|
394
|
+
function proxyToUpstream(opts) {
|
|
395
|
+
const { baseUrl, secret, upstreamPath, clientReq, clientRes } = opts;
|
|
396
|
+
const base = new URL(baseUrl);
|
|
397
|
+
const headers = { ...clientReq.headers };
|
|
398
|
+
delete headers.host;
|
|
399
|
+
delete headers.connection;
|
|
400
|
+
delete headers["content-length"];
|
|
401
|
+
headers[require_dist.DATAPLANE_SECRET_HEADER] = secret;
|
|
402
|
+
const upstream = (0, node_http.request)({
|
|
403
|
+
protocol: base.protocol,
|
|
404
|
+
hostname: base.hostname,
|
|
405
|
+
port: base.port,
|
|
406
|
+
method: clientReq.method ?? "GET",
|
|
407
|
+
path: upstreamPath,
|
|
408
|
+
headers
|
|
409
|
+
}, (upRes) => {
|
|
410
|
+
clientRes.writeHead(upRes.statusCode ?? 502, upRes.headers);
|
|
411
|
+
upRes.pipe(clientRes);
|
|
412
|
+
});
|
|
413
|
+
upstream.on("error", (err) => {
|
|
414
|
+
if (!clientRes.headersSent) clientRes.writeHead(502, { "content-type": "application/json" });
|
|
415
|
+
clientRes.end(JSON.stringify({
|
|
416
|
+
error: "data-plane upstream error",
|
|
417
|
+
detail: String(err)
|
|
418
|
+
}));
|
|
419
|
+
});
|
|
420
|
+
clientRes.on("close", () => {
|
|
421
|
+
if (!clientRes.writableFinished && !upstream.destroyed) upstream.destroy();
|
|
422
|
+
});
|
|
423
|
+
clientReq.pipe(upstream);
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
61
426
|
//#region src/kernel/heap-watch.ts
|
|
62
427
|
/**
|
|
63
428
|
* heap-watch — hub-main's own memory, on the record, and returned to the OS.
|
|
@@ -317,529 +682,11 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
317
682
|
tick();
|
|
318
683
|
let stopped = false;
|
|
319
684
|
return () => {
|
|
320
|
-
if (stopped) return;
|
|
321
|
-
stopped = true;
|
|
322
|
-
clearInterval(timer);
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
//#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
|
-
};
|
|
685
|
+
if (stopped) return;
|
|
686
|
+
stopped = true;
|
|
687
|
+
clearInterval(timer);
|
|
688
|
+
};
|
|
689
|
+
}
|
|
843
690
|
//#endregion
|
|
844
691
|
//#region src/lifecycle/lifecycle-state-machine.ts
|
|
845
692
|
var VALID_TRANSITIONS = {
|
|
@@ -936,114 +783,6 @@ var LifecycleStateMachine = class {
|
|
|
936
783
|
}
|
|
937
784
|
};
|
|
938
785
|
//#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;
|
|
1004
|
-
}
|
|
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;
|
|
1007
|
-
}
|
|
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);
|
|
1016
|
-
}
|
|
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);
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
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
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
getRecent(filter, limit) {
|
|
1043
|
-
return this.ringBuffer.query(filter, limit);
|
|
1044
|
-
}
|
|
1045
|
-
};
|
|
1046
|
-
//#endregion
|
|
1047
786
|
//#region src/logging/log-ring-buffer.ts
|
|
1048
787
|
/**
|
|
1049
788
|
* Per-tag filter matcher with semantic-aware comparisons:
|
|
@@ -1475,92 +1214,601 @@ var LogManager = class {
|
|
|
1475
1214
|
} catch {}
|
|
1476
1215
|
});
|
|
1477
1216
|
}
|
|
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;
|
|
1487
|
-
return {
|
|
1488
|
-
...entry,
|
|
1489
|
-
tags: {
|
|
1490
|
-
...tags,
|
|
1491
|
-
deviceName: name
|
|
1492
|
-
}
|
|
1493
|
-
};
|
|
1217
|
+
enrichDeviceName(entry) {
|
|
1218
|
+
if (!this.deviceNameLookup) return entry;
|
|
1219
|
+
const tags = entry.tags;
|
|
1220
|
+
if (!tags) return entry;
|
|
1221
|
+
const deviceId = tags.deviceId;
|
|
1222
|
+
if (typeof deviceId !== "number" || !Number.isFinite(deviceId)) return entry;
|
|
1223
|
+
if (typeof tags.deviceName === "string") return entry;
|
|
1224
|
+
const name = this.deviceNameLookup(deviceId);
|
|
1225
|
+
if (!name) return entry;
|
|
1226
|
+
return {
|
|
1227
|
+
...entry,
|
|
1228
|
+
tags: {
|
|
1229
|
+
...tags,
|
|
1230
|
+
deviceName: name
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
/** Subscribe to live logs matching a filter. Returns an unsubscribe function. */
|
|
1235
|
+
subscribe(filter, callback) {
|
|
1236
|
+
const id = Math.random().toString(36).slice(2);
|
|
1237
|
+
this.subscribers.set(id, {
|
|
1238
|
+
filter,
|
|
1239
|
+
callback
|
|
1240
|
+
});
|
|
1241
|
+
return () => {
|
|
1242
|
+
this.subscribers.delete(id);
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
matchesFilter(entry, filter) {
|
|
1246
|
+
if (filter.level) {
|
|
1247
|
+
const levels = [
|
|
1248
|
+
"debug",
|
|
1249
|
+
"info",
|
|
1250
|
+
"warn",
|
|
1251
|
+
"error"
|
|
1252
|
+
];
|
|
1253
|
+
const minIdx = levels.indexOf(filter.level);
|
|
1254
|
+
if (levels.indexOf(entry.level) < minIdx) return false;
|
|
1255
|
+
}
|
|
1256
|
+
if (filter.tags) {
|
|
1257
|
+
if (!matchesLogTags(entry.tags, filter.tags)) return false;
|
|
1258
|
+
}
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Register a log destination.
|
|
1263
|
+
*
|
|
1264
|
+
* @param opts.replay If true (default), replay every entry currently in
|
|
1265
|
+
* the ring buffer to the new destination before live
|
|
1266
|
+
* traffic starts. Lets a destination added mid-boot
|
|
1267
|
+
* still receive boot-time log entries.
|
|
1268
|
+
*/
|
|
1269
|
+
/** Apply new in-memory buffer limits at runtime (see PartitionedLogBuffer). */
|
|
1270
|
+
reconfigureBuffer(options) {
|
|
1271
|
+
this.ringBuffer.reconfigure(options);
|
|
1272
|
+
}
|
|
1273
|
+
/** Set the per-destination enablement check (see {@link isDestinationEnabled}). */
|
|
1274
|
+
setDestinationEnabledCheck(check) {
|
|
1275
|
+
this.isDestinationEnabled = check ?? void 0;
|
|
1276
|
+
}
|
|
1277
|
+
addDestination(dest, opts = {}) {
|
|
1278
|
+
const replay = opts.replay ?? true;
|
|
1279
|
+
this.destinations.push(opts.addonId === void 0 ? { dest } : {
|
|
1280
|
+
dest,
|
|
1281
|
+
addonId: opts.addonId
|
|
1282
|
+
});
|
|
1283
|
+
if (replay) for (const entry of this.ringBuffer.query({})) try {
|
|
1284
|
+
dest.write(entry);
|
|
1285
|
+
} catch {}
|
|
1286
|
+
}
|
|
1287
|
+
removeDestination(dest) {
|
|
1288
|
+
const index = this.destinations.findIndex((d) => d.dest === dest);
|
|
1289
|
+
if (index !== -1) this.destinations.splice(index, 1);
|
|
1290
|
+
}
|
|
1291
|
+
query(filter) {
|
|
1292
|
+
return this.ringBuffer.query(filter);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Drop ring-buffer entries matching `filter`. Returns the count
|
|
1296
|
+
* removed. Backs the UI's "Clear logs" admin action so an operator
|
|
1297
|
+
* can wipe the historical window for a scope (per-device, per-addon,
|
|
1298
|
+
* agent-level) without restarting the server. External destinations
|
|
1299
|
+
* (file, forwarder) are NOT touched — only the in-memory ring.
|
|
1300
|
+
*/
|
|
1301
|
+
clear(filter = {}) {
|
|
1302
|
+
return this.ringBuffer.clear(filter);
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
//#endregion
|
|
1306
|
+
//#region src/network/network-quality.ts
|
|
1307
|
+
var NetworkQualityTracker = class NetworkQualityTracker {
|
|
1308
|
+
devices = /* @__PURE__ */ new Map();
|
|
1309
|
+
static MAX_SAMPLES = 30;
|
|
1310
|
+
reportStreamStats(deviceId, streamId, bitrateKbps, packetLoss) {
|
|
1311
|
+
const device = this.getOrCreateDevice(deviceId);
|
|
1312
|
+
let stream = device.streams.get(streamId);
|
|
1313
|
+
if (!stream) {
|
|
1314
|
+
stream = {
|
|
1315
|
+
nominalBitrateKbps: bitrateKbps,
|
|
1316
|
+
samples: [],
|
|
1317
|
+
peakBitrateKbps: 0,
|
|
1318
|
+
packetLossSamples: [],
|
|
1319
|
+
lastUpdated: 0
|
|
1320
|
+
};
|
|
1321
|
+
device.streams.set(streamId, stream);
|
|
1322
|
+
}
|
|
1323
|
+
stream.samples.push(bitrateKbps);
|
|
1324
|
+
if (stream.samples.length > NetworkQualityTracker.MAX_SAMPLES) stream.samples.shift();
|
|
1325
|
+
stream.peakBitrateKbps = Math.max(stream.peakBitrateKbps, bitrateKbps);
|
|
1326
|
+
if (packetLoss !== void 0) {
|
|
1327
|
+
stream.packetLossSamples.push(packetLoss);
|
|
1328
|
+
if (stream.packetLossSamples.length > NetworkQualityTracker.MAX_SAMPLES) stream.packetLossSamples.shift();
|
|
1329
|
+
}
|
|
1330
|
+
stream.lastUpdated = Date.now();
|
|
1331
|
+
}
|
|
1332
|
+
reportClientStats(deviceId, stats) {
|
|
1333
|
+
const device = this.getOrCreateDevice(deviceId);
|
|
1334
|
+
device.client = {
|
|
1335
|
+
...stats,
|
|
1336
|
+
lastUpdated: Date.now()
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
getDeviceStats(deviceId) {
|
|
1340
|
+
const device = this.devices.get(deviceId);
|
|
1341
|
+
if (!device) return null;
|
|
1342
|
+
const streams = {};
|
|
1343
|
+
for (const [streamId, s] of device.streams) {
|
|
1344
|
+
const avg = (arr) => arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
1345
|
+
streams[streamId] = {
|
|
1346
|
+
nominalBitrateKbps: s.nominalBitrateKbps,
|
|
1347
|
+
observedBitrateKbps: Math.round(avg(s.samples)),
|
|
1348
|
+
peakBitrateKbps: s.peakBitrateKbps,
|
|
1349
|
+
packetLossPercent: Math.round(avg(s.packetLossSamples) * 10) / 10,
|
|
1350
|
+
lastUpdated: s.lastUpdated
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
deviceId,
|
|
1355
|
+
streams,
|
|
1356
|
+
client: device.client ?? void 0
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
getAllStats() {
|
|
1360
|
+
const results = [];
|
|
1361
|
+
for (const deviceId of this.devices.keys()) {
|
|
1362
|
+
const stats = this.getDeviceStats(deviceId);
|
|
1363
|
+
if (stats) results.push(stats);
|
|
1364
|
+
}
|
|
1365
|
+
return results;
|
|
1366
|
+
}
|
|
1367
|
+
getOrCreateDevice(deviceId) {
|
|
1368
|
+
let device = this.devices.get(deviceId);
|
|
1369
|
+
if (!device) {
|
|
1370
|
+
device = {
|
|
1371
|
+
streams: /* @__PURE__ */ new Map(),
|
|
1372
|
+
client: null
|
|
1373
|
+
};
|
|
1374
|
+
this.devices.set(deviceId, device);
|
|
1375
|
+
}
|
|
1376
|
+
return device;
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/notification/notification-service.ts
|
|
1381
|
+
/**
|
|
1382
|
+
* Central notification service that routes canonical notifications to
|
|
1383
|
+
* configured `(addonId, targetId)` targets. Framework-agnostic —
|
|
1384
|
+
* dependencies injected via constructor.
|
|
1385
|
+
*
|
|
1386
|
+
* Providers are resolved from the ICapabilityRegistry by `addonId` on each
|
|
1387
|
+
* call (proxy pattern). Falls back to a local map when no registry is
|
|
1388
|
+
* provided (tests / backward compat).
|
|
1389
|
+
*/
|
|
1390
|
+
var NotificationService = class {
|
|
1391
|
+
logger;
|
|
1392
|
+
localProviders = /* @__PURE__ */ new Map();
|
|
1393
|
+
routing = /* @__PURE__ */ new Map();
|
|
1394
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
1395
|
+
lastSent = /* @__PURE__ */ new Map();
|
|
1396
|
+
registry = null;
|
|
1397
|
+
constructor(logger) {
|
|
1398
|
+
this.logger = logger;
|
|
1399
|
+
}
|
|
1400
|
+
/** Set the registry for live provider lookup. Called once during boot. */
|
|
1401
|
+
setRegistry(registry) {
|
|
1402
|
+
this.registry = registry;
|
|
1403
|
+
}
|
|
1404
|
+
/** Resolve the collection provider for an addon — registry first, then local map. */
|
|
1405
|
+
resolveProvider(addonId) {
|
|
1406
|
+
if (this.registry) return this.registry.getProviderByAddon("notification-output", addonId) ?? void 0;
|
|
1407
|
+
return this.localProviders.get(addonId);
|
|
1408
|
+
}
|
|
1409
|
+
/** Register a provider in the local fallback map (used when no registry is set). */
|
|
1410
|
+
registerLocalProvider(addonId, provider) {
|
|
1411
|
+
this.localProviders.set(addonId, provider);
|
|
1412
|
+
this.logger.info("Notification provider added", { meta: { addonId } });
|
|
1413
|
+
}
|
|
1414
|
+
/** Remove a provider from the local fallback map. */
|
|
1415
|
+
unregisterLocalProvider(addonId) {
|
|
1416
|
+
this.localProviders.delete(addonId);
|
|
1417
|
+
this.logger.info("Notification provider removed", { meta: { addonId } });
|
|
1418
|
+
}
|
|
1419
|
+
/** Route a category to a set of `(addonId, targetId)` targets. */
|
|
1420
|
+
setRouting(category, targets) {
|
|
1421
|
+
this.routing.set(category, [...targets]);
|
|
1422
|
+
}
|
|
1423
|
+
setRateLimit(category, minIntervalMs) {
|
|
1424
|
+
this.rateLimits.set(category, minIntervalMs);
|
|
1425
|
+
}
|
|
1426
|
+
async notify(notification) {
|
|
1427
|
+
const { category, deviceId } = notification;
|
|
1428
|
+
const rateLimitKey = deviceId ? `${category}:${deviceId}` : category;
|
|
1429
|
+
const minInterval = this.rateLimits.get(category) ?? 0;
|
|
1430
|
+
if (minInterval > 0) {
|
|
1431
|
+
const last = this.lastSent.get(rateLimitKey) ?? 0;
|
|
1432
|
+
if (notification.timestamp - last < minInterval) {
|
|
1433
|
+
this.logger.debug("Rate limited", { meta: { rateLimitKey } });
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const targets = this.routing.get(category) ?? this.routing.get("*") ?? [];
|
|
1438
|
+
if (targets.length === 0) {
|
|
1439
|
+
this.logger.debug("No routing configured for category", { meta: { category } });
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
this.lastSent.set(rateLimitKey, notification.timestamp);
|
|
1443
|
+
const { category: _category, timestamp: _timestamp, ...canonical } = notification;
|
|
1444
|
+
await Promise.allSettled(targets.map(async (ref) => {
|
|
1445
|
+
const provider = this.resolveProvider(ref.addonId);
|
|
1446
|
+
if (!provider) {
|
|
1447
|
+
this.logger.debug("No provider for target", { meta: { addonId: ref.addonId } });
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
try {
|
|
1451
|
+
await provider.send({
|
|
1452
|
+
targetId: ref.targetId,
|
|
1453
|
+
notification: canonical
|
|
1454
|
+
});
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
this.logger.error("Notification target failed", { meta: {
|
|
1457
|
+
addonId: ref.addonId,
|
|
1458
|
+
targetId: ref.targetId,
|
|
1459
|
+
error: require_dist.errMsg(err)
|
|
1460
|
+
} });
|
|
1461
|
+
}
|
|
1462
|
+
}));
|
|
1463
|
+
}
|
|
1464
|
+
getRouting() {
|
|
1465
|
+
return this.routing;
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/notification/toast-service.ts
|
|
1470
|
+
/**
|
|
1471
|
+
* Service for broadcasting toast notifications to connected UI clients.
|
|
1472
|
+
* Framework-agnostic — integrates with tRPC subscriptions via subscribe().
|
|
1473
|
+
*/
|
|
1474
|
+
var ToastService = class {
|
|
1475
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1476
|
+
/**
|
|
1477
|
+
* Subscribe to toast events for a specific user.
|
|
1478
|
+
* Returns an unsubscribe function.
|
|
1479
|
+
*/
|
|
1480
|
+
subscribe(connectionId, userId, callback) {
|
|
1481
|
+
this.listeners.set(connectionId, {
|
|
1482
|
+
userId,
|
|
1483
|
+
callback
|
|
1484
|
+
});
|
|
1485
|
+
return () => {
|
|
1486
|
+
this.listeners.delete(connectionId);
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Broadcast a toast to all connected clients.
|
|
1491
|
+
*/
|
|
1492
|
+
broadcast(toast) {
|
|
1493
|
+
for (const entry of this.listeners.values()) entry.callback(toast);
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Send a toast to a specific user's connections only.
|
|
1497
|
+
*/
|
|
1498
|
+
sendToUser(userId, toast) {
|
|
1499
|
+
for (const entry of this.listeners.values()) if (entry.userId === userId) entry.callback(toast);
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
//#endregion
|
|
1503
|
+
//#region src/pipeline/engine-manager-resolver.ts
|
|
1504
|
+
/**
|
|
1505
|
+
* Adapter that wraps an IEngineManager + addonConfigs as an IAddonResolver.
|
|
1506
|
+
*
|
|
1507
|
+
* Used for backwards compatibility — the in-process AddonEngineManager
|
|
1508
|
+
* still uses the old getOrCreateEngine(addonId, globalConfig, override) API.
|
|
1509
|
+
* This adapter bridges it to the new resolve(addonId, config) interface.
|
|
1510
|
+
*/
|
|
1511
|
+
var EngineManagerResolver = class {
|
|
1512
|
+
engineManager;
|
|
1513
|
+
addonConfigs;
|
|
1514
|
+
constructor(engineManager, addonConfigs) {
|
|
1515
|
+
this.engineManager = engineManager;
|
|
1516
|
+
this.addonConfigs = addonConfigs;
|
|
1517
|
+
}
|
|
1518
|
+
async resolve(addonId, config) {
|
|
1519
|
+
const globalConfig = this.addonConfigs.get(addonId) ?? {};
|
|
1520
|
+
const engine = await this.engineManager.getOrCreateEngine(addonId, globalConfig, config);
|
|
1521
|
+
return { process: async (input) => {
|
|
1522
|
+
return engine.process(input);
|
|
1523
|
+
} };
|
|
1524
|
+
}
|
|
1525
|
+
async shutdownAll() {
|
|
1526
|
+
await this.engineManager.shutdownAll();
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
//#endregion
|
|
1530
|
+
//#region src/pipeline/pipeline-runner.ts
|
|
1531
|
+
var PipelineRunner = class {
|
|
1532
|
+
addonResolver;
|
|
1533
|
+
constructor(addonResolver) {
|
|
1534
|
+
this.addonResolver = addonResolver;
|
|
1535
|
+
}
|
|
1536
|
+
async run(frame, config) {
|
|
1537
|
+
const startTime = performance.now();
|
|
1538
|
+
const results = [];
|
|
1539
|
+
const timings = {};
|
|
1540
|
+
for (const rootNode of config.video) await this.executeNode(rootNode, frame, null, results, timings);
|
|
1541
|
+
if (config.audio) await this.executeNode(config.audio, frame, null, results, timings);
|
|
1542
|
+
return {
|
|
1543
|
+
results,
|
|
1544
|
+
totalMs: performance.now() - startTime,
|
|
1545
|
+
timings,
|
|
1546
|
+
frameTimestamp: frame.timestamp
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
async executeNode(node, frame, parentDetection, results, timings) {
|
|
1550
|
+
const resultId = (0, node_crypto.randomUUID)();
|
|
1551
|
+
const stepStart = performance.now();
|
|
1552
|
+
try {
|
|
1553
|
+
const effectiveConfig = { ...node.configOverride };
|
|
1554
|
+
const caller = await this.addonResolver.resolve(node.addon, effectiveConfig);
|
|
1555
|
+
let output;
|
|
1556
|
+
if (parentDetection) {
|
|
1557
|
+
const cropInput = {
|
|
1558
|
+
frame,
|
|
1559
|
+
roi: parentDetection.det.bbox,
|
|
1560
|
+
parentDetection: parentDetection.det
|
|
1561
|
+
};
|
|
1562
|
+
output = await caller.process(cropInput);
|
|
1563
|
+
} else output = await caller.process(frame);
|
|
1564
|
+
const stepMs = performance.now() - stepStart;
|
|
1565
|
+
const slot = "detections" in output ? "detector" : "crops" in output ? "cropper" : "classifier";
|
|
1566
|
+
const stepResult = {
|
|
1567
|
+
addon: node.addon,
|
|
1568
|
+
slot,
|
|
1569
|
+
output,
|
|
1570
|
+
parentResultId: parentDetection?.resultId,
|
|
1571
|
+
resultId,
|
|
1572
|
+
inferenceMs: output.inferenceMs,
|
|
1573
|
+
preprocessMs: 0,
|
|
1574
|
+
totalMs: stepMs
|
|
1575
|
+
};
|
|
1576
|
+
results.push(stepResult);
|
|
1577
|
+
timings[node.step] = stepMs;
|
|
1578
|
+
if (node.children?.length) {
|
|
1579
|
+
const detections = "detections" in output ? output.detections : "crops" in output ? output.crops : [];
|
|
1580
|
+
await this.executeChildren(node.children, frame, detections, resultId, results, timings);
|
|
1581
|
+
}
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
const stepMs = performance.now() - stepStart;
|
|
1584
|
+
const message = require_dist.errMsg(error);
|
|
1585
|
+
results.push({
|
|
1586
|
+
addon: node.addon,
|
|
1587
|
+
slot: "detector",
|
|
1588
|
+
output: {
|
|
1589
|
+
detections: [],
|
|
1590
|
+
inferenceMs: 0,
|
|
1591
|
+
modelId: ""
|
|
1592
|
+
},
|
|
1593
|
+
parentResultId: parentDetection?.resultId,
|
|
1594
|
+
resultId,
|
|
1595
|
+
inferenceMs: 0,
|
|
1596
|
+
preprocessMs: 0,
|
|
1597
|
+
totalMs: stepMs,
|
|
1598
|
+
error: {
|
|
1599
|
+
code: "ADDON_ERROR",
|
|
1600
|
+
message,
|
|
1601
|
+
childrenSkipped: true
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async executeChildren(children, frame, parentDetections, parentResultId, results, timings) {
|
|
1607
|
+
const promises = [];
|
|
1608
|
+
for (const detection of parentDetections) {
|
|
1609
|
+
if (detection.bbox.w < 1 || detection.bbox.h < 1) continue;
|
|
1610
|
+
for (const child of children) promises.push(this.executeNode(child, frame, {
|
|
1611
|
+
det: detection,
|
|
1612
|
+
resultId: parentResultId
|
|
1613
|
+
}, results, timings));
|
|
1614
|
+
}
|
|
1615
|
+
await Promise.allSettled(promises);
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/pipeline/pipeline-validator.ts
|
|
1620
|
+
var PipelineValidator = class {
|
|
1621
|
+
stepExists;
|
|
1622
|
+
constructor(stepExists) {
|
|
1623
|
+
this.stepExists = stepExists;
|
|
1494
1624
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
const
|
|
1498
|
-
this.
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1625
|
+
validate(config) {
|
|
1626
|
+
const errors = [];
|
|
1627
|
+
const warnings = [];
|
|
1628
|
+
for (const node of config.video) this.validateNode(node, errors, warnings);
|
|
1629
|
+
if (config.audio) this.validateNode(config.audio, errors, warnings);
|
|
1630
|
+
return {
|
|
1631
|
+
valid: errors.length === 0,
|
|
1632
|
+
errors,
|
|
1633
|
+
warnings
|
|
1504
1634
|
};
|
|
1505
1635
|
}
|
|
1506
|
-
|
|
1507
|
-
if (
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
"
|
|
1512
|
-
"error"
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
if (levels.indexOf(entry.level) < minIdx) return false;
|
|
1636
|
+
validateNode(node, errors, warnings) {
|
|
1637
|
+
if (!this.stepExists(node.addon)) {
|
|
1638
|
+
errors.push({
|
|
1639
|
+
step: node.step,
|
|
1640
|
+
addon: node.addon,
|
|
1641
|
+
message: `Step "${node.addon}" has no provider — the addon that provided it may have been removed`,
|
|
1642
|
+
severity: "error"
|
|
1643
|
+
});
|
|
1644
|
+
return;
|
|
1516
1645
|
}
|
|
1517
|
-
if (
|
|
1518
|
-
|
|
1646
|
+
if (node.children) for (const child of node.children) this.validateNode(child, errors, warnings);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
//#endregion
|
|
1650
|
+
//#region src/python/python-env-manager.ts
|
|
1651
|
+
var execFileAsync$4 = (0, node_util.promisify)(node_child_process.execFile);
|
|
1652
|
+
var PythonEnvManager = class {
|
|
1653
|
+
venvPath;
|
|
1654
|
+
cachedProbe = null;
|
|
1655
|
+
constructor(dataDir) {
|
|
1656
|
+
this.venvPath = node_path.join(dataDir, ".venv");
|
|
1657
|
+
}
|
|
1658
|
+
async probe() {
|
|
1659
|
+
if (this.cachedProbe) return this.cachedProbe;
|
|
1660
|
+
for (const cmd of ["python3", "python"]) try {
|
|
1661
|
+
const { stdout } = await execFileAsync$4(cmd, ["--version"]);
|
|
1662
|
+
const version = stdout.trim().replace("Python ", "");
|
|
1663
|
+
const major = parseInt(version.split(".")[0] ?? "0", 10);
|
|
1664
|
+
const minor = parseInt(version.split(".")[1] ?? "0", 10);
|
|
1665
|
+
if (major < 3 || major === 3 && minor < 10) continue;
|
|
1666
|
+
const { stdout: pathOut } = await execFileAsync$4(cmd, ["-c", "import sys; print(sys.executable)"]);
|
|
1667
|
+
this.cachedProbe = {
|
|
1668
|
+
available: true,
|
|
1669
|
+
version,
|
|
1670
|
+
path: pathOut.trim()
|
|
1671
|
+
};
|
|
1672
|
+
return this.cachedProbe;
|
|
1673
|
+
} catch {
|
|
1674
|
+
continue;
|
|
1519
1675
|
}
|
|
1520
|
-
|
|
1676
|
+
this.cachedProbe = { available: false };
|
|
1677
|
+
return this.cachedProbe;
|
|
1521
1678
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1679
|
+
async ensure(options) {
|
|
1680
|
+
const probe = await this.probe();
|
|
1681
|
+
if (!probe.available || !probe.path) throw new Error("Python 3.10+ is required but not found on this system");
|
|
1682
|
+
if (!node_fs.existsSync(node_path.join(this.venvPath, "bin", "python"))) await execFileAsync$4(probe.path, [
|
|
1683
|
+
"-m",
|
|
1684
|
+
"venv",
|
|
1685
|
+
this.venvPath
|
|
1686
|
+
]);
|
|
1687
|
+
const venvPython = node_path.join(this.venvPath, "bin", "python");
|
|
1688
|
+
if (options.packages.length > 0) await execFileAsync$4(venvPython, [
|
|
1689
|
+
"-m",
|
|
1690
|
+
"pip",
|
|
1691
|
+
"install",
|
|
1692
|
+
"-q",
|
|
1693
|
+
...options.packages
|
|
1694
|
+
]);
|
|
1695
|
+
return {
|
|
1696
|
+
pythonPath: venvPython,
|
|
1697
|
+
venvPath: this.venvPath,
|
|
1698
|
+
spawn: (script, args) => {
|
|
1699
|
+
return (0, node_child_process.spawn)(venvPython, [script, ...args]);
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1533
1702
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1703
|
+
spawn(script, args) {
|
|
1704
|
+
const venvPython = node_path.join(this.venvPath, "bin", "python");
|
|
1705
|
+
if (!node_fs.existsSync(venvPython)) throw new Error("Python venv not initialized. Call ensure() first.");
|
|
1706
|
+
return (0, node_child_process.spawn)(venvPython, [script, ...args]);
|
|
1537
1707
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1708
|
+
};
|
|
1709
|
+
//#endregion
|
|
1710
|
+
//#region src/repl/repl-engine.ts
|
|
1711
|
+
var EXECUTION_TIMEOUT_MS = 5e3;
|
|
1712
|
+
var COMPLETION_TIMEOUT_MS = 1e3;
|
|
1713
|
+
var ReplEngine = class {
|
|
1714
|
+
contextProvider;
|
|
1715
|
+
constructor(contextProvider) {
|
|
1716
|
+
this.contextProvider = contextProvider;
|
|
1547
1717
|
}
|
|
1548
|
-
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1718
|
+
async execute(code, context) {
|
|
1719
|
+
const start = Date.now();
|
|
1720
|
+
const sandbox = await this.buildSandbox(context);
|
|
1721
|
+
try {
|
|
1722
|
+
const vmContext = node_vm.createContext(sandbox);
|
|
1723
|
+
let result = new node_vm.Script(code).runInContext(vmContext, { timeout: EXECUTION_TIMEOUT_MS });
|
|
1724
|
+
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))]);
|
|
1725
|
+
const duration = Date.now() - start;
|
|
1726
|
+
if (result === void 0) return {
|
|
1727
|
+
output: "undefined",
|
|
1728
|
+
type: "void",
|
|
1729
|
+
duration
|
|
1730
|
+
};
|
|
1731
|
+
return {
|
|
1732
|
+
output: node_util.inspect(result, {
|
|
1733
|
+
depth: 4,
|
|
1734
|
+
colors: false,
|
|
1735
|
+
maxArrayLength: 50,
|
|
1736
|
+
maxStringLength: 1e3
|
|
1737
|
+
}),
|
|
1738
|
+
type: "value",
|
|
1739
|
+
duration
|
|
1740
|
+
};
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
return {
|
|
1743
|
+
output: err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : String(err),
|
|
1744
|
+
type: "error",
|
|
1745
|
+
duration: Date.now() - start
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1551
1748
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1749
|
+
async getCompletions(partial, context) {
|
|
1750
|
+
const sandbox = await this.buildSandbox(context);
|
|
1751
|
+
const keys = Object.keys(sandbox);
|
|
1752
|
+
if (!partial) return keys;
|
|
1753
|
+
const lastDot = partial.lastIndexOf(".");
|
|
1754
|
+
if (lastDot === -1) return keys.filter((k) => k.startsWith(partial));
|
|
1755
|
+
const objPath = partial.substring(0, lastDot);
|
|
1756
|
+
const propPrefix = partial.substring(lastDot + 1);
|
|
1757
|
+
try {
|
|
1758
|
+
const vmContext = node_vm.createContext(sandbox);
|
|
1759
|
+
const obj = node_vm.runInContext(objPath, vmContext, { timeout: COMPLETION_TIMEOUT_MS });
|
|
1760
|
+
if (obj && typeof obj === "object") {
|
|
1761
|
+
const proto = Object.getPrototypeOf(obj);
|
|
1762
|
+
const objKeys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertyNames(proto ?? {})).filter((k) => k.startsWith(propPrefix));
|
|
1763
|
+
return [...new Set(objKeys)].map((k) => `${objPath}.${k}`);
|
|
1764
|
+
}
|
|
1765
|
+
} catch {}
|
|
1766
|
+
return [];
|
|
1554
1767
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1768
|
+
async buildSandbox(context) {
|
|
1769
|
+
const base = {
|
|
1770
|
+
console: {
|
|
1771
|
+
log: (...args) => node_util.inspect(args),
|
|
1772
|
+
warn: (...args) => node_util.inspect(args)
|
|
1773
|
+
},
|
|
1774
|
+
JSON,
|
|
1775
|
+
Math,
|
|
1776
|
+
Date,
|
|
1777
|
+
Array,
|
|
1778
|
+
Object,
|
|
1779
|
+
String,
|
|
1780
|
+
Number,
|
|
1781
|
+
Boolean,
|
|
1782
|
+
Map,
|
|
1783
|
+
Set,
|
|
1784
|
+
Promise,
|
|
1785
|
+
Buffer,
|
|
1786
|
+
setTimeout: void 0,
|
|
1787
|
+
setInterval: void 0,
|
|
1788
|
+
fetch: void 0,
|
|
1789
|
+
require: void 0,
|
|
1790
|
+
process: void 0,
|
|
1791
|
+
...context.variables
|
|
1792
|
+
};
|
|
1793
|
+
switch (context.scope.type) {
|
|
1794
|
+
case "system": return {
|
|
1795
|
+
...base,
|
|
1796
|
+
...await this.contextProvider.getSystemSandbox()
|
|
1797
|
+
};
|
|
1798
|
+
case "device": return {
|
|
1799
|
+
...base,
|
|
1800
|
+
...await this.contextProvider.getDeviceSandbox(context.scope.deviceId)
|
|
1801
|
+
};
|
|
1802
|
+
case "provider": return {
|
|
1803
|
+
...base,
|
|
1804
|
+
...await this.contextProvider.getProviderSandbox(context.scope.providerId)
|
|
1805
|
+
};
|
|
1806
|
+
case "addon": return {
|
|
1807
|
+
...base,
|
|
1808
|
+
...await this.contextProvider.getAddonSandbox(context.scope.addonId)
|
|
1809
|
+
};
|
|
1810
|
+
default: return base;
|
|
1811
|
+
}
|
|
1564
1812
|
}
|
|
1565
1813
|
};
|
|
1566
1814
|
//#endregion
|
|
@@ -1902,216 +2150,6 @@ var StorageManager = class {
|
|
|
1902
2150
|
}
|
|
1903
2151
|
};
|
|
1904
2152
|
//#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
2153
|
//#region src/tls/cert-manager.ts
|
|
2116
2154
|
var execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
|
|
2117
2155
|
/**
|
|
@@ -2185,54 +2223,6 @@ function loadTlsCert(certPath, keyPath) {
|
|
|
2185
2223
|
};
|
|
2186
2224
|
}
|
|
2187
2225
|
//#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
2226
|
//#region src/builtins/sqlite-storage/integration-registry.ts
|
|
2237
2227
|
function serializeSetting(value) {
|
|
2238
2228
|
if (typeof value === "number") return {
|
|
@@ -2807,6 +2797,268 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2807
2797
|
}
|
|
2808
2798
|
};
|
|
2809
2799
|
//#endregion
|
|
2800
|
+
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
|
2801
|
+
/**
|
|
2802
|
+
* `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
|
|
2803
|
+
* shared `RootUpdateService` runs while staging a `@camstack/server` closure,
|
|
2804
|
+
* BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
|
|
2805
|
+
* decides whether to arm the swap.
|
|
2806
|
+
*
|
|
2807
|
+
* Why this exists (Mac desktop incident, 2026-07-21): a runtime
|
|
2808
|
+
* `applyServerUpdate` on a packaged desktop stages the closure with
|
|
2809
|
+
* `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
|
|
2810
|
+
* TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
|
|
2811
|
+
* its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
|
|
2812
|
+
* with no build toolchain (and where the auto `prebuild-install` does not
|
|
2813
|
+
* target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
|
|
2814
|
+
* `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
|
|
2815
|
+
* that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
|
|
2816
|
+
* `.node` → the validator refuses the swap ("staged closure missing native
|
|
2817
|
+
* prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
|
|
2818
|
+
* has a published prebuild the auto path fetches cleanly, and it carries a
|
|
2819
|
+
* build toolchain fallback.
|
|
2820
|
+
*
|
|
2821
|
+
* This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
|
|
2822
|
+
* into the runtime staging path: for each traditional native that is present
|
|
2823
|
+
* in the closure but missing its compiled `.node`, fetch the prebuild for the
|
|
2824
|
+
* RUNNING Node ABI/arch (the same Node that will load the swapped closure).
|
|
2825
|
+
*
|
|
2826
|
+
* N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
|
|
2827
|
+
* platform SIBLING packages that npm fetches reliably with no compile step, so
|
|
2828
|
+
* they are not in scope here — a genuine miss there is a broken install the
|
|
2829
|
+
* validator should still reject, not something a prebuild re-fetch can heal.
|
|
2830
|
+
*
|
|
2831
|
+
* Injected into the update service from the construction site so the zero-dep
|
|
2832
|
+
* `@camstack/node-root` layer never depends on npm/@camstack/system, and the
|
|
2833
|
+
* fetch is unit-testable with a fake fetcher.
|
|
2834
|
+
*/
|
|
2835
|
+
var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
|
|
2836
|
+
/** Bounded recursive `.node` scan depth inside a module dir. */
|
|
2837
|
+
var NATIVE_SCAN_DEPTH = 4;
|
|
2838
|
+
/** Timeout for a single `prebuild-install` fetch. */
|
|
2839
|
+
var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
|
|
2840
|
+
/**
|
|
2841
|
+
* Traditional (compile-or-prebuild) natives an installed closure must carry a
|
|
2842
|
+
* `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
|
|
2843
|
+
* are excluded (see file header).
|
|
2844
|
+
*/
|
|
2845
|
+
var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
|
|
2846
|
+
/** Bounded recursive scan for ANY `*.node` file under `dir`. */
|
|
2847
|
+
function hasDotNode(dir, maxDepth) {
|
|
2848
|
+
let entries;
|
|
2849
|
+
try {
|
|
2850
|
+
entries = node_fs.readdirSync(dir, { withFileTypes: true });
|
|
2851
|
+
} catch {
|
|
2852
|
+
return false;
|
|
2853
|
+
}
|
|
2854
|
+
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
2855
|
+
if (maxDepth <= 0) return false;
|
|
2856
|
+
for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
|
|
2857
|
+
return false;
|
|
2858
|
+
}
|
|
2859
|
+
/** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
|
|
2860
|
+
function resolvePrebuildInstallBin(closureDir, moduleDir) {
|
|
2861
|
+
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;
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Default fetcher: run the closure's own `prebuild-install` under the CURRENT
|
|
2865
|
+
* Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
|
|
2866
|
+
* app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
|
|
2867
|
+
* `scripts/stage-deps.mjs`.
|
|
2868
|
+
*/
|
|
2869
|
+
function makeDefaultFetch(closureDir, logger) {
|
|
2870
|
+
return async (moduleDir, target) => {
|
|
2871
|
+
const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
|
|
2872
|
+
if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
|
|
2873
|
+
logger.info("fetching native prebuild via prebuild-install", { meta: {
|
|
2874
|
+
moduleDir,
|
|
2875
|
+
bin,
|
|
2876
|
+
nodeVersion: target.nodeVersion,
|
|
2877
|
+
arch: target.arch
|
|
2878
|
+
} });
|
|
2879
|
+
await execFileAsync$2(process.execPath, [
|
|
2880
|
+
bin,
|
|
2881
|
+
"--runtime",
|
|
2882
|
+
"node",
|
|
2883
|
+
"--target",
|
|
2884
|
+
target.nodeVersion,
|
|
2885
|
+
"--arch",
|
|
2886
|
+
target.arch
|
|
2887
|
+
], {
|
|
2888
|
+
cwd: moduleDir,
|
|
2889
|
+
timeout: PREBUILD_FETCH_TIMEOUT_MS
|
|
2890
|
+
});
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Ensure every traditional native in `<closureDir>/node_modules` carries its
|
|
2895
|
+
* compiled `.node`, fetching the prebuild for the running ABI when missing.
|
|
2896
|
+
*
|
|
2897
|
+
* A NO-OP when nothing is missing (docker / tests) and when a native package is
|
|
2898
|
+
* absent from the closure. Never throws for a single package's fetch failure —
|
|
2899
|
+
* it logs and continues so the downstream `findMissingNativePrebuilds`
|
|
2900
|
+
* validator makes the final swap/abort decision. Returns the packages whose
|
|
2901
|
+
* prebuild was successfully fetched (for logging/tests).
|
|
2902
|
+
*/
|
|
2903
|
+
async function ensureNativePrebuilds(closureDir, options) {
|
|
2904
|
+
const nm = node_path.join(closureDir, "node_modules");
|
|
2905
|
+
const target = options.target ?? {
|
|
2906
|
+
nodeVersion: process.versions.node,
|
|
2907
|
+
arch: process.arch
|
|
2908
|
+
};
|
|
2909
|
+
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
2910
|
+
const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
|
|
2911
|
+
const fetched = [];
|
|
2912
|
+
for (const pkg of packages) {
|
|
2913
|
+
const moduleDir = node_path.join(nm, pkg);
|
|
2914
|
+
if (!node_fs.existsSync(moduleDir)) continue;
|
|
2915
|
+
if (hasDotNode(moduleDir, 4)) continue;
|
|
2916
|
+
options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
|
|
2917
|
+
pkg,
|
|
2918
|
+
moduleDir,
|
|
2919
|
+
nodeVersion: target.nodeVersion,
|
|
2920
|
+
arch: target.arch
|
|
2921
|
+
} });
|
|
2922
|
+
try {
|
|
2923
|
+
await fetchPrebuild(moduleDir, target);
|
|
2924
|
+
} catch (err) {
|
|
2925
|
+
options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
|
|
2926
|
+
pkg,
|
|
2927
|
+
error: (0, _camstack_types_addon.errMsg)(err)
|
|
2928
|
+
} });
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
if (hasDotNode(moduleDir, 4)) {
|
|
2932
|
+
fetched.push(pkg);
|
|
2933
|
+
options.logger.info("native prebuild fetched", { meta: { pkg } });
|
|
2934
|
+
} else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
|
|
2935
|
+
}
|
|
2936
|
+
return fetched;
|
|
2937
|
+
}
|
|
2938
|
+
//#endregion
|
|
2939
|
+
//#region src/kernel/deps/ensure-addon-natives.ts
|
|
2940
|
+
/**
|
|
2941
|
+
* `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
|
|
2942
|
+
* addon install runs on the INSTALLED addon dir AFTER `npm install`.
|
|
2943
|
+
*
|
|
2944
|
+
* Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
|
|
2945
|
+
* copies `dist/`, plants prebuilt natives from the bundled addon copy
|
|
2946
|
+
* (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
|
|
2947
|
+
* On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
|
|
2948
|
+
* `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
|
|
2949
|
+
* copy just planted — and the runner later dies at addon init with a cryptic
|
|
2950
|
+
* "Could not locate the bindings file". The copy-BEFORE-install fix was
|
|
2951
|
+
* empirically insufficient: npm's own install in the copied dir replaces the
|
|
2952
|
+
* satisfied dep without its binary.
|
|
2953
|
+
*
|
|
2954
|
+
* This pass runs LAST, so it always sees the final on-disk state npm left, and
|
|
2955
|
+
* per referenced traditional native still lacking its compiled `.node`:
|
|
2956
|
+
* 1. bundled-copy first — if the bundled addon copy carries the matching
|
|
2957
|
+
* `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
|
|
2958
|
+
* Cheapest + offline-safe; runs BEFORE any network fetch.
|
|
2959
|
+
* 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
|
|
2960
|
+
* shared `ensureNativePrebuilds` pointed at the addon dir.
|
|
2961
|
+
* 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
|
|
2962
|
+
* error naming module + addon and THROW so the install fails at install
|
|
2963
|
+
* time rather than at addon init.
|
|
2964
|
+
*
|
|
2965
|
+
* N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
|
|
2966
|
+
* SIBLING packages npm fetches without a compile step, so they are excluded from
|
|
2967
|
+
* the default set — a miss there is a broken install, not something a prebuild
|
|
2968
|
+
* re-fetch heals (same rationale as `ensureNativePrebuilds`).
|
|
2969
|
+
*/
|
|
2970
|
+
/**
|
|
2971
|
+
* Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
|
|
2972
|
+
* Returns whether the destination ends up with a compiled binary.
|
|
2973
|
+
*/
|
|
2974
|
+
function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
|
|
2975
|
+
if (!hasNative(srcModuleDir)) return false;
|
|
2976
|
+
try {
|
|
2977
|
+
node_fs.rmSync(dstModuleDir, {
|
|
2978
|
+
recursive: true,
|
|
2979
|
+
force: true
|
|
2980
|
+
});
|
|
2981
|
+
node_fs.mkdirSync(node_path.dirname(dstModuleDir), { recursive: true });
|
|
2982
|
+
node_fs.cpSync(srcModuleDir, dstModuleDir, {
|
|
2983
|
+
recursive: true,
|
|
2984
|
+
dereference: true
|
|
2985
|
+
});
|
|
2986
|
+
} catch {
|
|
2987
|
+
return false;
|
|
2988
|
+
}
|
|
2989
|
+
return hasNative(dstModuleDir);
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Ensure every referenced traditional native in `<addonDir>/node_modules`
|
|
2993
|
+
* carries its compiled `.node` after the addon's `npm install`. Bundled-copy
|
|
2994
|
+
* first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
|
|
2995
|
+
*
|
|
2996
|
+
* SKIPS a native absent from the addon's own `node_modules` (it resolves via a
|
|
2997
|
+
* hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
|
|
2998
|
+
* npm produced a working `.node` (already-present). Returns the per-native
|
|
2999
|
+
* outcomes for logging/tests.
|
|
3000
|
+
*
|
|
3001
|
+
* @throws if any required native is still source-only after copy + fetch.
|
|
3002
|
+
*/
|
|
3003
|
+
async function ensureAddonNativePrebuilds(addonDir, options) {
|
|
3004
|
+
const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
|
|
3005
|
+
const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
|
|
3006
|
+
const nm = node_path.join(addonDir, "node_modules");
|
|
3007
|
+
const results = [];
|
|
3008
|
+
const missing = [];
|
|
3009
|
+
for (const pkg of packages) {
|
|
3010
|
+
const moduleDir = node_path.join(nm, pkg);
|
|
3011
|
+
if (!node_fs.existsSync(moduleDir)) continue;
|
|
3012
|
+
if (hasNative(moduleDir)) {
|
|
3013
|
+
results.push({
|
|
3014
|
+
pkg,
|
|
3015
|
+
outcome: "already-present"
|
|
3016
|
+
});
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
if (options.bundledSourceDir) {
|
|
3020
|
+
const src = node_path.join(options.bundledSourceDir, "node_modules", pkg);
|
|
3021
|
+
if (replantFromBundled(src, moduleDir, hasNative)) {
|
|
3022
|
+
options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
|
|
3023
|
+
addon: options.addonName,
|
|
3024
|
+
pkg,
|
|
3025
|
+
src,
|
|
3026
|
+
moduleDir
|
|
3027
|
+
} });
|
|
3028
|
+
results.push({
|
|
3029
|
+
pkg,
|
|
3030
|
+
outcome: "copied"
|
|
3031
|
+
});
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
if ((await ensureNativePrebuilds(addonDir, {
|
|
3036
|
+
logger: options.logger,
|
|
3037
|
+
packages: [pkg],
|
|
3038
|
+
...options.target ? { target: options.target } : {},
|
|
3039
|
+
...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
|
|
3040
|
+
})).includes(pkg)) {
|
|
3041
|
+
results.push({
|
|
3042
|
+
pkg,
|
|
3043
|
+
outcome: "fetched"
|
|
3044
|
+
});
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
|
|
3048
|
+
addon: options.addonName,
|
|
3049
|
+
pkg,
|
|
3050
|
+
moduleDir
|
|
3051
|
+
} });
|
|
3052
|
+
results.push({
|
|
3053
|
+
pkg,
|
|
3054
|
+
outcome: "missing"
|
|
3055
|
+
});
|
|
3056
|
+
missing.push(pkg);
|
|
3057
|
+
}
|
|
3058
|
+
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(", ")}`);
|
|
3059
|
+
return results;
|
|
3060
|
+
}
|
|
3061
|
+
//#endregion
|
|
2810
3062
|
//#region src/kernel/addon-loader.ts
|
|
2811
3063
|
function isRecord$1(value) {
|
|
2812
3064
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3168,7 +3420,7 @@ var AddonEngineManager = class {
|
|
|
3168
3420
|
};
|
|
3169
3421
|
//#endregion
|
|
3170
3422
|
//#region src/kernel/fs-utils.ts
|
|
3171
|
-
var execFileAsync$
|
|
3423
|
+
var execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
|
|
3172
3424
|
/**
|
|
3173
3425
|
* Ensure a directory exists (recursive).
|
|
3174
3426
|
* Single source of truth — replaces scattered mkdirSync calls.
|
|
@@ -3357,7 +3609,7 @@ function ensureLibraryBuilt(packageName, packagesDir) {
|
|
|
3357
3609
|
async function installPackageFromNpm(packageName, targetDir) {
|
|
3358
3610
|
const tmpDir = await node_fs.promises.mkdtemp(node_path.join(node_os.tmpdir(), "camstack-install-"));
|
|
3359
3611
|
try {
|
|
3360
|
-
const { stdout } = await execFileAsync$
|
|
3612
|
+
const { stdout } = await execFileAsync$1("npm", [
|
|
3361
3613
|
"pack",
|
|
3362
3614
|
packageName,
|
|
3363
3615
|
"--pack-destination",
|
|
@@ -3371,7 +3623,7 @@ async function installPackageFromNpm(packageName, targetDir) {
|
|
|
3371
3623
|
const tgzPath = node_path.join(tmpDir, tgzFilename);
|
|
3372
3624
|
const extractDir = node_path.join(tmpDir, "extracted");
|
|
3373
3625
|
ensureDir(extractDir);
|
|
3374
|
-
await execFileAsync$
|
|
3626
|
+
await execFileAsync$1("tar", [
|
|
3375
3627
|
"-xzf",
|
|
3376
3628
|
tgzPath,
|
|
3377
3629
|
"-C",
|
|
@@ -3675,268 +3927,6 @@ var AddonManifest = class {
|
|
|
3675
3927
|
}
|
|
3676
3928
|
};
|
|
3677
3929
|
//#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
3930
|
//#region src/kernel/addon-installer.ts
|
|
3941
3931
|
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
3942
3932
|
function parseInstallSource(value) {
|
|
@@ -5453,6 +5443,19 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
5453
5443
|
_ready = false;
|
|
5454
5444
|
/** Boot-time dependency graph: capability → dependsOn names (from CapabilityDeclaration) */
|
|
5455
5445
|
capabilityDeps = /* @__PURE__ */ new Map();
|
|
5446
|
+
/**
|
|
5447
|
+
* capability → addon ids whose MANIFEST declares it.
|
|
5448
|
+
*
|
|
5449
|
+
* Populated by {@link declareFromManifest}, which runs at addon-LOAD time —
|
|
5450
|
+
* before a forked addon's runner has spawned, let alone registered anything.
|
|
5451
|
+
* So this map answers a question `providers` cannot: **which addons still owe
|
|
5452
|
+
* this capability a provider**, i.e. is an absence final or merely early.
|
|
5453
|
+
*
|
|
5454
|
+
* It is not a second registry and nothing waits on it: it stores an argument
|
|
5455
|
+
* `declareFromManifest` was already being handed and dropping, and it is read
|
|
5456
|
+
* synchronously at the moment a caller needs the answer ([D3](../../../../docs/decisions/adr-0003.md)).
|
|
5457
|
+
*/
|
|
5458
|
+
manifestDeclarers = /* @__PURE__ */ new Map();
|
|
5456
5459
|
/** Per-device singleton overrides: deviceId → (capability → addonId) */
|
|
5457
5460
|
deviceOverrides = /* @__PURE__ */ new Map();
|
|
5458
5461
|
/** capName → (nodeId → bare addonId) per-node singleton overrides. */
|
|
@@ -5578,6 +5581,11 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
5578
5581
|
*/
|
|
5579
5582
|
declareFromManifest(declaration, addonId) {
|
|
5580
5583
|
if (declaration.dependsOn?.length) this.capabilityDeps.set(declaration.name, declaration.dependsOn);
|
|
5584
|
+
if (addonId !== void 0) {
|
|
5585
|
+
const declarers = this.manifestDeclarers.get(declaration.name);
|
|
5586
|
+
if (declarers) declarers.add(addonId);
|
|
5587
|
+
else this.manifestDeclarers.set(declaration.name, new Set([addonId]));
|
|
5588
|
+
}
|
|
5581
5589
|
if (addonId !== void 0 && declaration.autoActivate !== void 0) this.autoActivateFlags.set(`${declaration.name}/${addonId}`, declaration.autoActivate);
|
|
5582
5590
|
const state = this.capabilities.get(declaration.name);
|
|
5583
5591
|
if (!state) {
|
|
@@ -5700,6 +5708,22 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
5700
5708
|
hasProvider(capabilityName, addonId) {
|
|
5701
5709
|
return this.capabilities.get(capabilityName)?.providers.has(addonId) ?? false;
|
|
5702
5710
|
}
|
|
5711
|
+
/**
|
|
5712
|
+
* Addon ids whose manifest declares `capabilityName`, whether or not they
|
|
5713
|
+
* have registered a provider yet.
|
|
5714
|
+
*
|
|
5715
|
+
* Pair it with {@link hasProvider} to separate "this capability's collection
|
|
5716
|
+
* is still filling up" from "this is everything there will ever be" — the
|
|
5717
|
+
* difference between a 503 a client should retry and a 400 it should not.
|
|
5718
|
+
* Every entry is an addon INSTALLED on this node; an addon that is installed
|
|
5719
|
+
* but disabled therefore stays listed, which is the honest answer (the
|
|
5720
|
+
* capability is owed by something that is not going to supply it, and naming
|
|
5721
|
+
* it is the diagnosis).
|
|
5722
|
+
*/
|
|
5723
|
+
getManifestDeclarers(capabilityName) {
|
|
5724
|
+
const declarers = this.manifestDeclarers.get(capabilityName);
|
|
5725
|
+
return declarers ? [...declarers] : [];
|
|
5726
|
+
}
|
|
5703
5727
|
/** Unregister a provider. For singleton: clears active. For collection: removes from list. */
|
|
5704
5728
|
unregisterProvider(capabilityName, addonId) {
|
|
5705
5729
|
const state = this.capabilities.get(capabilityName);
|
|
@@ -94964,7 +94988,6 @@ exports.HEAP_RECLAIM_MIN_INTERVAL_MS = HEAP_RECLAIM_MIN_INTERVAL_MS;
|
|
|
94964
94988
|
exports.HEAP_RECLAIM_TRIGGER_MB = HEAP_RECLAIM_TRIGGER_MB;
|
|
94965
94989
|
exports.HEAP_WATCH_INTERVAL_MS = HEAP_WATCH_INTERVAL_MS;
|
|
94966
94990
|
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
94991
|
exports.HUB_CAP_FWD_ACTION = require_manifest_python_deps.HUB_CAP_FWD_ACTION;
|
|
94969
94992
|
exports.HUB_CAP_FWD_SERVICE = require_manifest_python_deps.HUB_CAP_FWD_SERVICE;
|
|
94970
94993
|
exports.HubForwarderAddon = require_builtins_hub_forwarder_index.HubForwarderAddon$1;
|
|
@@ -94985,7 +95008,7 @@ exports.LogRingBuffer = LogRingBuffer;
|
|
|
94985
95008
|
exports.LokiDestination = require_builtins_loki_logging_index.LokiDestination$1;
|
|
94986
95009
|
exports.LokiLoggingAddon = require_builtins_loki_logging_index.LokiLoggingAddon$1;
|
|
94987
95010
|
exports.METHOD_ACCESS_MAP = require_dist.METHOD_ACCESS_MAP;
|
|
94988
|
-
exports.ModelDownloadService =
|
|
95011
|
+
exports.ModelDownloadService = require_file_data_plane.ModelDownloadService;
|
|
94989
95012
|
exports.NATIVE_PROVIDER_SERVICE_INFIX = require_manifest_python_deps.NATIVE_PROVIDER_SERVICE_INFIX;
|
|
94990
95013
|
exports.NATIVE_SCAN_DEPTH = NATIVE_SCAN_DEPTH;
|
|
94991
95014
|
exports.NativeMetricsAddon = require_builtins_native_metrics_native_metrics_addon.default;
|
|
@@ -95059,18 +95082,18 @@ exports.classifyCapRoute = require_manifest_python_deps.classifyCapRoute;
|
|
|
95059
95082
|
exports.clearPendingRestart = clearPendingRestart;
|
|
95060
95083
|
exports.clusterEventTopic = require_manifest_python_deps.clusterEventTopic;
|
|
95061
95084
|
exports.clusterSecretMatches = clusterSecretMatches;
|
|
95062
|
-
exports.collectModelFiles =
|
|
95063
|
-
exports.contentTypeFor =
|
|
95085
|
+
exports.collectModelFiles = require_file_data_plane.collectModelFiles;
|
|
95086
|
+
exports.contentTypeFor = require_file_data_plane.contentTypeFor;
|
|
95064
95087
|
exports.copyDirRecursive = copyDirRecursive;
|
|
95065
95088
|
exports.copyExtraFileDirs = copyExtraFileDirs;
|
|
95066
95089
|
exports.createAddonContext = require_manifest_python_deps.createAddonContext;
|
|
95067
95090
|
exports.createAddonDataPlaneFacility = require_manifest_python_deps.createAddonDataPlaneFacility;
|
|
95068
95091
|
exports.createAddonService = require_manifest_python_deps.createAddonService;
|
|
95069
|
-
exports.createAuthenticatedFileServer =
|
|
95092
|
+
exports.createAuthenticatedFileServer = require_file_data_plane.createAuthenticatedFileServer;
|
|
95070
95093
|
exports.createBroker = createBroker;
|
|
95071
95094
|
exports.createBrokerDeviceManagerApi = require_manifest_python_deps.createBrokerDeviceManagerApi;
|
|
95072
95095
|
exports.createCoreCapService = createCoreCapService;
|
|
95073
|
-
exports.createFileDataPlaneHandler =
|
|
95096
|
+
exports.createFileDataPlaneHandler = require_file_data_plane.createFileDataPlaneHandler;
|
|
95074
95097
|
exports.createHubCapForwardService = require_manifest_python_deps.createHubCapForwardService;
|
|
95075
95098
|
exports.createHubService = createHubService;
|
|
95076
95099
|
exports.createKernelHwAccel = require_manifest_python_deps.createKernelHwAccel;
|
|
@@ -95087,7 +95110,7 @@ exports.createUdsEventBus = require_manifest_python_deps.createUdsEventBus;
|
|
|
95087
95110
|
exports.createUdsLogger = require_manifest_python_deps.createUdsLogger;
|
|
95088
95111
|
exports.createUdsLoggerWithControl = require_manifest_python_deps.createUdsLoggerWithControl;
|
|
95089
95112
|
exports.createV8Reclaimer = createV8Reclaimer;
|
|
95090
|
-
exports.deleteModelFromDisk =
|
|
95113
|
+
exports.deleteModelFromDisk = require_file_data_plane.deleteModelFromDisk;
|
|
95091
95114
|
exports.deriveAgentListenPort = deriveAgentListenPort;
|
|
95092
95115
|
exports.describeProviderKindDrift = describeProviderKindDrift;
|
|
95093
95116
|
exports.detectWorkspacePackagesDir = detectWorkspacePackagesDir;
|
|
@@ -95097,8 +95120,8 @@ Object.defineProperty(exports, "downloadBinary", {
|
|
|
95097
95120
|
return _camstack_types_node.downloadBinary;
|
|
95098
95121
|
}
|
|
95099
95122
|
});
|
|
95100
|
-
exports.downloadFile =
|
|
95101
|
-
exports.downloadModel =
|
|
95123
|
+
exports.downloadFile = require_file_data_plane.downloadFile;
|
|
95124
|
+
exports.downloadModel = require_file_data_plane.downloadModel;
|
|
95102
95125
|
exports.emitDownForOwnedCaps = require_dist.emitDownForOwnedCaps;
|
|
95103
95126
|
exports.encodeFrame = require_manifest_python_deps.encodeFrame;
|
|
95104
95127
|
exports.ensureAddonNativePrebuilds = ensureAddonNativePrebuilds;
|
|
@@ -95116,7 +95139,7 @@ Object.defineProperty(exports, "ensureFfmpeg", {
|
|
|
95116
95139
|
}
|
|
95117
95140
|
});
|
|
95118
95141
|
exports.ensureLibraryBuilt = ensureLibraryBuilt;
|
|
95119
|
-
exports.ensureModel =
|
|
95142
|
+
exports.ensureModel = require_file_data_plane.ensureModel;
|
|
95120
95143
|
exports.ensureNativePrebuilds = ensureNativePrebuilds;
|
|
95121
95144
|
Object.defineProperty(exports, "ensurePython", {
|
|
95122
95145
|
enumerable: true,
|
|
@@ -95126,7 +95149,7 @@ Object.defineProperty(exports, "ensurePython", {
|
|
|
95126
95149
|
});
|
|
95127
95150
|
exports.ensureTlsCert = ensureTlsCert;
|
|
95128
95151
|
exports.expandCapMethods = require_dist.expandCapMethods;
|
|
95129
|
-
exports.fetchJson =
|
|
95152
|
+
exports.fetchJson = require_file_data_plane.fetchJson;
|
|
95130
95153
|
Object.defineProperty(exports, "findInPath", {
|
|
95131
95154
|
enumerable: true,
|
|
95132
95155
|
get: function() {
|
|
@@ -95142,7 +95165,7 @@ Object.defineProperty(exports, "getFfmpegDownloadUrl", {
|
|
|
95142
95165
|
return _camstack_types_node.getFfmpegDownloadUrl;
|
|
95143
95166
|
}
|
|
95144
95167
|
});
|
|
95145
|
-
exports.getModelFilePath =
|
|
95168
|
+
exports.getModelFilePath = require_file_data_plane.getModelFilePath;
|
|
95146
95169
|
exports.getMoleculerEventStats = require_manifest_python_deps.getMoleculerEventStats;
|
|
95147
95170
|
exports.getOrInitReadinessRegistry = require_manifest_python_deps.getOrInitReadinessRegistry;
|
|
95148
95171
|
exports.getOrInitReadinessRegistryForClient = require_manifest_python_deps.getOrInitReadinessRegistryForClient;
|
|
@@ -95186,27 +95209,28 @@ exports.isArrayOutputSchema = require_dist.isArrayOutputSchema;
|
|
|
95186
95209
|
exports.isClusterSecretMismatchError = isClusterSecretMismatchError;
|
|
95187
95210
|
exports.isCollectionArrayMethod = require_dist.isCollectionArrayMethod;
|
|
95188
95211
|
exports.isInfraCapability = isInfraCapability;
|
|
95189
|
-
exports.isModelDownloaded =
|
|
95212
|
+
exports.isModelDownloaded = require_file_data_plane.isModelDownloaded;
|
|
95190
95213
|
exports.isSourceNewer = isSourceNewer;
|
|
95191
95214
|
exports.loadTlsCert = loadTlsCert;
|
|
95192
95215
|
exports.localEndpointPath = require_manifest_python_deps.localEndpointPath;
|
|
95193
95216
|
exports.localProviderLink = require_manifest_python_deps.localProviderLink;
|
|
95194
95217
|
exports.mountNativeCapService = require_manifest_python_deps.mountNativeCapService;
|
|
95195
95218
|
exports.parseCapAction = require_manifest_python_deps.parseCapAction;
|
|
95196
|
-
exports.parseRangeHeader =
|
|
95197
|
-
exports.parseTokenizedUrl =
|
|
95219
|
+
exports.parseRangeHeader = require_file_data_plane.parseRangeHeader;
|
|
95220
|
+
exports.parseTokenizedUrl = require_file_data_plane.parseTokenizedUrl;
|
|
95198
95221
|
exports.proxyToUpstream = proxyToUpstream;
|
|
95199
95222
|
exports.quarantineAddonResidue = quarantineAddonResidue;
|
|
95200
95223
|
exports.readPendingRestart = readPendingRestart;
|
|
95201
95224
|
exports.readinessKey = require_dist.readinessKey;
|
|
95202
95225
|
exports.registerEventBusService = require_manifest_python_deps.registerEventBusService;
|
|
95203
|
-
exports.resolveFilePath =
|
|
95226
|
+
exports.resolveFilePath = require_file_data_plane.resolveFilePath;
|
|
95204
95227
|
exports.resolveHwAccel = require_manifest_python_deps.resolveHwAccel;
|
|
95205
95228
|
exports.resolveNpmInvocation = require_manifest_python_deps.resolveNpmInvocation;
|
|
95206
95229
|
exports.runNpm = require_manifest_python_deps.runNpm;
|
|
95207
95230
|
exports.scheduleSelfRestart = scheduleSelfRestart;
|
|
95208
95231
|
exports.scopeKey = require_dist.scopeKey;
|
|
95209
|
-
exports.
|
|
95232
|
+
exports.scopesAllowAddon = require_dist.scopesAllowAddon;
|
|
95233
|
+
exports.scopesAllowDeviceCap = require_dist.scopesAllowDeviceCap;
|
|
95210
95234
|
exports.selectAddonResidue = selectAddonResidue;
|
|
95211
95235
|
exports.serializeTypedArrays = require_manifest_python_deps.serializeTypedArrays;
|
|
95212
95236
|
exports.setHubConnected = require_manifest_python_deps.setHubConnected;
|