@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.
- 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
|
@@ -24,9 +24,7 @@
|
|
|
24
24
|
import { METHOD_ACCESS_MAP } from '@camstack/types'
|
|
25
25
|
import type { MethodAccess, TokenScope } from '@camstack/types'
|
|
26
26
|
|
|
27
|
-
export type ScopeAccessResult =
|
|
28
|
-
| { ok: true; access: MethodAccess }
|
|
29
|
-
| { ok: false; reason: string }
|
|
27
|
+
export type ScopeAccessResult = { ok: true; access: MethodAccess } | { ok: false; reason: string }
|
|
30
28
|
|
|
31
29
|
/**
|
|
32
30
|
* Resolves a deviceId to its ancestor chain (parent, grandparent, …).
|
|
@@ -101,10 +99,12 @@ export function checkScopeAccess(
|
|
|
101
99
|
reason: `No scope grants ${meta.access} on '${meta.capName}' (${meta.capScope}-scope cap${
|
|
102
100
|
deviceId !== null ? `, device=${deviceId}` : ''
|
|
103
101
|
}). Have: ${
|
|
104
|
-
scopes
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
scopes
|
|
103
|
+
.map((s) => {
|
|
104
|
+
const target = s.type === 'device' ? `[${s.targets.join(',')}]` : s.target
|
|
105
|
+
return `${s.type}:${target}[${s.access.join(',')}]`
|
|
106
|
+
})
|
|
107
|
+
.join(', ') || '(none)'
|
|
108
108
|
}`,
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -117,7 +117,9 @@ async function resolveUser(
|
|
|
117
117
|
// protectedProcedure can still gate by scope match.
|
|
118
118
|
if (token.startsWith('cst_')) {
|
|
119
119
|
try {
|
|
120
|
-
const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management') as
|
|
120
|
+
const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management') as
|
|
121
|
+
| UserManagementLike
|
|
122
|
+
| undefined
|
|
121
123
|
if (!userMgmt) return null
|
|
122
124
|
const record = await userMgmt.validateScopedToken({ token })
|
|
123
125
|
if (!record) return null
|
|
@@ -182,7 +184,9 @@ async function resolveUser(
|
|
|
182
184
|
* Bounded by hop count (defence-in-depth — the device tree should
|
|
183
185
|
* never exceed 2-3 levels but a corrupt registry shouldn't loop forever).
|
|
184
186
|
*/
|
|
185
|
-
function makeAncestorLookup(
|
|
187
|
+
function makeAncestorLookup(
|
|
188
|
+
addonRegistry: AddonRegistryService,
|
|
189
|
+
): (deviceId: number) => readonly number[] {
|
|
186
190
|
return (deviceId: number) => {
|
|
187
191
|
const out: number[] = []
|
|
188
192
|
const registry = addonRegistry.getDeviceRegistry()
|
|
@@ -243,8 +247,7 @@ export async function createWsTrpcContext(
|
|
|
243
247
|
// 1. connectionParams.token (sent by BackendClient's createWSClient)
|
|
244
248
|
const paramToken = opts.info.connectionParams?.['token']
|
|
245
249
|
const token =
|
|
246
|
-
(typeof paramToken === 'string' ? paramToken : null) ??
|
|
247
|
-
extractTokenFromRequest(opts.req)
|
|
250
|
+
(typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req)
|
|
248
251
|
|
|
249
252
|
const user = await resolveUser(token, authService, addonRegistry)
|
|
250
253
|
return {
|
|
@@ -21,7 +21,7 @@ const t = initTRPC.context<TrpcContext>().create({
|
|
|
21
21
|
* @param subscribe — called once; receives a `push` callback and must return an unsubscribe fn.
|
|
22
22
|
*/
|
|
23
23
|
export async function* iterableSubscription<T>(
|
|
24
|
-
subscribe: (push: (value: T) => void) => (
|
|
24
|
+
subscribe: (push: (value: T) => void) => () => void,
|
|
25
25
|
): AsyncGenerator<T> {
|
|
26
26
|
const queue: T[] = []
|
|
27
27
|
let resolve: (() => void) | null = null
|
|
@@ -36,7 +36,9 @@ export async function* iterableSubscription<T>(
|
|
|
36
36
|
while (queue.length > 0) {
|
|
37
37
|
yield queue.shift()!
|
|
38
38
|
}
|
|
39
|
-
await new Promise<void>((r) => {
|
|
39
|
+
await new Promise<void>((r) => {
|
|
40
|
+
resolve = r
|
|
41
|
+
})
|
|
40
42
|
}
|
|
41
43
|
} finally {
|
|
42
44
|
unsub()
|
|
@@ -124,10 +124,9 @@ type HandleOfferInput = Parameters<WebrtcSessionProvider['handleOffer']>[0]
|
|
|
124
124
|
* unchanged. Any client-supplied `userAgent` is OVERWRITTEN — the hub
|
|
125
125
|
* trusts only the request context, never the client.
|
|
126
126
|
*/
|
|
127
|
-
export function enrichInputWithUserAgent<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
): TInput {
|
|
127
|
+
export function enrichInputWithUserAgent<
|
|
128
|
+
TInput extends { consumerAttribution?: BrokerConsumerAttribution },
|
|
129
|
+
>(input: TInput, userAgent: string | null): TInput {
|
|
131
130
|
if (userAgent === null) return input
|
|
132
131
|
const base: BrokerConsumerAttribution = input.consumerAttribution ?? { kind: 'webrtc-browser' }
|
|
133
132
|
return {
|
|
@@ -217,27 +216,26 @@ function buildCapabilityRouters(services: RouterServices) {
|
|
|
217
216
|
// CapabilityRegistry — the provider is built on-demand from
|
|
218
217
|
// backend services. `mountAllCaps` would return `null` for them
|
|
219
218
|
// (registry lookup miss), so we re-mount with `buildXProvider`.
|
|
220
|
-
networkQuality: createCapRouter_networkQuality(
|
|
221
|
-
|
|
219
|
+
networkQuality: createCapRouter_networkQuality((_ctx) =>
|
|
220
|
+
buildNetworkQualityProvider(services.networkQualityService),
|
|
222
221
|
),
|
|
223
|
-
system: createCapRouter_system(
|
|
224
|
-
|
|
222
|
+
system: createCapRouter_system((_ctx) =>
|
|
223
|
+
buildSystemProvider(services.featureService, services.capabilityRegistry),
|
|
225
224
|
),
|
|
226
|
-
toast: createCapRouter_toast(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
integrations: createCapRouter_integrations(
|
|
230
|
-
(_ctx) => buildIntegrationsProvider(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry),
|
|
231
|
-
),
|
|
232
|
-
nodes: createCapRouter_nodes(
|
|
233
|
-
(_ctx) => buildNodesProvider(
|
|
234
|
-
services.agentRegistry,
|
|
235
|
-
services.moleculer,
|
|
225
|
+
toast: createCapRouter_toast((ctx) => buildToastProvider(services.toastService, ctx)),
|
|
226
|
+
integrations: createCapRouter_integrations((_ctx) =>
|
|
227
|
+
buildIntegrationsProvider(
|
|
236
228
|
services.addonRegistry,
|
|
229
|
+
services.eventBus,
|
|
230
|
+
services.loggingService,
|
|
231
|
+
services.capabilityRegistry,
|
|
237
232
|
),
|
|
238
233
|
),
|
|
239
|
-
|
|
240
|
-
(
|
|
234
|
+
nodes: createCapRouter_nodes((_ctx) =>
|
|
235
|
+
buildNodesProvider(services.agentRegistry, services.moleculer, services.addonRegistry),
|
|
236
|
+
),
|
|
237
|
+
addons: createCapRouter_addons((ctx) =>
|
|
238
|
+
buildAddonsProvider(
|
|
241
239
|
services.addonRegistry,
|
|
242
240
|
services.addonPackageService,
|
|
243
241
|
services.loggingService,
|
|
@@ -254,32 +252,68 @@ function buildCapabilityRouters(services: RouterServices) {
|
|
|
254
252
|
// distinct. Casting at the override site is cheaper than reworking
|
|
255
253
|
// the provider declarations. Auto-mount can't infer the cast.
|
|
256
254
|
pipelineExecutor: createCapRouter_pipelineExecutor(
|
|
257
|
-
(_ctx) =>
|
|
258
|
-
|
|
255
|
+
(_ctx) =>
|
|
256
|
+
requireSingleton(services.capabilityRegistry, 'pipeline-executor') as InferProvider<
|
|
257
|
+
typeof pipelineExecutorCapability
|
|
258
|
+
> | null,
|
|
259
|
+
(capName, nodeId) =>
|
|
260
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
261
|
+
typeof pipelineExecutorCapability
|
|
262
|
+
> | null,
|
|
259
263
|
),
|
|
260
264
|
pipelineRunner: createCapRouter_pipelineRunner(
|
|
261
|
-
(_ctx) =>
|
|
262
|
-
|
|
265
|
+
(_ctx) =>
|
|
266
|
+
requireSingleton(services.capabilityRegistry, 'pipeline-runner') as InferProvider<
|
|
267
|
+
typeof pipelineRunnerCapability
|
|
268
|
+
> | null,
|
|
269
|
+
(capName, nodeId) =>
|
|
270
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
271
|
+
typeof pipelineRunnerCapability
|
|
272
|
+
> | null,
|
|
263
273
|
),
|
|
264
274
|
pipelineOrchestrator: createCapRouter_pipelineOrchestrator(
|
|
265
|
-
(_ctx) =>
|
|
266
|
-
|
|
275
|
+
(_ctx) =>
|
|
276
|
+
requireSingleton(services.capabilityRegistry, 'pipeline-orchestrator') as InferProvider<
|
|
277
|
+
typeof pipelineOrchestratorCapability
|
|
278
|
+
> | null,
|
|
279
|
+
(capName, nodeId) =>
|
|
280
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
281
|
+
typeof pipelineOrchestratorCapability
|
|
282
|
+
> | null,
|
|
267
283
|
),
|
|
268
284
|
audioAnalyzer: createCapRouter_audioAnalyzer(
|
|
269
285
|
(_ctx) => requireSingleton(services.capabilityRegistry, 'audio-analyzer'),
|
|
270
|
-
(capName, nodeId) =>
|
|
286
|
+
(capName, nodeId) =>
|
|
287
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
288
|
+
typeof audioAnalyzerCapability
|
|
289
|
+
> | null,
|
|
271
290
|
),
|
|
272
291
|
audioCodec: createCapRouter_audioCodec(
|
|
273
|
-
(_ctx) =>
|
|
274
|
-
|
|
292
|
+
(_ctx) =>
|
|
293
|
+
requireSingleton(services.capabilityRegistry, 'audio-codec') as InferProvider<
|
|
294
|
+
typeof audioCodecCapability
|
|
295
|
+
> | null,
|
|
296
|
+
(capName, nodeId) =>
|
|
297
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
298
|
+
typeof audioCodecCapability
|
|
299
|
+
> | null,
|
|
275
300
|
),
|
|
276
301
|
decoder: createCapRouter_decoder(
|
|
277
|
-
(_ctx) =>
|
|
278
|
-
|
|
302
|
+
(_ctx) =>
|
|
303
|
+
requireSingleton(services.capabilityRegistry, 'decoder') as InferProvider<
|
|
304
|
+
typeof decoderCapability
|
|
305
|
+
> | null,
|
|
306
|
+
(capName, nodeId) =>
|
|
307
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
308
|
+
typeof decoderCapability
|
|
309
|
+
> | null,
|
|
279
310
|
),
|
|
280
311
|
platformProbe: createCapRouter_platformProbe(
|
|
281
312
|
(_ctx) => requireSingleton(services.capabilityRegistry, 'platform-probe'),
|
|
282
|
-
(capName, nodeId) =>
|
|
313
|
+
(capName, nodeId) =>
|
|
314
|
+
services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<
|
|
315
|
+
typeof platformProbeCapability
|
|
316
|
+
> | null,
|
|
283
317
|
),
|
|
284
318
|
|
|
285
319
|
// ── Cap overrides: hub-only, no remote fallback ─────────────────
|
|
@@ -302,18 +336,17 @@ function buildCapabilityRouters(services: RouterServices) {
|
|
|
302
336
|
// `getSnapshot` picks the first one that claims the device. The
|
|
303
337
|
// generic first-provider resolver from the auto-mount can't model
|
|
304
338
|
// this — we hand-write the probe + fan-out logic.
|
|
305
|
-
snapshotProvider: createCapRouter_snapshotProvider(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
),
|
|
339
|
+
snapshotProvider: createCapRouter_snapshotProvider((_ctx) => {
|
|
340
|
+
const reg = services.capabilityRegistry
|
|
341
|
+
if (!reg) return null
|
|
342
|
+
type SnapshotProviderInferred =
|
|
343
|
+
import('@camstack/types').CapabilityProviderMap['snapshot-provider']
|
|
344
|
+
const providers = reg.getCollection<SnapshotProviderInferred>('snapshot-provider')
|
|
345
|
+
if (!providers || providers.length === 0) return null
|
|
346
|
+
const supportsDevice = anySupports(providers, 'supportsDevice')
|
|
347
|
+
const getSnapshot = firstSupported(providers, 'supportsDevice', 'getSnapshot')
|
|
348
|
+
return { supportsDevice, getSnapshot }
|
|
349
|
+
}),
|
|
317
350
|
|
|
318
351
|
// ── Cap override: server-detected remote → relay-only ────────────
|
|
319
352
|
// The broker (a forked addon) can't see the HTTP request, so it
|
|
@@ -326,8 +359,8 @@ function buildCapabilityRouters(services: RouterServices) {
|
|
|
326
359
|
// remote-proxy routing is preserved (forked/agent-hosted brokers).
|
|
327
360
|
webrtcSession: createCapRouter_webrtcSession(
|
|
328
361
|
(ctx) => {
|
|
329
|
-
const provider =
|
|
330
|
-
?.getSingleton<WebrtcSessionProvider>('webrtc-session') ?? null
|
|
362
|
+
const provider =
|
|
363
|
+
services.capabilityRegistry?.getSingleton<WebrtcSessionProvider>('webrtc-session') ?? null
|
|
331
364
|
return provider ? wrapWebrtcSessionProviderWithRelay(provider, ctx) : null
|
|
332
365
|
},
|
|
333
366
|
(capName, nodeId) =>
|
|
@@ -42,3 +42,13 @@ export function shouldRedirectToLogin(method: string, accept: string | undefined
|
|
|
42
42
|
export function loginRedirectUrl(originalUrl: string): string {
|
|
43
43
|
return `/login?next=${encodeURIComponent(originalUrl)}`
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** Allowed redirect target for `GET /api/embed-auth`: a same-origin RELATIVE
|
|
47
|
+
* path to a stream-broker embed page. Defeats open-redirects — the endpoint
|
|
48
|
+
* sets the session cookie from a Bearer token, so the `next` must be safe to
|
|
49
|
+
* bounce to. Rejects absolute/protocol-relative URLs, backslashes, and `..`. */
|
|
50
|
+
export function isEmbedRedirectTarget(next: string): boolean {
|
|
51
|
+
if (!next.startsWith('/addon/stream-broker/embed/')) return false
|
|
52
|
+
if (next.includes('\\') || next.includes('://') || next.includes('..')) return false
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
planIntegrationIdBackfill,
|
|
4
|
+
planDeleteTimeStamps,
|
|
5
|
+
runIntegrationIdBackfill,
|
|
6
|
+
} from '../integration-id-backfill'
|
|
3
7
|
|
|
4
8
|
describe('planDeleteTimeStamps', () => {
|
|
5
|
-
it(
|
|
9
|
+
it("claims an untagged top-level device of the deleted integration's single-integration addon", () => {
|
|
6
10
|
const stamps = planDeleteTimeStamps(
|
|
7
11
|
'int_rtsp',
|
|
8
12
|
[{ id: 'int_rtsp', addonId: 'provider-rtsp' }],
|
|
@@ -20,7 +24,10 @@ describe('planDeleteTimeStamps', () => {
|
|
|
20
24
|
it('returns no stamps for a multi-integration addon (ambiguous — never auto-claim)', () => {
|
|
21
25
|
const stamps = planDeleteTimeStamps(
|
|
22
26
|
'int_a',
|
|
23
|
-
[
|
|
27
|
+
[
|
|
28
|
+
{ id: 'int_a', addonId: 'provider-ha' },
|
|
29
|
+
{ id: 'int_b', addonId: 'provider-ha' },
|
|
30
|
+
],
|
|
24
31
|
[{ id: 11, addonId: 'provider-ha', parentDeviceId: null }],
|
|
25
32
|
)
|
|
26
33
|
expect(stamps).toEqual([])
|
|
@@ -29,7 +36,10 @@ describe('planDeleteTimeStamps', () => {
|
|
|
29
36
|
it('only returns stamps for the integration being deleted, not siblings of other addons', () => {
|
|
30
37
|
const stamps = planDeleteTimeStamps(
|
|
31
38
|
'int_rtsp',
|
|
32
|
-
[
|
|
39
|
+
[
|
|
40
|
+
{ id: 'int_rtsp', addonId: 'provider-rtsp' },
|
|
41
|
+
{ id: 'int_onvif', addonId: 'provider-onvif' },
|
|
42
|
+
],
|
|
33
43
|
[
|
|
34
44
|
{ id: 4, addonId: 'provider-rtsp', parentDeviceId: null },
|
|
35
45
|
{ id: 5, addonId: 'provider-onvif', parentDeviceId: null },
|
|
@@ -59,7 +69,10 @@ describe('planIntegrationIdBackfill', () => {
|
|
|
59
69
|
|
|
60
70
|
it('skips devices whose addon hosts multiple integrations (ambiguous)', () => {
|
|
61
71
|
const stamps = planIntegrationIdBackfill(
|
|
62
|
-
[
|
|
72
|
+
[
|
|
73
|
+
{ id: 'int_a', addonId: 'provider-ha' },
|
|
74
|
+
{ id: 'int_b', addonId: 'provider-ha' },
|
|
75
|
+
],
|
|
63
76
|
[{ id: 11, addonId: 'provider-ha', parentDeviceId: null }],
|
|
64
77
|
)
|
|
65
78
|
expect(stamps).toEqual([])
|
|
@@ -108,7 +121,9 @@ describe('runIntegrationIdBackfill', () => {
|
|
|
108
121
|
const result = await runIntegrationIdBackfill({
|
|
109
122
|
listIntegrations: async () => [],
|
|
110
123
|
listDevices: async () => [],
|
|
111
|
-
setIntegrationId: async () => {
|
|
124
|
+
setIntegrationId: async () => {
|
|
125
|
+
throw new Error('should not be called')
|
|
126
|
+
},
|
|
112
127
|
logger: { info: () => {}, warn: () => {} },
|
|
113
128
|
})
|
|
114
129
|
expect(result).toEqual({ stamped: 0 })
|