@camstack/server 0.1.3
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/.env.example +17 -0
- package/package.json +55 -0
- package/src/__tests__/addon-install-e2e.test.ts +75 -0
- package/src/__tests__/addon-pages-e2e.test.ts +178 -0
- package/src/__tests__/addon-route-session.test.ts +17 -0
- package/src/__tests__/addon-settings-router.spec.ts +62 -0
- package/src/__tests__/addon-upload.spec.ts +355 -0
- package/src/__tests__/agent-registry.spec.ts +162 -0
- package/src/__tests__/agent-status-page.spec.ts +84 -0
- package/src/__tests__/auth-session-cookie.test.ts +21 -0
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +23 -0
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +64 -0
- package/src/__tests__/cap-routers/_meta.spec.ts +200 -0
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +106 -0
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +142 -0
- package/src/__tests__/cap-routers/harness.ts +159 -0
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +119 -0
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +66 -0
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +135 -0
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +247 -0
- package/src/__tests__/capability-e2e.test.ts +386 -0
- package/src/__tests__/cli-e2e.test.ts +129 -0
- package/src/__tests__/core-cap-bridge.spec.ts +89 -0
- package/src/__tests__/embedded-deps-e2e.test.ts +109 -0
- package/src/__tests__/event-bus-proxy-router.spec.ts +72 -0
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +37 -0
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +37 -0
- package/src/__tests__/fixtures/mock-log-addon.ts +37 -0
- package/src/__tests__/fixtures/mock-storage-addon.ts +40 -0
- package/src/__tests__/framework-allowlist.spec.ts +95 -0
- package/src/__tests__/https-e2e.test.ts +118 -0
- package/src/__tests__/lifecycle-e2e.test.ts +140 -0
- package/src/__tests__/live-events-subscription.spec.ts +150 -0
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +229 -0
- package/src/__tests__/oauth2-account-linking.spec.ts +736 -0
- package/src/__tests__/post-boot-restart.spec.ts +161 -0
- package/src/__tests__/singleton-contention.test.ts +487 -0
- package/src/__tests__/streaming-diagnostic.test.ts +512 -0
- package/src/__tests__/streaming-scale.test.ts +280 -0
- package/src/agent-status-page.ts +121 -0
- package/src/api/__tests__/addons-custom.spec.ts +134 -0
- package/src/api/__tests__/capabilities.router.test.ts +47 -0
- package/src/api/addon-upload.ts +472 -0
- package/src/api/addons-custom.router.ts +100 -0
- package/src/api/auth-whoami.ts +99 -0
- package/src/api/bridge-addons.router.ts +120 -0
- package/src/api/capabilities.router.ts +226 -0
- package/src/api/core/__tests__/auth-router-totp.spec.ts +256 -0
- package/src/api/core/addon-settings.router.ts +124 -0
- package/src/api/core/agents.router.ts +87 -0
- package/src/api/core/auth.router.ts +303 -0
- package/src/api/core/cap-providers.ts +993 -0
- package/src/api/core/capabilities.router.ts +119 -0
- package/src/api/core/collection-preference.ts +40 -0
- package/src/api/core/event-bus-proxy.router.ts +45 -0
- package/src/api/core/hwaccel.router.ts +81 -0
- package/src/api/core/live-events.router.ts +60 -0
- package/src/api/core/logs.router.ts +162 -0
- package/src/api/core/notifications.router.ts +65 -0
- package/src/api/core/repl.router.ts +41 -0
- package/src/api/core/settings-backend.router.ts +142 -0
- package/src/api/core/stream-probe.router.ts +57 -0
- package/src/api/core/system-events.router.ts +116 -0
- package/src/api/health/health.routes.ts +123 -0
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +52 -0
- package/src/api/oauth2/consent-page.ts +42 -0
- package/src/api/oauth2/oauth2-routes.ts +248 -0
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +223 -0
- package/src/api/trpc/__tests__/scope-access.spec.ts +107 -0
- package/src/api/trpc/cap-mount-helpers.ts +225 -0
- package/src/api/trpc/core-cap-bridge.ts +152 -0
- package/src/api/trpc/generated-cap-mounts.ts +707 -0
- package/src/api/trpc/generated-cap-routers.ts +6340 -0
- package/src/api/trpc/scope-access.ts +110 -0
- package/src/api/trpc/trpc.context.ts +255 -0
- package/src/api/trpc/trpc.middleware.ts +140 -0
- package/src/api/trpc/trpc.router.ts +275 -0
- package/src/auth/session-cookie.ts +44 -0
- package/src/boot/boot-config.ts +278 -0
- package/src/boot/post-boot.service.ts +103 -0
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +53 -0
- package/src/core/addon/addon-package.service.ts +1684 -0
- package/src/core/addon/addon-registry.service.ts +2926 -0
- package/src/core/addon/addon-search.service.ts +90 -0
- package/src/core/addon/addon-settings-provider.ts +276 -0
- package/src/core/addon/addon.tokens.ts +2 -0
- package/src/core/addon-bridge/addon-bridge.service.ts +125 -0
- package/src/core/addon-pages/addon-pages.service.spec.ts +117 -0
- package/src/core/addon-pages/addon-pages.service.ts +80 -0
- package/src/core/addon-widgets/addon-widgets.service.ts +92 -0
- package/src/core/agent/agent-registry.service.ts +507 -0
- package/src/core/auth/auth.service.spec.ts +88 -0
- package/src/core/auth/auth.service.ts +8 -0
- package/src/core/capability/capability.service.ts +57 -0
- package/src/core/config/config.schema.ts +3 -0
- package/src/core/config/config.service.spec.ts +175 -0
- package/src/core/config/config.service.ts +7 -0
- package/src/core/events/event-bus.service.spec.ts +212 -0
- package/src/core/events/event-bus.service.ts +85 -0
- package/src/core/feature/feature.service.spec.ts +96 -0
- package/src/core/feature/feature.service.ts +8 -0
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +168 -0
- package/src/core/lifecycle/lifecycle-state-machine.ts +3 -0
- package/src/core/logging/log-ring-buffer.ts +3 -0
- package/src/core/logging/logging.service.spec.ts +247 -0
- package/src/core/logging/logging.service.ts +129 -0
- package/src/core/logging/scoped-logger.ts +3 -0
- package/src/core/moleculer/moleculer.service.ts +612 -0
- package/src/core/network/network-quality.service.spec.ts +47 -0
- package/src/core/network/network-quality.service.ts +5 -0
- package/src/core/notification/notification-wrapper.service.ts +36 -0
- package/src/core/notification/toast-wrapper.service.ts +31 -0
- package/src/core/provider/provider.tokens.ts +1 -0
- package/src/core/repl/repl-engine.service.spec.ts +417 -0
- package/src/core/repl/repl-engine.service.ts +156 -0
- package/src/core/storage/fs-storage-backend.spec.ts +70 -0
- package/src/core/storage/fs-storage-backend.ts +3 -0
- package/src/core/storage/settings-store.spec.ts +213 -0
- package/src/core/storage/settings-store.ts +2 -0
- package/src/core/storage/sql-schema.spec.ts +140 -0
- package/src/core/storage/sql-schema.ts +3 -0
- package/src/core/storage/storage-location-manager.spec.ts +121 -0
- package/src/core/storage/storage-location-manager.ts +3 -0
- package/src/core/storage/storage.service.spec.ts +73 -0
- package/src/core/storage/storage.service.ts +3 -0
- package/src/core/streaming/stream-probe.service.ts +212 -0
- package/src/core/topology/topology-emitter.service.ts +101 -0
- package/src/launcher.ts +309 -0
- package/src/main.ts +1049 -0
- package/src/manual-boot.ts +322 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small helpers for wiring capability routers to the `CapabilityRegistry`
|
|
3
|
+
* in `trpc.router.ts`. These functions don't generate code — they just
|
|
4
|
+
* remove boilerplate from the mount lambdas we pass to `createCapRouter_X`.
|
|
5
|
+
*
|
|
6
|
+
* When to use what:
|
|
7
|
+
* - `requireSingleton(registry, name)` — singleton caps with no custom
|
|
8
|
+
* composition. Returns the active provider or null (the codegen'd
|
|
9
|
+
* router itself throws PRECONDITION_FAILED when null).
|
|
10
|
+
* - `concatCollection(providers, method)` — collection caps whose
|
|
11
|
+
* methods return arrays and where the desired behaviour is "union of
|
|
12
|
+
* every provider's contribution" (e.g. `turn-provider.getTurnServers`).
|
|
13
|
+
* - `firstSupported(providers, probe, action)` — collection caps where
|
|
14
|
+
* exactly one provider should handle each request, selected by a
|
|
15
|
+
* probe method (e.g. `snapshot-provider.supportsDevice`).
|
|
16
|
+
*
|
|
17
|
+
* Collections that route by an input key (e.g. `webrtc` picking the
|
|
18
|
+
* provider responsible for a given `streamId`) are NOT covered here —
|
|
19
|
+
* they have app-specific routing logic that belongs in the mount.
|
|
20
|
+
*/
|
|
21
|
+
import { TRPCError } from '@trpc/server'
|
|
22
|
+
import type { CapabilityRegistry } from '@camstack/kernel'
|
|
23
|
+
import type { CapabilityProviderMap } from '@camstack/types'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch the currently active singleton provider for a capability.
|
|
27
|
+
* Returns null if no provider is registered; the downstream codegen'd
|
|
28
|
+
* router surfaces this as `PRECONDITION_FAILED` to the caller.
|
|
29
|
+
*/
|
|
30
|
+
export function requireSingleton<K extends keyof CapabilityProviderMap>(
|
|
31
|
+
registry: CapabilityRegistry | null,
|
|
32
|
+
capName: K,
|
|
33
|
+
): CapabilityProviderMap[K] | null {
|
|
34
|
+
return registry?.getSingleton(capName) ?? null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a per-device dispatcher that satisfies the singleton-provider
|
|
39
|
+
* shape but resolves the actual implementation lazily via
|
|
40
|
+
* `registry.getNativeProvider(capName, deviceId)` on every method call.
|
|
41
|
+
*
|
|
42
|
+
* Use for device-scoped caps that have NO system-level wrapper (PTZ,
|
|
43
|
+
* reboot, doorbell, brightness, motion-trigger, switch, …) — drivers
|
|
44
|
+
* register per-device native providers via
|
|
45
|
+
* `DeviceContext.registerNativeCap`, and this helper bridges the cap-
|
|
46
|
+
* router's "fetch a singleton then call methods on it" flow into a
|
|
47
|
+
* "resolve native by deviceId per call" flow.
|
|
48
|
+
*
|
|
49
|
+
* Method input MUST carry `deviceId: number`. Methods without that
|
|
50
|
+
* field (auto-injected `getStatus({deviceId})` for caps with a status
|
|
51
|
+
* block, every business method that follows the cap-definition
|
|
52
|
+
* convention) work transparently.
|
|
53
|
+
*
|
|
54
|
+
* Throws PRECONDITION_FAILED with a device-specific message when no
|
|
55
|
+
* native provider exists for the requested deviceId — much friendlier
|
|
56
|
+
* than the singleton fallthrough's "no provider" generic error.
|
|
57
|
+
*/
|
|
58
|
+
export function requireDeviceScoped<K extends keyof CapabilityProviderMap>(
|
|
59
|
+
registry: CapabilityRegistry | null,
|
|
60
|
+
capName: K,
|
|
61
|
+
): CapabilityProviderMap[K] | null {
|
|
62
|
+
if (!registry) return null
|
|
63
|
+
// The Proxy is the singleton stand-in. Each property access returns
|
|
64
|
+
// a function that, on call, looks up the per-device native and
|
|
65
|
+
// forwards the call. No caching — the lookup is cheap (Map.get) and
|
|
66
|
+
// re-doing it per call lets devices come/go without stale refs.
|
|
67
|
+
const dispatcher = new Proxy({}, {
|
|
68
|
+
get(_target, prop: string | symbol) {
|
|
69
|
+
if (typeof prop !== 'string') return undefined
|
|
70
|
+
return async (input: { deviceId?: number } & Record<string, unknown>) => {
|
|
71
|
+
const deviceId = input?.deviceId
|
|
72
|
+
if (typeof deviceId !== 'number') {
|
|
73
|
+
throw new TRPCError({
|
|
74
|
+
code: 'BAD_REQUEST',
|
|
75
|
+
message: `${String(capName)}.${prop}: input must carry numeric "deviceId"`,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
const native = registry.getNativeProvider<Record<string, (i: unknown) => unknown>>(
|
|
79
|
+
capName,
|
|
80
|
+
deviceId,
|
|
81
|
+
)
|
|
82
|
+
if (!native) {
|
|
83
|
+
throw new TRPCError({
|
|
84
|
+
code: 'PRECONDITION_FAILED',
|
|
85
|
+
message: `Capability "${String(capName)}" not registered for device ${deviceId}`,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
const fn = native[prop]
|
|
89
|
+
if (typeof fn !== 'function') {
|
|
90
|
+
throw new TRPCError({
|
|
91
|
+
code: 'NOT_IMPLEMENTED',
|
|
92
|
+
message: `Capability "${String(capName)}" provider for device ${deviceId} does not implement "${prop}"`,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
return fn.call(native, input)
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
return dispatcher as unknown as CapabilityProviderMap[K]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Method-on-provider callable types ────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/** A key on T whose value is a function with array / promise-array return. */
|
|
105
|
+
type ArrayReturningMethodKey<T> = {
|
|
106
|
+
[K in keyof T]: T[K] extends (...args: infer _A) => readonly unknown[] | Promise<readonly unknown[]>
|
|
107
|
+
? K
|
|
108
|
+
: never
|
|
109
|
+
}[keyof T]
|
|
110
|
+
|
|
111
|
+
/** A key on T whose value is a function returning boolean / promise-boolean. */
|
|
112
|
+
type BoolReturningMethodKey<T> = {
|
|
113
|
+
[K in keyof T]: T[K] extends (...args: infer _A) => boolean | Promise<boolean>
|
|
114
|
+
? K
|
|
115
|
+
: never
|
|
116
|
+
}[keyof T]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a method that fan-outs a call to every provider in a collection
|
|
120
|
+
* and concatenates their array results. Useful for contribution-style
|
|
121
|
+
* caps where each provider adds to a shared pool.
|
|
122
|
+
*/
|
|
123
|
+
export function concatCollection<
|
|
124
|
+
T extends object,
|
|
125
|
+
K extends ArrayReturningMethodKey<T>,
|
|
126
|
+
>(
|
|
127
|
+
providers: readonly T[],
|
|
128
|
+
method: K,
|
|
129
|
+
): T[K] extends (...args: infer A) => readonly (infer R)[] | Promise<readonly (infer R)[]>
|
|
130
|
+
? (...args: A) => Promise<readonly R[]>
|
|
131
|
+
: never {
|
|
132
|
+
const wrapper = async (...args: unknown[]): Promise<readonly unknown[]> => {
|
|
133
|
+
const results = await Promise.all(
|
|
134
|
+
providers.map(async (p): Promise<readonly unknown[]> => {
|
|
135
|
+
const member = Reflect.get(p, method)
|
|
136
|
+
if (typeof member !== 'function') return []
|
|
137
|
+
// `Reflect.apply` returns `any`; funnel through unknown.
|
|
138
|
+
const out: unknown = await Reflect.apply(member, p, args)
|
|
139
|
+
if (!Array.isArray(out)) return []
|
|
140
|
+
const arr: readonly unknown[] = out
|
|
141
|
+
return arr
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
return results.flat()
|
|
145
|
+
}
|
|
146
|
+
// Type-level bridge: the runtime wrapper signature (unknown → Promise<unknown[]>)
|
|
147
|
+
// matches the declared generic conditional return; TypeScript's
|
|
148
|
+
// conditional types can't be narrowed inside a function body, so this
|
|
149
|
+
// boundary assertion is required.
|
|
150
|
+
return wrapper as T[K] extends (...args: infer A) => readonly (infer R)[] | Promise<readonly (infer R)[]>
|
|
151
|
+
? (...args: A) => Promise<readonly R[]>
|
|
152
|
+
: never
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Iterate a collection asking each provider "do you handle this?" via a
|
|
157
|
+
* probe method, then call an action on the first one that answers yes.
|
|
158
|
+
* Each provider is tried in registration order; errors are swallowed and
|
|
159
|
+
* the next provider is attempted. Returns null if no provider matches or
|
|
160
|
+
* if every matching provider's action throws/returns null.
|
|
161
|
+
*/
|
|
162
|
+
export function firstSupported<
|
|
163
|
+
T extends object,
|
|
164
|
+
Probe extends BoolReturningMethodKey<T>,
|
|
165
|
+
Action extends keyof T,
|
|
166
|
+
>(
|
|
167
|
+
providers: readonly T[],
|
|
168
|
+
probe: Probe,
|
|
169
|
+
action: Action,
|
|
170
|
+
): T[Action] extends (...args: infer A) => infer R
|
|
171
|
+
? (...args: A) => Promise<Awaited<R> | null>
|
|
172
|
+
: never {
|
|
173
|
+
const wrapper = async (...args: unknown[]): Promise<unknown> => {
|
|
174
|
+
const [first] = args
|
|
175
|
+
for (const p of providers) {
|
|
176
|
+
try {
|
|
177
|
+
const probeMember = Reflect.get(p, probe)
|
|
178
|
+
if (typeof probeMember !== 'function') continue
|
|
179
|
+
const supported: unknown = await Reflect.apply(probeMember, p, [first])
|
|
180
|
+
if (supported !== true) continue
|
|
181
|
+
const actionMember = Reflect.get(p, action)
|
|
182
|
+
if (typeof actionMember !== 'function') continue
|
|
183
|
+
const result: unknown = await Reflect.apply(actionMember, p, args)
|
|
184
|
+
if (result !== null && result !== undefined) return result
|
|
185
|
+
} catch {
|
|
186
|
+
// try next provider
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
// Type-level bridge — see concatCollection for the same pattern.
|
|
192
|
+
return wrapper as T[Action] extends (...args: infer A) => infer R
|
|
193
|
+
? (...args: A) => Promise<Awaited<R> | null>
|
|
194
|
+
: never
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convenience for collection caps that want a "logical OR of probes"
|
|
199
|
+
* (e.g. `supportsDevice` across every snapshot-provider).
|
|
200
|
+
*/
|
|
201
|
+
export function anySupports<
|
|
202
|
+
T extends object,
|
|
203
|
+
K extends BoolReturningMethodKey<T>,
|
|
204
|
+
>(
|
|
205
|
+
providers: readonly T[],
|
|
206
|
+
probe: K,
|
|
207
|
+
): T[K] extends (...args: infer A) => boolean | Promise<boolean>
|
|
208
|
+
? (...args: A) => Promise<boolean>
|
|
209
|
+
: never {
|
|
210
|
+
const wrapper = async (...args: unknown[]): Promise<boolean> => {
|
|
211
|
+
for (const p of providers) {
|
|
212
|
+
const member = Reflect.get(p, probe)
|
|
213
|
+
if (typeof member !== 'function') continue
|
|
214
|
+
try {
|
|
215
|
+
const result: unknown = await Reflect.apply(member, p, args)
|
|
216
|
+
if (result === true) return true
|
|
217
|
+
} catch { /* next */ }
|
|
218
|
+
}
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
// Type-level bridge — see concatCollection for the same pattern.
|
|
222
|
+
return wrapper as T[K] extends (...args: infer A) => boolean | Promise<boolean>
|
|
223
|
+
? (...args: A) => Promise<boolean>
|
|
224
|
+
: never
|
|
225
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core-capability mesh bridge.
|
|
3
|
+
*
|
|
4
|
+
* The hub's CORE routers — hand-written single-impl routers plus the
|
|
5
|
+
* handful of service-backed cap routers — are mounted only as the hub's
|
|
6
|
+
* tRPC `appRouter`. No addon registers a provider for them, so
|
|
7
|
+
* `addon-service-factory` never publishes a Moleculer service exposing
|
|
8
|
+
* their actions. A forked addon calling `ctx.api.<coreCap>.<method>`
|
|
9
|
+
* therefore falls through `localProviderLink` into `brokerTransportLink`,
|
|
10
|
+
* whose load-balanced discovery wait has no deadline — the call hangs
|
|
11
|
+
* forever (this was the `export-alexa` `redetectHubUrl` hang).
|
|
12
|
+
*
|
|
13
|
+
* `buildCoreCapService` walks the appRouter, picks the core namespaces,
|
|
14
|
+
* and wraps each query/mutation as a `$core-caps.<kebab-cap>.<method>`
|
|
15
|
+
* Moleculer action invoked through a trusted-mesh tRPC caller. With the
|
|
16
|
+
* service mounted on the hub node, `brokerTransportLink` resolves and
|
|
17
|
+
* routes the call exactly like any addon-provided cap.
|
|
18
|
+
*/
|
|
19
|
+
import type { ServiceSchema } from 'moleculer'
|
|
20
|
+
import { createCoreCapService, type CoreCapAction } from '@camstack/kernel'
|
|
21
|
+
import { createCallerFactory } from './trpc.middleware.js'
|
|
22
|
+
import { createMeshTrpcContext } from './trpc.context.js'
|
|
23
|
+
import type { AppRouter } from './trpc.router.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* appRouter namespaces that back the CORE API surface and must be
|
|
27
|
+
* reachable cross-process. This is the core-router list from
|
|
28
|
+
* `trpc.router.ts` (`buildCapabilityRouters`) minus the deliberate
|
|
29
|
+
* exclusions below.
|
|
30
|
+
*
|
|
31
|
+
* Excluded on purpose:
|
|
32
|
+
* - `auth` — issuing service / scoped tokens over the trusted mesh
|
|
33
|
+
* would let any forked addon mint admin credentials. Addons must
|
|
34
|
+
* not perform authentication operations; this stays hub-local.
|
|
35
|
+
* - `live`, `systemEvents` — listed in `NEVER_BRIDGED_CAPS`
|
|
36
|
+
* (`trpc-links.ts`). They are hub-only push streams, not
|
|
37
|
+
* request/reply, and the worker side fast-fails on them by design.
|
|
38
|
+
*/
|
|
39
|
+
const CORE_NAMESPACES: ReadonlySet<string> = new Set<string>([
|
|
40
|
+
// Service-backed cap routers (no addon provider).
|
|
41
|
+
'system',
|
|
42
|
+
'toast',
|
|
43
|
+
'integrations',
|
|
44
|
+
'nodes',
|
|
45
|
+
'addons',
|
|
46
|
+
// Hand-written single-impl core routers.
|
|
47
|
+
'capabilities',
|
|
48
|
+
'notifications',
|
|
49
|
+
'logs',
|
|
50
|
+
'hwaccel',
|
|
51
|
+
'streamProbe',
|
|
52
|
+
'settingsBackend',
|
|
53
|
+
'eventBusProxy',
|
|
54
|
+
'repl',
|
|
55
|
+
'addonSettingsRaw',
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* camelCase → kebab-case. Mirrors `toKebab` in `trpc-links.ts` so the
|
|
60
|
+
* action name matches what `brokerTransportLink` derives from `op.path`.
|
|
61
|
+
*/
|
|
62
|
+
function toKebab(name: string): string {
|
|
63
|
+
return name.replace(/[A-Z]/g, (m, i: number) => (i > 0 ? '-' : '') + m.toLowerCase())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A tRPC procedure node — distinguished from a router by `_def.procedure`. */
|
|
67
|
+
interface ProcedureNode {
|
|
68
|
+
readonly _def: { readonly procedure?: unknown; readonly type?: unknown }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isProcedureNode(value: unknown): value is ProcedureNode {
|
|
72
|
+
// A built tRPC procedure is a callable object — `_def.procedures`
|
|
73
|
+
// stores the procedure function directly, not a wrapper object.
|
|
74
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false
|
|
75
|
+
const def: unknown = (value as { _def?: unknown })._def
|
|
76
|
+
return def !== null && typeof def === 'object' && (def as { procedure?: unknown }).procedure === true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface DiscoveredProcedure {
|
|
80
|
+
readonly path: string
|
|
81
|
+
readonly type: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Recursively flatten a tRPC router record to dotted procedure paths.
|
|
86
|
+
* Robust to both layouts tRPC v11 may use for `_def.procedures`: a
|
|
87
|
+
* nested record of sub-routers, or a flat record already keyed by
|
|
88
|
+
* dotted path (a flat key simply yields its key verbatim).
|
|
89
|
+
*/
|
|
90
|
+
function collectProcedures(
|
|
91
|
+
record: Readonly<Record<string, unknown>>,
|
|
92
|
+
prefix: string,
|
|
93
|
+
out: DiscoveredProcedure[],
|
|
94
|
+
): void {
|
|
95
|
+
for (const [key, value] of Object.entries(record)) {
|
|
96
|
+
const path = prefix.length > 0 ? `${prefix}.${key}` : key
|
|
97
|
+
if (isProcedureNode(value)) {
|
|
98
|
+
const type = value._def.type
|
|
99
|
+
out.push({ path, type: typeof type === 'string' ? type : 'query' })
|
|
100
|
+
} else if (value !== null && typeof value === 'object') {
|
|
101
|
+
collectProcedures(value as Readonly<Record<string, unknown>>, path, out)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type ProcedureInvoker = (input: unknown) => Promise<unknown>
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Navigate the decorated caller record (a recursive proxy) to the
|
|
110
|
+
* procedure function at `path`. Every node on the proxy is callable,
|
|
111
|
+
* so navigation uses `Reflect.get` across both objects and functions.
|
|
112
|
+
*/
|
|
113
|
+
function resolveInvoker(caller: unknown, path: string): ProcedureInvoker | null {
|
|
114
|
+
let node: unknown = caller
|
|
115
|
+
for (const segment of path.split('.')) {
|
|
116
|
+
if (node === null || (typeof node !== 'object' && typeof node !== 'function')) return null
|
|
117
|
+
// Documented boundary: `node` is an object or function past the
|
|
118
|
+
// guard above; `Reflect.get` is typed for `object` targets.
|
|
119
|
+
node = Reflect.get(node as object, segment)
|
|
120
|
+
}
|
|
121
|
+
return typeof node === 'function' ? (node as ProcedureInvoker) : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build the `$core-caps` Moleculer service schema from the hub
|
|
126
|
+
* appRouter. Query + mutation procedures in {@link CORE_NAMESPACES} are
|
|
127
|
+
* exposed; subscriptions are skipped (the broker transport is
|
|
128
|
+
* request/reply only).
|
|
129
|
+
*/
|
|
130
|
+
export function buildCoreCapService(appRouter: AppRouter): ServiceSchema {
|
|
131
|
+
const meshCaller: unknown = createCallerFactory(appRouter)(createMeshTrpcContext())
|
|
132
|
+
|
|
133
|
+
const discovered: DiscoveredProcedure[] = []
|
|
134
|
+
collectProcedures(appRouter._def.procedures, '', discovered)
|
|
135
|
+
|
|
136
|
+
const actions: CoreCapAction[] = []
|
|
137
|
+
for (const { path, type } of discovered) {
|
|
138
|
+
const dot = path.indexOf('.')
|
|
139
|
+
if (dot < 0) continue
|
|
140
|
+
const namespace = path.slice(0, dot)
|
|
141
|
+
if (!CORE_NAMESPACES.has(namespace)) continue
|
|
142
|
+
if (type === 'subscription') continue
|
|
143
|
+
|
|
144
|
+
const invoke = resolveInvoker(meshCaller, path)
|
|
145
|
+
if (invoke === null) continue
|
|
146
|
+
|
|
147
|
+
const method = path.slice(dot + 1)
|
|
148
|
+
actions.push({ actionName: `${toKebab(namespace)}.${method}`, invoke })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return createCoreCapService({ actions })
|
|
152
|
+
}
|