@camstack/server 0.1.5 → 0.1.7
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 +1 -1
- package/src/__tests__/addon-upload.spec.ts +58 -0
- package/src/__tests__/bulk-update-coordinator.spec.ts +286 -0
- package/src/__tests__/cap-ownership-authority.spec.ts +400 -0
- package/src/__tests__/cap-providers-bulk-update.spec.ts +388 -0
- package/src/__tests__/cap-route-adapter.spec.ts +289 -0
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +123 -0
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +55 -0
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +30 -0
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +249 -0
- package/src/__tests__/framework-installer-defer-restart.spec.ts +165 -0
- package/src/__tests__/moleculer/uds-readiness.spec.ts +143 -0
- package/src/__tests__/moleculer/uds-topology.spec.ts +390 -0
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +123 -0
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +39 -4
- package/src/__tests__/native-cap-route.spec.ts +404 -0
- package/src/__tests__/oauth2-account-linking.spec.ts +85 -0
- package/src/__tests__/uds-addon-call-wiring.spec.ts +237 -0
- package/src/__tests__/uds-log-ingest.spec.ts +183 -0
- package/src/api/addon-upload.ts +27 -1
- package/src/api/capabilities.router.ts +1 -1
- package/src/api/core/bulk-update-coordinator.ts +302 -0
- package/src/api/core/cap-providers.ts +59 -6
- package/src/api/core/capabilities.router.ts +26 -3
- package/src/api/oauth2/oauth2-routes.ts +5 -1
- package/src/api/trpc/__tests__/client-ip.spec.ts +120 -0
- package/src/api/trpc/cap-route-error-formatter.ts +163 -0
- package/src/api/trpc/client-ip.ts +130 -0
- package/src/api/trpc/generated-cap-mounts.ts +19 -1
- package/src/api/trpc/generated-cap-routers.ts +180 -1
- package/src/api/trpc/trpc.middleware.ts +5 -1
- package/src/api/trpc/trpc.router.ts +45 -0
- package/src/core/addon/addon-call-gateway.ts +157 -0
- package/src/core/addon/addon-package.service.ts +9 -0
- package/src/core/addon/addon-registry.service.ts +364 -105
- package/src/core/addon/addon-settings-provider.ts +40 -116
- package/src/core/capability/capability.service.ts +9 -0
- package/src/core/moleculer/cap-call-fn.spec.ts +166 -0
- package/src/core/moleculer/cap-call-fn.ts +103 -0
- package/src/core/moleculer/cap-route-authority.ts +182 -0
- package/src/core/moleculer/moleculer.service.ts +380 -36
- package/src/main.ts +45 -12
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter factories that bridge the server-side registry/service state into the
|
|
3
|
+
* narrow interfaces that CapRouteResolver (in @camstack/kernel) requires.
|
|
4
|
+
*
|
|
5
|
+
* Layer note: this file is in server/backend and may import server-side types.
|
|
6
|
+
* The resolver itself (in @camstack/kernel) must NOT import from here —
|
|
7
|
+
* it depends only on the narrow interfaces (NodeCapAuthority, InProcessProviderLookup)
|
|
8
|
+
* defined in the kernel. These factories are the wiring adapters.
|
|
9
|
+
*
|
|
10
|
+
* nodeIsAgent format rule (from agent-registry.service.ts:74-77):
|
|
11
|
+
* hub → 'hub' → not an agent
|
|
12
|
+
* hub child → 'hub/<runnerId>' → not an agent
|
|
13
|
+
* agent → bare id with no '/' → agent
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { NodeCapAuthority, InProcessProviderLookup } from '@camstack/kernel'
|
|
17
|
+
import type { InProcessProviderRef } from '@camstack/kernel'
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Minimal interface for the HubNodeRegistry dependency
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal view of HubNodeRegistry that this adapter needs.
|
|
25
|
+
* Keeps the adapter decoupled from the concrete class — only structural match
|
|
26
|
+
* is required. HubNodeRegistry satisfies this interface structurally.
|
|
27
|
+
*/
|
|
28
|
+
export interface NodeRegistryLike {
|
|
29
|
+
getNodeManifest(nodeId: string): readonly { readonly addonId: string; readonly capabilities: readonly string[] }[] | undefined
|
|
30
|
+
listNodeIds(): readonly string[]
|
|
31
|
+
/**
|
|
32
|
+
* Optional: returns flat (nodeId, addonId, capName, deviceId) native-cap tuples.
|
|
33
|
+
* When provided, `nodeKnowsCap` and `isNativeCap` also consult native caps so
|
|
34
|
+
* device-scoped native caps (ptz, motion-zones, …) are visible to the resolver.
|
|
35
|
+
*/
|
|
36
|
+
listNativeCapEntries?(): readonly { readonly nodeId: string; readonly addonId: string; readonly capName: string; readonly deviceId: number }[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Minimal interface for the CapabilityService dependency
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Minimal view of CapabilityService that the InProcessProviderLookup adapter needs.
|
|
45
|
+
* The real CapabilityService satisfies this interface structurally.
|
|
46
|
+
*/
|
|
47
|
+
export interface CapabilityServiceLike {
|
|
48
|
+
getSingleton(capability: string): Record<string, unknown> | null
|
|
49
|
+
/** Resolve the hub-local provider honoring the 'hub' per-node override. */
|
|
50
|
+
getSingletonForNode?(capability: string, nodeId: string): Record<string, unknown> | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// createNodeCapAuthority
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optional resolver injected into createNodeCapAuthority so that getAddonId
|
|
59
|
+
* can honor per-node singleton overrides and the cluster-global default,
|
|
60
|
+
* constrained to addons the node actually hosts in its manifest.
|
|
61
|
+
*/
|
|
62
|
+
export interface SingletonNodeResolver {
|
|
63
|
+
/** Bare addonId a node should use for a singleton cap (override→default→first), or null. */
|
|
64
|
+
resolveSingleton(capName: string, nodeId: string): string | null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a NodeCapAuthority backed by a HubNodeRegistry.
|
|
69
|
+
*
|
|
70
|
+
* nodeIsAgent rule: a node is an agent when its id is not 'hub' and does not
|
|
71
|
+
* contain '/' (hub children are `hub/<runnerId>`; agents are bare ids).
|
|
72
|
+
*
|
|
73
|
+
* nodeOnline: uses registry membership — per CLAUDE.md "the registry is the
|
|
74
|
+
* union of registerNode manifests minus disconnected nodes" because
|
|
75
|
+
* `removeNode` is called on `$node.disconnected`. Registry membership is the
|
|
76
|
+
* cleanest, cast-free liveness check.
|
|
77
|
+
*
|
|
78
|
+
* getAgentChildId: always returns null — the hub cannot resolve which forked
|
|
79
|
+
* child under an agent provides a cap (the agent flattens its subtree into
|
|
80
|
+
* one merged manifest). The agent resolves its own child locally (Task 6).
|
|
81
|
+
*/
|
|
82
|
+
export function createNodeCapAuthority(
|
|
83
|
+
nodeRegistry: NodeRegistryLike,
|
|
84
|
+
resolver?: SingletonNodeResolver,
|
|
85
|
+
): NodeCapAuthority {
|
|
86
|
+
return {
|
|
87
|
+
nodeKnowsCap(nodeId: string, capName: string): boolean {
|
|
88
|
+
// Check system (manifest) caps first
|
|
89
|
+
const manifest = nodeRegistry.getNodeManifest(nodeId)
|
|
90
|
+
if (manifest !== undefined && manifest.some((addon) => addon.capabilities.includes(capName))) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
// Also check device-scoped native caps — these are NOT in the addon manifest
|
|
94
|
+
const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
|
|
95
|
+
return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
getAddonId(nodeId: string, capName: string): string | null {
|
|
99
|
+
// Check system (manifest) caps first
|
|
100
|
+
const manifest = nodeRegistry.getNodeManifest(nodeId)
|
|
101
|
+
if (manifest !== undefined) {
|
|
102
|
+
const manifestAddons = manifest
|
|
103
|
+
.filter((addon) => addon.capabilities.includes(capName))
|
|
104
|
+
.map((addon) => addon.addonId)
|
|
105
|
+
if (manifestAddons.length > 0) {
|
|
106
|
+
// Per-node override / global-default resolution, constrained to what
|
|
107
|
+
// the node actually hosts. Falls back to the first manifest match.
|
|
108
|
+
const resolved = resolver?.resolveSingleton(capName, nodeId) ?? null
|
|
109
|
+
if (resolved !== null && manifestAddons.includes(resolved)) return resolved
|
|
110
|
+
return manifestAddons[0] ?? null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Check device-scoped native caps (unchanged path)
|
|
114
|
+
const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
|
|
115
|
+
const nat = nativeEntries.find((n) => n.nodeId === nodeId && n.capName === capName)
|
|
116
|
+
return nat?.addonId ?? null
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
nodeIsAgent(nodeId: string): boolean {
|
|
120
|
+
return nodeId !== 'hub' && !nodeId.includes('/')
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
nodeOnline(nodeId: string): boolean {
|
|
124
|
+
// O(1) Map lookup — registry membership is the authoritative liveness
|
|
125
|
+
// check: HubNodeRegistry.removeNode is called on $node.disconnected,
|
|
126
|
+
// so a defined manifest means the node is connected.
|
|
127
|
+
return nodeRegistry.getNodeManifest(nodeId) !== undefined
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
listNodeIds(): readonly string[] {
|
|
131
|
+
return nodeRegistry.listNodeIds()
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
getAgentChildId(_agentNodeId: string, _capName: string): string | null {
|
|
135
|
+
// The hub cannot resolve the agent's child — the agent resolves locally (Task 6).
|
|
136
|
+
return null
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
isNativeCap(nodeId: string, capName: string, deviceId?: number): boolean {
|
|
140
|
+
const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
|
|
141
|
+
if (deviceId !== undefined) {
|
|
142
|
+
return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId)
|
|
143
|
+
}
|
|
144
|
+
return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// createInProcessProviderLookup
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build an InProcessProviderLookup backed by a CapabilityService.
|
|
155
|
+
*
|
|
156
|
+
* Cast-free design: `getSingleton<Record<string, unknown>>` makes the provider
|
|
157
|
+
* indexable without any `as` casts. `typeof fn === 'function'` narrows to
|
|
158
|
+
* Function (implicit `any` return); calling via `fn.call(provider, args)` is
|
|
159
|
+
* then safe and the result is captured as `unknown`. No `as` casts anywhere.
|
|
160
|
+
*/
|
|
161
|
+
export function createInProcessProviderLookup(
|
|
162
|
+
capabilityService: CapabilityServiceLike,
|
|
163
|
+
): InProcessProviderLookup {
|
|
164
|
+
return (capName: string): InProcessProviderRef | null => {
|
|
165
|
+
const provider =
|
|
166
|
+
capabilityService.getSingletonForNode?.(capName, 'hub')
|
|
167
|
+
?? capabilityService.getSingleton(capName)
|
|
168
|
+
if (provider === null || provider === undefined) return null
|
|
169
|
+
|
|
170
|
+
const ref: InProcessProviderRef = {
|
|
171
|
+
invoke: (method: string, args: unknown): Promise<unknown> => {
|
|
172
|
+
const fn = provider[method]
|
|
173
|
+
if (typeof fn !== 'function') {
|
|
174
|
+
return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`))
|
|
175
|
+
}
|
|
176
|
+
const result: unknown = fn.call(provider, args)
|
|
177
|
+
return Promise.resolve(result)
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
return ref
|
|
181
|
+
}
|
|
182
|
+
}
|