@camstack/server 0.1.8 → 0.2.0
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/package.json +9 -7
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +346 -202
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +54 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
- package/src/api/trpc/cap-mount-helpers.ts +64 -55
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +593 -351
- package/src/api/trpc/generated-cap-routers.ts +3680 -579
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +79 -46
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1163 -1305
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +12 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +120 -32
- package/src/core/network/network-quality.service.spec.ts +6 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +602 -531
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
package/src/boot/boot-config.ts
CHANGED
|
@@ -4,26 +4,26 @@
|
|
|
4
4
|
* Extracted from main.ts — pure extraction, no behavior change.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as fs from
|
|
8
|
-
import * as path from
|
|
9
|
-
import * as yaml from
|
|
10
|
-
import { randomBytes } from
|
|
11
|
-
import { asJsonObject } from
|
|
12
|
-
import { bootstrapSchema } from
|
|
13
|
-
import type { BootstrapConfig } from
|
|
14
|
-
import { StorageLocationManager } from
|
|
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
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Types
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
|
-
export type { BootstrapConfig }
|
|
20
|
+
export type { BootstrapConfig }
|
|
21
21
|
|
|
22
22
|
export interface InfraContext {
|
|
23
|
-
readonly bootstrapConfig: BootstrapConfig
|
|
24
|
-
readonly dataPath: string
|
|
25
|
-
readonly locationManager: StorageLocationManager
|
|
26
|
-
readonly tlsOptions: { key: Buffer; cert: Buffer } | undefined
|
|
23
|
+
readonly bootstrapConfig: BootstrapConfig
|
|
24
|
+
readonly dataPath: string
|
|
25
|
+
readonly locationManager: StorageLocationManager
|
|
26
|
+
readonly tlsOptions: { key: Buffer; cert: Buffer } | undefined
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -31,28 +31,28 @@ export interface InfraContext {
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
const CONFIG_DEFAULTS: Record<string, unknown> = {
|
|
34
|
-
server: { port: 4443, host:
|
|
34
|
+
server: { port: 4443, host: '0.0.0.0', dataPath: 'camstack-data' },
|
|
35
35
|
auth: {
|
|
36
36
|
jwtSecret: null,
|
|
37
|
-
adminUsername:
|
|
38
|
-
adminPassword:
|
|
37
|
+
adminUsername: 'admin',
|
|
38
|
+
adminPassword: 'changeme',
|
|
39
39
|
},
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
41
|
|
|
42
42
|
const ENV_VAR_MAP: Record<string, string> = {
|
|
43
|
-
CAMSTACK_PORT:
|
|
44
|
-
CAMSTACK_HOST:
|
|
45
|
-
CAMSTACK_DATA:
|
|
46
|
-
CAMSTACK_JWT_SECRET:
|
|
47
|
-
CAMSTACK_ADMIN_USER:
|
|
48
|
-
CAMSTACK_ADMIN_PASS:
|
|
49
|
-
CAMSTACK_HUB_URL:
|
|
50
|
-
CAMSTACK_HUB_TOKEN:
|
|
51
|
-
CAMSTACK_AGENT_NAME:
|
|
52
|
-
CAMSTACK_TLS_ENABLED:
|
|
53
|
-
CAMSTACK_TLS_CERT:
|
|
54
|
-
CAMSTACK_TLS_KEY:
|
|
55
|
-
}
|
|
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
56
|
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
// Helpers
|
|
@@ -63,11 +63,11 @@ function setNested(
|
|
|
63
63
|
p: string,
|
|
64
64
|
value: unknown,
|
|
65
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(
|
|
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
71
|
}
|
|
72
72
|
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
@@ -82,72 +82,68 @@ export function loadBootstrapConfig(configPath: string): BootstrapConfig {
|
|
|
82
82
|
// Only bootstrap sections live in config.yaml.
|
|
83
83
|
// All runtime settings are stored in the SQL system_settings table.
|
|
84
84
|
|
|
85
|
-
let raw: Record<string, unknown
|
|
85
|
+
let raw: Record<string, unknown>
|
|
86
86
|
|
|
87
87
|
if (fs.existsSync(configPath)) {
|
|
88
|
-
const content = fs.readFileSync(configPath,
|
|
89
|
-
raw = asJsonObject(yaml.load(content)) ?? {}
|
|
88
|
+
const content = fs.readFileSync(configPath, 'utf-8')
|
|
89
|
+
raw = asJsonObject(yaml.load(content)) ?? {}
|
|
90
90
|
// Merge in any missing bootstrap sections (server, auth only)
|
|
91
|
-
let updated = false
|
|
91
|
+
let updated = false
|
|
92
92
|
for (const [key, defaults] of Object.entries(CONFIG_DEFAULTS)) {
|
|
93
93
|
if (!(key in raw)) {
|
|
94
|
-
raw[key] = defaults
|
|
95
|
-
updated = true
|
|
94
|
+
raw[key] = defaults
|
|
95
|
+
updated = true
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
if (updated) {
|
|
99
99
|
try {
|
|
100
|
-
const tmpPath = `${configPath}.tmp
|
|
100
|
+
const tmpPath = `${configPath}.tmp`
|
|
101
101
|
fs.writeFileSync(
|
|
102
102
|
tmpPath,
|
|
103
103
|
yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
fs.renameSync(tmpPath, configPath)
|
|
107
|
-
console.log(
|
|
108
|
-
`[Phase1] Updated config.yaml with missing bootstrap defaults`,
|
|
109
|
-
);
|
|
104
|
+
'utf-8',
|
|
105
|
+
)
|
|
106
|
+
fs.renameSync(tmpPath, configPath)
|
|
107
|
+
console.log(`[Phase1] Updated config.yaml with missing bootstrap defaults`)
|
|
110
108
|
} catch (err) {
|
|
111
|
-
console.warn(`[Phase1] Could not update config.yaml:`, err)
|
|
109
|
+
console.warn(`[Phase1] Could not update config.yaml:`, err)
|
|
112
110
|
}
|
|
113
111
|
}
|
|
114
|
-
console.log(`[Phase1] Loaded bootstrap config from: ${configPath}`)
|
|
112
|
+
console.log(`[Phase1] Loaded bootstrap config from: ${configPath}`)
|
|
115
113
|
} else {
|
|
116
|
-
console.log(
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
const defaults = { ...CONFIG_DEFAULTS };
|
|
114
|
+
console.log(`[Phase1] Config file not found at: ${configPath} — writing defaults`)
|
|
115
|
+
const defaults = { ...CONFIG_DEFAULTS }
|
|
120
116
|
try {
|
|
121
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
122
|
-
const tmpPath = `${configPath}.tmp
|
|
117
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
118
|
+
const tmpPath = `${configPath}.tmp`
|
|
123
119
|
fs.writeFileSync(
|
|
124
120
|
tmpPath,
|
|
125
121
|
yaml.dump(defaults, { lineWidth: 120, indent: 2, quotingType: '"' }),
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
fs.renameSync(tmpPath, configPath)
|
|
129
|
-
console.log(`[Phase1] Default config.yaml written to: ${configPath}`)
|
|
122
|
+
'utf-8',
|
|
123
|
+
)
|
|
124
|
+
fs.renameSync(tmpPath, configPath)
|
|
125
|
+
console.log(`[Phase1] Default config.yaml written to: ${configPath}`)
|
|
130
126
|
} catch (err) {
|
|
131
|
-
console.warn(`[Phase1] Could not write default config.yaml:`, err)
|
|
127
|
+
console.warn(`[Phase1] Could not write default config.yaml:`, err)
|
|
132
128
|
}
|
|
133
|
-
raw = defaults
|
|
129
|
+
raw = defaults
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
// Apply env var overrides for bootstrap keys
|
|
137
133
|
for (const [envKey, configPath_] of Object.entries(ENV_VAR_MAP)) {
|
|
138
|
-
const envValue = process.env[envKey]
|
|
139
|
-
if (envValue === undefined || envValue ===
|
|
134
|
+
const envValue = process.env[envKey]
|
|
135
|
+
if (envValue === undefined || envValue === '') continue
|
|
140
136
|
const coerced: unknown =
|
|
141
|
-
configPath_ ===
|
|
137
|
+
configPath_ === 'server.port'
|
|
142
138
|
? Number(envValue)
|
|
143
|
-
: configPath_ ===
|
|
144
|
-
? envValue ===
|
|
145
|
-
: envValue
|
|
146
|
-
raw = setNested(raw, configPath_, coerced)
|
|
147
|
-
console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`)
|
|
139
|
+
: configPath_ === 'tls.enabled'
|
|
140
|
+
? envValue === 'true'
|
|
141
|
+
: envValue
|
|
142
|
+
raw = setNested(raw, configPath_, coerced)
|
|
143
|
+
console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`)
|
|
148
144
|
}
|
|
149
145
|
|
|
150
|
-
return bootstrapSchema.parse(raw)
|
|
146
|
+
return bootstrapSchema.parse(raw)
|
|
151
147
|
}
|
|
152
148
|
|
|
153
149
|
// ---------------------------------------------------------------------------
|
|
@@ -163,42 +159,37 @@ export function autoGenerateJwtSecret(
|
|
|
163
159
|
bootstrapConfig: BootstrapConfig,
|
|
164
160
|
): BootstrapConfig {
|
|
165
161
|
if (bootstrapConfig.auth.jwtSecret !== null) {
|
|
166
|
-
return bootstrapConfig
|
|
162
|
+
return bootstrapConfig
|
|
167
163
|
}
|
|
168
164
|
|
|
169
|
-
const secret = randomBytes(32).toString(
|
|
170
|
-
console.log(
|
|
171
|
-
"[Phase1] jwtSecret is null — auto-generating and writing to config.yaml",
|
|
172
|
-
);
|
|
165
|
+
const secret = randomBytes(32).toString('hex')
|
|
166
|
+
console.log('[Phase1] jwtSecret is null — auto-generating and writing to config.yaml')
|
|
173
167
|
|
|
174
|
-
let raw: Record<string, unknown> = {}
|
|
168
|
+
let raw: Record<string, unknown> = {}
|
|
175
169
|
if (fs.existsSync(configPath)) {
|
|
176
|
-
raw = asJsonObject(yaml.load(fs.readFileSync(configPath,
|
|
170
|
+
raw = asJsonObject(yaml.load(fs.readFileSync(configPath, 'utf-8'))) ?? {}
|
|
177
171
|
}
|
|
178
172
|
|
|
179
|
-
const authSection = asJsonObject(raw.auth) ?? {}
|
|
180
|
-
raw.auth = { ...authSection, jwtSecret: secret }
|
|
173
|
+
const authSection = asJsonObject(raw.auth) ?? {}
|
|
174
|
+
raw.auth = { ...authSection, jwtSecret: secret }
|
|
181
175
|
|
|
182
|
-
const tmpPath = `${configPath}.tmp
|
|
176
|
+
const tmpPath = `${configPath}.tmp`
|
|
183
177
|
try {
|
|
184
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
178
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
185
179
|
fs.writeFileSync(
|
|
186
180
|
tmpPath,
|
|
187
181
|
yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
|
|
188
|
-
|
|
189
|
-
)
|
|
190
|
-
fs.renameSync(tmpPath, configPath)
|
|
182
|
+
'utf-8',
|
|
183
|
+
)
|
|
184
|
+
fs.renameSync(tmpPath, configPath)
|
|
191
185
|
} catch (err) {
|
|
192
|
-
console.warn(
|
|
193
|
-
"[Phase1] Could not write auto-generated jwtSecret to config.yaml:",
|
|
194
|
-
err,
|
|
195
|
-
);
|
|
186
|
+
console.warn('[Phase1] Could not write auto-generated jwtSecret to config.yaml:', err)
|
|
196
187
|
}
|
|
197
188
|
|
|
198
189
|
return {
|
|
199
190
|
...bootstrapConfig,
|
|
200
191
|
auth: { ...bootstrapConfig.auth, jwtSecret: secret },
|
|
201
|
-
}
|
|
192
|
+
}
|
|
202
193
|
}
|
|
203
194
|
|
|
204
195
|
// ---------------------------------------------------------------------------
|
|
@@ -214,58 +205,48 @@ export async function setupInfra(
|
|
|
214
205
|
bootstrapConfig: BootstrapConfig,
|
|
215
206
|
): Promise<InfraContext> {
|
|
216
207
|
// Auto-generate jwtSecret if not set
|
|
217
|
-
const config = autoGenerateJwtSecret(configPath, bootstrapConfig)
|
|
208
|
+
const config = autoGenerateJwtSecret(configPath, bootstrapConfig)
|
|
218
209
|
|
|
219
|
-
const dataPath = path.resolve(config.server.dataPath)
|
|
220
|
-
const port = config.server.port
|
|
221
|
-
const host = config.server.host
|
|
210
|
+
const dataPath = path.resolve(config.server.dataPath)
|
|
211
|
+
const port = config.server.port
|
|
212
|
+
const host = config.server.host
|
|
222
213
|
|
|
223
|
-
console.log(
|
|
224
|
-
`[Phase1] Bootstrap: port=${port}, host=${host}, dataPath=${dataPath}`,
|
|
225
|
-
);
|
|
214
|
+
console.log(`[Phase1] Bootstrap: port=${port}, host=${host}, dataPath=${dataPath}`)
|
|
226
215
|
|
|
227
216
|
// --- Phase 2: Init StorageLocationManager → ensure all dirs exist ---
|
|
228
|
-
console.log(
|
|
229
|
-
const locationManager = new StorageLocationManager(dataPath)
|
|
230
|
-
await locationManager.initializeDefaults()
|
|
217
|
+
console.log('[Phase2] Initializing storage locations…')
|
|
218
|
+
const locationManager = new StorageLocationManager(dataPath)
|
|
219
|
+
await locationManager.initializeDefaults()
|
|
231
220
|
|
|
232
|
-
const locationStatus = locationManager.getStatus()
|
|
221
|
+
const locationStatus = locationManager.getStatus()
|
|
233
222
|
for (const { name, available, path: locPath } of locationStatus) {
|
|
234
|
-
console.log(
|
|
235
|
-
`[Phase2] Location "${name}": ${available ? "OK" : "UNAVAILABLE"} → ${locPath}`,
|
|
236
|
-
);
|
|
223
|
+
console.log(`[Phase2] Location "${name}": ${available ? 'OK' : 'UNAVAILABLE'} → ${locPath}`)
|
|
237
224
|
}
|
|
238
225
|
|
|
239
226
|
// --- Phase 2c: TLS certificate setup ---
|
|
240
|
-
let tlsOptions: { key: Buffer; cert: Buffer } | undefined
|
|
227
|
+
let tlsOptions: { key: Buffer; cert: Buffer } | undefined
|
|
241
228
|
|
|
242
229
|
if (config.tls.enabled) {
|
|
243
230
|
// Use require() instead of import() — the ESM build of @camstack/core has
|
|
244
231
|
// broken chunks with require("fs") when leaked .js files exist in core/src/.
|
|
245
232
|
// CJS build works correctly and tsx supports require().
|
|
246
|
-
const core = require(
|
|
247
|
-
const { ensureTlsCert, loadTlsCert } = core
|
|
233
|
+
const core = require('@camstack/core') as typeof import('@camstack/core')
|
|
234
|
+
const { ensureTlsCert, loadTlsCert } = core
|
|
248
235
|
if (config.tls.certPath && config.tls.keyPath) {
|
|
249
236
|
// User-provided cert
|
|
250
|
-
console.log(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const pair = loadTlsCert(config.tls.certPath, config.tls.keyPath);
|
|
254
|
-
tlsOptions = { key: pair.key, cert: pair.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 }
|
|
255
240
|
} else {
|
|
256
241
|
// Auto-generate self-signed
|
|
257
|
-
const tlsResult = await ensureTlsCert(dataPath)
|
|
242
|
+
const tlsResult = await ensureTlsCert(dataPath)
|
|
258
243
|
if (tlsResult.generated) {
|
|
259
|
-
console.log(
|
|
260
|
-
`[Phase2c] Generated self-signed TLS cert at ${tlsResult.certPath}`,
|
|
261
|
-
);
|
|
244
|
+
console.log(`[Phase2c] Generated self-signed TLS cert at ${tlsResult.certPath}`)
|
|
262
245
|
} else {
|
|
263
|
-
console.log(
|
|
264
|
-
`[Phase2c] Using existing TLS cert at ${tlsResult.certPath}`,
|
|
265
|
-
);
|
|
246
|
+
console.log(`[Phase2c] Using existing TLS cert at ${tlsResult.certPath}`)
|
|
266
247
|
}
|
|
267
|
-
const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath)
|
|
268
|
-
tlsOptions = { key: pair.key, cert: pair.cert }
|
|
248
|
+
const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath)
|
|
249
|
+
tlsOptions = { key: pair.key, cert: pair.cert }
|
|
269
250
|
}
|
|
270
251
|
}
|
|
271
252
|
|
|
@@ -274,5 +255,5 @@ export async function setupInfra(
|
|
|
274
255
|
dataPath,
|
|
275
256
|
locationManager,
|
|
276
257
|
tlsOptions,
|
|
277
|
-
}
|
|
258
|
+
}
|
|
278
259
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { readPendingRestart } from '@camstack/kernel'
|
|
3
|
+
import { EventCategory } from '@camstack/types'
|
|
3
4
|
import type { IScopedLogger, PendingRestartMarkerPayload } from '@camstack/types'
|
|
4
5
|
import { AddonRegistryService } from '../core/addon/addon-registry.service'
|
|
5
6
|
import { EventBusService } from '../core/events/event-bus.service'
|
|
@@ -21,7 +22,8 @@ export class PostBootService {
|
|
|
21
22
|
* after they reconnect — the live event itself is emitted before the
|
|
22
23
|
* WS resubscribe has a chance to land.
|
|
23
24
|
*/
|
|
24
|
-
private static lastRestart: { payload: PendingRestartMarkerPayload; expiresAt: number } | null =
|
|
25
|
+
private static lastRestart: { payload: PendingRestartMarkerPayload; expiresAt: number } | null =
|
|
26
|
+
null
|
|
25
27
|
|
|
26
28
|
/** How long the last-restart marker stays queryable after boot (5 min). */
|
|
27
29
|
static readonly LAST_RESTART_RETENTION_MS = 5 * 60_000
|
|
@@ -62,7 +64,7 @@ export class PostBootService {
|
|
|
62
64
|
id: randomUUID(),
|
|
63
65
|
timestamp: new Date(),
|
|
64
66
|
source: { type: 'core', id: 'system' },
|
|
65
|
-
category:
|
|
67
|
+
category: EventCategory.SystemBoot,
|
|
66
68
|
data: { port, host, trpcRegistered, dataPath },
|
|
67
69
|
})
|
|
68
70
|
|
|
@@ -96,7 +98,7 @@ export class PostBootService {
|
|
|
96
98
|
id: randomUUID(),
|
|
97
99
|
timestamp: new Date(),
|
|
98
100
|
source: { type: 'core', id: 'system' },
|
|
99
|
-
category:
|
|
101
|
+
category: EventCategory.SystemRestartCompleted,
|
|
100
102
|
data: payload,
|
|
101
103
|
})
|
|
102
104
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
// server/backend/src/core/addon/__tests__/addon-registry-capability.test.ts
|
|
3
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
4
3
|
import { CapabilityRegistry } from '@camstack/kernel'
|
|
@@ -32,7 +31,12 @@ describe('AddonRegistryService -- CapabilityRegistry integration', () => {
|
|
|
32
31
|
})
|
|
33
32
|
|
|
34
33
|
it('streaming-engine singleton wired after provider registers', () => {
|
|
35
|
-
registry.declareCapability({
|
|
34
|
+
registry.declareCapability({
|
|
35
|
+
name: 'streaming-engine',
|
|
36
|
+
scope: 'system',
|
|
37
|
+
mode: 'singleton',
|
|
38
|
+
methods: {},
|
|
39
|
+
})
|
|
36
40
|
|
|
37
41
|
const mockEngine = { initialize: vi.fn() }
|
|
38
42
|
registry.registerProvider('streaming-engine', 'go2rtc', mockEngine)
|
|
@@ -41,7 +45,12 @@ describe('AddonRegistryService -- CapabilityRegistry integration', () => {
|
|
|
41
45
|
})
|
|
42
46
|
|
|
43
47
|
it('log-destination collection receives all providers', () => {
|
|
44
|
-
registry.declareCapability({
|
|
48
|
+
registry.declareCapability({
|
|
49
|
+
name: 'log-destination',
|
|
50
|
+
scope: 'system',
|
|
51
|
+
mode: 'collection',
|
|
52
|
+
methods: {},
|
|
53
|
+
})
|
|
45
54
|
|
|
46
55
|
const dest1 = { id: 'winston' }
|
|
47
56
|
const dest2 = { id: 'loki' }
|
|
@@ -92,7 +92,11 @@ export class AddonCallGateway {
|
|
|
92
92
|
* must invoke the in-process instance directly (the invocation is
|
|
93
93
|
* surface-specific; only the ROUTING is centralised here).
|
|
94
94
|
*/
|
|
95
|
-
async callForked(
|
|
95
|
+
async callForked(
|
|
96
|
+
addonId: string,
|
|
97
|
+
input: AddonCallSurface,
|
|
98
|
+
explicitNodeId?: string,
|
|
99
|
+
): Promise<unknown> {
|
|
96
100
|
const dest = this.classify(addonId, explicitNodeId)
|
|
97
101
|
const fullInput: AddonCallInput = { ...input, addonId }
|
|
98
102
|
switch (dest.kind) {
|
|
@@ -115,7 +119,11 @@ export class AddonCallGateway {
|
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
/** Map an addon-level call to the remote agent's Moleculer action. */
|
|
118
|
-
private async callRemoteAgent(
|
|
122
|
+
private async callRemoteAgent(
|
|
123
|
+
addonId: string,
|
|
124
|
+
baseNodeId: string,
|
|
125
|
+
input: AddonCallInput,
|
|
126
|
+
): Promise<unknown> {
|
|
119
127
|
const workerNodeId = this.resolveWorkerNodeId(addonId, baseNodeId)
|
|
120
128
|
const opts = workerNodeId
|
|
121
129
|
? { nodeID: workerNodeId, timeout: REMOTE_TIMEOUT_MS }
|
|
@@ -132,7 +140,9 @@ export class AddonCallGateway {
|
|
|
132
140
|
}
|
|
133
141
|
// routes/custom are hub-local-child surfaces (mounted / invoked on the
|
|
134
142
|
// owning node); they are not proxied to a remote agent through this gateway.
|
|
135
|
-
throw new Error(
|
|
143
|
+
throw new Error(
|
|
144
|
+
`AddonCallGateway: target "${input.target}" not supported for remote agent "${baseNodeId}"`,
|
|
145
|
+
)
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
/**
|
|
@@ -143,9 +153,13 @@ export class AddonCallGateway {
|
|
|
143
153
|
*/
|
|
144
154
|
private resolveWorkerNodeId(addonId: string, baseNodeId: string): string | null {
|
|
145
155
|
const registry = this.deps.broker.registry
|
|
146
|
-
const services = (
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
const services = (
|
|
157
|
+
registry as unknown as {
|
|
158
|
+
getServiceList: (opts: {
|
|
159
|
+
onlyAvailable: boolean
|
|
160
|
+
}) => readonly { name: string; nodeID: string }[]
|
|
161
|
+
}
|
|
162
|
+
).getServiceList({ onlyAvailable: true })
|
|
149
163
|
const exactNode = `${baseNodeId}/${addonId}`
|
|
150
164
|
const preferred = services.find((s) => s.name === addonId && s.nodeID === exactNode)
|
|
151
165
|
if (preferred) return preferred.nodeID
|