@camstack/server 1.2.407 → 1.2.409
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/dist/api/capabilities.router.js +38 -11
- package/dist/api/core/addon-settings.router.js +68 -114
- package/dist/api/core/cap-providers.js +1 -1
- package/dist/api/core/capabilities.router.js +4 -4
- package/dist/api/core/collection-preference.js +2 -2
- package/dist/api/trpc/generated-cap-routers.js +9 -0
- package/dist/core/addon/addon-registry.service.js +23 -43
- package/dist/core/addon/settings-door-wiring.js +45 -0
- package/dist/core/moleculer/moleculer.service.js +8 -3
- package/package.json +8 -8
|
@@ -47,26 +47,52 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
47
47
|
}));
|
|
48
48
|
}),
|
|
49
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.
|
|
50
55
|
getPreference: trpc_middleware_1.adminProcedure.input(zod_1.z.object({ capability: zod_1.z.string() })).query(({ input }) => {
|
|
51
56
|
const registry = addonRegistry.getCapabilityRegistry();
|
|
52
57
|
const mode = registry.getMode(input.capability);
|
|
53
58
|
if (!mode)
|
|
54
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
|
+
}
|
|
55
77
|
if (mode === 'singleton') {
|
|
56
|
-
const addonId = configService.get(`capabilities.singleton.${input.capability}`);
|
|
57
78
|
return {
|
|
58
79
|
capability: input.capability,
|
|
59
|
-
mode:
|
|
60
|
-
|
|
80
|
+
mode: 'singleton',
|
|
81
|
+
state: 'known',
|
|
82
|
+
preference: raw ? { addonId: raw } : null,
|
|
61
83
|
};
|
|
62
84
|
}
|
|
63
85
|
// collection
|
|
64
|
-
const raw = configService.get((0, collection_preference_1.collectionPreferenceKey)(input.capability));
|
|
65
86
|
let disabled = [];
|
|
66
87
|
if (raw) {
|
|
67
88
|
try {
|
|
68
89
|
const parsed = JSON.parse(raw);
|
|
69
|
-
|
|
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
|
+
}
|
|
70
96
|
}
|
|
71
97
|
catch {
|
|
72
98
|
/* ignore malformed */
|
|
@@ -74,7 +100,8 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
74
100
|
}
|
|
75
101
|
return {
|
|
76
102
|
capability: input.capability,
|
|
77
|
-
mode:
|
|
103
|
+
mode: 'collection',
|
|
104
|
+
state: 'known',
|
|
78
105
|
preference: { disabled },
|
|
79
106
|
};
|
|
80
107
|
}),
|
|
@@ -104,7 +131,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
104
131
|
await registry.setActiveSingleton(capability, addonId);
|
|
105
132
|
}
|
|
106
133
|
// Persist preference
|
|
107
|
-
configService.set(`capabilities.singleton.${capability}`, addonId);
|
|
134
|
+
await configService.set(`capabilities.singleton.${capability}`, addonId);
|
|
108
135
|
logger.info('Singleton preference set', {
|
|
109
136
|
tags: { addonId },
|
|
110
137
|
meta: { capability, requiresRestart },
|
|
@@ -133,7 +160,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
133
160
|
}
|
|
134
161
|
// Persist disabled list via the shared canonical writer.
|
|
135
162
|
const updatedCap = registry.listCapabilities().find((c) => c.name === capability);
|
|
136
|
-
(0, collection_preference_1.persistCollectionDisabled)(configService, capability, updatedCap?.disabledProviders ?? []);
|
|
163
|
+
await (0, collection_preference_1.persistCollectionDisabled)(configService, capability, updatedCap?.disabledProviders ?? []);
|
|
137
164
|
logger.info('Collection provider toggled', {
|
|
138
165
|
tags: { addonId },
|
|
139
166
|
meta: { capability, enabled },
|
|
@@ -143,7 +170,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
143
170
|
// ─── Reset preference to default ──────────────────────────────────
|
|
144
171
|
resetPreference: trpc_middleware_1.adminProcedure
|
|
145
172
|
.input(zod_1.z.object({ capability: zod_1.z.string() }))
|
|
146
|
-
.mutation(({ input }) => {
|
|
173
|
+
.mutation(async ({ input }) => {
|
|
147
174
|
const registry = addonRegistry.getCapabilityRegistry();
|
|
148
175
|
const mode = registry.getMode(input.capability);
|
|
149
176
|
if (!mode)
|
|
@@ -152,7 +179,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
152
179
|
message: `Unknown capability: ${input.capability}`,
|
|
153
180
|
});
|
|
154
181
|
if (mode === 'singleton') {
|
|
155
|
-
configService.set(`capabilities.singleton.${input.capability}`, null);
|
|
182
|
+
await configService.set(`capabilities.singleton.${input.capability}`, null);
|
|
156
183
|
logger.info('Singleton preference reset (takes effect on restart)', {
|
|
157
184
|
meta: { capability: input.capability },
|
|
158
185
|
});
|
|
@@ -166,7 +193,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
166
193
|
registry.enableCollectionProvider(input.capability, addonId);
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
|
-
configService.set(`capabilities.collection.${input.capability}`, null);
|
|
196
|
+
await configService.set(`capabilities.collection.${input.capability}`, null);
|
|
170
197
|
logger.info('Collection preference reset (all providers re-enabled)', {
|
|
171
198
|
meta: { capability: input.capability },
|
|
172
199
|
});
|
|
@@ -2,153 +2,107 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createAddonSettingsRouter = createAddonSettingsRouter;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* `addonSettingsRaw` — admin read of an addon's RAW stored settings.
|
|
6
6
|
*
|
|
7
|
-
* ##
|
|
7
|
+
* ## It reads the same rows `ctx.settings` does — through the same door
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* There is ONE store: the `addon-settings` collection, one row per key,
|
|
10
|
+
* `id = "<addonId>.<key>"`, `data = {addonId, key, value}` (and
|
|
11
|
+
* `addon-device-settings`, `id = "<addonId>:<deviceId>.<key>"`, for the
|
|
12
|
+
* per-device overrides). An in-process addon's `ctx.settings` reads exactly
|
|
13
|
+
* those rows.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Until D675 this router read them through `ConfigManager.getAddonConfig`, a
|
|
16
|
+
* SYNCHRONOUS reader that needs an in-process `ISettingsStore`. Since D231
|
|
17
|
+
* isolated `sqlite-settings`, hub-main never has one — only the async door — so
|
|
18
|
+
* `getAddonConfig` fell back to the bootstrap file, which holds no addon key,
|
|
19
|
+
* and every `getGlobal` answered `{}` as a success. That `{}` was once
|
|
20
|
+
* explained here as "two different physical stores wear the name
|
|
21
|
+
* addon-settings". That diagnosis was wrong: the rows were exactly where this
|
|
22
|
+
* router looked; the reader simply had no backend. It sent at least one
|
|
23
|
+
* session hunting for a key mismatch that did not exist.
|
|
18
24
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* read
|
|
23
|
-
*
|
|
24
|
-
* `addonSettingsRaw.getGlobal` answering `{}` for an addon that plainly has
|
|
25
|
-
* settings is the tell.
|
|
25
|
+
* Now both reads go through the settings door, THREE-WAY:
|
|
26
|
+
* - the stored record, when there is one;
|
|
27
|
+
* - `{}` when the store ANSWERED and holds nothing for this addon/device;
|
|
28
|
+
* - a `SERVICE_UNAVAILABLE` error when the store could not be read — never
|
|
29
|
+
* `{}`, which would be "never written" to a caller that asked (D315, D393).
|
|
26
30
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
31
|
+
* A forked addon's settings live in its own namespaced blob (`namespace
|
|
32
|
+
* <addonId>`, collection `addon-settings`, key `root`), which this router does
|
|
33
|
+
* not read — use the `addonSettings` cap (`getGlobalSettings`) for an addon's
|
|
34
|
+
* hydrated settings, whichever process it runs in.
|
|
35
|
+
*
|
|
36
|
+
* The write methods (`updateGlobal` / `updateDevice` / `replaceGlobal`) are
|
|
37
|
+
* GONE (D675): they had no callers, threw `SettingsStore not initialized` on
|
|
38
|
+
* every isolated-engine hub, and bypassed the schema gate. Write an addon's
|
|
39
|
+
* settings through the `addonSettings` cap (`updateGlobalSettings`, which also
|
|
40
|
+
* applies per-node scoping).
|
|
30
41
|
*
|
|
31
42
|
* ## ⚠️ ADMIN-ONLY — every method here is `adminProcedure`
|
|
32
43
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* any authenticated non-admin token could read and rewrite every addon's
|
|
38
|
-
* config. The `:139` comment claimed "only workers with valid hub tokens can
|
|
39
|
-
* call this" but nothing enforced it.
|
|
44
|
+
* Both reads return RAW, un-redacted addon config — the same rows that hold
|
|
45
|
+
* notifier tokens, webhook secrets and API keys. Before 2026-08-13 they were
|
|
46
|
+
* `protectedProcedure`, so any authenticated non-admin token could read every
|
|
47
|
+
* addon's config.
|
|
40
48
|
*
|
|
41
49
|
* Gating with `adminProcedure` costs the legitimate callers NOTHING: this
|
|
42
50
|
* router is in `CORE_NAMESPACES` (`core-cap-bridge.ts`), so a forked addon's
|
|
43
51
|
* `ctx.api.addonSettingsRaw.*` arrives over the trusted core-cap/UDS mesh whose
|
|
44
52
|
* context is `createMeshTrpcContext` (`isAdmin: true`), and the admin UI is an
|
|
45
|
-
* admin session.
|
|
46
|
-
* 1. Forked addons (via `ctx.api` over the mesh) reading/writing their
|
|
47
|
-
* 3-level settings chain from the worker process.
|
|
48
|
-
* 2. Admin UI flows inspecting/mutating addon settings.
|
|
49
|
-
*
|
|
50
|
-
* The router is deliberately thin — it does NOT perform schema-based
|
|
51
|
-
* resolver merging (defaults → global → per-device). That happens on the
|
|
52
|
-
* consumer side where the addon's `ConfigUISchema` is available:
|
|
53
|
-
* - In-process addons: handled by `SettingsResolverService.createView()`
|
|
54
|
-
* wired into `AddonContext.settings` during `createAddonContext()`.
|
|
55
|
-
* - Forked addons: handled by the `AddonSettingsView` constructed inside
|
|
56
|
-
* `WorkerBootstrapService`, which has access to the worker's local
|
|
57
|
-
* addon schema.
|
|
53
|
+
* admin session.
|
|
58
54
|
*
|
|
59
|
-
*
|
|
55
|
+
* The router is deliberately thin — it does NOT perform schema-based resolver
|
|
56
|
+
* merging (defaults → global → per-device). That happens on the consumer side
|
|
57
|
+
* where the addon's `ConfigUISchema` is available.
|
|
60
58
|
*/
|
|
59
|
+
const server_1 = require("@trpc/server");
|
|
60
|
+
const system_1 = require("@camstack/system");
|
|
61
61
|
const zod_1 = require("zod");
|
|
62
62
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
63
63
|
const AddonSettingsRecordSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
|
|
64
64
|
const AddonIdInputSchema = zod_1.z.object({
|
|
65
|
-
addonId: zod_1.z.string(),
|
|
65
|
+
addonId: zod_1.z.string().min(1),
|
|
66
66
|
});
|
|
67
|
+
/**
|
|
68
|
+
* `deviceId` stays a STRING on the wire (the shape callers already send) but
|
|
69
|
+
* must be a numeric device id: the per-device rows are keyed by the numeric id.
|
|
70
|
+
*/
|
|
67
71
|
const AddonDeviceInputSchema = zod_1.z.object({
|
|
68
|
-
addonId: zod_1.z.string(),
|
|
69
|
-
deviceId: zod_1.z.string(),
|
|
70
|
-
});
|
|
71
|
-
const UpdateGlobalInputSchema = zod_1.z.object({
|
|
72
|
-
addonId: zod_1.z.string(),
|
|
73
|
-
field: zod_1.z.string(),
|
|
74
|
-
value: zod_1.z.unknown(),
|
|
75
|
-
});
|
|
76
|
-
const UpdateDeviceInputSchema = zod_1.z.object({
|
|
77
|
-
addonId: zod_1.z.string(),
|
|
78
|
-
deviceId: zod_1.z.string(),
|
|
79
|
-
field: zod_1.z.string(),
|
|
80
|
-
value: zod_1.z.unknown(),
|
|
81
|
-
});
|
|
82
|
-
const SuccessSchema = zod_1.z.object({ success: zod_1.z.literal(true) });
|
|
83
|
-
const ReplaceGlobalInputSchema = zod_1.z.object({
|
|
84
|
-
addonId: zod_1.z.string(),
|
|
85
|
-
config: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
72
|
+
addonId: zod_1.z.string().min(1),
|
|
73
|
+
deviceId: zod_1.z.string().regex(/^\d+$/, 'deviceId must be a numeric device id'),
|
|
86
74
|
});
|
|
75
|
+
/** A settings read that could not be answered is an error, never `{}`. */
|
|
76
|
+
async function answerOrRefuse(read) {
|
|
77
|
+
try {
|
|
78
|
+
return await read();
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if ((0, system_1.isSettingsReadUnavailable)(err)) {
|
|
82
|
+
throw new server_1.TRPCError({ code: 'SERVICE_UNAVAILABLE', message: err.message, cause: err });
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
87
|
function createAddonSettingsRouter(cfg) {
|
|
88
88
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
90
|
+
* The addon-global stored record for the given addon — raw values, no
|
|
91
|
+
* defaults, no device overrides. `{}` only when the store answered with no
|
|
92
|
+
* row for this addon; a failed read is `SERVICE_UNAVAILABLE`.
|
|
92
93
|
*/
|
|
93
94
|
getGlobal: trpc_middleware_js_1.adminProcedure
|
|
94
95
|
.input(AddonIdInputSchema)
|
|
95
96
|
.output(AddonSettingsRecordSchema)
|
|
96
|
-
.query(({ input }) => cfg.
|
|
97
|
+
.query(({ input }) => answerOrRefuse(() => cfg.readAddonGlobalStrict(input.addonId))),
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* The per-device override record for the given addon × device — raw
|
|
100
|
+
* values. `{}` only when the store answered with none; a failed read is
|
|
101
|
+
* `SERVICE_UNAVAILABLE`.
|
|
101
102
|
*/
|
|
102
103
|
getDeviceOverrides: trpc_middleware_js_1.adminProcedure
|
|
103
104
|
.input(AddonDeviceInputSchema)
|
|
104
105
|
.output(AddonSettingsRecordSchema)
|
|
105
|
-
.query(({ input }) => cfg.
|
|
106
|
-
/**
|
|
107
|
-
* Update a single field in the addon-global settings record.
|
|
108
|
-
* Reads the current record, merges the new value, and writes back
|
|
109
|
-
* via `setAddonConfig` (bulk replace). Intended for small per-field
|
|
110
|
-
* writes from addon code; bulk updates should use a dedicated admin
|
|
111
|
-
* endpoint (not exposed here).
|
|
112
|
-
*/
|
|
113
|
-
updateGlobal: trpc_middleware_js_1.adminProcedure
|
|
114
|
-
.input(UpdateGlobalInputSchema)
|
|
115
|
-
.output(SuccessSchema)
|
|
116
|
-
.mutation(({ input }) => {
|
|
117
|
-
const current = cfg.getAddonConfig(input.addonId);
|
|
118
|
-
cfg.setAddonConfig(input.addonId, { ...current, [input.field]: input.value });
|
|
119
|
-
return { success: true };
|
|
120
|
-
}),
|
|
121
|
-
/**
|
|
122
|
-
* Update a single field in the per-device override record for the
|
|
123
|
-
* given addon × device. Merges with the existing overrides and
|
|
124
|
-
* writes back via `setAddonDevice`. Scope enforcement (dropping
|
|
125
|
-
* fields not declared as `scope: 'device'`) is the consumer's
|
|
126
|
-
* responsibility — we preserve the raw shape at this layer so the
|
|
127
|
-
* resolver contract remains symmetric with `getDeviceOverrides`.
|
|
128
|
-
*/
|
|
129
|
-
updateDevice: trpc_middleware_js_1.adminProcedure
|
|
130
|
-
.input(UpdateDeviceInputSchema)
|
|
131
|
-
.output(SuccessSchema)
|
|
132
|
-
.mutation(({ input }) => {
|
|
133
|
-
const current = cfg.getAddonDevice(input.addonId, input.deviceId);
|
|
134
|
-
cfg.setAddonDevice(input.addonId, input.deviceId, {
|
|
135
|
-
...current,
|
|
136
|
-
[input.field]: input.value,
|
|
137
|
-
});
|
|
138
|
-
return { success: true };
|
|
139
|
-
}),
|
|
140
|
-
/**
|
|
141
|
-
* Replace the entire addon-global settings record in one call.
|
|
142
|
-
* Used by forked workers for `context.config.setAll()`. Unlike
|
|
143
|
-
* `updateGlobal` (single-field merge), this overwrites the full record.
|
|
144
|
-
* Admin-level write: only workers with valid hub tokens can call this.
|
|
145
|
-
*/
|
|
146
|
-
replaceGlobal: trpc_middleware_js_1.adminProcedure
|
|
147
|
-
.input(ReplaceGlobalInputSchema)
|
|
148
|
-
.output(SuccessSchema)
|
|
149
|
-
.mutation(({ input }) => {
|
|
150
|
-
cfg.setAddonConfig(input.addonId, input.config);
|
|
151
|
-
return { success: true };
|
|
152
|
-
}),
|
|
106
|
+
.query(({ input }) => answerOrRefuse(() => cfg.readAddonDeviceStrict(input.addonId, Number(input.deviceId)))),
|
|
153
107
|
});
|
|
154
108
|
}
|
|
@@ -1475,7 +1475,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1475
1475
|
// Reuses the same `capabilities.collection.<cap>` key/format the
|
|
1476
1476
|
// `capabilities` core router writes — via the shared canonical writer.
|
|
1477
1477
|
const updated = registry.listCapabilities().find((c) => c.name === input.capName);
|
|
1478
|
-
(0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
|
|
1478
|
+
await (0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
|
|
1479
1479
|
return { success: true };
|
|
1480
1480
|
},
|
|
1481
1481
|
getVersions: async (input) => ps.getPackageVersions(input.name),
|
|
@@ -33,24 +33,24 @@ function createCapabilitiesRouter(registry, config) {
|
|
|
33
33
|
if (input.nodeId !== undefined) {
|
|
34
34
|
// Per-node override: persist under the node-qualified key so the boot
|
|
35
35
|
// restorer (`setNodeConfigReader`) replays it on restart.
|
|
36
|
-
config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, input.addonId);
|
|
36
|
+
await config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, input.addonId);
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
39
|
// Cluster-global default: persist to the SAME key the boot restorer reads
|
|
40
40
|
// (addon-registry `setConfigReader` → `capabilities.singleton.<cap>`).
|
|
41
|
-
config.set(`capabilities.singleton.${input.capability}`, input.addonId);
|
|
41
|
+
await config.set(`capabilities.singleton.${input.capability}`, input.addonId);
|
|
42
42
|
}
|
|
43
43
|
}),
|
|
44
44
|
clearSingletonNodeOverride: trpc_middleware_js_1.adminProcedure
|
|
45
45
|
.input(zod_1.z.object({ capability: zod_1.z.string(), nodeId: zod_1.z.string() }))
|
|
46
46
|
.output(zod_1.z.void())
|
|
47
|
-
.mutation(({ input }) => {
|
|
47
|
+
.mutation(async ({ input }) => {
|
|
48
48
|
if (!registry)
|
|
49
49
|
throw new Error('Capability registry unavailable');
|
|
50
50
|
registry.clearSingletonNodeOverride(input.capability, input.nodeId);
|
|
51
51
|
// Persist the cleared state as null so the boot restorer doesn't re-apply
|
|
52
52
|
// a stale override on restart.
|
|
53
|
-
config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, null);
|
|
53
|
+
await config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, null);
|
|
54
54
|
}),
|
|
55
55
|
/**
|
|
56
56
|
* Set the enabled set of providers for a collection capability in
|
|
@@ -12,6 +12,6 @@ function collectionPreferenceKey(capability) {
|
|
|
12
12
|
* enable/disable mutation so the stored value always mirrors the live
|
|
13
13
|
* registry state.
|
|
14
14
|
*/
|
|
15
|
-
function persistCollectionDisabled(configService, capability, disabled) {
|
|
16
|
-
configService.set(collectionPreferenceKey(capability), JSON.stringify({ disabled: [...disabled] }));
|
|
15
|
+
async function persistCollectionDisabled(configService, capability, disabled) {
|
|
16
|
+
await configService.set(collectionPreferenceKey(capability), JSON.stringify({ disabled: [...disabled] }));
|
|
17
17
|
}
|
|
@@ -3247,6 +3247,15 @@ function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
|
|
|
3247
3247
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3248
3248
|
return p.setWrapperActive(methodInput);
|
|
3249
3249
|
}),
|
|
3250
|
+
getWrapperActivation: trpc_middleware_js_1.protectedProcedure
|
|
3251
|
+
.input(types_45.deviceManagerCapability.methods.getWrapperActivation.input.loose())
|
|
3252
|
+
.output(types_45.deviceManagerCapability.methods.getWrapperActivation.output)
|
|
3253
|
+
.query(async ({ input, ctx }) => {
|
|
3254
|
+
const { nodeId, ...methodInput } = input;
|
|
3255
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
3256
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3257
|
+
return p.getWrapperActivation(methodInput);
|
|
3258
|
+
}),
|
|
3250
3259
|
listWrappersForCap: trpc_middleware_js_1.protectedProcedure
|
|
3251
3260
|
.input(types_45.deviceManagerCapability.methods.listWrappersForCap.input.loose())
|
|
3252
3261
|
.output(types_45.deviceManagerCapability.methods.listWrappersForCap.output)
|
|
@@ -48,6 +48,7 @@ const types_1 = require("@camstack/types");
|
|
|
48
48
|
const client_1 = require("@trpc/client");
|
|
49
49
|
const addon_call_gateway_js_1 = require("./addon-call-gateway.js");
|
|
50
50
|
const addon_row_manifest_1 = require("./addon-row-manifest");
|
|
51
|
+
const settings_door_wiring_js_1 = require("./settings-door-wiring.js");
|
|
51
52
|
const addon_settings_provider_js_1 = require("./addon-settings-provider.js");
|
|
52
53
|
const device_meta_mirror_js_1 = require("./device-meta-mirror.js");
|
|
53
54
|
const integration_visibility_js_1 = require("./integration-visibility.js");
|
|
@@ -409,12 +410,18 @@ class AddonRegistryService {
|
|
|
409
410
|
});
|
|
410
411
|
// Create capability registry with config reader for singleton preferences
|
|
411
412
|
this.capabilityRegistry = new system_1.CapabilityRegistry(this.loggingService.createLogger('CapabilityRegistry'), this.eventBusService);
|
|
413
|
+
// The preference readers answer "no preference" while the persisted
|
|
414
|
+
// preferences cannot be read yet — before the settings door exists, or
|
|
415
|
+
// while its one boot read is in flight (D675: `get` then THROWS rather
|
|
416
|
+
// than answering "unset"). The default wins meanwhile, and
|
|
417
|
+
// `wireSettingsDoor` re-applies the persisted choice
|
|
418
|
+
// the moment the read settles.
|
|
412
419
|
this.capabilityRegistry.setConfigReader((capability) => {
|
|
413
420
|
try {
|
|
414
421
|
return this.configService.get(`capabilities.singleton.${capability}`) ?? undefined;
|
|
415
422
|
}
|
|
416
423
|
catch (err) {
|
|
417
|
-
this.logger.debug('
|
|
424
|
+
this.logger.debug('capability preference not readable yet — default applies until replay', {
|
|
418
425
|
meta: { capability, error: (0, types_1.errMsg)(err) },
|
|
419
426
|
});
|
|
420
427
|
return undefined;
|
|
@@ -426,8 +433,9 @@ class AddonRegistryService {
|
|
|
426
433
|
undefined);
|
|
427
434
|
}
|
|
428
435
|
catch (err) {
|
|
429
|
-
this.logger.debug('
|
|
430
|
-
|
|
436
|
+
this.logger.debug('capability preference not readable yet — default applies', {
|
|
437
|
+
tags: { nodeId },
|
|
438
|
+
meta: { capability, error: (0, types_1.errMsg)(err) },
|
|
431
439
|
});
|
|
432
440
|
return undefined;
|
|
433
441
|
}
|
|
@@ -452,7 +460,7 @@ class AddonRegistryService {
|
|
|
452
460
|
return undefined;
|
|
453
461
|
}
|
|
454
462
|
catch (err) {
|
|
455
|
-
this.logger.debug('
|
|
463
|
+
this.logger.debug('collection preference not readable yet, or malformed', {
|
|
456
464
|
meta: { capability, error: (0, types_1.errMsg)(err) },
|
|
457
465
|
});
|
|
458
466
|
return undefined;
|
|
@@ -2498,7 +2506,11 @@ class AddonRegistryService {
|
|
|
2498
2506
|
const store = this.capabilityRegistry.getProviderByAddon('settings-store', addonId);
|
|
2499
2507
|
if (!store)
|
|
2500
2508
|
return;
|
|
2501
|
-
|
|
2509
|
+
void (0, settings_door_wiring_js_1.wireSettingsDoor)({
|
|
2510
|
+
configService: this.configService,
|
|
2511
|
+
registry: this.capabilityRegistry,
|
|
2512
|
+
logger: this.logger,
|
|
2513
|
+
}, store);
|
|
2502
2514
|
this.integrationRegistry = new system_1.IntegrationRegistry(store);
|
|
2503
2515
|
void this.integrationRegistry.initialize().then(() => {
|
|
2504
2516
|
this.logger.info('IntegrationRegistry initialized', { meta: { phase: 'v2' } });
|
|
@@ -2770,42 +2782,6 @@ class AddonRegistryService {
|
|
|
2770
2782
|
}
|
|
2771
2783
|
return null;
|
|
2772
2784
|
}
|
|
2773
|
-
/**
|
|
2774
|
-
* Build the addonConfig for a given addon — strictly per-addon now.
|
|
2775
|
-
*
|
|
2776
|
-
* The legacy cross-addon merge (hardcoded `ADDON_SYSTEM_SETTINGS` map
|
|
2777
|
-
* copying `system_settings.<section>.*` into the bootstrap config) has
|
|
2778
|
-
* been removed. Addons that need to read shared system settings
|
|
2779
|
-
* sections (`ffmpeg`, `logging`, `recording`, …) do so at runtime via
|
|
2780
|
-
* `ctx.settings.getGlobal({ section: '<name>' })`, which delegates to
|
|
2781
|
-
* `ConfigManager.getSection()` and covers the full SQL → YAML →
|
|
2782
|
-
* RUNTIME_DEFAULTS fallback chain.
|
|
2783
|
-
*
|
|
2784
|
-
* This keeps the kernel agnostic: every addon declares its own needs
|
|
2785
|
-
* in its own `initialize(ctx)` code, with zero kernel-side addon id
|
|
2786
|
-
* special-casing. The only remaining exception is `sqlite-settings`,
|
|
2787
|
-
* which receives `_runtimeDefaults` for first-boot seeding (tracked
|
|
2788
|
-
* separately under the kernel-cleanup roadmap — Point 3d).
|
|
2789
|
-
*/
|
|
2790
|
-
buildAddonConfig(addonId) {
|
|
2791
|
-
// Start with per-addon SQL settings (may be empty for most addons)
|
|
2792
|
-
let addonSpecific = {};
|
|
2793
|
-
try {
|
|
2794
|
-
addonSpecific = this.configService.getAddonConfig(addonId);
|
|
2795
|
-
}
|
|
2796
|
-
catch (err) {
|
|
2797
|
-
this.logger.debug('ConfigManager not ready for addon', {
|
|
2798
|
-
tags: { addonId },
|
|
2799
|
-
meta: { error: (0, types_1.errMsg)(err) },
|
|
2800
|
-
});
|
|
2801
|
-
}
|
|
2802
|
-
// No addon id special-casing here anymore. Bootstrap-level
|
|
2803
|
-
// concerns (e.g. first-boot seeding from RUNTIME_DEFAULTS) are
|
|
2804
|
-
// owned by the addon itself — sqlite-settings imports the
|
|
2805
|
-
// constant directly from `@camstack/types` instead of relying on
|
|
2806
|
-
// a kernel-injected bootstrap field.
|
|
2807
|
-
return addonSpecific;
|
|
2808
|
-
}
|
|
2809
2785
|
async createAddonContext(addon) {
|
|
2810
2786
|
const addonId = addon.manifest.id;
|
|
2811
2787
|
const brokerNodeId = this.broker.nodeID;
|
|
@@ -2816,7 +2792,6 @@ class AddonRegistryService {
|
|
|
2816
2792
|
const logger = this.loggingService
|
|
2817
2793
|
.createLogger()
|
|
2818
2794
|
.withTags({ addonId, nodeId: brokerNodeId, agentId });
|
|
2819
|
-
const bootstrapConfig = this.buildAddonConfig(addonId);
|
|
2820
2795
|
// Per-addon private data directory — resolved from active storage
|
|
2821
2796
|
// provider if available, otherwise falls back to a deterministic
|
|
2822
2797
|
// hardcoded path. Addons that need the full storage provider now
|
|
@@ -2920,7 +2895,12 @@ class AddonRegistryService {
|
|
|
2920
2895
|
id: `addon:${addonId}`,
|
|
2921
2896
|
logger,
|
|
2922
2897
|
eventBus: this.eventBusService,
|
|
2923
|
-
|
|
2898
|
+
// No store data (D675). An in-process addon reads its persisted settings
|
|
2899
|
+
// through `ctx.settings`; `addonConfig` used to be filled from the sync
|
|
2900
|
+
// `getAddonConfig`, which has no backend on an isolated engine and handed
|
|
2901
|
+
// every addon `{}`. Nothing in production reads it — the same empty
|
|
2902
|
+
// record a forked runner's context carries (`addon-context-factory`).
|
|
2903
|
+
addonConfig: {},
|
|
2924
2904
|
dataDir,
|
|
2925
2905
|
nodeDataDir,
|
|
2926
2906
|
dataPlane: dataPlaneFacility.dataPlane,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wireSettingsDoor = wireSettingsDoor;
|
|
4
|
+
/**
|
|
5
|
+
* Wiring the async `settings-store` door into `ConfigService` — and replaying
|
|
6
|
+
* the operator's persisted capability preferences once its boot read settles
|
|
7
|
+
* (D675).
|
|
8
|
+
*
|
|
9
|
+
* One function, so the replay cannot be dropped from the wiring without a
|
|
10
|
+
* spec noticing: `setSettingsDoor` starts the ONE boot read of the preloaded
|
|
11
|
+
* `system-settings` keys, and that read's own promise drives a single
|
|
12
|
+
* `applyPersistedPreferences` pass over the providers that registered before
|
|
13
|
+
* it. No timer, no retry (D3). A failed read is logged at error and leaves
|
|
14
|
+
* every preference UNKNOWN until the next boot.
|
|
15
|
+
*/
|
|
16
|
+
const system_1 = require("@camstack/system");
|
|
17
|
+
const types_1 = require("@camstack/types");
|
|
18
|
+
/**
|
|
19
|
+
* Wire `door` and schedule the one preference replay. Returns the replay's
|
|
20
|
+
* promise (it never rejects) so a caller — or a spec — can await it.
|
|
21
|
+
*/
|
|
22
|
+
function wireSettingsDoor(deps, door) {
|
|
23
|
+
deps.configService.setSettingsDoor(door);
|
|
24
|
+
const preload = deps.configService.whenSystemSettingsPreloaded();
|
|
25
|
+
if (preload === null)
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
return preload
|
|
28
|
+
.then((result) => {
|
|
29
|
+
if (result.kind === 'failed') {
|
|
30
|
+
deps.logger.error('capability preferences could not be read at boot — every persisted preference is UNKNOWN until the next boot, and the defaults apply', { meta: { error: result.error } });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const replay = deps.registry.applyPersistedPreferences((capability) => (0, system_1.isInfraCapability)(capability));
|
|
34
|
+
deps.logger.info('capability preferences loaded from the settings store', {
|
|
35
|
+
meta: {
|
|
36
|
+
keyCount: result.keyCount,
|
|
37
|
+
singletonsApplied: replay.singletons.map((s) => `${s.capability}=${s.addonId}`),
|
|
38
|
+
collectionProvidersDisabled: replay.disabled.map((d) => `${d.capability}/${d.addonId}`),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
})
|
|
42
|
+
.catch((err) => {
|
|
43
|
+
deps.logger.error('capability preference replay failed', { meta: { error: (0, types_1.errMsg)(err) } });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -205,9 +205,14 @@ class MoleculerService {
|
|
|
205
205
|
async onModuleInit() {
|
|
206
206
|
const logger = this.logging.createLogger('moleculer');
|
|
207
207
|
const hubDeps = {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
// Through the settings door, three-way (D675): the stored record, `{}`
|
|
209
|
+
// when the store answered with none, a THROW when it could not be read.
|
|
210
|
+
// The sync `getAddonConfig` has no backend on an isolated engine and
|
|
211
|
+
// answered `{}` for every addon.
|
|
212
|
+
getAddonConfig: (addonId) => this.config.readAddonGlobalStrict(addonId),
|
|
213
|
+
// `ConfigManager.get` REFUSES (throws) a key the isolated engine may hold
|
|
214
|
+
// but cannot read synchronously (D675) — the caller gets that error over
|
|
215
|
+
// Moleculer rather than a default dressed as the stored value.
|
|
211
216
|
getSettings: (scope, key) => {
|
|
212
217
|
return this.config.get(key ? `${scope}.${key}` : scope);
|
|
213
218
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.409",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
41
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
40
|
+
"@camstack/addon-admin-ui": "1.2.286",
|
|
41
|
+
"@camstack/addon-agent-ui": "1.2.155",
|
|
42
42
|
"@camstack/addon-auth": "1.2.123",
|
|
43
43
|
"@camstack/addon-decoder-nodeav": "1.2.115",
|
|
44
44
|
"@camstack/addon-notifiers": "1.2.121",
|
|
45
45
|
"@camstack/addon-pipeline": "1.2.305",
|
|
46
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
47
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
46
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.262",
|
|
47
|
+
"@camstack/addon-post-analysis": "1.2.296",
|
|
48
48
|
"@camstack/sdk": "1.2.118",
|
|
49
|
-
"@camstack/system": "1.2.
|
|
50
|
-
"@camstack/types": "1.2.
|
|
51
|
-
"@camstack/ui-library": "1.2.
|
|
49
|
+
"@camstack/system": "1.2.318",
|
|
50
|
+
"@camstack/types": "1.2.272",
|
|
51
|
+
"@camstack/ui-library": "1.2.213",
|
|
52
52
|
"@fastify/compress": "^9.0.0",
|
|
53
53
|
"@fastify/cookie": "^11.0.2",
|
|
54
54
|
"@fastify/cors": "^11.2.0",
|