@camstack/server 0.2.2 → 1.0.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.
- package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
package/src/boot/boot-config.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 1: Bootstrap config loading and infrastructure setup.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from main.ts — pure extraction, no behavior change.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as fs from 'node:fs'
|
|
8
|
-
import * as path from 'node:path'
|
|
9
|
-
import * as yaml from 'js-yaml'
|
|
10
|
-
import { randomBytes } from 'node:crypto'
|
|
11
|
-
import { asJsonObject } from '@camstack/types'
|
|
12
|
-
import { bootstrapSchema } from '../core/config/config.schema'
|
|
13
|
-
import type { BootstrapConfig } from '../core/config/config.schema'
|
|
14
|
-
import { StorageLocationManager } from '../core/storage/storage-location-manager'
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Types
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
export type { BootstrapConfig }
|
|
21
|
-
|
|
22
|
-
export interface InfraContext {
|
|
23
|
-
readonly bootstrapConfig: BootstrapConfig
|
|
24
|
-
readonly dataPath: string
|
|
25
|
-
readonly locationManager: StorageLocationManager
|
|
26
|
-
readonly tlsOptions: { key: Buffer; cert: Buffer } | undefined
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Constants
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
const CONFIG_DEFAULTS: Record<string, unknown> = {
|
|
34
|
-
server: { port: 4443, host: '0.0.0.0', dataPath: 'camstack-data' },
|
|
35
|
-
auth: {
|
|
36
|
-
jwtSecret: null,
|
|
37
|
-
adminUsername: 'admin',
|
|
38
|
-
adminPassword: 'changeme',
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ENV_VAR_MAP: Record<string, string> = {
|
|
43
|
-
CAMSTACK_PORT: 'server.port',
|
|
44
|
-
CAMSTACK_HOST: 'server.host',
|
|
45
|
-
CAMSTACK_DATA: 'server.dataPath',
|
|
46
|
-
CAMSTACK_JWT_SECRET: 'auth.jwtSecret',
|
|
47
|
-
CAMSTACK_ADMIN_USER: 'auth.adminUsername',
|
|
48
|
-
CAMSTACK_ADMIN_PASS: 'auth.adminPassword',
|
|
49
|
-
CAMSTACK_HUB_URL: 'hub.url',
|
|
50
|
-
CAMSTACK_HUB_TOKEN: 'hub.token',
|
|
51
|
-
CAMSTACK_AGENT_NAME: 'agent.name',
|
|
52
|
-
CAMSTACK_TLS_ENABLED: 'tls.enabled',
|
|
53
|
-
CAMSTACK_TLS_CERT: 'tls.certPath',
|
|
54
|
-
CAMSTACK_TLS_KEY: 'tls.keyPath',
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Helpers
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
function setNested(
|
|
62
|
-
obj: Record<string, unknown>,
|
|
63
|
-
p: string,
|
|
64
|
-
value: unknown,
|
|
65
|
-
): Record<string, unknown> {
|
|
66
|
-
const [head, ...rest] = p.split('.')
|
|
67
|
-
if (!head) return obj
|
|
68
|
-
if (rest.length === 0) return { ...obj, [head]: value }
|
|
69
|
-
const child = asJsonObject(obj[head]) ?? {}
|
|
70
|
-
return { ...obj, [head]: setNested(child, rest.join('.'), value) }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// loadBootstrapConfig
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Load bootstrap config from a YAML file, apply env-var overrides, and
|
|
79
|
-
* validate via the bootstrap Zod schema.
|
|
80
|
-
*/
|
|
81
|
-
export function loadBootstrapConfig(configPath: string): BootstrapConfig {
|
|
82
|
-
// Only bootstrap sections live in config.yaml.
|
|
83
|
-
// All runtime settings are stored in the SQL system_settings table.
|
|
84
|
-
|
|
85
|
-
let raw: Record<string, unknown>
|
|
86
|
-
|
|
87
|
-
if (fs.existsSync(configPath)) {
|
|
88
|
-
const content = fs.readFileSync(configPath, 'utf-8')
|
|
89
|
-
raw = asJsonObject(yaml.load(content)) ?? {}
|
|
90
|
-
// Merge in any missing bootstrap sections (server, auth only)
|
|
91
|
-
let updated = false
|
|
92
|
-
for (const [key, defaults] of Object.entries(CONFIG_DEFAULTS)) {
|
|
93
|
-
if (!(key in raw)) {
|
|
94
|
-
raw[key] = defaults
|
|
95
|
-
updated = true
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (updated) {
|
|
99
|
-
try {
|
|
100
|
-
const tmpPath = `${configPath}.tmp`
|
|
101
|
-
fs.writeFileSync(
|
|
102
|
-
tmpPath,
|
|
103
|
-
yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
|
|
104
|
-
'utf-8',
|
|
105
|
-
)
|
|
106
|
-
fs.renameSync(tmpPath, configPath)
|
|
107
|
-
console.log(`[Phase1] Updated config.yaml with missing bootstrap defaults`)
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.warn(`[Phase1] Could not update config.yaml:`, err)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
console.log(`[Phase1] Loaded bootstrap config from: ${configPath}`)
|
|
113
|
-
} else {
|
|
114
|
-
console.log(`[Phase1] Config file not found at: ${configPath} — writing defaults`)
|
|
115
|
-
const defaults = { ...CONFIG_DEFAULTS }
|
|
116
|
-
try {
|
|
117
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
118
|
-
const tmpPath = `${configPath}.tmp`
|
|
119
|
-
fs.writeFileSync(
|
|
120
|
-
tmpPath,
|
|
121
|
-
yaml.dump(defaults, { lineWidth: 120, indent: 2, quotingType: '"' }),
|
|
122
|
-
'utf-8',
|
|
123
|
-
)
|
|
124
|
-
fs.renameSync(tmpPath, configPath)
|
|
125
|
-
console.log(`[Phase1] Default config.yaml written to: ${configPath}`)
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.warn(`[Phase1] Could not write default config.yaml:`, err)
|
|
128
|
-
}
|
|
129
|
-
raw = defaults
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Apply env var overrides for bootstrap keys
|
|
133
|
-
for (const [envKey, configPath_] of Object.entries(ENV_VAR_MAP)) {
|
|
134
|
-
const envValue = process.env[envKey]
|
|
135
|
-
if (envValue === undefined || envValue === '') continue
|
|
136
|
-
const coerced: unknown =
|
|
137
|
-
configPath_ === 'server.port'
|
|
138
|
-
? Number(envValue)
|
|
139
|
-
: configPath_ === 'tls.enabled'
|
|
140
|
-
? envValue === 'true'
|
|
141
|
-
: envValue
|
|
142
|
-
raw = setNested(raw, configPath_, coerced)
|
|
143
|
-
console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return bootstrapSchema.parse(raw)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// autoGenerateJwtSecret
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* If `jwtSecret` is null in the parsed config, generate a random secret,
|
|
155
|
-
* persist it to the YAML file, and return an updated config.
|
|
156
|
-
*/
|
|
157
|
-
export function autoGenerateJwtSecret(
|
|
158
|
-
configPath: string,
|
|
159
|
-
bootstrapConfig: BootstrapConfig,
|
|
160
|
-
): BootstrapConfig {
|
|
161
|
-
if (bootstrapConfig.auth.jwtSecret !== null) {
|
|
162
|
-
return bootstrapConfig
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const secret = randomBytes(32).toString('hex')
|
|
166
|
-
console.log('[Phase1] jwtSecret is null — auto-generating and writing to config.yaml')
|
|
167
|
-
|
|
168
|
-
let raw: Record<string, unknown> = {}
|
|
169
|
-
if (fs.existsSync(configPath)) {
|
|
170
|
-
raw = asJsonObject(yaml.load(fs.readFileSync(configPath, 'utf-8'))) ?? {}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const authSection = asJsonObject(raw.auth) ?? {}
|
|
174
|
-
raw.auth = { ...authSection, jwtSecret: secret }
|
|
175
|
-
|
|
176
|
-
const tmpPath = `${configPath}.tmp`
|
|
177
|
-
try {
|
|
178
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
179
|
-
fs.writeFileSync(
|
|
180
|
-
tmpPath,
|
|
181
|
-
yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
|
|
182
|
-
'utf-8',
|
|
183
|
-
)
|
|
184
|
-
fs.renameSync(tmpPath, configPath)
|
|
185
|
-
} catch (err) {
|
|
186
|
-
console.warn('[Phase1] Could not write auto-generated jwtSecret to config.yaml:', err)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
...bootstrapConfig,
|
|
191
|
-
auth: { ...bootstrapConfig.auth, jwtSecret: secret },
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
// setupInfra
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Phase 2: Create StorageLocationManager, ensure directories, configure TLS.
|
|
201
|
-
* Returns the full InfraContext needed by subsequent boot phases.
|
|
202
|
-
*/
|
|
203
|
-
export async function setupInfra(
|
|
204
|
-
configPath: string,
|
|
205
|
-
bootstrapConfig: BootstrapConfig,
|
|
206
|
-
): Promise<InfraContext> {
|
|
207
|
-
// Auto-generate jwtSecret if not set
|
|
208
|
-
const config = autoGenerateJwtSecret(configPath, bootstrapConfig)
|
|
209
|
-
|
|
210
|
-
const dataPath = path.resolve(config.server.dataPath)
|
|
211
|
-
const port = config.server.port
|
|
212
|
-
const host = config.server.host
|
|
213
|
-
|
|
214
|
-
console.log(`[Phase1] Bootstrap: port=${port}, host=${host}, dataPath=${dataPath}`)
|
|
215
|
-
|
|
216
|
-
// --- Phase 2: Init StorageLocationManager → ensure all dirs exist ---
|
|
217
|
-
console.log('[Phase2] Initializing storage locations…')
|
|
218
|
-
const locationManager = new StorageLocationManager(dataPath)
|
|
219
|
-
await locationManager.initializeDefaults()
|
|
220
|
-
|
|
221
|
-
const locationStatus = locationManager.getStatus()
|
|
222
|
-
for (const { name, available, path: locPath } of locationStatus) {
|
|
223
|
-
console.log(`[Phase2] Location "${name}": ${available ? 'OK' : 'UNAVAILABLE'} → ${locPath}`)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// --- Phase 2c: TLS certificate setup ---
|
|
227
|
-
let tlsOptions: { key: Buffer; cert: Buffer } | undefined
|
|
228
|
-
|
|
229
|
-
if (config.tls.enabled) {
|
|
230
|
-
// Use require() instead of import() — the ESM build of @camstack/core has
|
|
231
|
-
// broken chunks with require("fs") when leaked .js files exist in core/src/.
|
|
232
|
-
// CJS build works correctly and tsx supports require().
|
|
233
|
-
const core = require('@camstack/core') as typeof import('@camstack/core')
|
|
234
|
-
const { ensureTlsCert, loadTlsCert } = core
|
|
235
|
-
if (config.tls.certPath && config.tls.keyPath) {
|
|
236
|
-
// User-provided cert
|
|
237
|
-
console.log(`[Phase2c] Loading custom TLS cert from ${config.tls.certPath}`)
|
|
238
|
-
const pair = loadTlsCert(config.tls.certPath, config.tls.keyPath)
|
|
239
|
-
tlsOptions = { key: pair.key, cert: pair.cert }
|
|
240
|
-
} else {
|
|
241
|
-
// Auto-generate self-signed
|
|
242
|
-
const tlsResult = await ensureTlsCert(dataPath)
|
|
243
|
-
if (tlsResult.generated) {
|
|
244
|
-
console.log(`[Phase2c] Generated self-signed TLS cert at ${tlsResult.certPath}`)
|
|
245
|
-
} else {
|
|
246
|
-
console.log(`[Phase2c] Using existing TLS cert at ${tlsResult.certPath}`)
|
|
247
|
-
}
|
|
248
|
-
const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath)
|
|
249
|
-
tlsOptions = { key: pair.key, cert: pair.cert }
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
bootstrapConfig: config,
|
|
255
|
-
dataPath,
|
|
256
|
-
locationManager,
|
|
257
|
-
tlsOptions,
|
|
258
|
-
}
|
|
259
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-time integration-id backfill for devices created before the
|
|
3
|
-
* device-manager forwarder started stamping `integrationId` (camera
|
|
4
|
-
* providers). Maps each addon that hosts EXACTLY ONE integration to that
|
|
5
|
-
* integration id, then stamps top-level untagged devices of those addons.
|
|
6
|
-
* Multi-instance addons (e.g. Home Assistant with several brokers) are
|
|
7
|
-
* ambiguous on `addonId` alone and are skipped — they stamp going forward.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface BackfillIntegration {
|
|
11
|
-
readonly id: string
|
|
12
|
-
readonly addonId: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface BackfillDevice {
|
|
16
|
-
readonly id: number
|
|
17
|
-
readonly addonId: string
|
|
18
|
-
readonly parentDeviceId: number | null
|
|
19
|
-
readonly integrationId?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface BackfillStamp {
|
|
23
|
-
readonly deviceId: number
|
|
24
|
-
readonly integrationId: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function planIntegrationIdBackfill(
|
|
28
|
-
integrations: readonly BackfillIntegration[],
|
|
29
|
-
devices: readonly BackfillDevice[],
|
|
30
|
-
): readonly BackfillStamp[] {
|
|
31
|
-
const singleByAddon = new Map<string, string>()
|
|
32
|
-
const ambiguous = new Set<string>()
|
|
33
|
-
for (const integration of integrations) {
|
|
34
|
-
if (ambiguous.has(integration.addonId)) continue
|
|
35
|
-
if (singleByAddon.has(integration.addonId)) {
|
|
36
|
-
singleByAddon.delete(integration.addonId)
|
|
37
|
-
ambiguous.add(integration.addonId)
|
|
38
|
-
continue
|
|
39
|
-
}
|
|
40
|
-
singleByAddon.set(integration.addonId, integration.id)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const stamps: BackfillStamp[] = []
|
|
44
|
-
for (const device of devices) {
|
|
45
|
-
if (device.parentDeviceId !== null) continue
|
|
46
|
-
if (device.integrationId !== undefined && device.integrationId !== '') continue
|
|
47
|
-
const integrationId = singleByAddon.get(device.addonId)
|
|
48
|
-
if (integrationId === undefined) continue
|
|
49
|
-
stamps.push({ deviceId: device.id, integrationId })
|
|
50
|
-
}
|
|
51
|
-
return stamps
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Stamps to apply at integration-DELETE time so a cascade removes legacy
|
|
56
|
-
* un-tagged devices too. The boot backfill only runs on startup; a device
|
|
57
|
-
* created before stamping (or whose provider never stamps, e.g. `provider-rtsp`)
|
|
58
|
-
* keeps no `integrationId`, so once its integration is deleted it would orphan
|
|
59
|
-
* forever — `removeByIntegration` matches on `integrationId` and finds nothing.
|
|
60
|
-
*
|
|
61
|
-
* Run this in the delete handler BEFORE deleting the integration record (while
|
|
62
|
-
* it is still present in `integrations`), then stamp the returned devices and
|
|
63
|
-
* let `removeByIntegration` cascade them. Reuses the boot backfill's safety
|
|
64
|
-
* rule (only addons hosting exactly ONE integration are unambiguous) and
|
|
65
|
-
* filters to the integration being deleted so siblings are never touched.
|
|
66
|
-
*/
|
|
67
|
-
export function planDeleteTimeStamps(
|
|
68
|
-
integrationId: string,
|
|
69
|
-
integrations: readonly BackfillIntegration[],
|
|
70
|
-
devices: readonly BackfillDevice[],
|
|
71
|
-
): readonly BackfillStamp[] {
|
|
72
|
-
return planIntegrationIdBackfill(integrations, devices).filter(
|
|
73
|
-
(stamp) => stamp.integrationId === integrationId,
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface BackfillLogger {
|
|
78
|
-
readonly info: (message: string, meta?: Record<string, unknown>) => void
|
|
79
|
-
readonly warn: (message: string, meta?: Record<string, unknown>) => void
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface IntegrationIdBackfillDeps {
|
|
83
|
-
readonly listIntegrations: () => Promise<readonly BackfillIntegration[]>
|
|
84
|
-
readonly listDevices: () => Promise<readonly BackfillDevice[]>
|
|
85
|
-
readonly setIntegrationId: (deviceId: number, integrationId: string) => Promise<void>
|
|
86
|
-
readonly logger: BackfillLogger
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function runIntegrationIdBackfill(
|
|
90
|
-
deps: IntegrationIdBackfillDeps,
|
|
91
|
-
): Promise<{ stamped: number }> {
|
|
92
|
-
const [integrations, devices] = await Promise.all([deps.listIntegrations(), deps.listDevices()])
|
|
93
|
-
const stamps = planIntegrationIdBackfill(integrations, devices)
|
|
94
|
-
let stamped = 0
|
|
95
|
-
for (const stamp of stamps) {
|
|
96
|
-
try {
|
|
97
|
-
await deps.setIntegrationId(stamp.deviceId, stamp.integrationId)
|
|
98
|
-
stamped++
|
|
99
|
-
} catch (err) {
|
|
100
|
-
deps.logger.warn('integrationId backfill: stamp failed', {
|
|
101
|
-
deviceId: stamp.deviceId,
|
|
102
|
-
integrationId: stamp.integrationId,
|
|
103
|
-
error: err instanceof Error ? err.message : String(err),
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (stamped > 0) deps.logger.info('integrationId backfill complete', { stamped })
|
|
108
|
-
return { stamped }
|
|
109
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { readPendingRestart } from '@camstack/kernel'
|
|
3
|
-
import { EventCategory } from '@camstack/types'
|
|
4
|
-
import type { IScopedLogger, PendingRestartMarkerPayload } from '@camstack/types'
|
|
5
|
-
import { AddonRegistryService } from '../core/addon/addon-registry.service'
|
|
6
|
-
import { EventBusService } from '../core/events/event-bus.service'
|
|
7
|
-
import { LoggingService } from '../core/logging/logging.service'
|
|
8
|
-
|
|
9
|
-
export interface PostBootContext {
|
|
10
|
-
readonly port: number
|
|
11
|
-
readonly host: string
|
|
12
|
-
readonly dataPath: string
|
|
13
|
-
readonly trpcRegistered: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class PostBootService {
|
|
17
|
-
private readonly logger: IScopedLogger
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Holds the marker that fired `system.restart-completed` on this boot
|
|
21
|
-
* for `LAST_RESTART_RETENTION_MS`. Lets clients query the marker
|
|
22
|
-
* after they reconnect — the live event itself is emitted before the
|
|
23
|
-
* WS resubscribe has a chance to land.
|
|
24
|
-
*/
|
|
25
|
-
private static lastRestart: { payload: PendingRestartMarkerPayload; expiresAt: number } | null =
|
|
26
|
-
null
|
|
27
|
-
|
|
28
|
-
/** How long the last-restart marker stays queryable after boot (5 min). */
|
|
29
|
-
static readonly LAST_RESTART_RETENTION_MS = 5 * 60_000
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
_addonRegistry: AddonRegistryService,
|
|
33
|
-
private readonly eventBus: EventBusService,
|
|
34
|
-
loggingService: LoggingService,
|
|
35
|
-
) {
|
|
36
|
-
this.logger = loggingService.createLogger('PostBoot')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Snapshot of the most-recent restart marker, or `null` after the
|
|
41
|
-
* retention window. The hub's `addons.getLastRestart` cap method
|
|
42
|
-
* delegates here.
|
|
43
|
-
*/
|
|
44
|
-
static getLastRestart(): PendingRestartMarkerPayload | null {
|
|
45
|
-
const entry = PostBootService.lastRestart
|
|
46
|
-
if (entry === null) return null
|
|
47
|
-
if (entry.expiresAt <= Date.now()) {
|
|
48
|
-
PostBootService.lastRestart = null
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
51
|
-
return entry.payload
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async run(context: PostBootContext): Promise<void> {
|
|
55
|
-
const { port, host, dataPath, trpcRegistered } = context
|
|
56
|
-
|
|
57
|
-
// Device stream wiring moved into `addon-stream-broker` — the addon
|
|
58
|
-
// does its own initial sync against `ctx.deviceRegistry` plus listens
|
|
59
|
-
// to `DeviceRegistered` events for future registrations. No kernel
|
|
60
|
-
// orchestration shim needed here anymore.
|
|
61
|
-
|
|
62
|
-
// Emit system.boot event
|
|
63
|
-
this.eventBus.emit({
|
|
64
|
-
id: randomUUID(),
|
|
65
|
-
timestamp: new Date(),
|
|
66
|
-
source: { type: 'core', id: 'system' },
|
|
67
|
-
category: EventCategory.SystemBoot,
|
|
68
|
-
data: { port, host, trpcRegistered, dataPath },
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// If the previous shutdown was driven by RestartCoordinator, emit a
|
|
72
|
-
// `system.restart-completed` event so the admin UI can surface a
|
|
73
|
-
// success toast describing what changed (framework update, manual
|
|
74
|
-
// restart, …). `readPendingRestart` clears the marker atomically so
|
|
75
|
-
// we never re-fire on a crash-loop boot.
|
|
76
|
-
this.emitRestartCompletedIfPending(dataPath)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private emitRestartCompletedIfPending(dataDir: string): void {
|
|
80
|
-
const marker = readPendingRestart(dataDir)
|
|
81
|
-
if (marker === null) return
|
|
82
|
-
|
|
83
|
-
const payload: PendingRestartMarkerPayload = {
|
|
84
|
-
kind: marker.kind,
|
|
85
|
-
requestedAt: marker.requestedAt,
|
|
86
|
-
...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
|
|
87
|
-
...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
|
|
88
|
-
...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
|
|
89
|
-
...(marker.requestedBy !== undefined ? { requestedBy: marker.requestedBy } : {}),
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
this.logger.info('Restart completed', { meta: payload })
|
|
93
|
-
PostBootService.lastRestart = {
|
|
94
|
-
payload,
|
|
95
|
-
expiresAt: Date.now() + PostBootService.LAST_RESTART_RETENTION_MS,
|
|
96
|
-
}
|
|
97
|
-
this.eventBus.emit({
|
|
98
|
-
id: randomUUID(),
|
|
99
|
-
timestamp: new Date(),
|
|
100
|
-
source: { type: 'core', id: 'system' },
|
|
101
|
-
category: EventCategory.SystemRestartCompleted,
|
|
102
|
-
data: payload,
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// server/backend/src/core/addon/__tests__/addon-registry-capability.test.ts
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
-
import { CapabilityRegistry } from '@camstack/kernel'
|
|
4
|
-
import type { IScopedLogger } from '@camstack/types'
|
|
5
|
-
|
|
6
|
-
function createMockLogger(): IScopedLogger {
|
|
7
|
-
return {
|
|
8
|
-
error: vi.fn(),
|
|
9
|
-
warn: vi.fn(),
|
|
10
|
-
info: vi.fn(),
|
|
11
|
-
debug: vi.fn(),
|
|
12
|
-
child: vi.fn().mockReturnThis(),
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe('AddonRegistryService -- CapabilityRegistry integration', () => {
|
|
17
|
-
let registry: CapabilityRegistry
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
registry = new CapabilityRegistry(createMockLogger())
|
|
21
|
-
registry.ready()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('singleton provider is available after registerProvider', () => {
|
|
25
|
-
registry.declareCapability({ name: 'storage', scope: 'system', mode: 'singleton', methods: {} })
|
|
26
|
-
|
|
27
|
-
const mockStorageProvider = { getLocation: vi.fn() }
|
|
28
|
-
registry.registerProvider('storage', 'sqlite-storage', mockStorageProvider)
|
|
29
|
-
|
|
30
|
-
expect(registry.getSingleton('storage')).toBe(mockStorageProvider)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('streaming-engine singleton wired after provider registers', () => {
|
|
34
|
-
registry.declareCapability({
|
|
35
|
-
name: 'streaming-engine',
|
|
36
|
-
scope: 'system',
|
|
37
|
-
mode: 'singleton',
|
|
38
|
-
methods: {},
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const mockEngine = { initialize: vi.fn() }
|
|
42
|
-
registry.registerProvider('streaming-engine', 'go2rtc', mockEngine)
|
|
43
|
-
|
|
44
|
-
expect(registry.getSingleton('streaming-engine')).toBe(mockEngine)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('log-destination collection receives all providers', () => {
|
|
48
|
-
registry.declareCapability({
|
|
49
|
-
name: 'log-destination',
|
|
50
|
-
scope: 'system',
|
|
51
|
-
mode: 'collection',
|
|
52
|
-
methods: {},
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const dest1 = { id: 'winston' }
|
|
56
|
-
const dest2 = { id: 'loki' }
|
|
57
|
-
registry.registerProvider('log-destination', 'winston-logging', dest1)
|
|
58
|
-
registry.registerProvider('log-destination', 'loki-logging', dest2)
|
|
59
|
-
|
|
60
|
-
expect(registry.getCollection('log-destination')).toEqual([dest1, dest2])
|
|
61
|
-
})
|
|
62
|
-
})
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { overlayDeclaration } from '../addon-row-manifest.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Regression: after the HA broker rework, redeploying provider-homeassistant
|
|
7
|
-
* (broker + device-adoption) left the integration picker without Home
|
|
8
|
-
* Assistant. `loadNewAddons` refreshed `entry.declaration` to the new caps but
|
|
9
|
-
* `listAddons` built its row manifest from the STALE `entry.addon.manifest`
|
|
10
|
-
* (still [broker, ha-discovery]), so `getAvailableTypes` filtered HA out.
|
|
11
|
-
*/
|
|
12
|
-
interface TestManifest {
|
|
13
|
-
id: string
|
|
14
|
-
name: string
|
|
15
|
-
icon?: string
|
|
16
|
-
brokerKind?: string
|
|
17
|
-
capabilities?: ReadonlyArray<{ name: string }>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('overlayDeclaration — listAddons row manifest freshness', () => {
|
|
21
|
-
const base = { id: 'provider-homeassistant', name: 'Home Assistant' }
|
|
22
|
-
|
|
23
|
-
it('prefers the fresh declaration capabilities over the stale instance manifest', () => {
|
|
24
|
-
const instanceManifest: TestManifest = {
|
|
25
|
-
...base,
|
|
26
|
-
capabilities: [{ name: 'broker' }, { name: 'ha-discovery' }],
|
|
27
|
-
}
|
|
28
|
-
const declaration: Partial<TestManifest> = {
|
|
29
|
-
capabilities: [{ name: 'broker' }, { name: 'device-adoption' }],
|
|
30
|
-
brokerKind: 'home-assistant',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const merged = overlayDeclaration(instanceManifest, declaration)
|
|
34
|
-
|
|
35
|
-
expect(merged.capabilities).toEqual([{ name: 'broker' }, { name: 'device-adoption' }])
|
|
36
|
-
expect(merged.brokerKind).toBe('home-assistant')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('keeps the instance manifest when no fresh declaration exists', () => {
|
|
40
|
-
const instanceManifest: TestManifest = { ...base, capabilities: [{ name: 'broker' }] }
|
|
41
|
-
|
|
42
|
-
const merged = overlayDeclaration(instanceManifest, undefined)
|
|
43
|
-
|
|
44
|
-
expect(merged.capabilities).toEqual([{ name: 'broker' }])
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('fills gaps from the instance manifest for keys the declaration omits', () => {
|
|
48
|
-
const instanceManifest: TestManifest = {
|
|
49
|
-
...base,
|
|
50
|
-
icon: 'assets/icon.svg',
|
|
51
|
-
capabilities: [{ name: 'broker' }],
|
|
52
|
-
}
|
|
53
|
-
const declaration: Partial<TestManifest> = {
|
|
54
|
-
capabilities: [{ name: 'broker' }, { name: 'device-adoption' }],
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const merged = overlayDeclaration(instanceManifest, declaration)
|
|
58
|
-
|
|
59
|
-
expect(merged.icon).toBe('assets/icon.svg')
|
|
60
|
-
expect(merged.capabilities).toContainEqual({ name: 'device-adoption' })
|
|
61
|
-
})
|
|
62
|
-
})
|