@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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/addon-upload.spec.ts +58 -0
  3. package/src/__tests__/bulk-update-coordinator.spec.ts +286 -0
  4. package/src/__tests__/cap-ownership-authority.spec.ts +400 -0
  5. package/src/__tests__/cap-providers-bulk-update.spec.ts +388 -0
  6. package/src/__tests__/cap-route-adapter.spec.ts +289 -0
  7. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +123 -0
  8. package/src/__tests__/cap-routers/capabilities-node.spec.ts +55 -0
  9. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +30 -0
  10. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +249 -0
  11. package/src/__tests__/framework-installer-defer-restart.spec.ts +165 -0
  12. package/src/__tests__/moleculer/uds-readiness.spec.ts +143 -0
  13. package/src/__tests__/moleculer/uds-topology.spec.ts +390 -0
  14. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +123 -0
  15. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +39 -4
  16. package/src/__tests__/native-cap-route.spec.ts +404 -0
  17. package/src/__tests__/oauth2-account-linking.spec.ts +85 -0
  18. package/src/__tests__/uds-addon-call-wiring.spec.ts +237 -0
  19. package/src/__tests__/uds-log-ingest.spec.ts +183 -0
  20. package/src/api/addon-upload.ts +27 -1
  21. package/src/api/capabilities.router.ts +1 -1
  22. package/src/api/core/bulk-update-coordinator.ts +302 -0
  23. package/src/api/core/cap-providers.ts +59 -6
  24. package/src/api/core/capabilities.router.ts +26 -3
  25. package/src/api/oauth2/oauth2-routes.ts +5 -1
  26. package/src/api/trpc/__tests__/client-ip.spec.ts +120 -0
  27. package/src/api/trpc/cap-route-error-formatter.ts +163 -0
  28. package/src/api/trpc/client-ip.ts +130 -0
  29. package/src/api/trpc/generated-cap-mounts.ts +19 -1
  30. package/src/api/trpc/generated-cap-routers.ts +180 -1
  31. package/src/api/trpc/trpc.middleware.ts +5 -1
  32. package/src/api/trpc/trpc.router.ts +45 -0
  33. package/src/core/addon/addon-call-gateway.ts +157 -0
  34. package/src/core/addon/addon-package.service.ts +9 -0
  35. package/src/core/addon/addon-registry.service.ts +364 -105
  36. package/src/core/addon/addon-settings-provider.ts +40 -116
  37. package/src/core/capability/capability.service.ts +9 -0
  38. package/src/core/moleculer/cap-call-fn.spec.ts +166 -0
  39. package/src/core/moleculer/cap-call-fn.ts +103 -0
  40. package/src/core/moleculer/cap-route-authority.ts +182 -0
  41. package/src/core/moleculer/moleculer.service.ts +380 -36
  42. 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
+ }