@camstack/system 1.2.68 → 1.2.70

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