@camstack/server 1.2.410 → 1.2.411
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.
|
@@ -80,7 +80,7 @@ const addon_package_service_js_1 = require("../../core/addon/addon-package.servi
|
|
|
80
80
|
const lifecycle_runner_singleton_js_1 = require("../../core/lifecycle/lifecycle-runner.singleton.js");
|
|
81
81
|
const agent_installed_packages_js_1 = require("../../core/updates/agent-installed-packages.js");
|
|
82
82
|
const http_request_census_singleton_js_1 = require("../trpc/http-request-census.singleton.js");
|
|
83
|
-
const
|
|
83
|
+
const capability_preference_writes_js_1 = require("./capability-preference-writes.js");
|
|
84
84
|
const failure_contributions_js_1 = require("./failure-contributions.js");
|
|
85
85
|
const load_contributions_js_1 = require("./load-contributions.js");
|
|
86
86
|
const logging_settings_js_1 = require("./logging-settings.js");
|
|
@@ -1444,38 +1444,14 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1444
1444
|
}));
|
|
1445
1445
|
},
|
|
1446
1446
|
setCapabilityProviderEnabled: async (input) => {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
if (found.mode !== 'collection') {
|
|
1457
|
-
throw new server_1.TRPCError({
|
|
1458
|
-
code: 'BAD_REQUEST',
|
|
1459
|
-
message: `Capability "${input.capName}" is not a collection`,
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
if (!found.providers.includes(input.addonId)) {
|
|
1463
|
-
throw new server_1.TRPCError({
|
|
1464
|
-
code: 'BAD_REQUEST',
|
|
1465
|
-
message: `Provider "${input.addonId}" is not registered for "${input.capName}"`,
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
if (input.enabled) {
|
|
1469
|
-
registry.enableCollectionProvider(input.capName, input.addonId);
|
|
1470
|
-
}
|
|
1471
|
-
else {
|
|
1472
|
-
registry.disableCollectionProvider(input.capName, input.addonId);
|
|
1473
|
-
}
|
|
1474
|
-
// Persist the new disabled-set so the choice survives a hub reboot.
|
|
1475
|
-
// Reuses the same `capabilities.collection.<cap>` key/format the
|
|
1476
|
-
// `capabilities` core router writes — via the shared canonical writer.
|
|
1477
|
-
const updated = registry.listCapabilities().find((c) => c.name === input.capName);
|
|
1478
|
-
await (0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
|
|
1447
|
+
// Refused (unknown cap, not a collection, unregistered provider) before
|
|
1448
|
+
// anything is written; otherwise the new disabled-set is PERSISTED first
|
|
1449
|
+
// and applied live only once the write landed (D675 amendment).
|
|
1450
|
+
await (0, capability_preference_writes_js_1.writeCollectionToggle)(configService, ar.getCapabilityRegistry(), {
|
|
1451
|
+
capability: input.capName,
|
|
1452
|
+
addonId: input.addonId,
|
|
1453
|
+
enabled: input.enabled,
|
|
1454
|
+
});
|
|
1479
1455
|
return { success: true };
|
|
1480
1456
|
},
|
|
1481
1457
|
getVersions: async (input) => ps.getPackageVersions(input.name),
|
|
@@ -9,6 +9,7 @@ exports.createCapabilitiesRouter = createCapabilitiesRouter;
|
|
|
9
9
|
*/
|
|
10
10
|
const zod_1 = require("zod");
|
|
11
11
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
12
|
+
const capability_preference_writes_js_1 = require("./capability-preference-writes.js");
|
|
12
13
|
function createCapabilitiesRouter(registry, config) {
|
|
13
14
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
14
15
|
listCapabilities: trpc_middleware_js_1.adminProcedure
|
|
@@ -29,17 +30,14 @@ function createCapabilitiesRouter(registry, config) {
|
|
|
29
30
|
.mutation(async ({ input }) => {
|
|
30
31
|
if (!registry)
|
|
31
32
|
throw new Error('Capability registry unavailable');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// (addon-registry `setConfigReader` → `capabilities.singleton.<cap>`).
|
|
41
|
-
await config.set(`capabilities.singleton.${input.capability}`, input.addonId);
|
|
42
|
-
}
|
|
33
|
+
// Persist FIRST, under the key the boot restorer reads
|
|
34
|
+
// (`capabilities.singleton.<cap>` / `capabilities.singletonNode.<cap>.<node>`),
|
|
35
|
+
// then switch live: a write that fails changes nothing (D675 amendment).
|
|
36
|
+
await (0, capability_preference_writes_js_1.writeSingletonPreference)(config, registry, {
|
|
37
|
+
capability: input.capability,
|
|
38
|
+
addonId: input.addonId,
|
|
39
|
+
...(input.nodeId !== undefined ? { nodeId: input.nodeId } : {}),
|
|
40
|
+
});
|
|
43
41
|
}),
|
|
44
42
|
clearSingletonNodeOverride: trpc_middleware_js_1.adminProcedure
|
|
45
43
|
.input(zod_1.z.object({ capability: zod_1.z.string(), nodeId: zod_1.z.string() }))
|
|
@@ -47,10 +45,10 @@ function createCapabilitiesRouter(registry, config) {
|
|
|
47
45
|
.mutation(async ({ input }) => {
|
|
48
46
|
if (!registry)
|
|
49
47
|
throw new Error('Capability registry unavailable');
|
|
48
|
+
// Persist the cleared state as null FIRST so the boot restorer doesn't
|
|
49
|
+
// re-apply a stale override; only a write that landed clears it live.
|
|
50
|
+
await config.set((0, capability_preference_writes_js_1.singletonPreferenceKey)(input.capability, input.nodeId), null);
|
|
50
51
|
registry.clearSingletonNodeOverride(input.capability, input.nodeId);
|
|
51
|
-
// Persist the cleared state as null so the boot restorer doesn't re-apply
|
|
52
|
-
// a stale override on restart.
|
|
53
|
-
await config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, null);
|
|
54
52
|
}),
|
|
55
53
|
/**
|
|
56
54
|
* Set the enabled set of providers for a collection capability in
|
|
@@ -65,25 +63,13 @@ function createCapabilitiesRouter(registry, config) {
|
|
|
65
63
|
enabledAddonIds: zod_1.z.array(zod_1.z.string()),
|
|
66
64
|
}))
|
|
67
65
|
.output(zod_1.z.void())
|
|
68
|
-
.mutation(({ input }) => {
|
|
66
|
+
.mutation(async ({ input }) => {
|
|
69
67
|
if (!registry)
|
|
70
68
|
throw new Error('Capability registry unavailable');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (entry.mode !== 'collection') {
|
|
76
|
-
throw new Error(`Capability "${input.capability}" is not a collection`);
|
|
77
|
-
}
|
|
78
|
-
const wanted = new Set(input.enabledAddonIds);
|
|
79
|
-
for (const providerId of entry.providers) {
|
|
80
|
-
if (wanted.has(providerId)) {
|
|
81
|
-
registry.enableCollectionProvider(input.capability, providerId);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
registry.disableCollectionProvider(input.capability, providerId);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
69
|
+
// Persisted under `capabilities.collection.<cap>` BEFORE the live
|
|
70
|
+
// switch. It used to switch live only, so the operator's pick
|
|
71
|
+
// reverted at the next boot (D62's defect, D675 amendment).
|
|
72
|
+
await (0, capability_preference_writes_js_1.writeCollectionEnabledSet)(config, registry, input.capability, input.enabledAddonIds);
|
|
87
73
|
}),
|
|
88
74
|
setDeviceCapability: trpc_middleware_js_1.adminProcedure
|
|
89
75
|
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string(), addonId: zod_1.z.string() }))
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.singletonPreferenceKey = singletonPreferenceKey;
|
|
4
|
+
exports.singletonSwitchRefusal = singletonSwitchRefusal;
|
|
5
|
+
exports.writeSingletonPreference = writeSingletonPreference;
|
|
6
|
+
exports.writeCollectionToggle = writeCollectionToggle;
|
|
7
|
+
exports.writeCollectionEnabledSet = writeCollectionEnabledSet;
|
|
8
|
+
/**
|
|
9
|
+
* The ONE write order for an operator's capability preference: PERSIST, then
|
|
10
|
+
* switch the live registry (D675 amendment, 2026-09-29).
|
|
11
|
+
*
|
|
12
|
+
* Every caller used to switch the registry first and persist second. Since
|
|
13
|
+
* D675 `ConfigManager.set` awaits the settings door and REJECTS when the write
|
|
14
|
+
* does not land, so that order produced a mutation that reported an error
|
|
15
|
+
* while the live switch had already happened — and silently reverted at the
|
|
16
|
+
* next boot, because the boot restorer reads the store the write never
|
|
17
|
+
* reached. The operator saw an error for a change that was visibly in effect.
|
|
18
|
+
*
|
|
19
|
+
* Here a refused switch (unknown cap, wrong mode, provider not registered)
|
|
20
|
+
* persists NOTHING, a failed write changes NOTHING live, and only a write that
|
|
21
|
+
* landed is applied. Writes to one capability are serialised, because a
|
|
22
|
+
* collection write computes its next disabled-set from the live one: two
|
|
23
|
+
* toggles planned against the same base would each persist half of the
|
|
24
|
+
* answer.
|
|
25
|
+
*
|
|
26
|
+
* Callers: `capabilities` core router (`setActiveSingleton`,
|
|
27
|
+
* `clearSingletonNodeOverride` via `singletonPreferenceKey`,
|
|
28
|
+
* `setCollectionEnabledProviders`) and the `addons` cap's
|
|
29
|
+
* `setCapabilityProviderEnabled`.
|
|
30
|
+
*/
|
|
31
|
+
const server_1 = require("@trpc/server");
|
|
32
|
+
const collection_preference_1 = require("./collection-preference");
|
|
33
|
+
/** ConfigService key holding a singleton preference (global or per-node). */
|
|
34
|
+
function singletonPreferenceKey(capability, nodeId) {
|
|
35
|
+
return nodeId === undefined
|
|
36
|
+
? `capabilities.singleton.${capability}`
|
|
37
|
+
: `capabilities.singletonNode.${capability}.${nodeId}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One write chain per capability, MODULE-LEVEL on purpose: the routers that
|
|
41
|
+
* write preferences are built per request, so the chain cannot live on them.
|
|
42
|
+
* It serialises only within this process — correct because only hub-main
|
|
43
|
+
* mounts these routers; a second process writing preferences would need the
|
|
44
|
+
* store itself to arbitrate.
|
|
45
|
+
*/
|
|
46
|
+
const writeChains = new Map();
|
|
47
|
+
/** Run `work` after every earlier write to the same capability has settled. */
|
|
48
|
+
async function serialised(capability, work) {
|
|
49
|
+
const previous = writeChains.get(capability) ?? Promise.resolve();
|
|
50
|
+
const next = previous.then(work, work);
|
|
51
|
+
const settled = next.then(() => undefined, () => undefined);
|
|
52
|
+
writeChains.set(capability, settled);
|
|
53
|
+
try {
|
|
54
|
+
await next;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (writeChains.get(capability) === settled)
|
|
58
|
+
writeChains.delete(capability);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function findCapability(registry, capability) {
|
|
62
|
+
return registry.listCapabilities().find((c) => c.name === capability) ?? null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The refusal `setActiveSingleton` would raise, asked BEFORE anything is
|
|
66
|
+
* persisted — so a switch the registry would refuse never reaches the store.
|
|
67
|
+
* Reads the registry's own projection (`providers`, `perNode`), the same sets
|
|
68
|
+
* `setActiveSingleton` checks.
|
|
69
|
+
*/
|
|
70
|
+
function singletonSwitchRefusal(registry, write) {
|
|
71
|
+
const cap = findCapability(registry, write.capability);
|
|
72
|
+
if (cap === null) {
|
|
73
|
+
return { code: 'NOT_FOUND', message: `Unknown capability: ${write.capability}` };
|
|
74
|
+
}
|
|
75
|
+
if (cap.mode !== 'singleton') {
|
|
76
|
+
return { code: 'BAD_REQUEST', message: `"${write.capability}" is not a singleton` };
|
|
77
|
+
}
|
|
78
|
+
if (write.nodeId !== undefined) {
|
|
79
|
+
const onNode = cap.perNode?.find((n) => n.nodeId === write.nodeId);
|
|
80
|
+
if (onNode === undefined || !onNode.providers.includes(write.addonId)) {
|
|
81
|
+
return {
|
|
82
|
+
code: 'BAD_REQUEST',
|
|
83
|
+
message: `Provider "${write.addonId}" is not available on node "${write.nodeId}" for "${write.capability}"`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!cap.providers.includes(write.addonId)) {
|
|
89
|
+
return {
|
|
90
|
+
code: 'BAD_REQUEST',
|
|
91
|
+
message: `Provider "${write.addonId}" is not registered for "${write.capability}"`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function refuse(refusal) {
|
|
97
|
+
throw new server_1.TRPCError({ code: refusal.code, message: refusal.message });
|
|
98
|
+
}
|
|
99
|
+
/** Persist a singleton preference, then switch the registry. */
|
|
100
|
+
async function writeSingletonPreference(store, registry, write) {
|
|
101
|
+
await serialised(write.capability, async () => {
|
|
102
|
+
const refusal = singletonSwitchRefusal(registry, write);
|
|
103
|
+
if (refusal !== null)
|
|
104
|
+
refuse(refusal);
|
|
105
|
+
await store.set(singletonPreferenceKey(write.capability, write.nodeId), write.addonId);
|
|
106
|
+
await registry.setActiveSingleton(write.capability, write.addonId, write.nodeId);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function requireCollection(registry, capability) {
|
|
110
|
+
const cap = findCapability(registry, capability);
|
|
111
|
+
if (cap === null)
|
|
112
|
+
refuse({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` });
|
|
113
|
+
if (cap.mode !== 'collection') {
|
|
114
|
+
refuse({ code: 'BAD_REQUEST', message: `"${capability}" is not a collection` });
|
|
115
|
+
}
|
|
116
|
+
return cap;
|
|
117
|
+
}
|
|
118
|
+
/** Switch the live registry from `current` to `next` — only after the write landed. */
|
|
119
|
+
function applyDisabledSet(registry, capability, current, next) {
|
|
120
|
+
for (const addonId of next) {
|
|
121
|
+
if (!current.has(addonId))
|
|
122
|
+
registry.disableCollectionProvider(capability, addonId);
|
|
123
|
+
}
|
|
124
|
+
for (const addonId of current) {
|
|
125
|
+
if (!next.has(addonId))
|
|
126
|
+
registry.enableCollectionProvider(capability, addonId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Persist one collection provider's enable/disable, then apply it live. */
|
|
130
|
+
async function writeCollectionToggle(store, registry, toggle) {
|
|
131
|
+
await serialised(toggle.capability, async () => {
|
|
132
|
+
const cap = requireCollection(registry, toggle.capability);
|
|
133
|
+
if (!cap.providers.includes(toggle.addonId)) {
|
|
134
|
+
refuse({
|
|
135
|
+
code: 'BAD_REQUEST',
|
|
136
|
+
message: `Provider "${toggle.addonId}" is not registered for "${toggle.capability}"`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const current = new Set(cap.disabledProviders);
|
|
140
|
+
const next = new Set(current);
|
|
141
|
+
if (toggle.enabled)
|
|
142
|
+
next.delete(toggle.addonId);
|
|
143
|
+
else
|
|
144
|
+
next.add(toggle.addonId);
|
|
145
|
+
await (0, collection_preference_1.persistCollectionDisabled)(store, toggle.capability, [...next]);
|
|
146
|
+
applyDisabledSet(registry, toggle.capability, current, next);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Persist the enabled set of a collection in one shot, then apply it live.
|
|
151
|
+
* Registered providers outside `enabledAddonIds` are disabled; a disabled id
|
|
152
|
+
* whose provider is not registered right now keeps its disabled mark.
|
|
153
|
+
*/
|
|
154
|
+
async function writeCollectionEnabledSet(store, registry, capability, enabledAddonIds) {
|
|
155
|
+
await serialised(capability, async () => {
|
|
156
|
+
const cap = requireCollection(registry, capability);
|
|
157
|
+
const wanted = new Set(enabledAddonIds);
|
|
158
|
+
const current = new Set(cap.disabledProviders);
|
|
159
|
+
const next = new Set(current);
|
|
160
|
+
for (const providerId of cap.providers) {
|
|
161
|
+
if (wanted.has(providerId))
|
|
162
|
+
next.delete(providerId);
|
|
163
|
+
else
|
|
164
|
+
next.add(providerId);
|
|
165
|
+
}
|
|
166
|
+
await (0, collection_preference_1.persistCollectionDisabled)(store, capability, [...next]);
|
|
167
|
+
applyDisabledSet(registry, capability, current, next);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -7,10 +7,8 @@ function collectionPreferenceKey(capability) {
|
|
|
7
7
|
return `capabilities.collection.${capability}`;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Persist the disabled-addonId set for a collection capability
|
|
11
|
-
*
|
|
12
|
-
* enable/disable mutation so the stored value always mirrors the live
|
|
13
|
-
* registry state.
|
|
10
|
+
* Persist the disabled-addonId set for a collection capability: the set the
|
|
11
|
+
* registry WILL hold once the write has landed and is applied.
|
|
14
12
|
*/
|
|
15
13
|
async function persistCollectionDisabled(configService, capability, disabled) {
|
|
16
14
|
await configService.set(collectionPreferenceKey(capability), JSON.stringify({ disabled: [...disabled] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.411",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@camstack/addon-pipeline-orchestrator": "1.2.262",
|
|
47
47
|
"@camstack/addon-post-analysis": "1.2.297",
|
|
48
48
|
"@camstack/sdk": "1.2.118",
|
|
49
|
-
"@camstack/system": "1.2.
|
|
49
|
+
"@camstack/system": "1.2.319",
|
|
50
50
|
"@camstack/types": "1.2.272",
|
|
51
51
|
"@camstack/ui-library": "1.2.213",
|
|
52
52
|
"@fastify/compress": "^9.0.0",
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createCapabilitiesRouter = createCapabilitiesRouter;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const server_1 = require("@trpc/server");
|
|
6
|
-
const trpc_middleware_1 = require("./trpc/trpc.middleware");
|
|
7
|
-
const system_1 = require("@camstack/system");
|
|
8
|
-
const collection_preference_1 = require("./core/collection-preference");
|
|
9
|
-
// ─── Zod Schemas ───────────────────────────────────────────────────────────
|
|
10
|
-
const setPreferenceInput = zod_1.z.discriminatedUnion('mode', [
|
|
11
|
-
zod_1.z.object({
|
|
12
|
-
mode: zod_1.z.literal('singleton'),
|
|
13
|
-
capability: zod_1.z.string(),
|
|
14
|
-
addonId: zod_1.z.string(),
|
|
15
|
-
}),
|
|
16
|
-
zod_1.z.object({
|
|
17
|
-
mode: zod_1.z.literal('collection'),
|
|
18
|
-
capability: zod_1.z.string(),
|
|
19
|
-
addonId: zod_1.z.string(),
|
|
20
|
-
enabled: zod_1.z.boolean(),
|
|
21
|
-
}),
|
|
22
|
-
]);
|
|
23
|
-
// ─── Router ────────────────────────────────────────────────────────────────
|
|
24
|
-
function createCapabilitiesRouter(addonRegistry, configService, loggingService) {
|
|
25
|
-
const logger = loggingService.createLogger('CapabilitiesRouter');
|
|
26
|
-
/** Build enriched provider details from addon metadata */
|
|
27
|
-
function getProviderDetails(addonIds) {
|
|
28
|
-
const allAddons = addonRegistry.listAllAddons();
|
|
29
|
-
return addonIds.map((addonId) => {
|
|
30
|
-
const addon = allAddons.find((a) => a.manifest.id === addonId);
|
|
31
|
-
return {
|
|
32
|
-
addonId,
|
|
33
|
-
displayName: addon?.manifest.name ?? addonId,
|
|
34
|
-
packageName: addon?.manifest.packageName ?? addonId,
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return (0, trpc_middleware_1.trpcRouter)({
|
|
39
|
-
// ─── List all capabilities with enriched metadata ──────────────────
|
|
40
|
-
list: trpc_middleware_1.adminProcedure.query(() => {
|
|
41
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
42
|
-
const caps = registry.listCapabilities();
|
|
43
|
-
return caps.map((cap) => ({
|
|
44
|
-
...cap,
|
|
45
|
-
providerDetails: getProviderDetails(cap.providers),
|
|
46
|
-
isInfra: (0, system_1.isInfraCapability)(cap.name),
|
|
47
|
-
}));
|
|
48
|
-
}),
|
|
49
|
-
// ─── Get current preference for a capability ──────────────────────
|
|
50
|
-
//
|
|
51
|
-
// `state` separates "the operator set nothing" (`known`, `preference`
|
|
52
|
-
// null / no disabled ids) from "the persisted preference cannot be read"
|
|
53
|
-
// (`unknown` — the boot read of the settings store is pending or failed,
|
|
54
|
-
// D675). The latter used to be reported as `null`: an unset preference.
|
|
55
|
-
getPreference: trpc_middleware_1.adminProcedure.input(zod_1.z.object({ capability: zod_1.z.string() })).query(({ input }) => {
|
|
56
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
57
|
-
const mode = registry.getMode(input.capability);
|
|
58
|
-
if (!mode)
|
|
59
|
-
return null;
|
|
60
|
-
let raw;
|
|
61
|
-
try {
|
|
62
|
-
raw = configService.get(mode === 'singleton'
|
|
63
|
-
? `capabilities.singleton.${input.capability}`
|
|
64
|
-
: (0, collection_preference_1.collectionPreferenceKey)(input.capability));
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
if (!(0, system_1.isSettingsReadUnavailable)(err))
|
|
68
|
-
throw err;
|
|
69
|
-
return {
|
|
70
|
-
capability: input.capability,
|
|
71
|
-
mode,
|
|
72
|
-
state: 'unknown',
|
|
73
|
-
reason: err.reason,
|
|
74
|
-
preference: null,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
if (mode === 'singleton') {
|
|
78
|
-
return {
|
|
79
|
-
capability: input.capability,
|
|
80
|
-
mode: 'singleton',
|
|
81
|
-
state: 'known',
|
|
82
|
-
preference: raw ? { addonId: raw } : null,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// collection
|
|
86
|
-
let disabled = [];
|
|
87
|
-
if (raw) {
|
|
88
|
-
try {
|
|
89
|
-
const parsed = JSON.parse(raw);
|
|
90
|
-
if (typeof parsed === 'object' && parsed !== null && 'disabled' in parsed) {
|
|
91
|
-
const list = parsed.disabled;
|
|
92
|
-
disabled = Array.isArray(list)
|
|
93
|
-
? list.filter((id) => typeof id === 'string')
|
|
94
|
-
: [];
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
/* ignore malformed */
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
capability: input.capability,
|
|
103
|
-
mode: 'collection',
|
|
104
|
-
state: 'known',
|
|
105
|
-
preference: { disabled },
|
|
106
|
-
};
|
|
107
|
-
}),
|
|
108
|
-
// ─── Set preference (singleton switch or collection toggle) ───────
|
|
109
|
-
setPreference: trpc_middleware_1.adminProcedure.input(setPreferenceInput).mutation(async ({ input }) => {
|
|
110
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
111
|
-
if (input.mode === 'singleton') {
|
|
112
|
-
const { capability, addonId } = input;
|
|
113
|
-
const caps = registry.listCapabilities();
|
|
114
|
-
const cap = caps.find((c) => c.name === capability);
|
|
115
|
-
if (!cap)
|
|
116
|
-
throw new server_1.TRPCError({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` });
|
|
117
|
-
if (cap.mode !== 'singleton')
|
|
118
|
-
throw new server_1.TRPCError({
|
|
119
|
-
code: 'BAD_REQUEST',
|
|
120
|
-
message: `"${capability}" is not a singleton`,
|
|
121
|
-
});
|
|
122
|
-
if (!cap.providers.includes(addonId)) {
|
|
123
|
-
throw new server_1.TRPCError({
|
|
124
|
-
code: 'BAD_REQUEST',
|
|
125
|
-
message: `Provider "${addonId}" is not registered for "${capability}"`,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
const requiresRestart = (0, system_1.isInfraCapability)(capability);
|
|
129
|
-
if (!requiresRestart) {
|
|
130
|
-
// Hot-swap at runtime
|
|
131
|
-
await registry.setActiveSingleton(capability, addonId);
|
|
132
|
-
}
|
|
133
|
-
// Persist preference
|
|
134
|
-
await configService.set(`capabilities.singleton.${capability}`, addonId);
|
|
135
|
-
logger.info('Singleton preference set', {
|
|
136
|
-
tags: { addonId },
|
|
137
|
-
meta: { capability, requiresRestart },
|
|
138
|
-
});
|
|
139
|
-
return { success: true, requiresRestart };
|
|
140
|
-
}
|
|
141
|
-
// collection toggle
|
|
142
|
-
const { capability, addonId, enabled } = input;
|
|
143
|
-
const caps = registry.listCapabilities();
|
|
144
|
-
const cap = caps.find((c) => c.name === capability);
|
|
145
|
-
if (!cap)
|
|
146
|
-
throw new server_1.TRPCError({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` });
|
|
147
|
-
if (cap.mode !== 'collection')
|
|
148
|
-
throw new server_1.TRPCError({ code: 'BAD_REQUEST', message: `"${capability}" is not a collection` });
|
|
149
|
-
if (!cap.providers.includes(addonId)) {
|
|
150
|
-
throw new server_1.TRPCError({
|
|
151
|
-
code: 'BAD_REQUEST',
|
|
152
|
-
message: `Provider "${addonId}" is not registered for "${capability}"`,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (enabled) {
|
|
156
|
-
registry.enableCollectionProvider(capability, addonId);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
registry.disableCollectionProvider(capability, addonId);
|
|
160
|
-
}
|
|
161
|
-
// Persist disabled list via the shared canonical writer.
|
|
162
|
-
const updatedCap = registry.listCapabilities().find((c) => c.name === capability);
|
|
163
|
-
await (0, collection_preference_1.persistCollectionDisabled)(configService, capability, updatedCap?.disabledProviders ?? []);
|
|
164
|
-
logger.info('Collection provider toggled', {
|
|
165
|
-
tags: { addonId },
|
|
166
|
-
meta: { capability, enabled },
|
|
167
|
-
});
|
|
168
|
-
return { success: true, requiresRestart: false };
|
|
169
|
-
}),
|
|
170
|
-
// ─── Reset preference to default ──────────────────────────────────
|
|
171
|
-
resetPreference: trpc_middleware_1.adminProcedure
|
|
172
|
-
.input(zod_1.z.object({ capability: zod_1.z.string() }))
|
|
173
|
-
.mutation(async ({ input }) => {
|
|
174
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
175
|
-
const mode = registry.getMode(input.capability);
|
|
176
|
-
if (!mode)
|
|
177
|
-
throw new server_1.TRPCError({
|
|
178
|
-
code: 'NOT_FOUND',
|
|
179
|
-
message: `Unknown capability: ${input.capability}`,
|
|
180
|
-
});
|
|
181
|
-
if (mode === 'singleton') {
|
|
182
|
-
await configService.set(`capabilities.singleton.${input.capability}`, null);
|
|
183
|
-
logger.info('Singleton preference reset (takes effect on restart)', {
|
|
184
|
-
meta: { capability: input.capability },
|
|
185
|
-
});
|
|
186
|
-
return { success: true, requiresRestart: true };
|
|
187
|
-
}
|
|
188
|
-
// collection: re-enable all disabled providers
|
|
189
|
-
const caps = registry.listCapabilities();
|
|
190
|
-
const cap = caps.find((c) => c.name === input.capability);
|
|
191
|
-
if (cap) {
|
|
192
|
-
for (const addonId of cap.disabledProviders) {
|
|
193
|
-
registry.enableCollectionProvider(input.capability, addonId);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
await configService.set(`capabilities.collection.${input.capability}`, null);
|
|
197
|
-
logger.info('Collection preference reset (all providers re-enabled)', {
|
|
198
|
-
meta: { capability: input.capability },
|
|
199
|
-
});
|
|
200
|
-
return { success: true, requiresRestart: false };
|
|
201
|
-
}),
|
|
202
|
-
// ─── Per-device overrides (existing, unchanged) ───────────────────
|
|
203
|
-
listCapabilities: trpc_middleware_1.adminProcedure.query(() => {
|
|
204
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
205
|
-
return registry.listCapabilities();
|
|
206
|
-
}),
|
|
207
|
-
setDeviceCapability: trpc_middleware_1.adminProcedure
|
|
208
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string(), addonId: zod_1.z.string() }))
|
|
209
|
-
.mutation(({ input }) => {
|
|
210
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
211
|
-
registry.setDeviceOverride(input.deviceId, input.capability, input.addonId);
|
|
212
|
-
logger.info('Device capability override set', {
|
|
213
|
-
tags: { deviceId: Number(input.deviceId), addonId: input.addonId },
|
|
214
|
-
meta: { capability: input.capability },
|
|
215
|
-
});
|
|
216
|
-
}),
|
|
217
|
-
clearDeviceCapability: trpc_middleware_1.adminProcedure
|
|
218
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string() }))
|
|
219
|
-
.mutation(({ input }) => {
|
|
220
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
221
|
-
registry.clearDeviceOverride(input.deviceId, input.capability);
|
|
222
|
-
logger.info('Device capability override cleared', {
|
|
223
|
-
tags: { deviceId: Number(input.deviceId) },
|
|
224
|
-
meta: { capability: input.capability },
|
|
225
|
-
});
|
|
226
|
-
}),
|
|
227
|
-
getDeviceCapabilities: trpc_middleware_1.adminProcedure
|
|
228
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string() }))
|
|
229
|
-
.output(zod_1.z.record(zod_1.z.string(), zod_1.z.string()))
|
|
230
|
-
.query(({ input }) => {
|
|
231
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
232
|
-
const overrides = registry.getDeviceOverrides(input.deviceId);
|
|
233
|
-
return Object.fromEntries(overrides);
|
|
234
|
-
}),
|
|
235
|
-
/*
|
|
236
|
-
* `setDeviceCollectionFilter` / `clearDeviceCollectionFilter` were REMOVED
|
|
237
|
-
* here (D556). They wrote an in-memory per-device allow list that nothing
|
|
238
|
-
* persisted, so an operator's pick survived until the next hub restart and
|
|
239
|
-
* then silently reverted — a control that looks like it works, which is the
|
|
240
|
-
* defect D62 exists for. Which sources a camera has is now asked of the ONE
|
|
241
|
-
* authority that already owned a per-camera function switch:
|
|
242
|
-
* `deviceManager.setWrapperActive`. The registry keeps only its projection,
|
|
243
|
-
* written by `device-source-activation.ts` and by nothing else.
|
|
244
|
-
*/
|
|
245
|
-
});
|
|
246
|
-
}
|