@camstack/server 0.1.7 → 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 +11 -9
- 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 +206 -0
- 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 +292 -0
- 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 +177 -0
- 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 +137 -0
- 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 +265 -5
- 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/__tests__/integration-markers.spec.ts +10 -0
- 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 +459 -166
- 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 +58 -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__/client-ip.spec.ts +27 -1
- 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 +136 -0
- package/src/api/trpc/cap-mount-helpers.ts +64 -44
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/client-ip.ts +17 -0
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +801 -286
- package/src/api/trpc/generated-cap-routers.ts +5723 -719
- 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 +117 -48
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/integration-id-backfill.ts +109 -0
- 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/__tests__/addon-row-manifest.spec.ts +62 -0
- 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 +1212 -1267
- package/src/core/addon/addon-row-manifest.ts +29 -0
- 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 +19 -5
- 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 +145 -29
- package/src/core/network/network-quality.service.spec.ts +7 -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 +658 -495
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
- package/src/core/storage/settings-store.spec.ts +0 -213
- package/src/core/storage/settings-store.ts +0 -2
- package/src/core/storage/sql-schema.spec.ts +0 -140
- package/src/core/storage/sql-schema.ts +0 -3
|
@@ -23,11 +23,16 @@ import { describe, it, expect, vi } from 'vitest'
|
|
|
23
23
|
import {
|
|
24
24
|
createParentUnownedCallHandler,
|
|
25
25
|
CapRouteResolver,
|
|
26
|
+
CapRouteError,
|
|
27
|
+
HubNodeRegistry,
|
|
26
28
|
} from '@camstack/kernel'
|
|
27
29
|
import type {
|
|
28
30
|
NodeCapAuthority,
|
|
29
31
|
InProcessProviderLookup,
|
|
30
32
|
CapRouteResolverDeps,
|
|
33
|
+
NodeNativeCapEntry,
|
|
34
|
+
HubLocalChildDispatcher,
|
|
35
|
+
CapCallInput,
|
|
31
36
|
} from '@camstack/kernel'
|
|
32
37
|
import type { ServiceBroker } from 'moleculer'
|
|
33
38
|
|
|
@@ -63,6 +68,44 @@ function hubBrokerFake(): ServiceBroker {
|
|
|
63
68
|
return broker as unknown as ServiceBroker
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Minimal `HubNodeRegistry`-shaped fake exposing only the lookup the unowned
|
|
73
|
+
* handler uses: `listNativeCapEntriesForDevice`. The handler depends solely on
|
|
74
|
+
* this method, so we don't need the full registry to exercise its routing.
|
|
75
|
+
*/
|
|
76
|
+
function nodeRegistryFake(
|
|
77
|
+
byDevice: Record<number, readonly NodeNativeCapEntry[]> = {},
|
|
78
|
+
): HubNodeRegistry {
|
|
79
|
+
const fake = {
|
|
80
|
+
listNativeCapEntriesForDevice: (deviceId: number): readonly NodeNativeCapEntry[] =>
|
|
81
|
+
byDevice[deviceId] ?? [],
|
|
82
|
+
}
|
|
83
|
+
return fake as unknown as HubNodeRegistry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hub-local UDS dispatcher fake (LocalChildRegistry surface). `resolveChildId`
|
|
88
|
+
* returns the configured childId on a (capName, deviceId) match else null;
|
|
89
|
+
* `callCapOnChild` records the call and echoes a sentinel result.
|
|
90
|
+
*/
|
|
91
|
+
function localDispatcherFake(owner?: { capName: string; deviceId: number; childId: string }): {
|
|
92
|
+
dispatcher: HubLocalChildDispatcher
|
|
93
|
+
resolveChildId: ReturnType<typeof vi.fn>
|
|
94
|
+
callCapOnChild: ReturnType<typeof vi.fn>
|
|
95
|
+
} {
|
|
96
|
+
const resolveChildId = vi.fn((capName: string, deviceId?: number): string | null =>
|
|
97
|
+
owner !== undefined && capName === owner.capName && deviceId === owner.deviceId
|
|
98
|
+
? owner.childId
|
|
99
|
+
: null,
|
|
100
|
+
)
|
|
101
|
+
const callCapOnChild = vi.fn(async (childId: string, input: CapCallInput) => ({
|
|
102
|
+
routedOverUds: childId,
|
|
103
|
+
capName: input.capName,
|
|
104
|
+
}))
|
|
105
|
+
const dispatcher: HubLocalChildDispatcher = { resolveChildId, callCapOnChild }
|
|
106
|
+
return { dispatcher, resolveChildId, callCapOnChild }
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
describe('hub onUnownedCall wiring (F0)', () => {
|
|
67
110
|
it('resolver-first: resolves a hub-in-process cap through the real CapRouteResolver', async () => {
|
|
68
111
|
const broker = hubBrokerFake()
|
|
@@ -71,7 +114,12 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
71
114
|
const settingsStore = { get: async (params: unknown) => ({ value: 'hub-resolved', params }) }
|
|
72
115
|
const inProcessProviders: InProcessProviderLookup = (capName) =>
|
|
73
116
|
capName === 'settings-store'
|
|
74
|
-
? {
|
|
117
|
+
? {
|
|
118
|
+
invoke: (method, args) =>
|
|
119
|
+
Promise.resolve(
|
|
120
|
+
(settingsStore as Record<string, (a: unknown) => unknown>)[method](args),
|
|
121
|
+
),
|
|
122
|
+
}
|
|
75
123
|
: null
|
|
76
124
|
|
|
77
125
|
const resolverDeps: CapRouteResolverDeps = {
|
|
@@ -83,12 +131,16 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
83
131
|
}
|
|
84
132
|
const resolver = new CapRouteResolver(resolverDeps)
|
|
85
133
|
|
|
86
|
-
const handler = createParentUnownedCallHandler({
|
|
134
|
+
const handler = createParentUnownedCallHandler({
|
|
135
|
+
getResolver: () => resolver,
|
|
136
|
+
broker,
|
|
137
|
+
nodeRegistry: nodeRegistryFake(),
|
|
138
|
+
})
|
|
87
139
|
|
|
88
140
|
const result = await handler({ capName: 'settings-store', method: 'get', args: { key: 'k' } })
|
|
89
141
|
expect(result).toEqual({ value: 'hub-resolved', params: { key: 'k' } })
|
|
90
142
|
// Resolver served it — broker untouched.
|
|
91
|
-
expect(
|
|
143
|
+
expect(broker.call as ReturnType<typeof vi.fn>).not.toHaveBeenCalled()
|
|
92
144
|
})
|
|
93
145
|
|
|
94
146
|
it('broker-fallback: a `$`-infra core service (`$core-caps.system.info`) routes via the broker', async () => {
|
|
@@ -104,7 +156,11 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
104
156
|
inProcessProviders: () => null,
|
|
105
157
|
})
|
|
106
158
|
|
|
107
|
-
const handler = createParentUnownedCallHandler({
|
|
159
|
+
const handler = createParentUnownedCallHandler({
|
|
160
|
+
getResolver: () => resolver,
|
|
161
|
+
broker,
|
|
162
|
+
nodeRegistry: nodeRegistryFake(),
|
|
163
|
+
})
|
|
108
164
|
|
|
109
165
|
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
110
166
|
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
@@ -115,9 +171,213 @@ describe('hub onUnownedCall wiring (F0)', () => {
|
|
|
115
171
|
const broker = hubBrokerFake()
|
|
116
172
|
// Mirrors the window before `this.resolver` is constructed: getResolver
|
|
117
173
|
// returns null, so the handler goes straight to the broker fallback.
|
|
118
|
-
const handler = createParentUnownedCallHandler({
|
|
174
|
+
const handler = createParentUnownedCallHandler({
|
|
175
|
+
getResolver: () => null,
|
|
176
|
+
broker,
|
|
177
|
+
nodeRegistry: nodeRegistryFake(),
|
|
178
|
+
})
|
|
119
179
|
|
|
120
180
|
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
121
181
|
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
122
182
|
})
|
|
183
|
+
|
|
184
|
+
it('deviceId-aware resolver: derives deviceId from args and routes via the resolver (no broker fallback)', async () => {
|
|
185
|
+
const broker = hubBrokerFake()
|
|
186
|
+
|
|
187
|
+
// A resolver-shaped fake that resolves a route ONLY when invoked with a
|
|
188
|
+
// deviceId — mirroring the real resolver routing a device-scoped native cap
|
|
189
|
+
// to its owning provider. Without a deviceId it returns no-provider.
|
|
190
|
+
let resolvedWithDeviceId: number | undefined
|
|
191
|
+
const resolver = {
|
|
192
|
+
resolveCapRoute: (capName: string, opts: { deviceId?: number }) => {
|
|
193
|
+
if (opts.deviceId === undefined) {
|
|
194
|
+
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
195
|
+
}
|
|
196
|
+
resolvedWithDeviceId = opts.deviceId
|
|
197
|
+
return { kind: 'hub-in-process', capName, deviceId: opts.deviceId }
|
|
198
|
+
},
|
|
199
|
+
dispatch: async () => ({ catalog: ['stream-a'] }),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const handler = createParentUnownedCallHandler({
|
|
203
|
+
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
204
|
+
broker,
|
|
205
|
+
nodeRegistry: nodeRegistryFake(),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// deviceId lives ONLY in args, NOT top-level — the handler must derive it.
|
|
209
|
+
const result = await handler({
|
|
210
|
+
capName: 'stream-catalog',
|
|
211
|
+
method: 'getCatalog',
|
|
212
|
+
args: { deviceId: 7 },
|
|
213
|
+
})
|
|
214
|
+
expect(result).toEqual({ catalog: ['stream-a'] })
|
|
215
|
+
expect(resolvedWithDeviceId).toBe(7)
|
|
216
|
+
// Resolver served it via the derived deviceId — broker untouched.
|
|
217
|
+
expect(broker.call).not.toHaveBeenCalled()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('pinned broker fallback: resolver finds nothing → call is pinned to the owning node', async () => {
|
|
221
|
+
const broker = hubBrokerFake()
|
|
222
|
+
|
|
223
|
+
// Resolver always returns no-provider for this device-scoped cap.
|
|
224
|
+
const resolver = {
|
|
225
|
+
resolveCapRoute: (capName: string) => {
|
|
226
|
+
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
227
|
+
},
|
|
228
|
+
dispatch: async () => {
|
|
229
|
+
throw new Error('dispatch should not be reached')
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const owner: NodeNativeCapEntry = {
|
|
234
|
+
nodeId: 'agent',
|
|
235
|
+
addonId: 'reolink',
|
|
236
|
+
capName: 'stream-catalog',
|
|
237
|
+
deviceId: 7,
|
|
238
|
+
}
|
|
239
|
+
const handler = createParentUnownedCallHandler({
|
|
240
|
+
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
241
|
+
broker,
|
|
242
|
+
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
246
|
+
() => undefined,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
// The broker call is pinned to the owning node via call-opts `{ nodeID }`.
|
|
250
|
+
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
251
|
+
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('back-compat: no resolvable device-owner → broker call stays UNPINNED (load-balanced)', async () => {
|
|
255
|
+
const broker = hubBrokerFake()
|
|
256
|
+
|
|
257
|
+
const resolver = {
|
|
258
|
+
resolveCapRoute: (capName: string) => {
|
|
259
|
+
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
260
|
+
},
|
|
261
|
+
dispatch: async () => {
|
|
262
|
+
throw new Error('dispatch should not be reached')
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const handler = createParentUnownedCallHandler({
|
|
267
|
+
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
268
|
+
broker,
|
|
269
|
+
nodeRegistry: nodeRegistryFake(), // no owners for any device
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const result = await handler({ capName: 'system', method: 'info', args: undefined })
|
|
273
|
+
expect(result).toEqual({ uptimeSec: 99, params: undefined })
|
|
274
|
+
// Unpinned: third arg (call-opts) is undefined — today's behavior preserved.
|
|
275
|
+
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
276
|
+
expect(callArgs[2]).toBeUndefined()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('hub-local owner: device-native cap owned by a hub-local UDS child routes over UDS (broker untouched)', async () => {
|
|
280
|
+
const broker = hubBrokerFake()
|
|
281
|
+
// Resolver misses the device-scoped native cap (mirrors live behavior).
|
|
282
|
+
const resolver = {
|
|
283
|
+
resolveCapRoute: (capName: string) => {
|
|
284
|
+
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
285
|
+
},
|
|
286
|
+
dispatch: async () => {
|
|
287
|
+
throw new Error('dispatch should not be reached')
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
const { dispatcher, resolveChildId, callCapOnChild } = localDispatcherFake({
|
|
291
|
+
capName: 'stream-catalog',
|
|
292
|
+
deviceId: 7,
|
|
293
|
+
childId: 'child-reolink',
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const handler = createParentUnownedCallHandler({
|
|
297
|
+
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
298
|
+
broker,
|
|
299
|
+
nodeRegistry: nodeRegistryFake(), // empty — hub-local child is NOT in HubNodeRegistry
|
|
300
|
+
getLocalDispatcher: () => dispatcher,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const result = await handler({
|
|
304
|
+
capName: 'stream-catalog',
|
|
305
|
+
method: 'getCatalog',
|
|
306
|
+
args: { deviceId: 7 },
|
|
307
|
+
})
|
|
308
|
+
expect(result).toEqual({ routedOverUds: 'child-reolink', capName: 'stream-catalog' })
|
|
309
|
+
expect(resolveChildId).toHaveBeenCalledWith('stream-catalog', 7)
|
|
310
|
+
expect(callCapOnChild).toHaveBeenCalledWith('child-reolink', {
|
|
311
|
+
capName: 'stream-catalog',
|
|
312
|
+
method: 'getCatalog',
|
|
313
|
+
args: { deviceId: 7 },
|
|
314
|
+
deviceId: 7,
|
|
315
|
+
})
|
|
316
|
+
expect(broker.call).not.toHaveBeenCalled()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('remote owner: hub-local dispatcher misses → pinned broker call', async () => {
|
|
320
|
+
const broker = hubBrokerFake()
|
|
321
|
+
const resolver = {
|
|
322
|
+
resolveCapRoute: (capName: string) => {
|
|
323
|
+
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
324
|
+
},
|
|
325
|
+
dispatch: async () => {
|
|
326
|
+
throw new Error('dispatch should not be reached')
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
const { dispatcher, callCapOnChild } = localDispatcherFake() // no local owner
|
|
330
|
+
const owner: NodeNativeCapEntry = {
|
|
331
|
+
nodeId: 'agent',
|
|
332
|
+
addonId: 'reolink',
|
|
333
|
+
capName: 'stream-catalog',
|
|
334
|
+
deviceId: 7,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const handler = createParentUnownedCallHandler({
|
|
338
|
+
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
339
|
+
broker,
|
|
340
|
+
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
341
|
+
getLocalDispatcher: () => dispatcher,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
345
|
+
() => undefined,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
expect(callCapOnChild).not.toHaveBeenCalled()
|
|
349
|
+
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
350
|
+
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('no local dispatcher (getter returns null): behaves as before — HubNodeRegistry → broker', async () => {
|
|
354
|
+
const broker = hubBrokerFake()
|
|
355
|
+
const resolver = {
|
|
356
|
+
resolveCapRoute: (capName: string) => {
|
|
357
|
+
throw new CapRouteError(capName, undefined, { reason: 'no-provider', rejected: [] })
|
|
358
|
+
},
|
|
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,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const handler = createParentUnownedCallHandler({
|
|
371
|
+
getResolver: () => resolver as unknown as CapRouteResolver,
|
|
372
|
+
broker,
|
|
373
|
+
nodeRegistry: nodeRegistryFake({ 7: [owner] }),
|
|
374
|
+
getLocalDispatcher: () => null,
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
await handler({ capName: 'stream-catalog', method: 'getCatalog', args: { deviceId: 7 } }).catch(
|
|
378
|
+
() => undefined,
|
|
379
|
+
)
|
|
380
|
+
const callArgs = (broker.call as ReturnType<typeof vi.fn>).mock.calls[0]
|
|
381
|
+
expect(callArgs[2]).toEqual({ nodeID: 'agent' })
|
|
382
|
+
})
|
|
123
383
|
})
|
|
@@ -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: (
|
|
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(
|
|
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(
|
|
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))
|
|
252
|
-
driver.onRegisterNode(singleton(LOCAL))
|
|
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))
|
|
261
|
-
driver.onRegisterNode(singleton(REMOTE))
|
|
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) => ({
|
|
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(
|
|
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
|
-
{
|
|
130
|
+
{
|
|
131
|
+
nodeId: 'hub/provider-reolink',
|
|
132
|
+
addonId: 'addon-provider-reolink',
|
|
133
|
+
capName: 'ptz',
|
|
134
|
+
deviceId: 7,
|
|
135
|
+
},
|
|
129
136
|
]
|
|
130
|
-
const nodeAuthority = makeNativeAwareNodeAuthority(
|
|
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 [
|
|
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
|
-
{
|
|
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(): {
|
|
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
|
-
{
|
|
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 {
|
|
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<
|
|
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: {
|
|
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: [
|
|
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(
|
|
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(
|
|
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<{
|
|
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<{
|
|
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<{
|
|
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
|
|
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
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
|