@camstack/server 0.1.8 → 0.2.1

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 (125) hide show
  1. package/package.json +9 -7
  2. package/src/__tests__/addon-install-e2e.test.ts +0 -1
  3. package/src/__tests__/addon-pages-e2e.test.ts +40 -18
  4. package/src/__tests__/addon-settings-router.spec.ts +6 -1
  5. package/src/__tests__/addon-upload.spec.ts +91 -29
  6. package/src/__tests__/agent-registry.spec.ts +26 -9
  7. package/src/__tests__/agent-status-page.spec.ts +1 -3
  8. package/src/__tests__/auth-session-cookie.test.ts +28 -1
  9. package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
  10. package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
  11. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
  12. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
  13. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
  14. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
  15. package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
  16. package/src/__tests__/cap-route-adapter.spec.ts +28 -15
  17. package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
  18. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
  19. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
  20. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
  21. package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
  22. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
  23. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
  24. package/src/__tests__/cap-routers/harness.ts +11 -7
  25. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
  26. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
  27. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
  28. package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
  29. package/src/__tests__/capability-e2e.test.ts +9 -11
  30. package/src/__tests__/cli-e2e.test.ts +80 -59
  31. package/src/__tests__/core-cap-bridge.spec.ts +3 -1
  32. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
  33. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
  34. package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
  35. package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
  36. package/src/__tests__/framework-allowlist.spec.ts +5 -4
  37. package/src/__tests__/https-e2e.test.ts +12 -6
  38. package/src/__tests__/lifecycle-e2e.test.ts +60 -11
  39. package/src/__tests__/live-events-subscription.spec.ts +17 -18
  40. package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
  41. package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
  42. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
  43. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
  44. package/src/__tests__/native-cap-route.spec.ts +42 -19
  45. package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
  46. package/src/__tests__/singleton-contention.test.ts +23 -11
  47. package/src/__tests__/streaming-diagnostic.test.ts +156 -53
  48. package/src/__tests__/streaming-scale.test.ts +69 -35
  49. package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
  50. package/src/agent-status-page.ts +4 -3
  51. package/src/api/__tests__/addons-custom.spec.ts +22 -8
  52. package/src/api/__tests__/capabilities.router.test.ts +18 -9
  53. package/src/api/addon-upload.ts +46 -15
  54. package/src/api/addons-custom.router.ts +7 -6
  55. package/src/api/auth-whoami.ts +3 -1
  56. package/src/api/bridge-addons.router.ts +3 -1
  57. package/src/api/capabilities.router.ts +117 -78
  58. package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
  59. package/src/api/core/addon-settings.router.ts +4 -1
  60. package/src/api/core/agents.router.ts +52 -53
  61. package/src/api/core/auth.router.ts +55 -36
  62. package/src/api/core/bulk-update-coordinator.ts +25 -22
  63. package/src/api/core/cap-providers.ts +346 -202
  64. package/src/api/core/capabilities.router.ts +30 -23
  65. package/src/api/core/hwaccel.router.ts +37 -10
  66. package/src/api/core/live-events.router.ts +16 -9
  67. package/src/api/core/logs.router.ts +54 -25
  68. package/src/api/core/notifications.router.ts +2 -1
  69. package/src/api/core/repl.router.ts +1 -3
  70. package/src/api/core/settings-backend.router.ts +68 -70
  71. package/src/api/core/system-events.router.ts +41 -32
  72. package/src/api/health/health.routes.ts +7 -13
  73. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  74. package/src/api/oauth2/consent-page.ts +4 -3
  75. package/src/api/oauth2/oauth2-routes.ts +41 -12
  76. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  77. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  78. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
  79. package/src/api/trpc/cap-mount-helpers.ts +64 -55
  80. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  81. package/src/api/trpc/core-cap-bridge.ts +3 -1
  82. package/src/api/trpc/generated-cap-mounts.ts +593 -351
  83. package/src/api/trpc/generated-cap-routers.ts +3680 -579
  84. package/src/api/trpc/scope-access.ts +7 -7
  85. package/src/api/trpc/trpc.context.ts +7 -4
  86. package/src/api/trpc/trpc.middleware.ts +4 -2
  87. package/src/api/trpc/trpc.router.ts +79 -46
  88. package/src/auth/session-cookie.ts +10 -0
  89. package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
  90. package/src/boot/boot-config.ts +103 -122
  91. package/src/boot/post-boot.service.ts +5 -3
  92. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  93. package/src/core/addon/addon-call-gateway.ts +20 -6
  94. package/src/core/addon/addon-package.service.ts +183 -89
  95. package/src/core/addon/addon-registry.service.ts +1163 -1305
  96. package/src/core/addon/addon-search.service.ts +2 -1
  97. package/src/core/addon/addon-settings-provider.ts +27 -7
  98. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  99. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  100. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  101. package/src/core/agent/agent-registry.service.ts +60 -38
  102. package/src/core/auth/auth.service.spec.ts +6 -8
  103. package/src/core/config/config.service.spec.ts +1 -1
  104. package/src/core/events/event-bus.service.spec.ts +44 -21
  105. package/src/core/events/event-bus.service.ts +5 -1
  106. package/src/core/feature/feature.service.spec.ts +4 -1
  107. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  108. package/src/core/logging/logging.service.spec.ts +61 -21
  109. package/src/core/logging/logging.service.ts +12 -3
  110. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  111. package/src/core/moleculer/cap-call-fn.ts +5 -1
  112. package/src/core/moleculer/cap-route-authority.ts +18 -6
  113. package/src/core/moleculer/moleculer.service.ts +120 -32
  114. package/src/core/network/network-quality.service.spec.ts +6 -1
  115. package/src/core/notification/notification-wrapper.service.ts +1 -3
  116. package/src/core/notification/toast-wrapper.service.ts +1 -5
  117. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  118. package/src/core/repl/repl-engine.service.ts +11 -12
  119. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  120. package/src/core/streaming/stream-probe.service.ts +22 -13
  121. package/src/core/topology/topology-emitter.service.ts +5 -1
  122. package/src/launcher.ts +14 -9
  123. package/src/main.ts +602 -531
  124. package/src/manual-boot.ts +133 -154
  125. package/tsconfig.json +20 -8
@@ -44,7 +44,7 @@ import type {
44
44
  IBrokerProvider,
45
45
  CapabilityMethodAuth,
46
46
  } from '@camstack/types'
47
- import { asJsonObject, asJsonArray, errMsg } from '@camstack/types'
47
+ import { asJsonObject, asJsonArray, errMsg, EventCategory } from '@camstack/types'
48
48
  import type { CapabilityRegistry } from '@camstack/kernel'
49
49
  import { getCapUsageRegistry } from '@camstack/kernel'
50
50
  import type { ToastService, NotificationService } from '@camstack/core'
@@ -68,7 +68,9 @@ const execFileAsync = promisify(execFile)
68
68
  // ── system ──────────────────────────────────────────────────────────
69
69
 
70
70
  function getRetention(registry: CapabilityRegistry | null) {
71
- return registry?.getSingleton<IAnalysisDataPersistence>('analysis-data-persistence')?.retention ?? null
71
+ return (
72
+ registry?.getSingleton<IAnalysisDataPersistence>('analysis-data-persistence')?.retention ?? null
73
+ )
72
74
  }
73
75
 
74
76
  export function buildSystemProvider(
@@ -107,9 +109,7 @@ export function buildSystemProvider(
107
109
 
108
110
  // ── network-quality ─────────────────────────────────────────────────
109
111
 
110
- export function buildNetworkQualityProvider(
111
- nq: NetworkQualityService,
112
- ): INetworkQualityProvider {
112
+ export function buildNetworkQualityProvider(nq: NetworkQualityService): INetworkQualityProvider {
113
113
  return {
114
114
  getDeviceStats: async (input) => nq.getDeviceStats(input.deviceId),
115
115
  getAllStats: async () => nq.getAllStats(),
@@ -141,7 +141,9 @@ export function buildToastProvider(
141
141
  if (!toastService) return () => {}
142
142
  const userId = ctx.user?.id ?? 'anonymous'
143
143
  const connectionId = randomUUID()
144
- const unsubscribe = toastService.subscribe(connectionId, userId, (toast: Toast) => push(toast))
144
+ const unsubscribe = toastService.subscribe(connectionId, userId, (toast: Toast) =>
145
+ push(toast),
146
+ )
145
147
  return unsubscribe ?? (() => {})
146
148
  },
147
149
  }
@@ -184,14 +186,14 @@ export async function computeTopology(
184
186
  ): Promise<readonly TopologyNode[]> {
185
187
  const nodes = await agentRegistry.listNodes()
186
188
  const allAddons = addonRegistry?.listAddons() ?? []
187
- const getInGroupAddonIds = (node: typeof nodes[number]): readonly string[] => {
189
+ const getInGroupAddonIds = (node: (typeof nodes)[number]): readonly string[] => {
188
190
  const subs = (node.subProcesses ?? []) as readonly SubProcessLite[]
189
191
  return subs.flatMap((p) => p.addonIds ?? [])
190
192
  }
191
193
  const addonCaps = new Map<string, readonly string[]>()
192
194
  for (const a of allAddons) {
193
195
  const id = a.manifest?.id ?? ''
194
- const caps = a.declaration?.capabilities?.map(c => typeof c === 'string' ? c : c.name) ?? []
196
+ const caps = a.declaration?.capabilities?.map((c) => (typeof c === 'string' ? c : c.name)) ?? []
195
197
  addonCaps.set(id, caps)
196
198
  }
197
199
  const addonCategory = new Map<string, string>()
@@ -202,7 +204,8 @@ export async function computeTopology(
202
204
  }
203
205
  return nodes.map((node) => {
204
206
  const inGroupAddonIds = new Set(getInGroupAddonIds(node))
205
- const agentAddonIds: readonly string[] = (node as { agentAddons?: readonly string[] }).agentAddons ?? []
207
+ const agentAddonIds: readonly string[] =
208
+ (node as { agentAddons?: readonly string[] }).agentAddons ?? []
206
209
  type NodeAddonEntry = { id: string; capabilities: readonly string[]; status: 'running' }
207
210
  const allNodeAddons: NodeAddonEntry[] = node.isHub
208
211
  ? allAddons.map((a) => {
@@ -215,31 +218,33 @@ export async function computeTopology(
215
218
  status: 'running' as const,
216
219
  }))
217
220
  const inProcessAddons = allNodeAddons.filter((a) => !inGroupAddonIds.has(a.id))
218
- const isolatedProcesses = ((node.subProcesses ?? []) as readonly SubProcessLite[])
219
- const mainProcessServices = inProcessAddons.map(a => ({
221
+ const isolatedProcesses = (node.subProcesses ?? []) as readonly SubProcessLite[]
222
+ const mainProcessServices = inProcessAddons.map((a) => ({
220
223
  addonId: a.id,
221
224
  capabilities: a.capabilities,
222
225
  status: a.status,
223
226
  }))
224
- const mainProcess = node.isHub ? {
225
- pid: process.pid,
226
- name: 'hub (core)',
227
- state: 'running' as const,
228
- cpuPercent: node.status?.cpuPercent ?? 0,
229
- memoryRss: process.memoryUsage().rss,
230
- uptimeSeconds: Math.floor(process.uptime()),
231
- services: mainProcessServices,
232
- } : {
233
- pid: 0,
234
- name: `${node.info.id} (core)`,
235
- state: 'running' as const,
236
- cpuPercent: node.status?.cpuPercent ?? 0,
237
- memoryRss: 0,
238
- uptimeSeconds: Math.floor((Date.now() - node.connectedSince) / 1000),
239
- services: mainProcessServices,
240
- }
227
+ const mainProcess = node.isHub
228
+ ? {
229
+ pid: process.pid,
230
+ name: 'hub (core)',
231
+ state: 'running' as const,
232
+ cpuPercent: node.status?.cpuPercent ?? 0,
233
+ memoryRss: process.memoryUsage().rss,
234
+ uptimeSeconds: Math.floor(process.uptime()),
235
+ services: mainProcessServices,
236
+ }
237
+ : {
238
+ pid: 0,
239
+ name: `${node.info.id} (core)`,
240
+ state: 'running' as const,
241
+ cpuPercent: node.status?.cpuPercent ?? 0,
242
+ memoryRss: 0,
243
+ uptimeSeconds: Math.floor((Date.now() - node.connectedSince) / 1000),
244
+ services: mainProcessServices,
245
+ }
241
246
  const childProcesses = isolatedProcesses.map((p) => {
242
- const memberIds = (p.addonIds && p.addonIds.length > 0) ? p.addonIds : [p.name]
247
+ const memberIds = p.addonIds && p.addonIds.length > 0 ? p.addonIds : [p.name]
243
248
  return {
244
249
  pid: p.pid,
245
250
  name: p.name,
@@ -258,16 +263,23 @@ export async function computeTopology(
258
263
  // Aggregate node-local addons by category. `allNodeAddons` already
259
264
  // contains the per-node addon roster (hub uses every installed
260
265
  // addon; agents use their assigned agentAddons subset).
261
- const byCategory = new Map<string, {
262
- category: string
263
- total: number
264
- healthy: number
265
- addons: { id: string; status: string; cpuPercent: number; memoryRss: number }[]
266
- }>()
266
+ const byCategory = new Map<
267
+ string,
268
+ {
269
+ category: string
270
+ total: number
271
+ healthy: number
272
+ addons: { id: string; status: string; cpuPercent: number; memoryRss: number }[]
273
+ }
274
+ >()
267
275
  const procByAddon = new Map<string, { cpuPercent: number; memoryRss: number; state: string }>()
268
276
  for (const p of (node.subProcesses ?? []) as readonly SubProcessLite[]) {
269
- for (const addonId of (p.addonIds ?? [])) {
270
- procByAddon.set(addonId, { cpuPercent: p.cpuPercent, memoryRss: p.memoryRss, state: p.state })
277
+ for (const addonId of p.addonIds ?? []) {
278
+ procByAddon.set(addonId, {
279
+ cpuPercent: p.cpuPercent,
280
+ memoryRss: p.memoryRss,
281
+ state: p.state,
282
+ })
271
283
  }
272
284
  }
273
285
  for (const a of allNodeAddons) {
@@ -304,10 +316,7 @@ export async function computeTopology(
304
316
  lastSeen: new Date().toISOString(),
305
317
  localIps: node.isHub ? getLocalIps() : (node.localIps ?? []),
306
318
  addons: allNodeAddons,
307
- processes: [
308
- mainProcess,
309
- ...childProcesses,
310
- ],
319
+ processes: [mainProcess, ...childProcesses],
311
320
  categories: categoriesProjection,
312
321
  }
313
322
  })
@@ -322,7 +331,11 @@ export async function computeTopology(
322
331
  * `addon-registry.service.ts`; consider hoisting if a third call site appears.
323
332
  */
324
333
  interface BrokerLike {
325
- call<T = unknown>(action: string, params?: unknown, opts?: { nodeID?: string; timeout?: number }): Promise<T>
334
+ call<T = unknown>(
335
+ action: string,
336
+ params?: unknown,
337
+ opts?: { nodeID?: string; timeout?: number },
338
+ ): Promise<T>
326
339
  }
327
340
 
328
341
  export function buildNodesProvider(
@@ -338,9 +351,13 @@ export function buildNodesProvider(
338
351
  return { success: true }
339
352
  },
340
353
  undeployAddon: async (input) => {
341
- await broker.call('$agent.undeploy', {
342
- addonId: input.addonId,
343
- }, { nodeID: input.nodeId, timeout: 30_000 })
354
+ await broker.call(
355
+ '$agent.undeploy',
356
+ {
357
+ addonId: input.addonId,
358
+ },
359
+ { nodeID: input.nodeId, timeout: 30_000 },
360
+ )
344
361
  return { success: true }
345
362
  },
346
363
  restartAddon: async (input) => {
@@ -353,31 +370,47 @@ export function buildNodesProvider(
353
370
  return { success: true }
354
371
  }
355
372
  const agentNodeId = input.nodeId.includes('/') ? input.nodeId.split('/')[0]! : input.nodeId
356
- await broker.call('$agent.restart', {
357
- addonId: input.addonId,
358
- }, { nodeID: agentNodeId, timeout: 30_000 })
373
+ await broker.call(
374
+ '$agent.restart',
375
+ {
376
+ addonId: input.addonId,
377
+ },
378
+ { nodeID: agentNodeId, timeout: 30_000 },
379
+ )
359
380
  return { success: true }
360
381
  },
361
382
  restartProcess: async (input) => {
362
- return await broker.call('$process.restart', {
363
- name: input.processName,
364
- }, { nodeID: input.nodeId, timeout: 30_000 }) as { success: boolean; reason?: string }
383
+ return (await broker.call(
384
+ '$process.restart',
385
+ {
386
+ name: input.processName,
387
+ },
388
+ { nodeID: input.nodeId, timeout: 30_000 },
389
+ )) as { success: boolean; reason?: string }
365
390
  },
366
391
  restartNode: async (input) => {
367
- return await broker.call('$process.restartAll', {}, {
368
- nodeID: input.nodeId,
369
- timeout: 60_000,
370
- }) as { restarted: readonly string[]; failed: readonly string[] }
392
+ return (await broker.call(
393
+ '$process.restartAll',
394
+ {},
395
+ {
396
+ nodeID: input.nodeId,
397
+ timeout: 60_000,
398
+ },
399
+ )) as { restarted: readonly string[]; failed: readonly string[] }
371
400
  },
372
401
  shutdownNode: async (input) => {
373
402
  if (input.nodeId === 'hub') {
374
403
  setTimeout(() => process.exit(0), 500)
375
404
  return { success: true }
376
405
  }
377
- await broker.call('$agent.shutdown', {}, {
378
- nodeID: input.nodeId,
379
- timeout: 10_000,
380
- })
406
+ await broker.call(
407
+ '$agent.shutdown',
408
+ {},
409
+ {
410
+ nodeID: input.nodeId,
411
+ timeout: 10_000,
412
+ },
413
+ )
381
414
  return { success: true }
382
415
  },
383
416
  renameNode: async (input) => {
@@ -390,9 +423,13 @@ export function buildNodesProvider(
390
423
  value: trimmed,
391
424
  })
392
425
  } else {
393
- await broker.call('$agent.rename', {
394
- name: trimmed,
395
- }, { nodeID: input.nodeId, timeout: 10_000 })
426
+ await broker.call(
427
+ '$agent.rename',
428
+ {
429
+ name: trimmed,
430
+ },
431
+ { nodeID: input.nodeId, timeout: 10_000 },
432
+ )
396
433
  agentRegistry.updateAgentName(input.nodeId, trimmed)
397
434
  }
398
435
  return { nodeId: input.nodeId, name: trimmed }
@@ -439,17 +476,50 @@ export function buildNodesProvider(
439
476
  const remoteStatuses = await Promise.all(
440
477
  remoteNodes.map(async (node) => {
441
478
  try {
442
- const status = await broker.call('$agent.status', {}, {
443
- nodeID: node.info.id, timeout: 5_000,
444
- }) as { addons?: readonly { id: string; status: string; version?: string; packageName?: string }[] }
445
- return { nodeId: node.info.id, name: node.info.name, online: true, addons: status.addons ?? [] }
479
+ const status = (await broker.call(
480
+ '$agent.status',
481
+ {},
482
+ {
483
+ nodeID: node.info.id,
484
+ timeout: 5_000,
485
+ },
486
+ )) as {
487
+ addons?: readonly {
488
+ id: string
489
+ status: string
490
+ version?: string
491
+ packageName?: string
492
+ }[]
493
+ }
494
+ return {
495
+ nodeId: node.info.id,
496
+ name: node.info.name,
497
+ online: true,
498
+ addons: status.addons ?? [],
499
+ }
446
500
  } catch {
447
- return { nodeId: node.info.id, name: node.info.name, online: false, addons: [] as readonly { id: string; status: string; version?: string; packageName?: string }[] }
501
+ return {
502
+ nodeId: node.info.id,
503
+ name: node.info.name,
504
+ online: false,
505
+ addons: [] as readonly {
506
+ id: string
507
+ status: string
508
+ version?: string
509
+ packageName?: string
510
+ }[],
511
+ }
448
512
  }
449
513
  }),
450
514
  )
451
515
 
452
- type NodeDeployment = { nodeId: string; name: string; version: string; status: string; synced: boolean }
516
+ type NodeDeployment = {
517
+ nodeId: string
518
+ name: string
519
+ version: string
520
+ status: string
521
+ synced: boolean
522
+ }
453
523
  const result: Record<string, { hubVersion: string; nodes: NodeDeployment[] }> = {}
454
524
 
455
525
  for (const [addonId, hubAddon] of hubMap) {
@@ -489,9 +559,13 @@ export function buildNodesProvider(
489
559
  // also work — both are safe to run in parallel during Phase E.
490
560
  const reachedViaUds = moleculer.setChildLogLevelByNodeId(input.nodeId, input.level)
491
561
  if (!reachedViaUds) {
492
- await broker.call('$node-mgmt.setLogLevel', {
493
- level: input.level,
494
- }, { nodeID: input.nodeId, timeout: 5_000 })
562
+ await broker.call(
563
+ '$node-mgmt.setLogLevel',
564
+ {
565
+ level: input.level,
566
+ },
567
+ { nodeID: input.nodeId, timeout: 5_000 },
568
+ )
495
569
  }
496
570
  return { success: true }
497
571
  },
@@ -518,9 +592,11 @@ function requireIntegrationRegistry(ar: AddonRegistryService): IIntegrationRegis
518
592
  }
519
593
 
520
594
  function isDeviceProvider(value: unknown): value is IDeviceProvider {
521
- return value !== null
522
- && typeof value === 'object'
523
- && typeof Reflect.get(value, 'discoverDevices') === 'function'
595
+ return (
596
+ value !== null &&
597
+ typeof value === 'object' &&
598
+ typeof Reflect.get(value, 'discoverDevices') === 'function'
599
+ )
524
600
  }
525
601
 
526
602
  function getDeviceProvider(ar: AddonRegistryService, addonId: string): IDeviceProvider | null {
@@ -554,8 +630,7 @@ export function buildIntegrationsProvider(
554
630
  const withProcessState = (i: Integration): IntegrationWithProcessState => ({
555
631
  ...i,
556
632
  processState:
557
- ar.listAddons().find(a => a.manifest.id === i.addonId)?.process?.state
558
- ?? 'unknown',
633
+ ar.listAddons().find((a) => a.manifest.id === i.addonId)?.process?.state ?? 'unknown',
559
634
  })
560
635
 
561
636
  return {
@@ -568,25 +643,27 @@ export function buildIntegrationsProvider(
568
643
  if (!integration) throw new Error(`Integration "${input.id}" not found`)
569
644
  return withProcessState(integration)
570
645
  },
571
- getByAddonId: async (input) => requireIntegrationRegistry(ar).getIntegrationByAddonId(input.addonId),
646
+ getByAddonId: async (input) =>
647
+ requireIntegrationRegistry(ar).getIntegrationByAddonId(input.addonId),
572
648
  create: async (input) => {
573
649
  const { skipRestart, ...payload } = input
574
650
  const reg = requireIntegrationRegistry(ar)
575
651
 
576
- logger.info('request', { tags: { addonId: input.addonId }, meta: { phase: 'create', name: input.name } })
652
+ logger.info('request', {
653
+ tags: { addonId: input.addonId },
654
+ meta: { phase: 'create', name: input.name },
655
+ })
577
656
 
578
657
  const addon = ar.listAddons().find((a) => a.manifest.id === input.addonId)
579
658
  const instanceMode =
580
659
  addon?.declaration?.instanceMode ?? addon?.manifest?.instanceMode ?? 'multiple'
581
660
  if (instanceMode === 'unique') {
582
- const existing = (await reg.listIntegrations()).filter(
583
- (i) => i.addonId === input.addonId,
584
- )
661
+ const existing = (await reg.listIntegrations()).filter((i) => i.addonId === input.addonId)
585
662
  if (existing.length > 0) {
586
- logger.warn(
587
- 'rejected duplicate unique',
588
- { tags: { addonId: input.addonId, integrationId: existing[0]!.id }, meta: { phase: 'create' } },
589
- )
663
+ logger.warn('rejected duplicate unique', {
664
+ tags: { addonId: input.addonId, integrationId: existing[0]!.id },
665
+ meta: { phase: 'create' },
666
+ })
590
667
  throw new Error(
591
668
  `Addon "${input.addonId}" is unique-instance and already has an integration (${existing[0]!.id})`,
592
669
  )
@@ -594,15 +671,26 @@ export function buildIntegrationsProvider(
594
671
  }
595
672
 
596
673
  const integration = await reg.createIntegration(payload)
597
- logger.info('persisted', { tags: { integrationId: integration.id, addonId: integration.addonId }, meta: { phase: 'create' } })
674
+ logger.info('persisted', {
675
+ tags: { integrationId: integration.id, addonId: integration.addonId },
676
+ meta: { phase: 'create' },
677
+ })
598
678
 
599
679
  const hasSettings = input.settings != null && Object.keys(input.settings).length > 0
600
680
  if (!skipRestart && hasSettings) {
601
- logger.info('settings present — restarting addon', { tags: { addonId: input.addonId }, meta: { phase: 'create' } })
681
+ logger.info('settings present — restarting addon', {
682
+ tags: { addonId: input.addonId },
683
+ meta: { phase: 'create' },
684
+ })
602
685
  await ar.restartAddon(input.addonId)
603
- logger.info('addon restart complete', { tags: { addonId: input.addonId }, meta: { phase: 'create' } })
686
+ logger.info('addon restart complete', {
687
+ tags: { addonId: input.addonId },
688
+ meta: { phase: 'create' },
689
+ })
604
690
  } else {
605
- logger.info('skipping restart (no settings or skipRestart=true)', { meta: { phase: 'create' } })
691
+ logger.info('skipping restart (no settings or skipRestart=true)', {
692
+ meta: { phase: 'create' },
693
+ })
606
694
  }
607
695
  return integration
608
696
  },
@@ -617,22 +705,21 @@ export function buildIntegrationsProvider(
617
705
  const changedFields = Object.keys(updates).filter(
618
706
  (k) => (updates as Record<string, unknown>)[k] !== undefined,
619
707
  )
620
- logger.info(
621
- 'request',
622
- { tags: { integrationId: input.id, addonId: previous.addonId }, meta: { phase: 'update', fields: changedFields } },
623
- )
708
+ logger.info('request', {
709
+ tags: { integrationId: input.id, addonId: previous.addonId },
710
+ meta: { phase: 'update', fields: changedFields },
711
+ })
624
712
 
625
713
  const result = await reg.updateIntegration(id, updates)
626
714
  if (!result) throw new Error(`Integration "${id}" not found`)
627
715
 
628
- const enabledChanged =
629
- input.enabled !== undefined && input.enabled !== previous.enabled
716
+ const enabledChanged = input.enabled !== undefined && input.enabled !== previous.enabled
630
717
  if (enabledChanged) {
631
718
  const category = input.enabled ? 'integration.enabled' : 'integration.disabled'
632
- logger.info(
633
- 'enabled state changed',
634
- { tags: { integrationId: result.id, addonId: result.addonId }, meta: { phase: 'update', enabled: input.enabled } },
635
- )
719
+ logger.info('enabled state changed', {
720
+ tags: { integrationId: result.id, addonId: result.addonId },
721
+ meta: { phase: 'update', enabled: input.enabled },
722
+ })
636
723
  eb.emit({
637
724
  id: `integration-${category}-${Date.now()}`,
638
725
  timestamp: new Date(),
@@ -646,17 +733,23 @@ export function buildIntegrationsProvider(
646
733
  }
647
734
 
648
735
  if (input.name !== undefined && input.name !== previous.name) {
649
- logger.info(
650
- 'renamed',
651
- { tags: { integrationId: result.id }, meta: { phase: 'update', previousName: previous.name, newName: input.name } },
652
- )
736
+ logger.info('renamed', {
737
+ tags: { integrationId: result.id },
738
+ meta: { phase: 'update', previousName: previous.name, newName: input.name },
739
+ })
653
740
  }
654
741
 
655
742
  const infoChanged = input.info !== undefined
656
743
  if (infoChanged) {
657
- logger.info('info changed — restarting addon', { tags: { addonId: result.addonId }, meta: { phase: 'update' } })
744
+ logger.info('info changed — restarting addon', {
745
+ tags: { addonId: result.addonId },
746
+ meta: { phase: 'update' },
747
+ })
658
748
  await ar.restartAddon(result.addonId)
659
- logger.info('addon restart complete', { tags: { addonId: result.addonId }, meta: { phase: 'update' } })
749
+ logger.info('addon restart complete', {
750
+ tags: { addonId: result.addonId },
751
+ meta: { phase: 'update' },
752
+ })
660
753
  } else {
661
754
  logger.info('no restart needed (only enabled/name changed)', { meta: { phase: 'update' } })
662
755
  }
@@ -671,21 +764,27 @@ export function buildIntegrationsProvider(
671
764
  logger.warn('not found', { tags: { integrationId: input.id }, meta: { phase: 'delete' } })
672
765
  throw new Error(`Integration "${input.id}" not found`)
673
766
  }
674
- logger.info(
675
- 'removing',
676
- { tags: { integrationId: input.id, addonId: integration.addonId }, meta: { phase: 'delete', name: integration.name } },
677
- )
767
+ logger.info('removing', {
768
+ tags: { integrationId: input.id, addonId: integration.addonId },
769
+ meta: { phase: 'delete', name: integration.name },
770
+ })
678
771
 
679
772
  // Cascade-delete every live device whose integrationId matches.
680
773
  // Best-effort: a device-removal hiccup must not abort the integration
681
774
  // delete — log a warning and continue so the record + event always fire.
682
- const dm = capabilityRegistry?.getSingleton<{
683
- removeByIntegration?: (input: { integrationId: string }) => Promise<{ removed: number }>
684
- listAll?: (input: Record<string, never>) => Promise<readonly {
685
- id: number; addonId: string; parentDeviceId: number | null; integrationId?: string
686
- }[]>
687
- setIntegrationId?: (input: { deviceId: number; integrationId: string }) => Promise<void>
688
- }>('device-manager') ?? null
775
+ const dm =
776
+ capabilityRegistry?.getSingleton<{
777
+ removeByIntegration?: (input: { integrationId: string }) => Promise<{ removed: number }>
778
+ listAll?: (input: Record<string, never>) => Promise<
779
+ readonly {
780
+ id: number
781
+ addonId: string
782
+ parentDeviceId: number | null
783
+ integrationId?: string
784
+ }[]
785
+ >
786
+ setIntegrationId?: (input: { deviceId: number; integrationId: string }) => Promise<void>
787
+ }>('device-manager') ?? null
689
788
 
690
789
  // Claim legacy un-tagged devices BEFORE the cascade. Devices created
691
790
  // before stamping (or whose provider never stamps, e.g. `provider-rtsp`)
@@ -711,7 +810,10 @@ export function buildIntegrationsProvider(
711
810
  })),
712
811
  )
713
812
  for (const stamp of stamps) {
714
- await dm.setIntegrationId({ deviceId: stamp.deviceId, integrationId: stamp.integrationId })
813
+ await dm.setIntegrationId({
814
+ deviceId: stamp.deviceId,
815
+ integrationId: stamp.integrationId,
816
+ })
715
817
  }
716
818
  if (stamps.length > 0) {
717
819
  logger.info('claimed legacy un-tagged devices for cascade', {
@@ -721,7 +823,8 @@ export function buildIntegrationsProvider(
721
823
  }
722
824
  } catch (err) {
723
825
  logger.warn('legacy device claim failed (best-effort — continuing)', {
724
- tags: { integrationId: input.id }, meta: { phase: 'delete', error: errMsg(err) },
826
+ tags: { integrationId: input.id },
827
+ meta: { phase: 'delete', error: errMsg(err) },
725
828
  })
726
829
  }
727
830
  }
@@ -729,21 +832,21 @@ export function buildIntegrationsProvider(
729
832
  if (dm?.removeByIntegration) {
730
833
  try {
731
834
  const result = await dm.removeByIntegration({ integrationId: input.id })
732
- logger.info(
733
- 'cascade-removed devices',
734
- { tags: { integrationId: input.id }, meta: { phase: 'delete', removed: result.removed } },
735
- )
835
+ logger.info('cascade-removed devices', {
836
+ tags: { integrationId: input.id },
837
+ meta: { phase: 'delete', removed: result.removed },
838
+ })
736
839
  } catch (err) {
737
- logger.warn(
738
- 'device cascade-remove failed (best-effort — continuing)',
739
- { tags: { integrationId: input.id }, meta: { phase: 'delete', error: errMsg(err) } },
740
- )
840
+ logger.warn('device cascade-remove failed (best-effort — continuing)', {
841
+ tags: { integrationId: input.id },
842
+ meta: { phase: 'delete', error: errMsg(err) },
843
+ })
741
844
  }
742
845
  } else {
743
- logger.warn(
744
- 'device-manager not available skipping cascade device removal',
745
- { tags: { integrationId: input.id }, meta: { phase: 'delete' } },
746
- )
846
+ logger.warn('device-manager not available — skipping cascade device removal', {
847
+ tags: { integrationId: input.id },
848
+ meta: { phase: 'delete' },
849
+ })
747
850
  }
748
851
 
749
852
  await reg.deleteIntegration(input.id)
@@ -752,13 +855,16 @@ export function buildIntegrationsProvider(
752
855
  id: `integration-deleted-${Date.now()}`,
753
856
  timestamp: new Date(),
754
857
  source: { type: 'integration', id: input.id },
755
- category: 'integration.deleted',
858
+ category: EventCategory.IntegrationDeleted,
756
859
  data: {
757
860
  integrationId: input.id,
758
861
  addonId: integration.addonId,
759
862
  },
760
863
  })
761
- logger.info('completed (no restart)', { tags: { integrationId: input.id }, meta: { phase: 'delete' } })
864
+ logger.info('completed (no restart)', {
865
+ tags: { integrationId: input.id },
866
+ meta: { phase: 'delete' },
867
+ })
762
868
 
763
869
  return { success: true, deletedId: input.id }
764
870
  },
@@ -772,19 +878,28 @@ export function buildIntegrationsProvider(
772
878
  const reg = requireIntegrationRegistry(ar)
773
879
  const integration = await reg.getIntegration(input.id)
774
880
  if (!integration) {
775
- logger.warn('not found', { tags: { integrationId: input.id }, meta: { phase: 'setSettings' } })
881
+ logger.warn('not found', {
882
+ tags: { integrationId: input.id },
883
+ meta: { phase: 'setSettings' },
884
+ })
776
885
  throw new Error(`Integration "${input.id}" not found`)
777
886
  }
778
887
  const settingsKeys = Object.keys(input.settings)
779
- logger.info(
780
- 'request',
781
- { tags: { integrationId: input.id, addonId: integration.addonId }, meta: { phase: 'setSettings', keys: settingsKeys } },
782
- )
888
+ logger.info('request', {
889
+ tags: { integrationId: input.id, addonId: integration.addonId },
890
+ meta: { phase: 'setSettings', keys: settingsKeys },
891
+ })
783
892
  await reg.setIntegrationSettings(input.id, input.settings)
784
893
 
785
- logger.info('persisted — restarting addon', { tags: { addonId: integration.addonId }, meta: { phase: 'setSettings' } })
894
+ logger.info('persisted — restarting addon', {
895
+ tags: { addonId: integration.addonId },
896
+ meta: { phase: 'setSettings' },
897
+ })
786
898
  await ar.restartAddon(integration.addonId)
787
- logger.info('addon restart complete', { tags: { addonId: integration.addonId }, meta: { phase: 'setSettings' } })
899
+ logger.info('addon restart complete', {
900
+ tags: { addonId: integration.addonId },
901
+ meta: { phase: 'setSettings' },
902
+ })
788
903
  return { success: true }
789
904
  },
790
905
  getAvailableTypes: async () => {
@@ -799,21 +914,22 @@ export function buildIntegrationsProvider(
799
914
  // Markers that flag an addon as a creatable integration type live
800
915
  // in the module-level `INTEGRATION_CAP_MARKERS` set (exported so the
801
916
  // integration-markers spec can assert the recognised caps).
802
- const providerAddons = addons.filter(a =>
803
- a.process?.state !== 'failed' &&
804
- a.manifest.capabilities?.some(c => {
805
- const name = typeof c === 'string' ? c : c.name
806
- return typeof name === 'string' && INTEGRATION_CAP_MARKERS.has(name)
807
- }),
917
+ const providerAddons = addons.filter(
918
+ (a) =>
919
+ a.process?.state !== 'failed' &&
920
+ a.manifest.capabilities?.some((c) => {
921
+ const name = typeof c === 'string' ? c : c.name
922
+ return typeof name === 'string' && INTEGRATION_CAP_MARKERS.has(name)
923
+ }),
808
924
  )
809
925
  const integrations = await reg.listIntegrations()
810
- return providerAddons.map(addon => {
926
+ return providerAddons.map((addon) => {
811
927
  const m = addon.manifest
812
928
  const d = addon.declaration
813
929
  const icon = d?.icon ?? m.icon
814
930
  const color = d?.color ?? m.color ?? '#78716c'
815
931
  const instanceMode = d?.instanceMode ?? m.instanceMode ?? 'multiple'
816
- const existing = integrations.filter(i => i.addonId === m.id)
932
+ const existing = integrations.filter((i) => i.addonId === m.id)
817
933
  const provider = getDeviceProvider(ar, m.id)
818
934
  const discoveryMode = provider?.discoveryMode ?? 'manual'
819
935
 
@@ -824,20 +940,21 @@ export function buildIntegrationsProvider(
824
940
  // marker wins when both are present (an integration-style addon may
825
941
  // also expose a `device-provider` shim); the broker step is the
826
942
  // intended entry point for it.
827
- const capNames = (m.capabilities ?? []).map(c => (typeof c === 'string' ? c : c.name))
828
- const kind: 'device-adoption' | 'device-provider' =
829
- capNames.includes('device-adoption') ? 'device-adoption' : 'device-provider'
943
+ const capNames = (m.capabilities ?? []).map((c) => (typeof c === 'string' ? c : c.name))
944
+ const kind: 'device-adoption' | 'device-provider' = capNames.includes('device-adoption')
945
+ ? 'device-adoption'
946
+ : 'device-provider'
830
947
 
831
948
  // For device-adoption addons, the broker kind to create/link comes
832
949
  // from the addon manifest (`brokerKind`). Null for device-provider
833
950
  // addons, which carry no broker.
834
- const brokerKind = kind === 'device-adoption'
835
- ? (d?.brokerKind ?? m.brokerKind ?? null)
836
- : null
951
+ const brokerKind =
952
+ kind === 'device-adoption' ? (d?.brokerKind ?? m.brokerKind ?? null) : null
837
953
 
838
- const supportsLocationImport = kind === 'device-adoption'
839
- ? (d?.supportsLocationImport ?? m.supportsLocationImport ?? false)
840
- : false
954
+ const supportsLocationImport =
955
+ kind === 'device-adoption'
956
+ ? (d?.supportsLocationImport ?? m.supportsLocationImport ?? false)
957
+ : false
841
958
 
842
959
  return {
843
960
  addonId: m.id,
@@ -850,7 +967,7 @@ export function buildIntegrationsProvider(
850
967
  kind,
851
968
  brokerKind,
852
969
  supportsLocationImport,
853
- existingInstances: existing.map(i => ({
970
+ existingInstances: existing.map((i) => ({
854
971
  id: i.id,
855
972
  name: i.name,
856
973
  })),
@@ -871,29 +988,43 @@ export function buildIntegrationsProvider(
871
988
  const registry = ar.getCapabilityRegistry()
872
989
  const brokerId = input.settings['brokerId']
873
990
  if (typeof brokerId === 'string' && brokerId.length > 0) {
874
- const brokerProvider = registry.getProviderByAddonId<IBrokerProvider>('broker', input.addonId)
991
+ const brokerProvider = registry.getProviderByAddonId<IBrokerProvider>(
992
+ 'broker',
993
+ input.addonId,
994
+ )
875
995
  if (!brokerProvider) {
876
- return { success: false, error: `Broker provider for addon '${input.addonId}' is not available` }
996
+ return {
997
+ success: false,
998
+ error: `Broker provider for addon '${input.addonId}' is not available`,
999
+ }
877
1000
  }
878
1001
  try {
879
1002
  const result = await brokerProvider.testConnection({ id: brokerId })
880
- return result.ok
881
- ? { success: true }
882
- : { success: false, error: result.error }
1003
+ return result.ok ? { success: true } : { success: false, error: result.error }
883
1004
  } catch (err) {
884
1005
  return { success: false, error: errMsg(err) }
885
1006
  }
886
1007
  }
887
1008
 
888
1009
  // Default — RTSP/Frigate/ONVIF legacy path: probe the stream URL.
889
- const url = String(
890
- input.settings['main_stream_url'] ?? input.settings['url'] ?? '',
891
- ).trim()
1010
+ const url = String(input.settings['main_stream_url'] ?? input.settings['url'] ?? '').trim()
892
1011
  if (!url) return { success: false, error: 'No stream URL provided' }
893
1012
  try {
894
1013
  const { stdout } = await execFileAsync(
895
1014
  'ffprobe',
896
- ['-v', 'error', '-rtsp_transport', 'tcp', '-timeout', '3000000', '-show_entries', 'stream=codec_name,width,height', '-of', 'json', url],
1015
+ [
1016
+ '-v',
1017
+ 'error',
1018
+ '-rtsp_transport',
1019
+ 'tcp',
1020
+ '-timeout',
1021
+ '3000000',
1022
+ '-show_entries',
1023
+ 'stream=codec_name,width,height',
1024
+ '-of',
1025
+ 'json',
1026
+ url,
1027
+ ],
897
1028
  { timeout: 5000 },
898
1029
  )
899
1030
  const parsed = asJsonObject(JSON.parse(stdout))
@@ -988,7 +1119,9 @@ export function buildAddonsProvider(
988
1119
 
989
1120
  const bulkCoordinator = new BulkUpdateCoordinator({
990
1121
  eventBus: bulkEventBus,
991
- updateAddon: async (i) => { await ps.updatePackage(i.name, i.version) },
1122
+ updateAddon: async (i) => {
1123
+ await ps.updatePackage(i.name, i.version)
1124
+ },
992
1125
  updateFrameworkPackage: async (i) => {
993
1126
  await ps.updateFrameworkPackage({
994
1127
  packageName: i.packageName,
@@ -1007,7 +1140,10 @@ export function buildAddonsProvider(
1007
1140
  return {
1008
1141
  list: async () => {
1009
1142
  const rollbackable = ps.getRollbackablePackages()
1010
- const healthByPackage = new Map<string, ReturnType<typeof ar.getAddonHealthSnapshot>[number]>()
1143
+ const healthByPackage = new Map<
1144
+ string,
1145
+ ReturnType<typeof ar.getAddonHealthSnapshot>[number]
1146
+ >()
1011
1147
  for (const h of ar.getAddonHealthSnapshot()) {
1012
1148
  healthByPackage.set(h.packageName, h)
1013
1149
  }
@@ -1017,11 +1153,12 @@ export function buildAddonsProvider(
1017
1153
  health: healthByPackage.get(item.manifest.packageName) ?? null,
1018
1154
  }))
1019
1155
  },
1020
- getLogs: async (input) => ls.query({
1021
- tags: { addonId: input.addonId },
1022
- limit: input.limit,
1023
- level: input.level,
1024
- }),
1156
+ getLogs: async (input) =>
1157
+ ls.query({
1158
+ tags: { addonId: input.addonId },
1159
+ limit: input.limit,
1160
+ level: input.level,
1161
+ }),
1025
1162
  listPackages: async () => ps.listInstalled(),
1026
1163
  installPackage: async (input) => ps.installAndLoad(input.packageName, input.version),
1027
1164
  installFromWorkspace: async (input) => ps.installFromWorkspaceAndLoad(input.packageName),
@@ -1039,10 +1176,11 @@ export function buildAddonsProvider(
1039
1176
  },
1040
1177
  listUpdates: async (input) => {
1041
1178
  const nodeId = input.nodeId
1042
- const updates = nodeId === undefined || isHubNode(nodeId)
1043
- ? await ps.checkUpdates()
1044
- : await ps.checkUpdatesForInstalled(await fetchAgentInstalledPackages(broker, nodeId))
1045
- return updates.map(u => ({ ...u, isSystem: frameworkAllowSet.has(u.name) }))
1179
+ const updates =
1180
+ nodeId === undefined || isHubNode(nodeId)
1181
+ ? await ps.checkUpdates()
1182
+ : await ps.checkUpdatesForInstalled(await fetchAgentInstalledPackages(broker, nodeId))
1183
+ return updates.map((u) => ({ ...u, isSystem: frameworkAllowSet.has(u.name) }))
1046
1184
  },
1047
1185
  updatePackage: async (input) => {
1048
1186
  const nodeId = input.nodeId
@@ -1052,7 +1190,11 @@ export function buildAddonsProvider(
1052
1190
  // Agent target: the hub packs the resolved version and ships the
1053
1191
  // tarball over `$agent.deploy` — the agent has no npm runtime.
1054
1192
  const packed = await ps.packPackage(input.name, input.version)
1055
- await broker.call('$agent.deploy', { addonId: input.name, bundle: packed.buffer }, { nodeID: nodeId, timeout: 120_000 })
1193
+ await broker.call(
1194
+ '$agent.deploy',
1195
+ { addonId: input.name, bundle: packed.buffer },
1196
+ { nodeID: nodeId, timeout: 120_000 },
1197
+ )
1056
1198
  await broker.call('$agent.reload', {}, { nodeID: nodeId, timeout: 120_000 })
1057
1199
  return { success: true, name: input.name, version: packed.version, nodeId }
1058
1200
  },
@@ -1078,14 +1220,12 @@ export function buildAddonsProvider(
1078
1220
  const caps = registry.listCapabilities()
1079
1221
  const found = caps.find((c) => c.name === input.capName)
1080
1222
  if (!found) return []
1081
- const mode = found.mode === 'collection' ? 'collection' as const : 'singleton' as const
1223
+ const mode = found.mode === 'collection' ? ('collection' as const) : ('singleton' as const)
1082
1224
  const disabled = new Set(found.disabledProviders)
1083
1225
  return found.providers.map((addonId) => ({
1084
1226
  addonId,
1085
1227
  mode,
1086
- isActive: mode === 'collection'
1087
- ? !disabled.has(addonId)
1088
- : found.activeProvider === addonId,
1228
+ isActive: mode === 'collection' ? !disabled.has(addonId) : found.activeProvider === addonId,
1089
1229
  }))
1090
1230
  },
1091
1231
  setCapabilityProviderEnabled: async (input) => {
@@ -1119,23 +1259,20 @@ export function buildAddonsProvider(
1119
1259
  // Reuses the same `capabilities.collection.<cap>` key/format the
1120
1260
  // `capabilities` core router writes — via the shared canonical writer.
1121
1261
  const updated = registry.listCapabilities().find((c) => c.name === input.capName)
1122
- persistCollectionDisabled(
1123
- configService,
1124
- input.capName,
1125
- updated?.disabledProviders ?? [],
1126
- )
1262
+ persistCollectionDisabled(configService, input.capName, updated?.disabledProviders ?? [])
1127
1263
  return { success: true as const }
1128
1264
  },
1129
- updateFrameworkPackage: async (input) => ps.updateFrameworkPackage({
1130
- packageName: input.packageName,
1131
- ...(input.version !== undefined ? { version: input.version } : {}),
1132
- ...(ctx.user?.username !== undefined
1133
- ? { requestedBy: ctx.user.username }
1134
- : ctx.user?.id !== undefined
1135
- ? { requestedBy: ctx.user.id }
1136
- : {}),
1137
- ...(input.deferRestart !== undefined ? { deferRestart: input.deferRestart } : {}),
1138
- }),
1265
+ updateFrameworkPackage: async (input) =>
1266
+ ps.updateFrameworkPackage({
1267
+ packageName: input.packageName,
1268
+ ...(input.version !== undefined ? { version: input.version } : {}),
1269
+ ...(ctx.user?.username !== undefined
1270
+ ? { requestedBy: ctx.user.username }
1271
+ : ctx.user?.id !== undefined
1272
+ ? { requestedBy: ctx.user.id }
1273
+ : {}),
1274
+ ...(input.deferRestart !== undefined ? { deferRestart: input.deferRestart } : {}),
1275
+ }),
1139
1276
  getVersions: async (input) => ps.getPackageVersions(input.name),
1140
1277
  restartAddon: async (input) => ar.restartAddon(input.addonId),
1141
1278
  retryLoad: async (input) => {
@@ -1143,7 +1280,8 @@ export function buildAddonsProvider(
1143
1280
  return { success: true as const }
1144
1281
  },
1145
1282
  getAutoUpdateSettings: async () => ps.getAutoUpdateSettings(),
1146
- setAutoUpdateSettings: async (input) => ps.setAutoUpdateSettings(input.channel, input.intervalSeconds),
1283
+ setAutoUpdateSettings: async (input) =>
1284
+ ps.setAutoUpdateSettings(input.channel, input.intervalSeconds),
1147
1285
  getAddonAutoUpdate: async (input) => ps.getAddonAutoUpdate(input.addonId),
1148
1286
  setAddonAutoUpdate: async (input) => ps.setAddonAutoUpdate(input.addonId, input.channel),
1149
1287
  applyAutoUpdateToAll: async (input) => {
@@ -1174,11 +1312,17 @@ export function buildAddonsProvider(
1174
1312
  onAddonLogs: (input, push) => {
1175
1313
  const unsubscribe = ls.subscribe(
1176
1314
  { tags: { addonId: input.addonId }, level: input.level },
1177
- (entry: { timestamp: Date | string | number; level: string; message: string; scope?: string }) => {
1315
+ (entry: {
1316
+ timestamp: Date | string | number
1317
+ level: string
1318
+ message: string
1319
+ scope?: string
1320
+ }) => {
1178
1321
  push({
1179
- timestamp: entry.timestamp instanceof Date
1180
- ? entry.timestamp.toISOString()
1181
- : String(entry.timestamp),
1322
+ timestamp:
1323
+ entry.timestamp instanceof Date
1324
+ ? entry.timestamp.toISOString()
1325
+ : String(entry.timestamp),
1182
1326
  level: entry.level,
1183
1327
  message: entry.message,
1184
1328
  scope: entry.scope,