@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
@@ -94,7 +94,9 @@ function localDispatcherFake(owner?: { capName: string; deviceId: number; childI
94
94
  callCapOnChild: ReturnType<typeof vi.fn>
95
95
  } {
96
96
  const resolveChildId = vi.fn((capName: string, deviceId?: number): string | null =>
97
- owner !== undefined && capName === owner.capName && deviceId === owner.deviceId ? owner.childId : null,
97
+ owner !== undefined && capName === owner.capName && deviceId === owner.deviceId
98
+ ? owner.childId
99
+ : null,
98
100
  )
99
101
  const callCapOnChild = vi.fn(async (childId: string, input: CapCallInput) => ({
100
102
  routedOverUds: childId,
@@ -112,7 +114,12 @@ describe('hub onUnownedCall wiring (F0)', () => {
112
114
  const settingsStore = { get: async (params: unknown) => ({ value: 'hub-resolved', params }) }
113
115
  const inProcessProviders: InProcessProviderLookup = (capName) =>
114
116
  capName === 'settings-store'
115
- ? { invoke: (method, args) => Promise.resolve((settingsStore as Record<string, (a: unknown) => unknown>)[method](args)) }
117
+ ? {
118
+ invoke: (method, args) =>
119
+ Promise.resolve(
120
+ (settingsStore as Record<string, (a: unknown) => unknown>)[method](args),
121
+ ),
122
+ }
116
123
  : null
117
124
 
118
125
  const resolverDeps: CapRouteResolverDeps = {
@@ -124,12 +131,16 @@ describe('hub onUnownedCall wiring (F0)', () => {
124
131
  }
125
132
  const resolver = new CapRouteResolver(resolverDeps)
126
133
 
127
- const handler = createParentUnownedCallHandler({ getResolver: () => resolver, broker, nodeRegistry: nodeRegistryFake() })
134
+ const handler = createParentUnownedCallHandler({
135
+ getResolver: () => resolver,
136
+ broker,
137
+ nodeRegistry: nodeRegistryFake(),
138
+ })
128
139
 
129
140
  const result = await handler({ capName: 'settings-store', method: 'get', args: { key: 'k' } })
130
141
  expect(result).toEqual({ value: 'hub-resolved', params: { key: 'k' } })
131
142
  // Resolver served it — broker untouched.
132
- expect((broker.call as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled()
143
+ expect(broker.call as ReturnType<typeof vi.fn>).not.toHaveBeenCalled()
133
144
  })
134
145
 
135
146
  it('broker-fallback: a `$`-infra core service (`$core-caps.system.info`) routes via the broker', async () => {
@@ -145,7 +156,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
145
156
  inProcessProviders: () => null,
146
157
  })
147
158
 
148
- const handler = createParentUnownedCallHandler({ getResolver: () => resolver, broker, nodeRegistry: nodeRegistryFake() })
159
+ const handler = createParentUnownedCallHandler({
160
+ getResolver: () => resolver,
161
+ broker,
162
+ nodeRegistry: nodeRegistryFake(),
163
+ })
149
164
 
150
165
  const result = await handler({ capName: 'system', method: 'info', args: undefined })
151
166
  expect(result).toEqual({ uptimeSec: 99, params: undefined })
@@ -156,7 +171,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
156
171
  const broker = hubBrokerFake()
157
172
  // Mirrors the window before `this.resolver` is constructed: getResolver
158
173
  // returns null, so the handler goes straight to the broker fallback.
159
- const handler = createParentUnownedCallHandler({ getResolver: () => null, broker, nodeRegistry: nodeRegistryFake() })
174
+ const handler = createParentUnownedCallHandler({
175
+ getResolver: () => null,
176
+ broker,
177
+ nodeRegistry: nodeRegistryFake(),
178
+ })
160
179
 
161
180
  const result = await handler({ capName: 'system', method: 'info', args: undefined })
162
181
  expect(result).toEqual({ uptimeSec: 99, params: undefined })
@@ -187,7 +206,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
187
206
  })
188
207
 
189
208
  // deviceId lives ONLY in args, NOT top-level — the handler must derive it.
190
- const result = await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } })
209
+ const result = await handler({
210
+ capName: 'stream-catalog',
211
+ method: 'getCatalog',
212
+ args: { deviceId: 7 },
213
+ })
191
214
  expect(result).toEqual({ catalog: ['stream-a'] })
192
215
  expect(resolvedWithDeviceId).toBe(7)
193
216
  // Resolver served it via the derived deviceId — broker untouched.
@@ -207,14 +230,21 @@ describe('hub onUnownedCall wiring (F0)', () => {
207
230
  },
208
231
  }
209
232
 
210
- const owner: NodeNativeCapEntry = { nodeId: 'agent', addonId: 'reolink', capName: 'stream-catalog', deviceId: 7 }
233
+ const owner: NodeNativeCapEntry = {
234
+ nodeId: 'agent',
235
+ addonId: 'reolink',
236
+ capName: 'stream-catalog',
237
+ deviceId: 7,
238
+ }
211
239
  const handler = createParentUnownedCallHandler({
212
240
  getResolver: () => resolver as unknown as CapRouteResolver,
213
241
  broker,
214
242
  nodeRegistry: nodeRegistryFake({ 7: [owner] }),
215
243
  })
216
244
 
217
- await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(() => undefined)
245
+ await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
246
+ () => undefined,
247
+ )
218
248
 
219
249
  // The broker call is pinned to the owning node via call-opts `{ nodeID }`.
220
250
  const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
@@ -253,7 +283,9 @@ describe('hub onUnownedCall wiring (F0)', () => {
253
283
  resolveCapRoute: (capName: string) => {
254
284
  throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
255
285
  },
256
- dispatch: async () => { throw new Error('dispatch should not be reached') },
286
+ dispatch: async () => {
287
+ throw new Error('dispatch should not be reached')
288
+ },
257
289
  }
258
290
  const { dispatcher, resolveChildId, callCapOnChild } = localDispatcherFake({
259
291
  capName: 'stream-catalog',
@@ -268,7 +300,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
268
300
  getLocalDispatcher: () => dispatcher,
269
301
  })
270
302
 
271
- const result = await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } })
303
+ const result = await handler({
304
+ capName: 'stream-catalog',
305
+ method: 'getCatalog',
306
+ args: { deviceId: 7 },
307
+ })
272
308
  expect(result).toEqual({ routedOverUds: 'child-reolink', capName: 'stream-catalog' })
273
309
  expect(resolveChildId).toHaveBeenCalledWith('stream-catalog', 7)
274
310
  expect(callCapOnChild).toHaveBeenCalledWith('child-reolink', {
@@ -286,10 +322,17 @@ describe('hub onUnownedCall wiring (F0)', () => {
286
322
  resolveCapRoute: (capName: string) => {
287
323
  throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
288
324
  },
289
- dispatch: async () => { throw new Error('dispatch should not be reached') },
325
+ dispatch: async () => {
326
+ throw new Error('dispatch should not be reached')
327
+ },
290
328
  }
291
329
  const { dispatcher, callCapOnChild } = localDispatcherFake() // no local owner
292
- const owner: NodeNativeCapEntry = { nodeId: 'agent', addonId: 'reolink', capName: 'stream-catalog', deviceId: 7 }
330
+ const owner: NodeNativeCapEntry = {
331
+ nodeId: 'agent',
332
+ addonId: 'reolink',
333
+ capName: 'stream-catalog',
334
+ deviceId: 7,
335
+ }
293
336
 
294
337
  const handler = createParentUnownedCallHandler({
295
338
  getResolver: () => resolver as unknown as CapRouteResolver,
@@ -298,7 +341,9 @@ describe('hub onUnownedCall wiring (F0)', () => {
298
341
  getLocalDispatcher: () => dispatcher,
299
342
  })
300
343
 
301
- await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(() => undefined)
344
+ await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
345
+ () => undefined,
346
+ )
302
347
 
303
348
  expect(callCapOnChild).not.toHaveBeenCalled()
304
349
  const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
@@ -311,9 +356,16 @@ describe('hub onUnownedCall wiring (F0)', () => {
311
356
  resolveCapRoute: (capName: string) => {
312
357
  throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
313
358
  },
314
- dispatch: async () => { throw new Error('dispatch should not be reached') },
359
+ dispatch: async () => {
360
+ throw new Error('dispatch should not be reached')
361
+ },
362
+ }
363
+ const owner: NodeNativeCapEntry = {
364
+ nodeId: 'agent',
365
+ addonId: 'reolink',
366
+ capName: 'stream-catalog',
367
+ deviceId: 7,
315
368
  }
316
- const owner: NodeNativeCapEntry = { nodeId: 'agent', addonId: 'reolink', capName: 'stream-catalog', deviceId: 7 }
317
369
 
318
370
  const handler = createParentUnownedCallHandler({
319
371
  getResolver: () => resolver as unknown as CapRouteResolver,
@@ -322,7 +374,9 @@ describe('hub onUnownedCall wiring (F0)', () => {
322
374
  getLocalDispatcher: () => null,
323
375
  })
324
376
 
325
- await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(() => undefined)
377
+ await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
378
+ () => undefined,
379
+ )
326
380
  const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
327
381
  expect(callArgs[2]).toEqual({ nodeID: 'agent' })
328
382
  })
@@ -46,7 +46,10 @@ import type { StreamProbeService } from '../core/streaming/stream-probe.service.
46
46
  */
47
47
  interface RegisterNodeDriver {
48
48
  onRegisterNode: (params: RegisterNodeParams) => void
49
- createCapabilityProxy: (capabilityName: string, nodeId: string) => Record<string, (params: unknown) => Promise<unknown>> | null
49
+ createCapabilityProxy: (
50
+ capabilityName: string,
51
+ nodeId: string,
52
+ ) => Record<string, (params: unknown) => Promise<unknown>> | null
50
53
  }
51
54
 
52
55
  /** Build a harness whose declared caps are SINGLETON, for active-provider preference tests. */
@@ -55,7 +58,10 @@ function createSingletonHarness(capNames: readonly string[]): Harness {
55
58
  }
56
59
 
57
60
  /** A minimal real `CapabilityDefinition` so `getDefinition`/`expandCapMethods` resolve. */
58
- function makeCapDef(name: string, mode: 'collection' | 'singleton' = 'collection'): CapabilityDefinition {
61
+ function makeCapDef(
62
+ name: string,
63
+ mode: 'collection' | 'singleton' = 'collection',
64
+ ): CapabilityDefinition {
59
65
  return {
60
66
  name,
61
67
  scope: 'system',
@@ -98,7 +104,10 @@ interface Harness {
98
104
  * The broker is never started, so there is nothing to stop in teardown —
99
105
  * no `afterEach` teardown is needed.
100
106
  */
101
- function createHarness(capNames: readonly string[], mode: 'collection' | 'singleton' = 'collection'): Harness {
107
+ function createHarness(
108
+ capNames: readonly string[],
109
+ mode: 'collection' | 'singleton' = 'collection',
110
+ ): Harness {
102
111
  const registry = new CapabilityRegistry(makeLogger())
103
112
  for (const name of capNames) {
104
113
  registry.declareCapability(makeCapDef(name, mode))
@@ -248,8 +257,8 @@ describe('MoleculerService.applyNodeManifest — singleton local-first preferenc
248
257
 
249
258
  it('prefers the hub-local provider when the REMOTE one registered first', () => {
250
259
  const { driver, registry } = createSingletonHarness(['pipeline-executor'])
251
- driver.onRegisterNode(singleton(REMOTE)) // agent first → would win under first-wins
252
- driver.onRegisterNode(singleton(LOCAL)) // hub-local second
260
+ driver.onRegisterNode(singleton(REMOTE)) // agent first → would win under first-wins
261
+ driver.onRegisterNode(singleton(LOCAL)) // hub-local second
253
262
  // Active provider must be the hub-local key (bare addonId, no '@').
254
263
  expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
255
264
  expect(registry.getSingletonAddonId('pipeline-executor')).not.toContain('@')
@@ -257,8 +266,8 @@ describe('MoleculerService.applyNodeManifest — singleton local-first preferenc
257
266
 
258
267
  it('keeps the hub-local provider active when a REMOTE one registers afterwards', () => {
259
268
  const { driver, registry } = createSingletonHarness(['pipeline-executor'])
260
- driver.onRegisterNode(singleton(LOCAL)) // hub-local first
261
- driver.onRegisterNode(singleton(REMOTE)) // agent second must NOT steal active
269
+ driver.onRegisterNode(singleton(LOCAL)) // hub-local first
270
+ driver.onRegisterNode(singleton(REMOTE)) // agent second must NOT steal active
262
271
  expect(registry.getSingletonAddonId('pipeline-executor')).toBe('detection-pipeline')
263
272
  })
264
273
  })
@@ -38,7 +38,10 @@ interface FakeHubLocalRegistry extends HubLocalChildDispatcher {
38
38
  function makeDeviceAwareHubLocalRegistry(
39
39
  caps: ReadonlyMap<string, ReadonlyMap<number | 'singleton', string>>,
40
40
  ): FakeHubLocalRegistry {
41
- const callCapOnChildSpy = vi.fn(async (_childId: string, _input: unknown) => ({ ok: true, from: 'uds' }))
41
+ const callCapOnChildSpy = vi.fn(async (_childId: string, _input: unknown) => ({
42
+ ok: true,
43
+ from: 'uds',
44
+ }))
42
45
  return {
43
46
  resolveChildId: (capName: string, deviceId?: number): string | null => {
44
47
  const capMap = caps.get(capName)
@@ -79,8 +82,7 @@ function makeNativeAwareNodeAuthority(
79
82
  return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
80
83
  },
81
84
 
82
- nodeIsAgent: (nodeId: string): boolean =>
83
- nodeId !== HUB_NODE_ID && !nodeId.includes('/'),
85
+ nodeIsAgent: (nodeId: string): boolean => nodeId !== HUB_NODE_ID && !nodeId.includes('/'),
84
86
 
85
87
  nodeOnline: (nodeId: string): boolean => onlineNodes.has(nodeId),
86
88
 
@@ -104,7 +106,9 @@ function makeNativeAwareNodeAuthority(
104
106
 
105
107
  isNativeCap: (nodeId: string, capName: string, deviceId?: number): boolean => {
106
108
  if (deviceId !== undefined) {
107
- return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId)
109
+ return nativeCaps.some(
110
+ (n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
111
+ )
108
112
  }
109
113
  return nativeCaps.some((n) => n.nodeId === nodeId && n.capName === capName)
110
114
  },
@@ -119,15 +123,22 @@ describe('Task5 – device-scoped native cap on hub-local child', () => {
119
123
  it('resolves to hub-local-uds and calls callCapOnChild with {capName, method, args+deviceId, deviceId}', async () => {
120
124
  // ptz is a device-scoped native cap owned by hub child 'provider-reolink'
121
125
  const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
122
- new Map([
123
- ['ptz', new Map([[7, 'provider-reolink']])],
124
- ]),
126
+ new Map([['ptz', new Map([[7, 'provider-reolink']])]]),
125
127
  )
126
128
 
127
129
  const nativeCaps: NativeCapSpec[] = [
128
- { nodeId: 'hub/provider-reolink', addonId: 'addon-provider-reolink', capName: 'ptz', deviceId: 7 },
130
+ {
131
+ nodeId: 'hub/provider-reolink',
132
+ addonId: 'addon-provider-reolink',
133
+ capName: 'ptz',
134
+ deviceId: 7,
135
+ },
129
136
  ]
130
- const nodeAuthority = makeNativeAwareNodeAuthority(new Map(), new Set(['hub/provider-reolink']), nativeCaps)
137
+ const nodeAuthority = makeNativeAwareNodeAuthority(
138
+ new Map(),
139
+ new Set(['hub/provider-reolink']),
140
+ nativeCaps,
141
+ )
131
142
 
132
143
  const deps: CapRouteResolverDeps = {
133
144
  hubNodeId: HUB_NODE_ID,
@@ -149,7 +160,10 @@ describe('Task5 – device-scoped native cap on hub-local child', () => {
149
160
  expect(result).toEqual({ ok: true, from: 'uds' })
150
161
 
151
162
  expect(hubLocalCaps.callCapOnChildSpy).toHaveBeenCalledOnce()
152
- const [calledChildId, calledInput] = hubLocalCaps.callCapOnChildSpy.mock.calls[0] as [string, unknown]
163
+ const [calledChildId, calledInput] = hubLocalCaps.callCapOnChildSpy.mock.calls[0] as [
164
+ string,
165
+ unknown,
166
+ ]
153
167
  expect(calledChildId).toBe('provider-reolink')
154
168
  expect(calledInput).toMatchObject({
155
169
  capName: 'ptz',
@@ -191,9 +205,7 @@ describe('Task5 – d9ba709 regression: singleton prefers hub-local over remote'
191
205
  it('singleton cap provided by both hub-local child AND remote node classifies to hub-local-uds (no nodeId given)', () => {
192
206
  // 'pipeline-executor' is on BOTH local child addon-detection-pipeline AND remote-agent-0
193
207
  const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
194
- new Map([
195
- ['pipeline-executor', new Map([['singleton', 'addon-detection-pipeline']])],
196
- ]),
208
+ new Map([['pipeline-executor', new Map([['singleton', 'addon-detection-pipeline']])]]),
197
209
  )
198
210
 
199
211
  const systemCaps = new Map([
@@ -307,7 +319,12 @@ describe('Task5 – remote native cap uses NATIVE_PROVIDER_SERVICE_INFIX in acti
307
319
  describe('Task5 – nodeKnowsCap includes native caps', () => {
308
320
  it('nodeKnowsCap returns true when the node has a native cap entry (even with no manifest cap)', () => {
309
321
  const nativeCaps: NativeCapSpec[] = [
310
- { nodeId: 'hub/provider-reolink', addonId: 'addon-provider-reolink', capName: 'ptz', deviceId: 7 },
322
+ {
323
+ nodeId: 'hub/provider-reolink',
324
+ addonId: 'addon-provider-reolink',
325
+ capName: 'ptz',
326
+ deviceId: 7,
327
+ },
311
328
  ]
312
329
  const nodeAuthority = makeNativeAwareNodeAuthority(
313
330
  new Map(), // no system caps
@@ -338,14 +355,20 @@ describe('native-fallback – wrapper must not shadow the hub-local native child
338
355
  // snapshot has an in-hub wrapper AND a forked native child for device 7.
339
356
  const wrapperRef: InProcessProviderRef = { invoke: vi.fn(async () => ({ from: 'wrapper' })) }
340
357
 
341
- function makeSnapshotResolver(): { resolver: CapRouteResolver; hubLocalCaps: FakeHubLocalRegistry } {
358
+ function makeSnapshotResolver(): {
359
+ resolver: CapRouteResolver
360
+ hubLocalCaps: FakeHubLocalRegistry
361
+ } {
342
362
  const hubLocalCaps = makeDeviceAwareHubLocalRegistry(
343
- new Map([
344
- ['snapshot', new Map([[7, 'provider-reolink']])],
345
- ]),
363
+ new Map([['snapshot', new Map([[7, 'provider-reolink']])]]),
346
364
  )
347
365
  const nativeCaps: NativeCapSpec[] = [
348
- { nodeId: 'hub/provider-reolink', addonId: 'addon-provider-reolink', capName: 'snapshot', deviceId: 7 },
366
+ {
367
+ nodeId: 'hub/provider-reolink',
368
+ addonId: 'addon-provider-reolink',
369
+ capName: 'snapshot',
370
+ deviceId: 7,
371
+ },
349
372
  ]
350
373
  const nodeAuthority = makeNativeAwareNodeAuthority(
351
374
  new Map(),
@@ -17,7 +17,11 @@ import * as crypto from 'node:crypto'
17
17
  import { registerOauth2Routes } from '../api/oauth2/oauth2-routes.js'
18
18
  import { createOauthGrants } from '../../../../packages/core/src/builtins/local-auth/oauth-grants.js'
19
19
  import type { ISsoBridgeProvider } from '@camstack/types'
20
- import type { IOauthIntegrationProvider, IUserManagementProvider, TokenScope } from '@camstack/types'
20
+ import type {
21
+ IOauthIntegrationProvider,
22
+ IUserManagementProvider,
23
+ TokenScope,
24
+ } from '@camstack/types'
21
25
  import type { OauthSession } from '../../../../packages/core/src/builtins/local-auth/oauth-session-manager.js'
22
26
  import { SESSION_COOKIE } from '../auth/session-cookie.js'
23
27
 
@@ -59,7 +63,10 @@ function verifyHmacJwt(token: string): Record<string, unknown> | null {
59
63
  )
60
64
  if (sig !== expected) return null
61
65
  try {
62
- const decoded = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as Record<string, unknown>
66
+ const decoded = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as Record<
67
+ string,
68
+ unknown
69
+ >
63
70
  if (typeof decoded['exp'] === 'number' && decoded['exp'] < Math.floor(Date.now() / 1000)) {
64
71
  return null
65
72
  }
@@ -92,7 +99,12 @@ function fakeSessionManager() {
92
99
 
93
100
  return {
94
101
  store,
95
- async create(input: { userId: string; username: string; integrationId: string; scopes: TokenScope[] }): Promise<OauthSession> {
102
+ async create(input: {
103
+ userId: string
104
+ username: string
105
+ integrationId: string
106
+ scopes: TokenScope[]
107
+ }): Promise<OauthSession> {
96
108
  const now = Date.now()
97
109
  const session: OauthSession = {
98
110
  id: `session-${++seq}`,
@@ -139,7 +151,13 @@ const VALID_SESSION_TOKEN = 'valid-session-token-abc123'
139
151
  const ALEXA_DESCRIPTOR = {
140
152
  integrationId: 'export-alexa',
141
153
  displayName: 'Alexa Smart Home',
142
- requestedScopes: [{ type: 'category' as const, target: 'device' as const, access: ['view', 'create'] as ('view' | 'create' | 'delete')[] }],
154
+ requestedScopes: [
155
+ {
156
+ type: 'category' as const,
157
+ target: 'device' as const,
158
+ access: ['view', 'create'] as ('view' | 'create' | 'delete')[],
159
+ },
160
+ ],
143
161
  allowedRedirectPrefixes: ['https://cb.example/'],
144
162
  }
145
163
 
@@ -204,7 +222,10 @@ function buildFakeRegistry(
204
222
 
205
223
  // ─── Test setup ───────────────────────────────────────────────────────────────
206
224
 
207
- function buildApp(grants: ReturnType<typeof createOauthGrants>, sessionManager: ReturnType<typeof fakeSessionManager>) {
225
+ function buildApp(
226
+ grants: ReturnType<typeof createOauthGrants>,
227
+ sessionManager: ReturnType<typeof fakeSessionManager>,
228
+ ) {
208
229
  const fastify = Fastify({ logger: false })
209
230
  void fastify.register(cookie)
210
231
 
@@ -291,7 +312,11 @@ describe('OAuth2 account-linking flow', () => {
291
312
 
292
313
  expect(res.statusCode).toBe(302)
293
314
  const location = res.headers['location'] as string
294
- expect(location).toMatch(new RegExp(`^${REDIRECT_URI.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\?code=.+&state=${STATE}`))
315
+ expect(location).toMatch(
316
+ new RegExp(
317
+ `^${REDIRECT_URI.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\?code=.+&state=${STATE}`,
318
+ ),
319
+ )
295
320
  })
296
321
 
297
322
  // ── Case 4 ──────────────────────────────────────────────────────────────────
@@ -336,7 +361,12 @@ describe('OAuth2 account-linking flow', () => {
336
361
  })
337
362
 
338
363
  expect(tokenRes.statusCode).toBe(200)
339
- const json = tokenRes.json<{ access_token: string; refresh_token: string; expires_in: number; token_type: string }>()
364
+ const json = tokenRes.json<{
365
+ access_token: string
366
+ refresh_token: string
367
+ expires_in: number
368
+ token_type: string
369
+ }>()
340
370
  expect(json).toMatchObject({
341
371
  expires_in: 3600,
342
372
  token_type: 'Bearer',
@@ -550,7 +580,12 @@ describe('OAuth2 account-linking flow', () => {
550
580
  })
551
581
 
552
582
  expect(refreshRes.statusCode).toBe(200)
553
- const refreshJson = refreshRes.json<{ access_token: string; refresh_token: string; expires_in: number; token_type: string }>()
583
+ const refreshJson = refreshRes.json<{
584
+ access_token: string
585
+ refresh_token: string
586
+ expires_in: number
587
+ token_type: string
588
+ }>()
554
589
  expect(refreshJson).toMatchObject({ expires_in: 3600, token_type: 'Bearer' })
555
590
  expect(typeof refreshJson.access_token).toBe('string')
556
591
  expect(refreshJson.access_token.length).toBeGreaterThan(0)
@@ -604,7 +639,11 @@ describe('OAuth2 account-linking flow', () => {
604
639
  describe('session registry lifecycle', () => {
605
640
  // Shared helper: run the full authorize → consent → exchange flow and
606
641
  // return the issued token pair plus the raw location URL.
607
- async function doFullFlow(): Promise<{ accessToken: string; refreshToken: string; location: string }> {
642
+ async function doFullFlow(): Promise<{
643
+ accessToken: string
644
+ refreshToken: string
645
+ location: string
646
+ }> {
608
647
  const authorizeBody = new URLSearchParams({
609
648
  consent: 'allow',
610
649
  integration: 'export-alexa',
@@ -661,9 +700,7 @@ describe('OAuth2 account-linking flow', () => {
661
700
 
662
701
  // Scopes must contain the category/device entry declared in ALEXA_DESCRIPTOR.
663
702
  expect(Array.isArray(session.scopes)).toBe(true)
664
- const deviceScope = session.scopes.find(
665
- (s) => s.type === 'category' && s.target === 'device',
666
- )
703
+ const deviceScope = session.scopes.find((s) => s.type === 'category' && s.target === 'device')
667
704
  expect(deviceScope).toBeDefined()
668
705
  })
669
706
 
@@ -692,7 +729,9 @@ describe('OAuth2 account-linking flow', () => {
692
729
  const sessionId = sessions[0]!.id
693
730
 
694
731
  // Revoke via the fake user-management method (same as the real addon does).
695
- const revokeResult = await sessionManager.markRevoked(sessionId).then((ok) => ({ success: ok }))
732
+ const revokeResult = await sessionManager
733
+ .markRevoked(sessionId)
734
+ .then((ok) => ({ success: ok }))
696
735
  expect(revokeResult).toEqual({ success: true })
697
736
 
698
737
  // The session must now carry a non-null revokedAt.
@@ -729,7 +768,9 @@ describe('OAuth2 account-linking flow', () => {
729
768
 
730
769
  // ── Case e ────────────────────────────────────────────────────────────────
731
770
  it('e) revokeOauthSession with unknown id → {success: false}', async () => {
732
- const result = await sessionManager.markRevoked('non-existent-session-id').then((ok) => ({ success: ok }))
771
+ const result = await sessionManager
772
+ .markRevoked('non-existent-session-id')
773
+ .then((ok) => ({ success: ok }))
733
774
  expect(result).toEqual({ success: false })
734
775
  })
735
776
  })
@@ -777,7 +818,8 @@ describe('integration-driven hubUrl claim (Phase A public-origin bridge)', () =>
777
818
  registerOauth2Routes(fastify, {
778
819
  getRegistry: () => reg as any,
779
820
  verifyToken: (token: string) => {
780
- if (token === VALID_SESSION_TOKEN) return { userId: OPERATOR_USER_ID, username: OPERATOR_USERNAME }
821
+ if (token === VALID_SESSION_TOKEN)
822
+ return { userId: OPERATOR_USER_ID, username: OPERATOR_USERNAME }
781
823
  throw new Error('invalid token')
782
824
  },
783
825
  publicHubUrl: () => GLOBAL_FALLBACK,
@@ -785,7 +827,9 @@ describe('integration-driven hubUrl claim (Phase A public-origin bridge)', () =>
785
827
  return fastify
786
828
  }
787
829
 
788
- async function issuedCodeHubUrl(app: ReturnType<typeof buildAppWithDescriptor>): Promise<unknown> {
830
+ async function issuedCodeHubUrl(
831
+ app: ReturnType<typeof buildAppWithDescriptor>,
832
+ ): Promise<unknown> {
789
833
  const body = new URLSearchParams({
790
834
  consent: 'allow',
791
835
  integration: 'export-alexa',
@@ -805,7 +849,9 @@ describe('integration-driven hubUrl claim (Phase A public-origin bridge)', () =>
805
849
  expect(res.statusCode).toBe(302)
806
850
  const loc = res.headers['location'] as string
807
851
  const code = decodeURIComponent(loc.match(/[?&]code=([^&]+)/)![1]!)
808
- const payload = JSON.parse(Buffer.from(code.split('.')[1]!, 'base64url').toString('utf8')) as Record<string, unknown>
852
+ const payload = JSON.parse(
853
+ Buffer.from(code.split('.')[1]!, 'base64url').toString('utf8'),
854
+ ) as Record<string, unknown>
809
855
  return payload['hubUrl']
810
856
  }
811
857
 
@@ -86,12 +86,12 @@ class TestAddonHarness {
86
86
  } as any
87
87
  const result = await entry.addon.initialize(context)
88
88
  if (result) {
89
- const regs = Array.isArray(result) ? result : (result as any).providers ?? []
89
+ const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
90
90
  for (const reg of regs) {
91
91
  const capName: string =
92
92
  typeof reg.capability === 'string'
93
93
  ? reg.capability
94
- : (reg.capability as any)?.name ?? String(reg.capability)
94
+ : ((reg.capability as any)?.name ?? String(reg.capability))
95
95
  self.registry.registerProvider(capName, id, reg.provider)
96
96
  }
97
97
  }
@@ -154,8 +154,12 @@ describe('Singleton contention E2E: two addons on the same singleton cap', () =>
154
154
  expect(info.activeProvider).toBe('mock-analysis-a')
155
155
 
156
156
  // Both providers individually addressable.
157
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(analysisA.provider)
158
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(analysisB.provider)
157
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
158
+ analysisA.provider,
159
+ )
160
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
161
+ analysisB.provider,
162
+ )
159
163
  })
160
164
 
161
165
  it('honours a configReader preference for the SECOND addon over first-registered', async () => {
@@ -191,7 +195,11 @@ describe('Singleton contention E2E: waitForProvider before registration', () =>
191
195
  harness.declareCapabilities(analysisA)
192
196
 
193
197
  // Consumer begins waiting BEFORE the addon initializes — no provider yet.
194
- const waitPromise = harness.registry.waitForProvider('object-detector', 'mock-analysis-a', 5_000)
198
+ const waitPromise = harness.registry.waitForProvider(
199
+ 'object-detector',
200
+ 'mock-analysis-a',
201
+ 5_000,
202
+ )
195
203
 
196
204
  // Addon initializes shortly after → registerProvider fulfils the waiter.
197
205
  setTimeout(() => {
@@ -263,7 +271,9 @@ describe('Singleton contention E2E: active provider removed', () => {
263
271
  const info = harness.registry.listCapabilities().find((c) => c.name === 'object-detector')!
264
272
  expect(info.providers).toEqual(['mock-analysis-b'])
265
273
  expect(info.activeProvider).toBe('mock-analysis-b')
266
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(analysisB.provider)
274
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
275
+ analysisB.provider,
276
+ )
267
277
  })
268
278
 
269
279
  it('removing a NON-active provider keeps the active one untouched', async () => {
@@ -369,19 +379,19 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
369
379
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
370
380
 
371
381
  // Switch to B.
372
- await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b', true)
382
+ await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b')
373
383
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisB.provider)
374
384
  expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-b')
375
385
 
376
386
  // Switch back to A.
377
- await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-a', true)
387
+ await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-a')
378
388
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
379
389
  expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-a')
380
390
  })
381
391
 
382
392
  it('setActiveSingleton throws when switching to an addon that never registered', async () => {
383
393
  await expect(
384
- harness.registry.setActiveSingleton('object-detector', 'mock-analysis-c', true),
394
+ harness.registry.setActiveSingleton('object-detector', 'mock-analysis-c'),
385
395
  ).rejects.toThrow(/[Nn]o provider/)
386
396
  // Active pointer unchanged after the failed switch.
387
397
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
@@ -389,7 +399,7 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
389
399
 
390
400
  it('unregistering the explicitly-selected active provider promotes the remaining one', async () => {
391
401
  // Operator explicitly selected B.
392
- await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b', true)
402
+ await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b')
393
403
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisB.provider)
394
404
 
395
405
  // B is removed. `unregisterProvider` promotes the remaining A rather
@@ -399,7 +409,9 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
399
409
  await harness.shutdownAddon('mock-analysis-b')
400
410
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
401
411
  expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-a')
402
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(analysisA.provider)
412
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
413
+ analysisA.provider,
414
+ )
403
415
  })
404
416
  })
405
417