@camstack/addon-mqtt-broker 1.2.150 → 1.2.152
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/CHANGELOG.md +17 -0
- package/dist/mqtt-broker.addon.js +1446 -1253
- package/dist/mqtt-broker.addon.mjs +1446 -1253
- package/i18n/en.json +6 -23
- package/i18n/it.json +6 -23
- package/package.json +2 -2
|
@@ -6021,7 +6021,7 @@ var ZodIssueCode = {
|
|
|
6021
6021
|
var ZodFirstPartyTypeKind;
|
|
6022
6022
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6023
6023
|
//#endregion
|
|
6024
|
-
//#region ../types/dist/sleep-
|
|
6024
|
+
//#region ../types/dist/sleep-BX_MqzUe.mjs
|
|
6025
6025
|
/**
|
|
6026
6026
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6027
6027
|
* window to float samples (D455).
|
|
@@ -6621,7 +6621,7 @@ var NOT_RUN = {
|
|
|
6621
6621
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6622
6622
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6623
6623
|
*/
|
|
6624
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6624
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6625
6625
|
const first = migrations[0];
|
|
6626
6626
|
if (first === void 0) return NOT_RUN;
|
|
6627
6627
|
const owner = first.meta.owner;
|
|
@@ -6650,7 +6650,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6650
6650
|
else if (status === "blocked") blocked.push(id);
|
|
6651
6651
|
else skipped.push(id);
|
|
6652
6652
|
};
|
|
6653
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6653
6654
|
for (const step of declaration.steps) {
|
|
6655
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6654
6656
|
const bound = byId.get(step.id);
|
|
6655
6657
|
if (bound === void 0 || step.action === "skip") {
|
|
6656
6658
|
skipped.push(step.id);
|
|
@@ -6828,6 +6830,12 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6828
6830
|
}
|
|
6829
6831
|
return out;
|
|
6830
6832
|
}
|
|
6833
|
+
/** Every scope, in the order the guard and the docs list them. */
|
|
6834
|
+
var SCOPED_SETTINGS_SCOPES = [
|
|
6835
|
+
"cluster",
|
|
6836
|
+
"device-defaults",
|
|
6837
|
+
"node"
|
|
6838
|
+
];
|
|
6831
6839
|
/**
|
|
6832
6840
|
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6833
6841
|
*
|
|
@@ -7642,6 +7650,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
|
|
|
7642
7650
|
/** Coalesces a burst of `keychain.changed` events into one re-resolve. */
|
|
7643
7651
|
var SECRET_CHANGE_DEBOUNCE_MS = 250;
|
|
7644
7652
|
/**
|
|
7653
|
+
* Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
|
|
7654
|
+
* place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
|
|
7655
|
+
* through `api` (guard `check-migration-ports-surface`).
|
|
7656
|
+
*/
|
|
7657
|
+
function migrationSet(catalogue, bindPorts) {
|
|
7658
|
+
return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
|
|
7659
|
+
}
|
|
7660
|
+
/** A mixed array is a mistake: the other kind would be dropped without a word. */
|
|
7661
|
+
function refuseMixed(form, kept, dropped) {
|
|
7662
|
+
if (dropped > 0) throw new Error(`runMigrations(${form}): ${dropped} of ${kept + dropped} entries are of the other kind and would be dropped; pass one kind per call`);
|
|
7663
|
+
}
|
|
7664
|
+
function catalogueOf(list) {
|
|
7665
|
+
const kept = list.filter((entry) => !("execute" in entry));
|
|
7666
|
+
refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
|
|
7667
|
+
return kept;
|
|
7668
|
+
}
|
|
7669
|
+
function boundOf(list) {
|
|
7670
|
+
const kept = list.filter((entry) => "execute" in entry);
|
|
7671
|
+
refuseMixed("bound migrations", kept.length, list.length - kept.length);
|
|
7672
|
+
return kept;
|
|
7673
|
+
}
|
|
7674
|
+
/**
|
|
7645
7675
|
* Base class for CamStack addons. Eliminates settings boilerplate:
|
|
7646
7676
|
*
|
|
7647
7677
|
* - Typed `config` property with automatic resolution from store + defaults
|
|
@@ -7719,6 +7749,13 @@ var BaseAddon = class {
|
|
|
7719
7749
|
*/
|
|
7720
7750
|
secretStatePaths = [];
|
|
7721
7751
|
_addonSecretTree = null;
|
|
7752
|
+
/**
|
|
7753
|
+
* D986 M4: the scoped schemas this addon declared, resolved once per capability
|
|
7754
|
+
* (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
|
|
7755
|
+
* see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
|
|
7756
|
+
*/
|
|
7757
|
+
_scopedByCap = /* @__PURE__ */ new Map();
|
|
7758
|
+
_nodeKeysByCap = /* @__PURE__ */ new Map();
|
|
7722
7759
|
_deviceSecretTree = null;
|
|
7723
7760
|
/** Refs the last resolved config pointed at — a change to one re-resolves. */
|
|
7724
7761
|
_configSecretRefs = /* @__PURE__ */ new Set();
|
|
@@ -7788,6 +7825,7 @@ var BaseAddon = class {
|
|
|
7788
7825
|
this._ctx = context;
|
|
7789
7826
|
this._secretSettings = null;
|
|
7790
7827
|
}
|
|
7828
|
+
await this.loadScopedSchemas();
|
|
7791
7829
|
await this.resolveConfig();
|
|
7792
7830
|
if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
|
|
7793
7831
|
if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
|
|
@@ -7797,27 +7835,30 @@ var BaseAddon = class {
|
|
|
7797
7835
|
const normalized = normalizeAddonInitResult(result);
|
|
7798
7836
|
const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
|
|
7799
7837
|
this._registeredCapNames = providers.map((p) => p.capability.name);
|
|
7838
|
+
await this.loadScopedSchemas();
|
|
7800
7839
|
return normalized;
|
|
7801
7840
|
}
|
|
7841
|
+
async runMigrations(first, second, third) {
|
|
7842
|
+
if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
|
|
7843
|
+
return this.runBound(boundOf(first), second);
|
|
7844
|
+
}
|
|
7802
7845
|
/**
|
|
7803
|
-
*
|
|
7804
|
-
*
|
|
7805
|
-
*
|
|
7806
|
-
* restored devices — with the catalogue bound to the addon's ports
|
|
7807
|
-
* (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
|
|
7808
|
-
*
|
|
7809
|
-
* An engine that cannot be reached or refuses the catalogue is logged and runs
|
|
7810
|
-
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7811
|
-
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7812
|
-
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7846
|
+
* Several catalogues of ONE owner in one declaration (the engine replaces an owner's
|
|
7847
|
+
* catalogue on every declare, so two calls would each judge the other's ids "newer").
|
|
7848
|
+
* Build each entry with `migrationSet(catalogue, (api) => ports)`.
|
|
7813
7849
|
*/
|
|
7814
|
-
async
|
|
7850
|
+
async runMigrationSets(sets, options) {
|
|
7851
|
+
const api = this.ctx.migrationApi;
|
|
7852
|
+
if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
|
|
7853
|
+
return this.runBound(sets.flatMap((set) => set.bind(api)), options);
|
|
7854
|
+
}
|
|
7855
|
+
async runBound(migrations, options) {
|
|
7815
7856
|
const api = this.ctx.api.migrations;
|
|
7816
7857
|
const report = await runAddonMigrations({
|
|
7817
7858
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7818
7859
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7819
7860
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7820
|
-
}, migrations, this.ctx.logger);
|
|
7861
|
+
}, migrations, this.ctx.logger, options);
|
|
7821
7862
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7822
7863
|
return report;
|
|
7823
7864
|
}
|
|
@@ -7881,6 +7922,16 @@ var BaseAddon = class {
|
|
|
7881
7922
|
schema(schema) {
|
|
7882
7923
|
return schema;
|
|
7883
7924
|
}
|
|
7925
|
+
/**
|
|
7926
|
+
* The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
|
|
7927
|
+
* {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
|
|
7928
|
+
* `onInitialize`, when the providers it registers are not known yet: the first config resolve
|
|
7929
|
+
* must already know which keys are per node. Names the providers register at init are added
|
|
7930
|
+
* once known.
|
|
7931
|
+
*/
|
|
7932
|
+
scopedSettingsCapabilities() {
|
|
7933
|
+
return [];
|
|
7934
|
+
}
|
|
7884
7935
|
/** Override to provide global-level settings UI schema. */
|
|
7885
7936
|
globalSettingsSchema(_cap) {
|
|
7886
7937
|
return null;
|
|
@@ -7924,7 +7975,8 @@ var BaseAddon = class {
|
|
|
7924
7975
|
return [];
|
|
7925
7976
|
}
|
|
7926
7977
|
async getGlobalSettings(overlay, cap, nodeId) {
|
|
7927
|
-
|
|
7978
|
+
await this.loadScopedSchemas(cap);
|
|
7979
|
+
const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
|
|
7928
7980
|
if (!schema) return { sections: [] };
|
|
7929
7981
|
const projected = await this.readRawProjectedStore(nodeId, cap);
|
|
7930
7982
|
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
|
|
@@ -7959,10 +8011,9 @@ var BaseAddon = class {
|
|
|
7959
8011
|
*
|
|
7960
8012
|
* Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
|
|
7961
8013
|
* needs its connection fields, the default integration the addon's own sections. It is the
|
|
7962
|
-
* SHAPE of the form only — the values live
|
|
7963
|
-
* unchanged (`integrations.setSettings` for an account integration).
|
|
8014
|
+
* SHAPE of the form only — the values live on the instance record.
|
|
7964
8015
|
*/
|
|
7965
|
-
async
|
|
8016
|
+
async integrationFormSchema(_instance) {
|
|
7966
8017
|
return null;
|
|
7967
8018
|
}
|
|
7968
8019
|
/**
|
|
@@ -8008,7 +8059,7 @@ var BaseAddon = class {
|
|
|
8008
8059
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8009
8060
|
*/
|
|
8010
8061
|
async getIntegrationFormSchema(instance, nodeId) {
|
|
8011
|
-
const scoped = await this.
|
|
8062
|
+
const scoped = await this.integrationFormSchema(instance);
|
|
8012
8063
|
if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
|
|
8013
8064
|
return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
|
|
8014
8065
|
}
|
|
@@ -8056,6 +8107,7 @@ var BaseAddon = class {
|
|
|
8056
8107
|
* resolved blob back is a no-op for every unchanged secret.
|
|
8057
8108
|
*/
|
|
8058
8109
|
async resolveGlobalStore(nodeId, cap) {
|
|
8110
|
+
await this.loadScopedSchemas(cap);
|
|
8059
8111
|
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
8060
8112
|
return this.projectForNode(raw, nodeId, cap);
|
|
8061
8113
|
}
|
|
@@ -8066,6 +8118,7 @@ var BaseAddon = class {
|
|
|
8066
8118
|
}
|
|
8067
8119
|
/** The PROJECTED store with its markers — the form hydrate only (D798). */
|
|
8068
8120
|
async readRawProjectedStore(nodeId, cap) {
|
|
8121
|
+
await this.loadScopedSchemas(cap);
|
|
8069
8122
|
const raw = await this.rawSettings()?.readAddonStore() ?? {};
|
|
8070
8123
|
return this.projectForNode(raw, nodeId, cap);
|
|
8071
8124
|
}
|
|
@@ -8168,6 +8221,7 @@ var BaseAddon = class {
|
|
|
8168
8221
|
await this.applyGlobalPatch(patch, nodeId);
|
|
8169
8222
|
}
|
|
8170
8223
|
async applyGlobalPatch(barePatch, nodeId) {
|
|
8224
|
+
await this.loadScopedSchemas();
|
|
8171
8225
|
const keys = this.perNodeKeys();
|
|
8172
8226
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
8173
8227
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
@@ -8181,7 +8235,8 @@ var BaseAddon = class {
|
|
|
8181
8235
|
await this.resolveConfig();
|
|
8182
8236
|
await this.onConfigChanged();
|
|
8183
8237
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
8184
|
-
|
|
8238
|
+
const legacy = this.globalSettingsSchema();
|
|
8239
|
+
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
8185
8240
|
}
|
|
8186
8241
|
/**
|
|
8187
8242
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -8189,14 +8244,67 @@ var BaseAddon = class {
|
|
|
8189
8244
|
* declarations). Empty set ⇒ every per-node code path is bypassed and the
|
|
8190
8245
|
* settings API behaves exactly like the legacy node-agnostic one.
|
|
8191
8246
|
*/
|
|
8192
|
-
|
|
8247
|
+
/**
|
|
8248
|
+
* Load the scoped schemas of every capability this addon declares (the override and the
|
|
8249
|
+
* providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
|
|
8250
|
+
* derived from the previous set. Idempotent.
|
|
8251
|
+
*/
|
|
8252
|
+
async loadScopedSchemas(extraCap) {
|
|
8253
|
+
const caps = new Set([
|
|
8254
|
+
...this.scopedSettingsCapabilities(),
|
|
8255
|
+
...this._registeredCapNames,
|
|
8256
|
+
...extraCap === void 0 ? [] : [extraCap]
|
|
8257
|
+
]);
|
|
8258
|
+
let added = false;
|
|
8259
|
+
for (const cap of caps) {
|
|
8260
|
+
if (this._scopedByCap.has(cap)) continue;
|
|
8261
|
+
const loaded = [];
|
|
8262
|
+
for (const scope of SCOPED_SETTINGS_SCOPES) {
|
|
8263
|
+
const schema = await this.getScopedSettingsSchema(scope, cap);
|
|
8264
|
+
if (schema !== null) loaded.push({
|
|
8265
|
+
scope,
|
|
8266
|
+
schema
|
|
8267
|
+
});
|
|
8268
|
+
}
|
|
8269
|
+
this._scopedByCap.set(cap, loaded);
|
|
8270
|
+
added = true;
|
|
8271
|
+
}
|
|
8272
|
+
if (!added) return;
|
|
8273
|
+
this._nodeKeysByCap = /* @__PURE__ */ new Map();
|
|
8274
|
+
this._addonSecretTree = null;
|
|
8275
|
+
}
|
|
8276
|
+
/**
|
|
8277
|
+
* The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
|
|
8278
|
+
* sections of its scoped schemas, so dropping the old hook does not blank the form.
|
|
8279
|
+
*/
|
|
8280
|
+
scopedFormSchema(cap) {
|
|
8281
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8282
|
+
for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
|
|
8283
|
+
return byId.size === 0 ? null : { sections: [...byId.values()] };
|
|
8284
|
+
}
|
|
8285
|
+
/** Every loaded scoped schema of `cap`, or of every loaded capability. */
|
|
8286
|
+
loadedScopedSchemas(cap) {
|
|
8287
|
+
if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
|
|
8288
|
+
return [...this._scopedByCap.values()].flat();
|
|
8289
|
+
}
|
|
8290
|
+
/**
|
|
8291
|
+
* The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
|
|
8292
|
+
* the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
|
|
8293
|
+
* per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
|
|
8294
|
+
* per-node code path is bypassed and the settings API behaves like the node-agnostic one.
|
|
8295
|
+
* Call {@link loadScopedSchemas} first (the async seams do).
|
|
8296
|
+
*/
|
|
8193
8297
|
perNodeKeys(cap) {
|
|
8194
8298
|
const cacheKey = cap ?? "";
|
|
8195
|
-
const cached = this.
|
|
8299
|
+
const cached = this._nodeKeysByCap.get(cacheKey);
|
|
8196
8300
|
if (cached) return cached;
|
|
8197
|
-
const
|
|
8198
|
-
const keys = new Set(
|
|
8199
|
-
this.
|
|
8301
|
+
const legacy = this.globalSettingsSchema(cap);
|
|
8302
|
+
const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
|
|
8303
|
+
for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
|
|
8304
|
+
if (scope !== "node") continue;
|
|
8305
|
+
for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
|
|
8306
|
+
}
|
|
8307
|
+
this._nodeKeysByCap.set(cacheKey, keys);
|
|
8200
8308
|
return keys;
|
|
8201
8309
|
}
|
|
8202
8310
|
/**
|
|
@@ -8206,10 +8314,9 @@ var BaseAddon = class {
|
|
|
8206
8314
|
* time to return its response before the addon is torn down and
|
|
8207
8315
|
* re-initialised by `AddonRegistryService.restartAddon`.
|
|
8208
8316
|
*/
|
|
8209
|
-
maybeAutoRestart(patchKeys,
|
|
8210
|
-
if (!schema) return;
|
|
8317
|
+
maybeAutoRestart(patchKeys, schemas) {
|
|
8211
8318
|
const restartKeys = /* @__PURE__ */ new Set();
|
|
8212
|
-
for (const section of schema.sections) for (const field of section.fields) {
|
|
8319
|
+
for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
|
|
8213
8320
|
if (field.type === "separator" || field.type === "info") continue;
|
|
8214
8321
|
if (field.requiresRestart) restartKeys.add(field.key);
|
|
8215
8322
|
}
|
|
@@ -8405,12 +8512,14 @@ var BaseAddon = class {
|
|
|
8405
8512
|
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
8406
8513
|
*/
|
|
8407
8514
|
async getDeclaredSettingsKeys() {
|
|
8515
|
+
await this.loadScopedSchemas();
|
|
8408
8516
|
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
8409
8517
|
}
|
|
8410
8518
|
declaredKeySet() {
|
|
8411
8519
|
const keys = new Set(Object.keys(this.defaults));
|
|
8412
8520
|
const schema = this.globalSettingsSchema();
|
|
8413
8521
|
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
8522
|
+
for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
8414
8523
|
for (const key of this._declaredStateKeys) keys.add(key);
|
|
8415
8524
|
for (const path of this.secretStatePaths) {
|
|
8416
8525
|
const root = path.split(/[.[]/, 1)[0];
|
|
@@ -8506,7 +8615,11 @@ var BaseAddon = class {
|
|
|
8506
8615
|
}
|
|
8507
8616
|
/** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
|
|
8508
8617
|
addonSecretTree() {
|
|
8509
|
-
this._addonSecretTree
|
|
8618
|
+
if (this._addonSecretTree === null) {
|
|
8619
|
+
const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
|
|
8620
|
+
for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
|
|
8621
|
+
this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
|
|
8622
|
+
}
|
|
8510
8623
|
return this._addonSecretTree;
|
|
8511
8624
|
}
|
|
8512
8625
|
deviceSecretTree() {
|
|
@@ -8756,6 +8869,23 @@ function dropPerNodeFields(fields) {
|
|
|
8756
8869
|
}
|
|
8757
8870
|
return kept;
|
|
8758
8871
|
}
|
|
8872
|
+
/** The keys of the value-holding fields, recursing into layout containers. */
|
|
8873
|
+
function collectValueFieldKeys(fields) {
|
|
8874
|
+
const collected = [];
|
|
8875
|
+
for (const field of fields) {
|
|
8876
|
+
if (field.type === "group") {
|
|
8877
|
+
collected.push(...collectValueFieldKeys(field.fields));
|
|
8878
|
+
continue;
|
|
8879
|
+
}
|
|
8880
|
+
if (field.type === "sub-tabs") {
|
|
8881
|
+
for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
|
|
8882
|
+
continue;
|
|
8883
|
+
}
|
|
8884
|
+
if (field.type === "separator" || field.type === "info") continue;
|
|
8885
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
8886
|
+
}
|
|
8887
|
+
return collected;
|
|
8888
|
+
}
|
|
8759
8889
|
function collectFieldKeys(fields) {
|
|
8760
8890
|
const collected = [];
|
|
8761
8891
|
for (const field of fields) {
|
|
@@ -9282,6 +9412,7 @@ function defineEntryKind(name, spec) {
|
|
|
9282
9412
|
class: "config",
|
|
9283
9413
|
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9284
9414
|
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9415
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9285
9416
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9286
9417
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9287
9418
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
@@ -11947,7 +12078,7 @@ object({
|
|
|
11947
12078
|
params: record(string(), union([string(), number()])).optional()
|
|
11948
12079
|
});
|
|
11949
12080
|
//#endregion
|
|
11950
|
-
//#region ../types/dist/composition-
|
|
12081
|
+
//#region ../types/dist/composition-pOHbTzRB.mjs
|
|
11951
12082
|
/**
|
|
11952
12083
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11953
12084
|
*
|
|
@@ -13428,7 +13559,7 @@ var RecordIntegrationInstances = class {
|
|
|
13428
13559
|
return record === null ? null : this.viewOf(record);
|
|
13429
13560
|
};
|
|
13430
13561
|
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13431
|
-
createInstance = async ({ id, name, kind }) => {
|
|
13562
|
+
createInstance = async ({ id, name, kind, values }) => {
|
|
13432
13563
|
const minted = id ?? this.hooks.mintId?.({
|
|
13433
13564
|
name,
|
|
13434
13565
|
...kind !== void 0 ? { kind } : {}
|
|
@@ -13438,10 +13569,20 @@ var RecordIntegrationInstances = class {
|
|
|
13438
13569
|
id: minted,
|
|
13439
13570
|
data: this.hooks.initialBody(name, kind)
|
|
13440
13571
|
}, { ifRev: 0 });
|
|
13572
|
+
const patch = this.patchOf(values ?? {});
|
|
13573
|
+
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
13441
13574
|
await this.hooks.onChanged?.(minted, "created");
|
|
13442
13575
|
return { id: minted };
|
|
13443
13576
|
};
|
|
13444
13577
|
updateInstance = async ({ id, name, values }) => {
|
|
13578
|
+
const patch = this.patchOf(values);
|
|
13579
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13580
|
+
await this.handle.patch(id, patch);
|
|
13581
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13582
|
+
return { ok: true };
|
|
13583
|
+
};
|
|
13584
|
+
/** The record patch a form `values` object means: secrets kept, cleared or replaced. */
|
|
13585
|
+
patchOf(values) {
|
|
13445
13586
|
const patch = {};
|
|
13446
13587
|
for (const [key, value] of Object.entries(values)) {
|
|
13447
13588
|
if (value === void 0) continue;
|
|
@@ -13452,11 +13593,8 @@ var RecordIntegrationInstances = class {
|
|
|
13452
13593
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13453
13594
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13454
13595
|
}
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
await this.hooks.onChanged?.(id, "updated");
|
|
13458
|
-
return { ok: true };
|
|
13459
|
-
};
|
|
13596
|
+
return patch;
|
|
13597
|
+
}
|
|
13460
13598
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13461
13599
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13462
13600
|
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
@@ -14538,7 +14676,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14538
14676
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14539
14677
|
* catalogue lists the id (a newer database).
|
|
14540
14678
|
*/
|
|
14541
|
-
meta: MigrationMetaSchema.optional()
|
|
14679
|
+
meta: MigrationMetaSchema.optional(),
|
|
14680
|
+
/**
|
|
14681
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14682
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14683
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14684
|
+
*/
|
|
14685
|
+
ownerNotLoaded: boolean().optional()
|
|
14542
14686
|
});
|
|
14543
14687
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14544
14688
|
var MigrationPendingEntrySchema = object({
|
|
@@ -15659,6 +15803,39 @@ var UnstampedEventMediaCountSchema = object({
|
|
|
15659
15803
|
/** Sum across both, or `null` when either lane could not be counted. */
|
|
15660
15804
|
total: number().int().nonnegative().nullable()
|
|
15661
15805
|
}).nullable();
|
|
15806
|
+
/**
|
|
15807
|
+
* Where the stored event media sits, per storage location: how many media rows
|
|
15808
|
+
* and how many bytes carry each location id, how many carry none (legacy,
|
|
15809
|
+
* resolved to the default `eventMedia` location), and how many name a location
|
|
15810
|
+
* that was not asked about (a deleted or never-listed one).
|
|
15811
|
+
*
|
|
15812
|
+
* One covering-index aggregate per asked location plus the NULL one, and one
|
|
15813
|
+
* unfiltered total that scans the covering index: O(rows), not a walk of the
|
|
15814
|
+
* rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
|
|
15815
|
+
* it is a deleted location only when every location id was asked.
|
|
15816
|
+
* `null` is "could not be measured", never zero.
|
|
15817
|
+
*/
|
|
15818
|
+
var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
|
|
15819
|
+
var MediaLocationCensusSchema = object({
|
|
15820
|
+
locations: array(object({
|
|
15821
|
+
locationId: string(),
|
|
15822
|
+
rows: number().int().nonnegative(),
|
|
15823
|
+
bytes: number().nonnegative()
|
|
15824
|
+
})).readonly(),
|
|
15825
|
+
unstamped: object({
|
|
15826
|
+
rows: number().int().nonnegative(),
|
|
15827
|
+
bytes: number().nonnegative()
|
|
15828
|
+
}),
|
|
15829
|
+
/** Rows stamped with a location id that was not in the asked list. */
|
|
15830
|
+
unlisted: object({
|
|
15831
|
+
rows: number().int().nonnegative(),
|
|
15832
|
+
bytes: number().nonnegative()
|
|
15833
|
+
}),
|
|
15834
|
+
total: object({
|
|
15835
|
+
rows: number().int().nonnegative(),
|
|
15836
|
+
bytes: number().nonnegative()
|
|
15837
|
+
})
|
|
15838
|
+
});
|
|
15662
15839
|
var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
|
|
15663
15840
|
/** The independently selectable logical storage classes — every class
|
|
15664
15841
|
* `storage.listLocationDeclarations` reports, so an operator never meets a
|
|
@@ -46179,52 +46356,7 @@ DeviceType.Light, method(object({
|
|
|
46179
46356
|
level: number().min(0).max(100),
|
|
46180
46357
|
lastChangedAt: number()
|
|
46181
46358
|
});
|
|
46182
|
-
|
|
46183
|
-
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
46184
|
-
*
|
|
46185
|
-
* The cap models any kind-tagged message broker the user wants to
|
|
46186
|
-
* register and that other addons might consume. The first two kinds
|
|
46187
|
-
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
46188
|
-
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
46189
|
-
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
46190
|
-
* the same surface.
|
|
46191
|
-
*
|
|
46192
|
-
* Why one cap, not one cap per kind:
|
|
46193
|
-
* - The integrations page wants a single table of "every broker"
|
|
46194
|
-
* across kinds; a unified cap drives it without join logic.
|
|
46195
|
-
* - Generic admin operations (add / remove / test / status) are
|
|
46196
|
-
* identical across kinds — duplicating them per cap was busywork.
|
|
46197
|
-
* - The `network-access` cap precedent: same interface, many
|
|
46198
|
-
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
46199
|
-
*
|
|
46200
|
-
* What stays kind-specific:
|
|
46201
|
-
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
46202
|
-
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
46203
|
-
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
46204
|
-
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
46205
|
-
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
46206
|
-
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
46207
|
-
* without rebreaking the cap signature.
|
|
46208
|
-
* - `add` `settings` payload (different fields per kind, validated by
|
|
46209
|
-
* the kind-specific provider).
|
|
46210
|
-
*
|
|
46211
|
-
* Bidirectionality:
|
|
46212
|
-
* - `publish` is RPC — synchronous result (provider-defined).
|
|
46213
|
-
* - `subscribe` returns a subscription handle; the actual message
|
|
46214
|
-
* stream flows over the typed event-bus as `broker.message` events
|
|
46215
|
-
* keyed by `(brokerId, subscriptionId)`. Consumers filter in their
|
|
46216
|
-
* event handler. Subscriptions persist across reconnects — the
|
|
46217
|
-
* provider re-subscribes upstream after a transport drop.
|
|
46218
|
-
*
|
|
46219
|
-
* Why event-bus push (not a subscription RPC stream):
|
|
46220
|
-
* - Matches the rest of CamStack's D8 contract — events for telemetry,
|
|
46221
|
-
* RPC for loss-is-a-bug. Broker messages are telemetry (a single
|
|
46222
|
-
* drop is recoverable via `getState({key})`).
|
|
46223
|
-
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
46224
|
-
* - Cross-process: events already route through `$event-bus`; no
|
|
46225
|
-
* per-broker wire-shaping needed.
|
|
46226
|
-
*/
|
|
46227
|
-
var BrokerStatusEnum = _enum([
|
|
46359
|
+
_enum([
|
|
46228
46360
|
"connected",
|
|
46229
46361
|
"disconnected",
|
|
46230
46362
|
"connecting",
|
|
@@ -46232,141 +46364,12 @@ var BrokerStatusEnum = _enum([
|
|
|
46232
46364
|
"unreachable",
|
|
46233
46365
|
"error"
|
|
46234
46366
|
]);
|
|
46235
|
-
var BrokerInfoSchema = object({
|
|
46236
|
-
/** Stable broker id. Persisted; survives addon restarts. */
|
|
46237
|
-
id: string(),
|
|
46238
|
-
/** Addon id of the provider that OWNS this broker.
|
|
46239
|
-
*
|
|
46240
|
-
* The `broker` cap is a system-scoped collection: several addons
|
|
46241
|
-
* register a `broker` provider, each owning a DISJOINT set of
|
|
46242
|
-
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
46243
|
-
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
46244
|
-
* device belongs to one integration. The admin UI threads this id
|
|
46245
|
-
* back as the `{ addonId }` system-collection selector on every
|
|
46246
|
-
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
46247
|
-
* `testConnection`), so the call routes to the OWNING provider
|
|
46248
|
-
* instead of defaulting to the first-registered one. */
|
|
46249
|
-
addonId: string(),
|
|
46250
|
-
/** Human-readable name (operator-chosen at add-time). */
|
|
46251
|
-
name: string(),
|
|
46252
|
-
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
46253
|
-
kind: string(),
|
|
46254
|
-
status: BrokerStatusEnum,
|
|
46255
|
-
/** Free-form provider-specific info (HA version, MQTT broker
|
|
46256
|
-
* flavour, latency, mTLS active, …). */
|
|
46257
|
-
info: record(string(), unknown()),
|
|
46258
|
-
/** Ms epoch of the last connection probe / status update. */
|
|
46259
|
-
lastCheckedAt: number().nullable(),
|
|
46260
|
-
/** Last error message, when `status` indicates failure. */
|
|
46261
|
-
error: string().nullable()
|
|
46262
|
-
});
|
|
46263
46367
|
var RegistryStatusSchema = object({
|
|
46264
46368
|
brokerCount: number().int().nonnegative(),
|
|
46265
46369
|
connectedCount: number().int().nonnegative()
|
|
46266
46370
|
});
|
|
46267
|
-
/**
|
|
46268
|
-
* One entry per `broker` provider — which addon provides which broker
|
|
46269
|
-
* kind(s). The unified create picker reads this to know, for each
|
|
46270
|
-
* (addonId, kind) pair the operator can pick, where to route the
|
|
46271
|
-
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
46272
|
-
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
46273
|
-
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
46274
|
-
* offers a kind whose creation form can't be rendered).
|
|
46275
|
-
*/
|
|
46276
|
-
var BrokerProviderInfoSchema = object({
|
|
46277
|
-
/** Addon id of the `broker` provider this entry describes. */
|
|
46278
|
-
addonId: string(),
|
|
46279
|
-
/** Broker kinds this provider can create, with a display label. */
|
|
46280
|
-
kinds: array(object({
|
|
46281
|
-
kind: string(),
|
|
46282
|
-
label: string(),
|
|
46283
|
-
/**
|
|
46284
|
-
* Relative hub path to start an interactive account link (OAuth popup).
|
|
46285
|
-
* May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
|
|
46286
|
-
*/
|
|
46287
|
-
interactiveConnectPath: string().min(1).optional(),
|
|
46288
|
-
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
46289
|
-
interactiveDisconnectPath: string().min(1).optional(),
|
|
46290
|
-
/**
|
|
46291
|
-
* The kind signs in through a server-held interactive session instead of
|
|
46292
|
-
* (or before) typed credentials. `basePath` is a hub-relative route set
|
|
46293
|
-
* served by the owning addon (`/addon/<id>/...`) speaking the sign-in
|
|
46294
|
-
* contract: `POST {basePath}/start` (body: the form's settings) answers
|
|
46295
|
-
* `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
|
|
46296
|
-
* `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
|
|
46297
|
-
* 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
|
|
46298
|
-
* (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
|
|
46299
|
-
* existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
|
|
46300
|
-
* (`{ sessionId }`) drops the session. The dialog that edits a broker
|
|
46301
|
-
* offers it as the default path; the fields the session fills are
|
|
46302
|
-
* marked `filledBySignIn` in the kind's settings schema.
|
|
46303
|
-
*
|
|
46304
|
-
* `methods` lists the ways the session can sign in, recommended first:
|
|
46305
|
-
* `'device-code'` adds `POST {basePath}/code/start` (body: the form's
|
|
46306
|
-
* settings) answering `{ sessionId, userCode, verificationUrl,
|
|
46307
|
-
* expiresAt }` — the operator types `userCode` at `verificationUrl` on
|
|
46308
|
-
* any device, and the same `status` / `commit` / `cancel` routes follow
|
|
46309
|
-
* (status then carries `method`, `userCode`, `verificationUrl`,
|
|
46310
|
-
* `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
|
|
46311
|
-
* `'proxy'` is the `{basePath}/start` flow above. Absent means
|
|
46312
|
-
* `['proxy']`.
|
|
46313
|
-
*/
|
|
46314
|
-
signIn: object({
|
|
46315
|
-
basePath: string().min(1),
|
|
46316
|
-
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
46317
|
-
}).optional()
|
|
46318
|
-
}))
|
|
46319
|
-
});
|
|
46320
|
-
var ListInputSchema = object({
|
|
46321
|
-
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
46322
|
-
* only HA brokers. Omit to list every broker. */
|
|
46323
|
-
kind: string().optional() });
|
|
46324
46371
|
var GetInputSchema = object({ id: string() });
|
|
46325
|
-
var AddInputSchema = object({
|
|
46326
|
-
kind: string().min(1),
|
|
46327
|
-
name: string().min(1),
|
|
46328
|
-
/**
|
|
46329
|
-
* ADOPT an existing id instead of minting a new one.
|
|
46330
|
-
*
|
|
46331
|
-
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
46332
|
-
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
46333
|
-
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
46334
|
-
* bound to a broker that no longer exists. Re-entering the password under the
|
|
46335
|
-
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
46336
|
-
* password.
|
|
46337
|
-
*
|
|
46338
|
-
* A provider MUST refuse an id that is already in use — adopting a live
|
|
46339
|
-
* broker's id would silently take it over.
|
|
46340
|
-
*/
|
|
46341
|
-
id: string().min(1).optional(),
|
|
46342
|
-
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
46343
|
-
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
46344
|
-
* branch on receipt — invalid shape rejects the add. */
|
|
46345
|
-
settings: record(string(), unknown())
|
|
46346
|
-
});
|
|
46347
|
-
var AddResultSchema = object({ id: string() });
|
|
46348
|
-
var RemoveInputSchema = object({ id: string() });
|
|
46349
|
-
var TestConnectionResultSchema = discriminatedUnion("ok", [object({
|
|
46350
|
-
ok: literal(true),
|
|
46351
|
-
latencyMs: number().nonnegative()
|
|
46352
|
-
}), object({
|
|
46353
|
-
ok: literal(false),
|
|
46354
|
-
error: string()
|
|
46355
|
-
})]);
|
|
46356
46372
|
var SettingsRecordSchema$1 = record(string(), unknown());
|
|
46357
|
-
var SettingsSchemaInputSchema = object({ kind: string() });
|
|
46358
|
-
var TestSettingsInputSchema = object({
|
|
46359
|
-
kind: string(),
|
|
46360
|
-
settings: SettingsRecordSchema$1
|
|
46361
|
-
});
|
|
46362
|
-
var TestSettingsResultSchema = discriminatedUnion("ok", [object({
|
|
46363
|
-
ok: literal(true),
|
|
46364
|
-
latencyMs: number().nonnegative().optional()
|
|
46365
|
-
}).strict(), object({
|
|
46366
|
-
ok: literal(false),
|
|
46367
|
-
error: string()
|
|
46368
|
-
})]);
|
|
46369
|
-
var SettingsSchemaResultSchema = unknown().nullable();
|
|
46370
46373
|
var PublishInputSchema = object({
|
|
46371
46374
|
brokerId: string(),
|
|
46372
46375
|
/** Kind-specific routing target.
|
|
@@ -46410,51 +46413,12 @@ var brokerCapability = {
|
|
|
46410
46413
|
kind: "poll"
|
|
46411
46414
|
},
|
|
46412
46415
|
methods: {
|
|
46413
|
-
list: method(ListInputSchema, array(BrokerInfoSchema)),
|
|
46414
|
-
get: method(GetInputSchema, BrokerInfoSchema.nullable()),
|
|
46415
|
-
/** Enumerate which addon provides which broker kind(s) for the
|
|
46416
|
-
* unified create picker. The auto-mount fans this array across
|
|
46417
|
-
* every registered `broker` provider (array-output method), so the
|
|
46418
|
-
* picker sees every kind from every provider in one call. */
|
|
46419
|
-
listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
46420
|
-
add: method(AddInputSchema, AddResultSchema, {
|
|
46421
|
-
kind: "mutation",
|
|
46422
|
-
auth: "admin"
|
|
46423
|
-
}),
|
|
46424
|
-
remove: method(RemoveInputSchema, _void(), {
|
|
46425
|
-
kind: "mutation",
|
|
46426
|
-
auth: "admin"
|
|
46427
|
-
}),
|
|
46428
|
-
testConnection: method(GetInputSchema, TestConnectionResultSchema, {
|
|
46429
|
-
kind: "mutation",
|
|
46430
|
-
auth: "admin"
|
|
46431
|
-
}),
|
|
46432
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
46433
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
46434
|
-
* when the broker id is unknown to the provider (the collection
|
|
46435
|
-
* fallback may route a foreign id to the first provider). */
|
|
46436
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46437
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
46438
|
-
* provider validates the shape and applies the change (reconnects
|
|
46439
|
-
* if credentials changed). */
|
|
46440
|
-
setSettings: method(object({
|
|
46441
|
-
id: string(),
|
|
46442
|
-
settings: SettingsRecordSchema$1
|
|
46443
|
-
}), _void(), {
|
|
46444
|
-
kind: "mutation",
|
|
46445
|
-
auth: "admin"
|
|
46446
|
-
}),
|
|
46447
46416
|
/** Returns the kind-specific connection config the consumer needs
|
|
46448
46417
|
* to open its own client (MQTT pattern: `{url, username, password,
|
|
46449
46418
|
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
46450
46419
|
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
46451
46420
|
* Returns `null` when the broker id is unknown to the provider. */
|
|
46452
46421
|
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46453
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
46454
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
46455
|
-
kind: "mutation",
|
|
46456
|
-
auth: "admin"
|
|
46457
|
-
}),
|
|
46458
46422
|
publish: method(PublishInputSchema, unknown(), {
|
|
46459
46423
|
kind: "mutation",
|
|
46460
46424
|
auth: "admin"
|
|
@@ -49078,6 +49042,9 @@ var RunningProposalEvidenceSchema = object({
|
|
|
49078
49042
|
runsFound: number().int().nonnegative(),
|
|
49079
49043
|
pausesFound: number().int().nonnegative(),
|
|
49080
49044
|
longestPauseMs: number().nonnegative(),
|
|
49045
|
+
/** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
|
|
49046
|
+
pulsesFound: number().int().nonnegative(),
|
|
49047
|
+
longestPulseMs: number().nonnegative(),
|
|
49081
49048
|
idleFloorW: number(),
|
|
49082
49049
|
noiseCeilingW: number(),
|
|
49083
49050
|
plateauW: number(),
|
|
@@ -50524,7 +50491,14 @@ var DiscoveryCandidateSchema = object({
|
|
|
50524
50491
|
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
50525
50492
|
parentStableId: string().optional(),
|
|
50526
50493
|
/** `false` = shown for context but not pickable. Default true. */
|
|
50527
|
-
selectable: boolean().optional()
|
|
50494
|
+
selectable: boolean().optional(),
|
|
50495
|
+
/**
|
|
50496
|
+
* Keys of the provider's creation form (`getChildCreationSchema`) this candidate
|
|
50497
|
+
* cannot be adopted without — a key for an encrypted board, a pairing code. The
|
|
50498
|
+
* admin asks for exactly these, on the creation form prefilled with
|
|
50499
|
+
* `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
|
|
50500
|
+
*/
|
|
50501
|
+
needsParams: array(string()).optional()
|
|
50528
50502
|
});
|
|
50529
50503
|
/**
|
|
50530
50504
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -50583,12 +50557,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
|
|
|
50583
50557
|
}), array(DiscoveryCandidateSchema), {
|
|
50584
50558
|
kind: "mutation",
|
|
50585
50559
|
auth: "admin"
|
|
50586
|
-
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50560
|
+
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50561
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50562
|
+
integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
|
|
50563
|
+
candidate: DiscoveryCandidateSchema,
|
|
50564
|
+
/** The integration the operator adopts into; a provider with several instances needs it. */
|
|
50565
|
+
integrationId: string().optional()
|
|
50566
|
+
}), DeviceSummarySchema, {
|
|
50587
50567
|
kind: "mutation",
|
|
50588
50568
|
auth: "admin"
|
|
50589
|
-
}), method(object({}), boolean()), method(object({
|
|
50569
|
+
}), method(object({}), boolean()), method(object({
|
|
50590
50570
|
type: _enum(DeviceType),
|
|
50591
|
-
|
|
50571
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50572
|
+
integrationId: string().optional()
|
|
50573
|
+
}), CreationSchemaOutputSchema), method(object({
|
|
50574
|
+
type: _enum(DeviceType),
|
|
50575
|
+
config: record(string(), unknown()),
|
|
50576
|
+
/** The integration the operator adds the device to; a provider with several instances needs it. */
|
|
50577
|
+
integrationId: string().optional()
|
|
50592
50578
|
}), DeviceSummarySchema, {
|
|
50593
50579
|
kind: "mutation",
|
|
50594
50580
|
auth: "admin"
|
|
@@ -51298,7 +51284,10 @@ method(object({
|
|
|
51298
51284
|
auth: "admin"
|
|
51299
51285
|
}), method(object({
|
|
51300
51286
|
addonId: string(),
|
|
51301
|
-
type: _enum(DeviceType)
|
|
51287
|
+
type: _enum(DeviceType),
|
|
51288
|
+
/** The integration the form is for; a provider whose integrations differ (a hub
|
|
51289
|
+
* integration takes no manual add) answers per integration. Absent = the addon's own. */
|
|
51290
|
+
integrationId: string().optional()
|
|
51302
51291
|
}), unknown().nullable()), method(object({
|
|
51303
51292
|
addonId: string(),
|
|
51304
51293
|
type: _enum(DeviceType),
|
|
@@ -51382,7 +51371,11 @@ method(object({
|
|
|
51382
51371
|
kind: "mutation",
|
|
51383
51372
|
auth: "admin",
|
|
51384
51373
|
timeoutMs: 12e4
|
|
51385
|
-
}), method(object({
|
|
51374
|
+
}), method(object({
|
|
51375
|
+
addonId: string(),
|
|
51376
|
+
/** The integration the Add-Device dialog is open on; absent = the addon's own. */
|
|
51377
|
+
integrationId: string().optional()
|
|
51378
|
+
}), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
51386
51379
|
deviceId: number(),
|
|
51387
51380
|
key: string(),
|
|
51388
51381
|
value: unknown()
|
|
@@ -53978,7 +53971,7 @@ var TestConnectionStatusEnum = _enum([
|
|
|
53978
53971
|
"inconclusive",
|
|
53979
53972
|
"unsupported"
|
|
53980
53973
|
]);
|
|
53981
|
-
var TestConnectionResultSchema
|
|
53974
|
+
var TestConnectionResultSchema = object({
|
|
53982
53975
|
/** True ONLY for `validated`. Never true for a test that did not run. */
|
|
53983
53976
|
success: boolean(),
|
|
53984
53977
|
error: string().optional(),
|
|
@@ -53999,11 +53992,7 @@ var CreateIntegrationInputSchema = object({
|
|
|
53999
53992
|
info: record(string(), unknown()).optional(),
|
|
54000
53993
|
/** `stableId` of the discovery candidate the form was seeded from (D984). */
|
|
54001
53994
|
originStableId: string().optional(),
|
|
54002
|
-
settings: record(string(), unknown()).optional()
|
|
54003
|
-
/** Keys of `settings` the form's schema declares secret (by type) — kept in
|
|
54004
|
-
* the keychain, not the table (D787). */
|
|
54005
|
-
secretKeys: array(string()).optional(),
|
|
54006
|
-
skipRestart: boolean().optional()
|
|
53995
|
+
settings: record(string(), unknown()).optional()
|
|
54007
53996
|
});
|
|
54008
53997
|
var UpdateIntegrationInputSchema = object({
|
|
54009
53998
|
id: string(),
|
|
@@ -54024,20 +54013,10 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54024
54013
|
}), method(object({ id: string() }), DeleteIntegrationResultSchema, {
|
|
54025
54014
|
kind: "mutation",
|
|
54026
54015
|
auth: "admin"
|
|
54027
|
-
}), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
|
|
54028
|
-
id: string(),
|
|
54029
|
-
settings: record(string(), unknown()),
|
|
54030
|
-
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
54031
|
-
secretKeys: array(string()).optional(),
|
|
54032
|
-
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
54033
|
-
skipRestart: boolean().optional()
|
|
54034
|
-
}), object({ success: boolean() }), {
|
|
54035
|
-
kind: "mutation",
|
|
54036
|
-
auth: "admin"
|
|
54037
54016
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
54038
54017
|
addonId: string(),
|
|
54039
54018
|
settings: record(string(), unknown())
|
|
54040
|
-
}), TestConnectionResultSchema
|
|
54019
|
+
}), TestConnectionResultSchema, {
|
|
54041
54020
|
kind: "mutation",
|
|
54042
54021
|
auth: "admin"
|
|
54043
54022
|
});
|
|
@@ -54064,6 +54043,14 @@ var ConfigSchemaPassthrough$3 = custom();
|
|
|
54064
54043
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54065
54044
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54066
54045
|
/**
|
|
54046
|
+
* What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
|
|
54047
|
+
* instance is an integration that brings devices (the registry owns its on/off switch);
|
|
54048
|
+
* `services`: a connection configured from the addon's own function page, never listed on
|
|
54049
|
+
* Integrations.
|
|
54050
|
+
* It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
|
|
54051
|
+
*/
|
|
54052
|
+
var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
|
|
54053
|
+
/**
|
|
54067
54054
|
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54068
54055
|
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54069
54056
|
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
@@ -54074,6 +54061,22 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54074
54061
|
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54075
54062
|
id: string().min(1),
|
|
54076
54063
|
label: string().min(1),
|
|
54064
|
+
/**
|
|
54065
|
+
* The function capability this kind is configured for (`broker`, `smtp-provider`,
|
|
54066
|
+
* `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
|
|
54067
|
+
* provider with more than one section must name it on every kind.
|
|
54068
|
+
*/
|
|
54069
|
+
functionCap: string().min(1),
|
|
54070
|
+
/**
|
|
54071
|
+
* For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
|
|
54072
|
+
* (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
|
|
54073
|
+
* finds the instances whose kind names it; absent takes the kind `id`.
|
|
54074
|
+
*/
|
|
54075
|
+
brokerKind: string().min(1).optional(),
|
|
54076
|
+
/** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
|
|
54077
|
+
role: IntegrationInstanceRoleSchema.optional(),
|
|
54078
|
+
/** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
|
|
54079
|
+
canTest: boolean().optional(),
|
|
54077
54080
|
/** The kind signs in through a server-held interactive session. */
|
|
54078
54081
|
signIn: object({
|
|
54079
54082
|
basePath: string().min(1),
|
|
@@ -54086,11 +54089,7 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54086
54089
|
});
|
|
54087
54090
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54088
54091
|
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54089
|
-
role:
|
|
54090
|
-
"devices",
|
|
54091
|
-
"services",
|
|
54092
|
-
"both"
|
|
54093
|
-
]),
|
|
54092
|
+
role: IntegrationInstanceRoleSchema,
|
|
54094
54093
|
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54095
54094
|
discovery: object({
|
|
54096
54095
|
mode: _enum([
|
|
@@ -54165,7 +54164,11 @@ var integrationInstancesCapability = {
|
|
|
54165
54164
|
methods: {
|
|
54166
54165
|
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54167
54166
|
/** The form; `id: null` is the create form. */
|
|
54168
|
-
instanceFormSchema: method(object({
|
|
54167
|
+
instanceFormSchema: method(object({
|
|
54168
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54169
|
+
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
54170
|
+
kind: string().min(1).optional()
|
|
54171
|
+
}), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54169
54172
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54170
54173
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54171
54174
|
/**
|
|
@@ -54175,7 +54178,9 @@ var integrationInstancesCapability = {
|
|
|
54175
54178
|
createInstance: method(object({
|
|
54176
54179
|
id: IntegrationInstanceIdSchema.optional(),
|
|
54177
54180
|
name: string().min(1),
|
|
54178
|
-
kind: string().min(1).optional()
|
|
54181
|
+
kind: string().min(1).optional(),
|
|
54182
|
+
/** Form values written with the record (a link picked in the create dialog). */
|
|
54183
|
+
values: record(string(), unknown()).optional()
|
|
54179
54184
|
}), object({ id: string() }), {
|
|
54180
54185
|
kind: "mutation",
|
|
54181
54186
|
auth: "admin"
|
|
@@ -54202,8 +54207,10 @@ var integrationInstancesCapability = {
|
|
|
54202
54207
|
}),
|
|
54203
54208
|
testInstance: optionalMethod(object({
|
|
54204
54209
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
54205
|
-
values: record(string(), unknown())
|
|
54206
|
-
|
|
54210
|
+
values: record(string(), unknown()),
|
|
54211
|
+
/** The kind to test when `id` is null and the provider has several. */
|
|
54212
|
+
kind: string().min(1).optional()
|
|
54213
|
+
}), TestConnectionResultSchema, {
|
|
54207
54214
|
kind: "mutation",
|
|
54208
54215
|
auth: "admin"
|
|
54209
54216
|
})
|
|
@@ -56708,6 +56715,380 @@ var SystemMetricsSchema = object({
|
|
|
56708
56715
|
gpuMemoryPercent: number().optional()
|
|
56709
56716
|
});
|
|
56710
56717
|
method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
|
|
56718
|
+
/**
|
|
56719
|
+
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
56720
|
+
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
56721
|
+
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
56722
|
+
*
|
|
56723
|
+
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
56724
|
+
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
56725
|
+
* that with the addon id closed over). It is deliberately NOT in any input
|
|
56726
|
+
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
56727
|
+
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
56728
|
+
* addons go through the owner's capability.
|
|
56729
|
+
*
|
|
56730
|
+
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
56731
|
+
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
56732
|
+
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
56733
|
+
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
56734
|
+
* row's own ref.
|
|
56735
|
+
*
|
|
56736
|
+
* Internal: addons reach it through `ctx.records`, never directly.
|
|
56737
|
+
*/
|
|
56738
|
+
var RecordIdSchema = string().min(1).max(256);
|
|
56739
|
+
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
56740
|
+
var RecordBodySchema = record(string(), unknown());
|
|
56741
|
+
var RecordInputSchema = object({
|
|
56742
|
+
id: RecordIdSchema,
|
|
56743
|
+
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
56744
|
+
deviceId: number().int().optional(),
|
|
56745
|
+
/** The domain clock, ms: observed / created / opened. */
|
|
56746
|
+
at: number().int().optional(),
|
|
56747
|
+
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
56748
|
+
expiresAt: number().int().optional(),
|
|
56749
|
+
data: RecordBodySchema
|
|
56750
|
+
});
|
|
56751
|
+
var RecordViewSchema = object({
|
|
56752
|
+
id: string(),
|
|
56753
|
+
deviceId: number().int().nullable(),
|
|
56754
|
+
at: number().nullable(),
|
|
56755
|
+
expiresAt: number().nullable(),
|
|
56756
|
+
rev: number().int(),
|
|
56757
|
+
updatedAt: number(),
|
|
56758
|
+
data: RecordBodySchema
|
|
56759
|
+
});
|
|
56760
|
+
/** Declared index fields only. Anything else does not compile, by design. */
|
|
56761
|
+
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
56762
|
+
string(),
|
|
56763
|
+
number(),
|
|
56764
|
+
boolean(),
|
|
56765
|
+
object({ in: array(union([
|
|
56766
|
+
string(),
|
|
56767
|
+
number(),
|
|
56768
|
+
boolean()
|
|
56769
|
+
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
56770
|
+
object({
|
|
56771
|
+
gt: union([string(), number()]).optional(),
|
|
56772
|
+
gte: union([string(), number()]).optional(),
|
|
56773
|
+
lt: union([string(), number()]).optional(),
|
|
56774
|
+
lte: union([string(), number()]).optional()
|
|
56775
|
+
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
56776
|
+
]));
|
|
56777
|
+
var KindShape = { kind: RecordKindNameSchema };
|
|
56778
|
+
var TimeWindowShape = {
|
|
56779
|
+
deviceId: number().int().optional(),
|
|
56780
|
+
atFrom: number().optional(),
|
|
56781
|
+
atTo: number().optional()
|
|
56782
|
+
};
|
|
56783
|
+
var RecordQueryInputSchema = object({
|
|
56784
|
+
...KindShape,
|
|
56785
|
+
...TimeWindowShape,
|
|
56786
|
+
where: RecordWhereSchema.optional(),
|
|
56787
|
+
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
56788
|
+
order: _enum(["at", "id"]).optional(),
|
|
56789
|
+
desc: boolean().optional(),
|
|
56790
|
+
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
56791
|
+
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
56792
|
+
cursor: string().min(1).optional()
|
|
56793
|
+
});
|
|
56794
|
+
var RecordQueryResultSchema = object({
|
|
56795
|
+
records: array(RecordViewSchema),
|
|
56796
|
+
nextCursor: string().nullable()
|
|
56797
|
+
});
|
|
56798
|
+
var RecordKindStateSchema = _enum([
|
|
56799
|
+
"created",
|
|
56800
|
+
"unchanged",
|
|
56801
|
+
"updated"
|
|
56802
|
+
]);
|
|
56803
|
+
var DeclareKindResultSchema = object({
|
|
56804
|
+
kindId: number().int(),
|
|
56805
|
+
state: RecordKindStateSchema,
|
|
56806
|
+
createdIndexes: array(string())
|
|
56807
|
+
});
|
|
56808
|
+
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
56809
|
+
var SecretRefsList = array(string());
|
|
56810
|
+
/**
|
|
56811
|
+
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
56812
|
+
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
56813
|
+
*/
|
|
56814
|
+
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
56815
|
+
object({
|
|
56816
|
+
op: literal("put"),
|
|
56817
|
+
kind: RecordKindNameSchema,
|
|
56818
|
+
record: RecordInputSchema,
|
|
56819
|
+
ifRev: number().int().min(0).optional()
|
|
56820
|
+
}),
|
|
56821
|
+
object({
|
|
56822
|
+
op: literal("patch"),
|
|
56823
|
+
kind: RecordKindNameSchema,
|
|
56824
|
+
id: RecordIdSchema,
|
|
56825
|
+
data: RecordBodySchema,
|
|
56826
|
+
at: number().int().nullable().optional(),
|
|
56827
|
+
expiresAt: number().int().nullable().optional(),
|
|
56828
|
+
ifRev: number().int().min(1).optional()
|
|
56829
|
+
}),
|
|
56830
|
+
object({
|
|
56831
|
+
op: literal("delete"),
|
|
56832
|
+
kind: RecordKindNameSchema,
|
|
56833
|
+
id: RecordIdSchema
|
|
56834
|
+
})
|
|
56835
|
+
]);
|
|
56836
|
+
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
56837
|
+
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
56838
|
+
kind: "mutation",
|
|
56839
|
+
auth: "admin"
|
|
56840
|
+
}), method(object({
|
|
56841
|
+
...KindShape,
|
|
56842
|
+
id: RecordIdSchema
|
|
56843
|
+
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
56844
|
+
...KindShape,
|
|
56845
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56846
|
+
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
56847
|
+
...KindShape,
|
|
56848
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56849
|
+
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
56850
|
+
...KindShape,
|
|
56851
|
+
record: RecordInputSchema,
|
|
56852
|
+
/** Compare-and-set. `0` means "the row must not exist". */
|
|
56853
|
+
ifRev: number().int().min(0).optional()
|
|
56854
|
+
}), object({
|
|
56855
|
+
rev: number().int(),
|
|
56856
|
+
releasedRefs: SecretRefsList
|
|
56857
|
+
}), {
|
|
56858
|
+
kind: "mutation",
|
|
56859
|
+
auth: "admin"
|
|
56860
|
+
}), method(object({
|
|
56861
|
+
...KindShape,
|
|
56862
|
+
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56863
|
+
}), object({
|
|
56864
|
+
count: number().int(),
|
|
56865
|
+
releasedRefs: SecretRefsList
|
|
56866
|
+
}), {
|
|
56867
|
+
kind: "mutation",
|
|
56868
|
+
auth: "admin"
|
|
56869
|
+
}), method(object({
|
|
56870
|
+
...KindShape,
|
|
56871
|
+
id: RecordIdSchema,
|
|
56872
|
+
data: RecordBodySchema,
|
|
56873
|
+
at: number().int().nullable().optional(),
|
|
56874
|
+
expiresAt: number().int().nullable().optional(),
|
|
56875
|
+
ifRev: number().int().min(1).optional()
|
|
56876
|
+
}), object({
|
|
56877
|
+
rev: number().int(),
|
|
56878
|
+
releasedRefs: SecretRefsList
|
|
56879
|
+
}), {
|
|
56880
|
+
kind: "mutation",
|
|
56881
|
+
auth: "admin"
|
|
56882
|
+
}), method(object({
|
|
56883
|
+
...KindShape,
|
|
56884
|
+
id: RecordIdSchema
|
|
56885
|
+
}), object({
|
|
56886
|
+
deleted: boolean(),
|
|
56887
|
+
secretRefs: SecretRefsList
|
|
56888
|
+
}), {
|
|
56889
|
+
kind: "mutation",
|
|
56890
|
+
auth: "admin"
|
|
56891
|
+
}), method(object({
|
|
56892
|
+
...KindShape,
|
|
56893
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56894
|
+
}), object({
|
|
56895
|
+
deleted: number().int(),
|
|
56896
|
+
secretRefs: SecretRefsList
|
|
56897
|
+
}), {
|
|
56898
|
+
kind: "mutation",
|
|
56899
|
+
auth: "admin"
|
|
56900
|
+
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
56901
|
+
...KindShape,
|
|
56902
|
+
...TimeWindowShape,
|
|
56903
|
+
where: RecordWhereSchema.optional()
|
|
56904
|
+
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
56905
|
+
...KindShape,
|
|
56906
|
+
deviceId: number().int().optional(),
|
|
56907
|
+
from: number(),
|
|
56908
|
+
to: number(),
|
|
56909
|
+
bucketMs: number().int().positive()
|
|
56910
|
+
}), object({ buckets: array(object({
|
|
56911
|
+
bucketStart: number(),
|
|
56912
|
+
count: number().int()
|
|
56913
|
+
})) }), { auth: "admin" }), method(object({
|
|
56914
|
+
...KindShape,
|
|
56915
|
+
op: _enum([
|
|
56916
|
+
"sum",
|
|
56917
|
+
"min",
|
|
56918
|
+
"max",
|
|
56919
|
+
"count"
|
|
56920
|
+
]),
|
|
56921
|
+
field: RecordFieldPathSchema.optional(),
|
|
56922
|
+
groupBy: RecordFieldPathSchema.optional(),
|
|
56923
|
+
where: RecordWhereSchema.optional()
|
|
56924
|
+
}), object({ rows: array(object({
|
|
56925
|
+
group: union([
|
|
56926
|
+
string(),
|
|
56927
|
+
number(),
|
|
56928
|
+
_null()
|
|
56929
|
+
]),
|
|
56930
|
+
value: number().nullable()
|
|
56931
|
+
})) }), { auth: "admin" }), method(object({
|
|
56932
|
+
...KindShape,
|
|
56933
|
+
deviceId: number().int().optional(),
|
|
56934
|
+
atBefore: number().optional(),
|
|
56935
|
+
where: RecordWhereSchema.optional()
|
|
56936
|
+
}), object({
|
|
56937
|
+
deleted: number().int(),
|
|
56938
|
+
secretRefs: SecretRefsList
|
|
56939
|
+
}), {
|
|
56940
|
+
kind: "mutation",
|
|
56941
|
+
auth: "admin"
|
|
56942
|
+
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
56943
|
+
results: array(object({
|
|
56944
|
+
rev: number().int().nullable(),
|
|
56945
|
+
deleted: boolean()
|
|
56946
|
+
})),
|
|
56947
|
+
releasedRefs: SecretRefsList,
|
|
56948
|
+
secretRefs: SecretRefsList
|
|
56949
|
+
}), {
|
|
56950
|
+
kind: "mutation",
|
|
56951
|
+
auth: "admin"
|
|
56952
|
+
}), method(object({
|
|
56953
|
+
deviceId: number().int(),
|
|
56954
|
+
kinds: array(RecordKindNameSchema).optional()
|
|
56955
|
+
}), object({
|
|
56956
|
+
deleted: record(string(), number().int()),
|
|
56957
|
+
secretRefs: SecretRefsList
|
|
56958
|
+
}), {
|
|
56959
|
+
kind: "mutation",
|
|
56960
|
+
auth: "admin"
|
|
56961
|
+
}), method(object(KindShape), object({ id: number().int() }), {
|
|
56962
|
+
kind: "mutation",
|
|
56963
|
+
auth: "admin"
|
|
56964
|
+
});
|
|
56965
|
+
/**
|
|
56966
|
+
* The wire shapes of the migration API, version 1 (planning#447).
|
|
56967
|
+
*
|
|
56968
|
+
* A migration is code that outlives the server it was written against: an addon at
|
|
56969
|
+
* version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
|
|
56970
|
+
* the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
|
|
56971
|
+
* is the contract of every method of it, one Zod pair per method.
|
|
56972
|
+
*
|
|
56973
|
+
* APPEND-ONLY. A released method is never removed and never changes meaning: an input
|
|
56974
|
+
* field is never removed or made required, an enum never narrows, an output field is
|
|
56975
|
+
* never removed. A change that cannot be expressed that way is `migration-api.v2`, next
|
|
56976
|
+
* to this file. `scripts/check-migration-api-append-only.ts` holds the line against
|
|
56977
|
+
* `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
|
|
56978
|
+
* `it('<id>'` in `migration-api.contract.spec.ts`.
|
|
56979
|
+
*/
|
|
56980
|
+
var Rows = record(string(), unknown());
|
|
56981
|
+
var IntegrationId = object({ integrationId: string().min(1) });
|
|
56982
|
+
var RecordId = object({ id: string().min(1) });
|
|
56983
|
+
var RecordRowSchema = object({
|
|
56984
|
+
id: string(),
|
|
56985
|
+
deviceId: number().int().nullable(),
|
|
56986
|
+
at: number().nullable(),
|
|
56987
|
+
expiresAt: number().nullable(),
|
|
56988
|
+
rev: number().int(),
|
|
56989
|
+
updatedAt: number(),
|
|
56990
|
+
data: Rows
|
|
56991
|
+
});
|
|
56992
|
+
var RecordPutShape = {
|
|
56993
|
+
id: string().min(1),
|
|
56994
|
+
deviceId: number().int().optional(),
|
|
56995
|
+
at: number().int().optional(),
|
|
56996
|
+
expiresAt: number().int().optional(),
|
|
56997
|
+
data: Rows
|
|
56998
|
+
};
|
|
56999
|
+
var MigrationIntegrationSchema = object({
|
|
57000
|
+
id: string(),
|
|
57001
|
+
addonId: string(),
|
|
57002
|
+
name: string(),
|
|
57003
|
+
info: Rows,
|
|
57004
|
+
originStableId: string().optional()
|
|
57005
|
+
});
|
|
57006
|
+
var MigrationDeviceSchema = object({
|
|
57007
|
+
id: number().int(),
|
|
57008
|
+
integrationId: string().nullable().optional(),
|
|
57009
|
+
/** The owning device; null or absent for a root. */
|
|
57010
|
+
parentDeviceId: number().int().nullable().optional(),
|
|
57011
|
+
type: string().optional(),
|
|
57012
|
+
name: string().optional(),
|
|
57013
|
+
stableId: string().optional()
|
|
57014
|
+
});
|
|
57015
|
+
/** What the engine says about the migration API it serves: lets an addon notice an older server. */
|
|
57016
|
+
var MigrationApiInfoSchema = object({
|
|
57017
|
+
version: number().int(),
|
|
57018
|
+
methods: array(string())
|
|
57019
|
+
});
|
|
57020
|
+
object({
|
|
57021
|
+
message: string(),
|
|
57022
|
+
meta: Rows.optional(),
|
|
57023
|
+
/** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
|
|
57024
|
+
tags: object({ deviceId: number().int().optional() }).optional()
|
|
57025
|
+
});
|
|
57026
|
+
object({
|
|
57027
|
+
owner: string().min(1),
|
|
57028
|
+
metas: array(MigrationMetaSchema)
|
|
57029
|
+
}), object({
|
|
57030
|
+
bootId: string(),
|
|
57031
|
+
snapshot: _enum([
|
|
57032
|
+
"none",
|
|
57033
|
+
"written",
|
|
57034
|
+
"refused"
|
|
57035
|
+
]),
|
|
57036
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57037
|
+
/** The migration API the engine serves; absent from a server that predates it. */
|
|
57038
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
57039
|
+
}), object({ id: string().min(1) }), object({
|
|
57040
|
+
proceed: boolean(),
|
|
57041
|
+
state: union([literal("pending"), MigrationStatusSchema]),
|
|
57042
|
+
reason: string().nullable()
|
|
57043
|
+
}), object({
|
|
57044
|
+
id: string().min(1),
|
|
57045
|
+
result: MigrationOwnerResultSchema
|
|
57046
|
+
}), object({
|
|
57047
|
+
status: MigrationStatusSchema,
|
|
57048
|
+
error: string().nullable(),
|
|
57049
|
+
rowsChanged: number().int().nonnegative()
|
|
57050
|
+
}), object({
|
|
57051
|
+
rows: number().int().nonnegative(),
|
|
57052
|
+
secrets: number().int().nonnegative()
|
|
57053
|
+
}), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
|
|
57054
|
+
present: boolean(),
|
|
57055
|
+
removed: Rows
|
|
57056
|
+
}), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
|
|
57057
|
+
...RecordPutShape,
|
|
57058
|
+
ifRev: number().int().optional()
|
|
57059
|
+
}), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
|
|
57060
|
+
partial: Rows,
|
|
57061
|
+
ifRev: number().int().optional()
|
|
57062
|
+
}), object({ rev: number().int() }), object({ deleted: boolean() }), object({
|
|
57063
|
+
deviceId: number().int().optional(),
|
|
57064
|
+
atFrom: number().optional(),
|
|
57065
|
+
atTo: number().optional(),
|
|
57066
|
+
where: RecordWhereSchema.optional(),
|
|
57067
|
+
limit: number().int().min(1).optional(),
|
|
57068
|
+
cursor: string().min(1).optional(),
|
|
57069
|
+
resolveSecrets: boolean().optional()
|
|
57070
|
+
}), object({
|
|
57071
|
+
records: array(RecordRowSchema),
|
|
57072
|
+
nextCursor: string().nullable()
|
|
57073
|
+
}), object({
|
|
57074
|
+
deviceId: number().int().optional(),
|
|
57075
|
+
atBefore: number().optional(),
|
|
57076
|
+
where: RecordWhereSchema.optional()
|
|
57077
|
+
}), object({ count: number().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string().min(1)) }), object({ removed: array(string()) }), object({ refs: array(string()) }), object({ values: record(string(), string().nullable()) }), object({
|
|
57078
|
+
ref: string().min(1),
|
|
57079
|
+
value: string()
|
|
57080
|
+
}), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
|
|
57081
|
+
addonId: string().min(1),
|
|
57082
|
+
integrationId: string().min(1).optional()
|
|
57083
|
+
}), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
|
|
57084
|
+
id: number().int(),
|
|
57085
|
+
integrationId: string().nullable(),
|
|
57086
|
+
/** Whether the device carries any stored config; its content is not offered. */
|
|
57087
|
+
hasConfig: boolean()
|
|
57088
|
+
}).nullable(), object({
|
|
57089
|
+
deviceId: number().int(),
|
|
57090
|
+
integrationId: string().min(1)
|
|
57091
|
+
}), object({}), object({}), object({ nodeId: string() }), object({}), object({});
|
|
56711
57092
|
method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
|
|
56712
57093
|
id: string().min(1),
|
|
56713
57094
|
error: string().min(1)
|
|
@@ -56737,7 +57118,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56737
57118
|
"written",
|
|
56738
57119
|
"refused"
|
|
56739
57120
|
]),
|
|
56740
|
-
steps: array(MigrationOwnerStepSchema)
|
|
57121
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57122
|
+
/** The migration API this engine serves (additive: a server that predates it omits it). */
|
|
57123
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
56741
57124
|
}), {
|
|
56742
57125
|
kind: "mutation",
|
|
56743
57126
|
auth: "admin"
|
|
@@ -56758,6 +57141,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56758
57141
|
}), {
|
|
56759
57142
|
kind: "mutation",
|
|
56760
57143
|
auth: "admin"
|
|
57144
|
+
}), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
|
|
57145
|
+
rows: number().int().nonnegative(),
|
|
57146
|
+
secrets: number().int().nonnegative()
|
|
57147
|
+
}), {
|
|
57148
|
+
kind: "mutation",
|
|
57149
|
+
auth: "admin"
|
|
57150
|
+
}), method(object({
|
|
57151
|
+
integrationId: string().min(1),
|
|
57152
|
+
patch: record(string(), unknown())
|
|
57153
|
+
}), object({ written: boolean() }), {
|
|
57154
|
+
kind: "mutation",
|
|
57155
|
+
auth: "admin"
|
|
57156
|
+
}), method(object({
|
|
57157
|
+
integrationId: string().min(1),
|
|
57158
|
+
keys: array(string().min(1))
|
|
57159
|
+
}), object({
|
|
57160
|
+
present: boolean(),
|
|
57161
|
+
removed: record(string(), unknown())
|
|
57162
|
+
}), {
|
|
57163
|
+
kind: "mutation",
|
|
57164
|
+
auth: "admin"
|
|
56761
57165
|
});
|
|
56762
57166
|
method(object({
|
|
56763
57167
|
sourceUrl: string(),
|
|
@@ -59383,7 +59787,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
59383
59787
|
}), method(RelocateMediaInputSchema, object({ jobId: string() }), {
|
|
59384
59788
|
kind: "mutation",
|
|
59385
59789
|
auth: "admin"
|
|
59386
|
-
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(
|
|
59790
|
+
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
|
|
59791
|
+
kind: "query",
|
|
59792
|
+
auth: "admin"
|
|
59793
|
+
}), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
|
|
59387
59794
|
kind: "query",
|
|
59388
59795
|
auth: "admin"
|
|
59389
59796
|
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
@@ -61790,7 +62197,7 @@ var BrokerStatusSchema$1 = _enum([
|
|
|
61790
62197
|
"unreachable",
|
|
61791
62198
|
"tls-error"
|
|
61792
62199
|
]);
|
|
61793
|
-
var BrokerInfoSchema
|
|
62200
|
+
var BrokerInfoSchema = object({
|
|
61794
62201
|
id: string(),
|
|
61795
62202
|
name: string(),
|
|
61796
62203
|
url: string(),
|
|
@@ -61863,7 +62270,7 @@ var mqttBrokerCapability = {
|
|
|
61863
62270
|
kind: "poll"
|
|
61864
62271
|
},
|
|
61865
62272
|
methods: {
|
|
61866
|
-
listBrokers: method(_void(), array(BrokerInfoSchema
|
|
62273
|
+
listBrokers: method(_void(), array(BrokerInfoSchema)),
|
|
61867
62274
|
getBrokerConfig: method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }),
|
|
61868
62275
|
addBroker: method(AddBrokerInputSchema, AddBrokerResultSchema, {
|
|
61869
62276
|
kind: "mutation",
|
|
@@ -68190,253 +68597,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
|
|
|
68190
68597
|
auth: "admin"
|
|
68191
68598
|
});
|
|
68192
68599
|
/**
|
|
68193
|
-
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
68194
|
-
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
68195
|
-
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
68196
|
-
*
|
|
68197
|
-
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
68198
|
-
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
68199
|
-
* that with the addon id closed over). It is deliberately NOT in any input
|
|
68200
|
-
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
68201
|
-
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
68202
|
-
* addons go through the owner's capability.
|
|
68203
|
-
*
|
|
68204
|
-
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
68205
|
-
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
68206
|
-
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
68207
|
-
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
68208
|
-
* row's own ref.
|
|
68209
|
-
*
|
|
68210
|
-
* Internal: addons reach it through `ctx.records`, never directly.
|
|
68211
|
-
*/
|
|
68212
|
-
var RecordIdSchema = string().min(1).max(256);
|
|
68213
|
-
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
68214
|
-
var RecordBodySchema = record(string(), unknown());
|
|
68215
|
-
var RecordInputSchema = object({
|
|
68216
|
-
id: RecordIdSchema,
|
|
68217
|
-
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
68218
|
-
deviceId: number().int().optional(),
|
|
68219
|
-
/** The domain clock, ms: observed / created / opened. */
|
|
68220
|
-
at: number().int().optional(),
|
|
68221
|
-
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
68222
|
-
expiresAt: number().int().optional(),
|
|
68223
|
-
data: RecordBodySchema
|
|
68224
|
-
});
|
|
68225
|
-
var RecordViewSchema = object({
|
|
68226
|
-
id: string(),
|
|
68227
|
-
deviceId: number().int().nullable(),
|
|
68228
|
-
at: number().nullable(),
|
|
68229
|
-
expiresAt: number().nullable(),
|
|
68230
|
-
rev: number().int(),
|
|
68231
|
-
updatedAt: number(),
|
|
68232
|
-
data: RecordBodySchema
|
|
68233
|
-
});
|
|
68234
|
-
/** Declared index fields only. Anything else does not compile, by design. */
|
|
68235
|
-
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
68236
|
-
string(),
|
|
68237
|
-
number(),
|
|
68238
|
-
boolean(),
|
|
68239
|
-
object({ in: array(union([
|
|
68240
|
-
string(),
|
|
68241
|
-
number(),
|
|
68242
|
-
boolean()
|
|
68243
|
-
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
68244
|
-
object({
|
|
68245
|
-
gt: union([string(), number()]).optional(),
|
|
68246
|
-
gte: union([string(), number()]).optional(),
|
|
68247
|
-
lt: union([string(), number()]).optional(),
|
|
68248
|
-
lte: union([string(), number()]).optional()
|
|
68249
|
-
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
68250
|
-
]));
|
|
68251
|
-
var KindShape = { kind: RecordKindNameSchema };
|
|
68252
|
-
var TimeWindowShape = {
|
|
68253
|
-
deviceId: number().int().optional(),
|
|
68254
|
-
atFrom: number().optional(),
|
|
68255
|
-
atTo: number().optional()
|
|
68256
|
-
};
|
|
68257
|
-
var RecordQueryInputSchema = object({
|
|
68258
|
-
...KindShape,
|
|
68259
|
-
...TimeWindowShape,
|
|
68260
|
-
where: RecordWhereSchema.optional(),
|
|
68261
|
-
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
68262
|
-
order: _enum(["at", "id"]).optional(),
|
|
68263
|
-
desc: boolean().optional(),
|
|
68264
|
-
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
68265
|
-
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
68266
|
-
cursor: string().min(1).optional()
|
|
68267
|
-
});
|
|
68268
|
-
var RecordQueryResultSchema = object({
|
|
68269
|
-
records: array(RecordViewSchema),
|
|
68270
|
-
nextCursor: string().nullable()
|
|
68271
|
-
});
|
|
68272
|
-
var RecordKindStateSchema = _enum([
|
|
68273
|
-
"created",
|
|
68274
|
-
"unchanged",
|
|
68275
|
-
"updated"
|
|
68276
|
-
]);
|
|
68277
|
-
var DeclareKindResultSchema = object({
|
|
68278
|
-
kindId: number().int(),
|
|
68279
|
-
state: RecordKindStateSchema,
|
|
68280
|
-
createdIndexes: array(string())
|
|
68281
|
-
});
|
|
68282
|
-
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
68283
|
-
var SecretRefsList = array(string());
|
|
68284
|
-
/**
|
|
68285
|
-
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
68286
|
-
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
68287
|
-
*/
|
|
68288
|
-
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
68289
|
-
object({
|
|
68290
|
-
op: literal("put"),
|
|
68291
|
-
kind: RecordKindNameSchema,
|
|
68292
|
-
record: RecordInputSchema,
|
|
68293
|
-
ifRev: number().int().min(0).optional()
|
|
68294
|
-
}),
|
|
68295
|
-
object({
|
|
68296
|
-
op: literal("patch"),
|
|
68297
|
-
kind: RecordKindNameSchema,
|
|
68298
|
-
id: RecordIdSchema,
|
|
68299
|
-
data: RecordBodySchema,
|
|
68300
|
-
at: number().int().nullable().optional(),
|
|
68301
|
-
expiresAt: number().int().nullable().optional(),
|
|
68302
|
-
ifRev: number().int().min(1).optional()
|
|
68303
|
-
}),
|
|
68304
|
-
object({
|
|
68305
|
-
op: literal("delete"),
|
|
68306
|
-
kind: RecordKindNameSchema,
|
|
68307
|
-
id: RecordIdSchema
|
|
68308
|
-
})
|
|
68309
|
-
]);
|
|
68310
|
-
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
68311
|
-
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
68312
|
-
kind: "mutation",
|
|
68313
|
-
auth: "admin"
|
|
68314
|
-
}), method(object({
|
|
68315
|
-
...KindShape,
|
|
68316
|
-
id: RecordIdSchema
|
|
68317
|
-
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
68318
|
-
...KindShape,
|
|
68319
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68320
|
-
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
68321
|
-
...KindShape,
|
|
68322
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68323
|
-
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
68324
|
-
...KindShape,
|
|
68325
|
-
record: RecordInputSchema,
|
|
68326
|
-
/** Compare-and-set. `0` means "the row must not exist". */
|
|
68327
|
-
ifRev: number().int().min(0).optional()
|
|
68328
|
-
}), object({
|
|
68329
|
-
rev: number().int(),
|
|
68330
|
-
releasedRefs: SecretRefsList
|
|
68331
|
-
}), {
|
|
68332
|
-
kind: "mutation",
|
|
68333
|
-
auth: "admin"
|
|
68334
|
-
}), method(object({
|
|
68335
|
-
...KindShape,
|
|
68336
|
-
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68337
|
-
}), object({
|
|
68338
|
-
count: number().int(),
|
|
68339
|
-
releasedRefs: SecretRefsList
|
|
68340
|
-
}), {
|
|
68341
|
-
kind: "mutation",
|
|
68342
|
-
auth: "admin"
|
|
68343
|
-
}), method(object({
|
|
68344
|
-
...KindShape,
|
|
68345
|
-
id: RecordIdSchema,
|
|
68346
|
-
data: RecordBodySchema,
|
|
68347
|
-
at: number().int().nullable().optional(),
|
|
68348
|
-
expiresAt: number().int().nullable().optional(),
|
|
68349
|
-
ifRev: number().int().min(1).optional()
|
|
68350
|
-
}), object({
|
|
68351
|
-
rev: number().int(),
|
|
68352
|
-
releasedRefs: SecretRefsList
|
|
68353
|
-
}), {
|
|
68354
|
-
kind: "mutation",
|
|
68355
|
-
auth: "admin"
|
|
68356
|
-
}), method(object({
|
|
68357
|
-
...KindShape,
|
|
68358
|
-
id: RecordIdSchema
|
|
68359
|
-
}), object({
|
|
68360
|
-
deleted: boolean(),
|
|
68361
|
-
secretRefs: SecretRefsList
|
|
68362
|
-
}), {
|
|
68363
|
-
kind: "mutation",
|
|
68364
|
-
auth: "admin"
|
|
68365
|
-
}), method(object({
|
|
68366
|
-
...KindShape,
|
|
68367
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68368
|
-
}), object({
|
|
68369
|
-
deleted: number().int(),
|
|
68370
|
-
secretRefs: SecretRefsList
|
|
68371
|
-
}), {
|
|
68372
|
-
kind: "mutation",
|
|
68373
|
-
auth: "admin"
|
|
68374
|
-
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
68375
|
-
...KindShape,
|
|
68376
|
-
...TimeWindowShape,
|
|
68377
|
-
where: RecordWhereSchema.optional()
|
|
68378
|
-
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
68379
|
-
...KindShape,
|
|
68380
|
-
deviceId: number().int().optional(),
|
|
68381
|
-
from: number(),
|
|
68382
|
-
to: number(),
|
|
68383
|
-
bucketMs: number().int().positive()
|
|
68384
|
-
}), object({ buckets: array(object({
|
|
68385
|
-
bucketStart: number(),
|
|
68386
|
-
count: number().int()
|
|
68387
|
-
})) }), { auth: "admin" }), method(object({
|
|
68388
|
-
...KindShape,
|
|
68389
|
-
op: _enum([
|
|
68390
|
-
"sum",
|
|
68391
|
-
"min",
|
|
68392
|
-
"max",
|
|
68393
|
-
"count"
|
|
68394
|
-
]),
|
|
68395
|
-
field: RecordFieldPathSchema.optional(),
|
|
68396
|
-
groupBy: RecordFieldPathSchema.optional(),
|
|
68397
|
-
where: RecordWhereSchema.optional()
|
|
68398
|
-
}), object({ rows: array(object({
|
|
68399
|
-
group: union([
|
|
68400
|
-
string(),
|
|
68401
|
-
number(),
|
|
68402
|
-
_null()
|
|
68403
|
-
]),
|
|
68404
|
-
value: number().nullable()
|
|
68405
|
-
})) }), { auth: "admin" }), method(object({
|
|
68406
|
-
...KindShape,
|
|
68407
|
-
deviceId: number().int().optional(),
|
|
68408
|
-
atBefore: number().optional(),
|
|
68409
|
-
where: RecordWhereSchema.optional()
|
|
68410
|
-
}), object({
|
|
68411
|
-
deleted: number().int(),
|
|
68412
|
-
secretRefs: SecretRefsList
|
|
68413
|
-
}), {
|
|
68414
|
-
kind: "mutation",
|
|
68415
|
-
auth: "admin"
|
|
68416
|
-
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
68417
|
-
results: array(object({
|
|
68418
|
-
rev: number().int().nullable(),
|
|
68419
|
-
deleted: boolean()
|
|
68420
|
-
})),
|
|
68421
|
-
releasedRefs: SecretRefsList,
|
|
68422
|
-
secretRefs: SecretRefsList
|
|
68423
|
-
}), {
|
|
68424
|
-
kind: "mutation",
|
|
68425
|
-
auth: "admin"
|
|
68426
|
-
}), method(object({
|
|
68427
|
-
deviceId: number().int(),
|
|
68428
|
-
kinds: array(RecordKindNameSchema).optional()
|
|
68429
|
-
}), object({
|
|
68430
|
-
deleted: record(string(), number().int()),
|
|
68431
|
-
secretRefs: SecretRefsList
|
|
68432
|
-
}), {
|
|
68433
|
-
kind: "mutation",
|
|
68434
|
-
auth: "admin"
|
|
68435
|
-
}), method(object(KindShape), object({ id: number().int() }), {
|
|
68436
|
-
kind: "mutation",
|
|
68437
|
-
auth: "admin"
|
|
68438
|
-
});
|
|
68439
|
-
/**
|
|
68440
68600
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
68441
68601
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
68442
68602
|
* declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
|
|
@@ -75938,11 +76098,15 @@ discriminatedUnion("kind", [object({
|
|
|
75938
76098
|
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
75939
76099
|
* near F. A device that is not near one level most of the time has no floor.
|
|
75940
76100
|
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
75941
|
-
* 3.
|
|
75942
|
-
*
|
|
76101
|
+
* 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
|
|
76102
|
+
* display waking): it is neither a run nor a pause. Among the remaining
|
|
76103
|
+
* episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
|
|
76104
|
+
* two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
|
|
76105
|
+
* a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
|
|
76106
|
+
* is no run.
|
|
75943
76107
|
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
75944
|
-
* grid a person would type; stop dwell covers the longest pause
|
|
75945
|
-
* covers the longest standby blip above T.
|
|
76108
|
+
* grid a person would type; stop dwell covers the longest pause and the
|
|
76109
|
+
* kind's floor; start dwell covers the longest pulse and standby blip above T.
|
|
75946
76110
|
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
75947
76111
|
* run must hold exactly one true interval and nothing may flap outside.
|
|
75948
76112
|
*/
|
|
@@ -75953,6 +76117,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
|
75953
76117
|
30 * SECOND_MS;
|
|
75954
76118
|
60 * SECOND_MS;
|
|
75955
76119
|
20 * MINUTE_MS$1;
|
|
76120
|
+
90 * SECOND_MS;
|
|
76121
|
+
30 * MINUTE_MS$1;
|
|
76122
|
+
10 * MINUTE_MS$1;
|
|
76123
|
+
DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
|
|
75956
76124
|
10 * SECOND_MS;
|
|
75957
76125
|
10 * SECOND_MS;
|
|
75958
76126
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
@@ -76971,20 +77139,6 @@ Object.freeze({
|
|
|
76971
77139
|
access: "create",
|
|
76972
77140
|
role: "viewer"
|
|
76973
77141
|
},
|
|
76974
|
-
"broker.add": {
|
|
76975
|
-
capName: "broker",
|
|
76976
|
-
capScope: "system",
|
|
76977
|
-
addonId: null,
|
|
76978
|
-
access: "create",
|
|
76979
|
-
role: "admin"
|
|
76980
|
-
},
|
|
76981
|
-
"broker.get": {
|
|
76982
|
-
capName: "broker",
|
|
76983
|
-
capScope: "system",
|
|
76984
|
-
addonId: null,
|
|
76985
|
-
access: "view",
|
|
76986
|
-
role: "admin"
|
|
76987
|
-
},
|
|
76988
77142
|
"broker.getBrokerConfig": {
|
|
76989
77143
|
capName: "broker",
|
|
76990
77144
|
capScope: "system",
|
|
@@ -76992,20 +77146,6 @@ Object.freeze({
|
|
|
76992
77146
|
access: "view",
|
|
76993
77147
|
role: "admin"
|
|
76994
77148
|
},
|
|
76995
|
-
"broker.getSettings": {
|
|
76996
|
-
capName: "broker",
|
|
76997
|
-
capScope: "system",
|
|
76998
|
-
addonId: null,
|
|
76999
|
-
access: "view",
|
|
77000
|
-
role: "admin"
|
|
77001
|
-
},
|
|
77002
|
-
"broker.getSettingsSchema": {
|
|
77003
|
-
capName: "broker",
|
|
77004
|
-
capScope: "system",
|
|
77005
|
-
addonId: null,
|
|
77006
|
-
access: "view",
|
|
77007
|
-
role: "admin"
|
|
77008
|
-
},
|
|
77009
77149
|
"broker.getState": {
|
|
77010
77150
|
capName: "broker",
|
|
77011
77151
|
capScope: "system",
|
|
@@ -77020,20 +77160,6 @@ Object.freeze({
|
|
|
77020
77160
|
access: "view",
|
|
77021
77161
|
role: "admin"
|
|
77022
77162
|
},
|
|
77023
|
-
"broker.list": {
|
|
77024
|
-
capName: "broker",
|
|
77025
|
-
capScope: "system",
|
|
77026
|
-
addonId: null,
|
|
77027
|
-
access: "view",
|
|
77028
|
-
role: "admin"
|
|
77029
|
-
},
|
|
77030
|
-
"broker.listProviders": {
|
|
77031
|
-
capName: "broker",
|
|
77032
|
-
capScope: "system",
|
|
77033
|
-
addonId: null,
|
|
77034
|
-
access: "view",
|
|
77035
|
-
role: "admin"
|
|
77036
|
-
},
|
|
77037
77163
|
"broker.publish": {
|
|
77038
77164
|
capName: "broker",
|
|
77039
77165
|
capScope: "system",
|
|
@@ -77041,20 +77167,6 @@ Object.freeze({
|
|
|
77041
77167
|
access: "create",
|
|
77042
77168
|
role: "admin"
|
|
77043
77169
|
},
|
|
77044
|
-
"broker.remove": {
|
|
77045
|
-
capName: "broker",
|
|
77046
|
-
capScope: "system",
|
|
77047
|
-
addonId: null,
|
|
77048
|
-
access: "delete",
|
|
77049
|
-
role: "admin"
|
|
77050
|
-
},
|
|
77051
|
-
"broker.setSettings": {
|
|
77052
|
-
capName: "broker",
|
|
77053
|
-
capScope: "system",
|
|
77054
|
-
addonId: null,
|
|
77055
|
-
access: "create",
|
|
77056
|
-
role: "admin"
|
|
77057
|
-
},
|
|
77058
77170
|
"broker.subscribe": {
|
|
77059
77171
|
capName: "broker",
|
|
77060
77172
|
capScope: "system",
|
|
@@ -77062,20 +77174,6 @@ Object.freeze({
|
|
|
77062
77174
|
access: "create",
|
|
77063
77175
|
role: "admin"
|
|
77064
77176
|
},
|
|
77065
|
-
"broker.testConnection": {
|
|
77066
|
-
capName: "broker",
|
|
77067
|
-
capScope: "system",
|
|
77068
|
-
addonId: null,
|
|
77069
|
-
access: "create",
|
|
77070
|
-
role: "admin"
|
|
77071
|
-
},
|
|
77072
|
-
"broker.testSettings": {
|
|
77073
|
-
capName: "broker",
|
|
77074
|
-
capScope: "system",
|
|
77075
|
-
addonId: null,
|
|
77076
|
-
access: "create",
|
|
77077
|
-
role: "admin"
|
|
77078
|
-
},
|
|
77079
77177
|
"broker.unsubscribe": {
|
|
77080
77178
|
capName: "broker",
|
|
77081
77179
|
capScope: "system",
|
|
@@ -79708,13 +79806,6 @@ Object.freeze({
|
|
|
79708
79806
|
access: "view",
|
|
79709
79807
|
role: "admin"
|
|
79710
79808
|
},
|
|
79711
|
-
"integrations.getSettings": {
|
|
79712
|
-
capName: "integrations",
|
|
79713
|
-
capScope: "system",
|
|
79714
|
-
addonId: null,
|
|
79715
|
-
access: "view",
|
|
79716
|
-
role: "admin"
|
|
79717
|
-
},
|
|
79718
79809
|
"integrations.list": {
|
|
79719
79810
|
capName: "integrations",
|
|
79720
79811
|
capScope: "system",
|
|
@@ -79722,13 +79813,6 @@ Object.freeze({
|
|
|
79722
79813
|
access: "view",
|
|
79723
79814
|
role: "admin"
|
|
79724
79815
|
},
|
|
79725
|
-
"integrations.setSettings": {
|
|
79726
|
-
capName: "integrations",
|
|
79727
|
-
capScope: "system",
|
|
79728
|
-
addonId: null,
|
|
79729
|
-
access: "create",
|
|
79730
|
-
role: "admin"
|
|
79731
|
-
},
|
|
79732
79816
|
"integrations.testConnection": {
|
|
79733
79817
|
capName: "integrations",
|
|
79734
79818
|
capScope: "system",
|
|
@@ -80723,6 +80807,13 @@ Object.freeze({
|
|
|
80723
80807
|
access: "view",
|
|
80724
80808
|
role: "admin"
|
|
80725
80809
|
},
|
|
80810
|
+
"migrations.clearLegacyIntegrationRows": {
|
|
80811
|
+
capName: "migrations",
|
|
80812
|
+
capScope: "system",
|
|
80813
|
+
addonId: null,
|
|
80814
|
+
access: "delete",
|
|
80815
|
+
role: "admin"
|
|
80816
|
+
},
|
|
80726
80817
|
"migrations.declareOwner": {
|
|
80727
80818
|
capName: "migrations",
|
|
80728
80819
|
capScope: "system",
|
|
@@ -80730,6 +80821,13 @@ Object.freeze({
|
|
|
80730
80821
|
access: "create",
|
|
80731
80822
|
role: "admin"
|
|
80732
80823
|
},
|
|
80824
|
+
"migrations.deleteLegacyIntegrationRowKeys": {
|
|
80825
|
+
capName: "migrations",
|
|
80826
|
+
capScope: "system",
|
|
80827
|
+
addonId: null,
|
|
80828
|
+
access: "delete",
|
|
80829
|
+
role: "admin"
|
|
80830
|
+
},
|
|
80733
80831
|
"migrations.dryRun": {
|
|
80734
80832
|
capName: "migrations",
|
|
80735
80833
|
capScope: "system",
|
|
@@ -80765,6 +80863,20 @@ Object.freeze({
|
|
|
80765
80863
|
access: "create",
|
|
80766
80864
|
role: "admin"
|
|
80767
80865
|
},
|
|
80866
|
+
"migrations.patchLegacyIntegrationRows": {
|
|
80867
|
+
capName: "migrations",
|
|
80868
|
+
capScope: "system",
|
|
80869
|
+
addonId: null,
|
|
80870
|
+
access: "create",
|
|
80871
|
+
role: "admin"
|
|
80872
|
+
},
|
|
80873
|
+
"migrations.readLegacyIntegrationRows": {
|
|
80874
|
+
capName: "migrations",
|
|
80875
|
+
capScope: "system",
|
|
80876
|
+
addonId: null,
|
|
80877
|
+
access: "view",
|
|
80878
|
+
role: "admin"
|
|
80879
|
+
},
|
|
80768
80880
|
"migrations.restorePreMigration": {
|
|
80769
80881
|
capName: "migrations",
|
|
80770
80882
|
capScope: "system",
|
|
@@ -82025,6 +82137,13 @@ Object.freeze({
|
|
|
82025
82137
|
access: "view",
|
|
82026
82138
|
role: "viewer"
|
|
82027
82139
|
},
|
|
82140
|
+
"pipelineAnalytics.getMediaLocationCensus": {
|
|
82141
|
+
capName: "pipeline-analytics",
|
|
82142
|
+
capScope: "device",
|
|
82143
|
+
addonId: null,
|
|
82144
|
+
access: "view",
|
|
82145
|
+
role: "admin"
|
|
82146
|
+
},
|
|
82028
82147
|
"pipelineAnalytics.getMediaReclaimStatus": {
|
|
82029
82148
|
capName: "pipeline-analytics",
|
|
82030
82149
|
capScope: "device",
|
|
@@ -90966,6 +91085,48 @@ object({
|
|
|
90966
91085
|
delay: number().nonnegative().optional(),
|
|
90967
91086
|
commands: record(string(), unknown())
|
|
90968
91087
|
});
|
|
91088
|
+
var BrokerInUseError = class extends Error {
|
|
91089
|
+
brokerId;
|
|
91090
|
+
users;
|
|
91091
|
+
name = "BrokerInUseError";
|
|
91092
|
+
constructor(brokerId, users) {
|
|
91093
|
+
super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
|
|
91094
|
+
this.brokerId = brokerId;
|
|
91095
|
+
this.users = users;
|
|
91096
|
+
}
|
|
91097
|
+
};
|
|
91098
|
+
/** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
|
|
91099
|
+
async function assertBrokerUnused(brokerId, finders) {
|
|
91100
|
+
const users = [];
|
|
91101
|
+
for (const finder of finders) users.push(...await finder.find(brokerId));
|
|
91102
|
+
if (users.length > 0) throw new BrokerInUseError(brokerId, users);
|
|
91103
|
+
}
|
|
91104
|
+
/**
|
|
91105
|
+
* Users reached through another addon's listed rows (an addon cannot read a record kind it does
|
|
91106
|
+
* not own): every row whose `field` holds the broker id. `list` is a cap call the owner serves.
|
|
91107
|
+
*/
|
|
91108
|
+
function fieldUserFinder(spec) {
|
|
91109
|
+
return { find: async (brokerId) => (await spec.list()).filter((row) => row.formValues[spec.field] === brokerId).map((row) => ({
|
|
91110
|
+
kind: spec.kind,
|
|
91111
|
+
id: row.id,
|
|
91112
|
+
...row.name !== void 0 ? { name: row.name } : {}
|
|
91113
|
+
})) };
|
|
91114
|
+
}
|
|
91115
|
+
/**
|
|
91116
|
+
* An account provider whose `deleteInstance` is refused while anything still uses the account
|
|
91117
|
+
* (D1012). The account is an `integration-instances` kind (D1013), so the refusal lives on the
|
|
91118
|
+
* delete the instances page calls, not on a separate broker cap.
|
|
91119
|
+
*/
|
|
91120
|
+
function guardBrokerDelete(base, users) {
|
|
91121
|
+
return {
|
|
91122
|
+
...base,
|
|
91123
|
+
deleteInstance: async (input) => {
|
|
91124
|
+
await assertBrokerUnused(input.id, users());
|
|
91125
|
+
return base.deleteInstance(input);
|
|
91126
|
+
}
|
|
91127
|
+
};
|
|
91128
|
+
}
|
|
91129
|
+
entryBody({ brokerId: string() });
|
|
90969
91130
|
//#endregion
|
|
90970
91131
|
//#region ../../node_modules/xtend/immutable.js
|
|
90971
91132
|
var require_immutable = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -123010,7 +123171,7 @@ function readOnboardPort(settings) {
|
|
|
123010
123171
|
return raw;
|
|
123011
123172
|
}
|
|
123012
123173
|
/**
|
|
123013
|
-
* Build an onboard {@link BuiltOnboardEntry} from
|
|
123174
|
+
* Build an onboard {@link BuiltOnboardEntry} from the create form's values.
|
|
123014
123175
|
* Pure — the port is pre-resolved by the caller (auto-allocation is a
|
|
123015
123176
|
* side-effecting concern kept out of this helper), so the helper performs no
|
|
123016
123177
|
* I/O of its own.
|
|
@@ -123096,7 +123257,7 @@ function resolveStoredPassword(value, legacy) {
|
|
|
123096
123257
|
function retiredCiphertextPaths(config) {
|
|
123097
123258
|
const out = [];
|
|
123098
123259
|
for (const b of config.brokers) if (b.password?.startsWith("enc:v1:") === true) out.push(`brokers[${b.id}].password`);
|
|
123099
|
-
if (config.embeddedPassword
|
|
123260
|
+
if (config.embeddedPassword?.startsWith("enc:v1:") === true) out.push("embeddedPassword");
|
|
123100
123261
|
return out;
|
|
123101
123262
|
}
|
|
123102
123263
|
//#endregion
|
|
@@ -123130,6 +123291,229 @@ var mqttBrokersEntryKind = defineEntryKind("brokers", {
|
|
|
123130
123291
|
writeTrigger: "an operator adds or removes an external MQTT broker entry"
|
|
123131
123292
|
});
|
|
123132
123293
|
//#endregion
|
|
123294
|
+
//#region src/mqtt-broker-users.ts
|
|
123295
|
+
/** The addon whose integrations name an MQTT broker, and the field they keep that id in. */
|
|
123296
|
+
var MQTT_HA_ADDON_ID = "provider-mqtt-ha";
|
|
123297
|
+
var MQTT_HA_BROKER_FIELD = "mqttBrokerId";
|
|
123298
|
+
/** The finders for everything that still points at an MQTT broker. */
|
|
123299
|
+
function mqttBrokerUsers(listMqttHaInstances) {
|
|
123300
|
+
return [fieldUserFinder({
|
|
123301
|
+
kind: "integration",
|
|
123302
|
+
field: MQTT_HA_BROKER_FIELD,
|
|
123303
|
+
list: listMqttHaInstances
|
|
123304
|
+
})];
|
|
123305
|
+
}
|
|
123306
|
+
/** MQTT Discovery is installed but has not registered as a provider: not known, NOT absent (D315). */
|
|
123307
|
+
var MqttDiscoveryNotReadyError = class extends Error {
|
|
123308
|
+
constructor() {
|
|
123309
|
+
super("MQTT Discovery is installed but not answering yet (still starting, or switched off), so its integrations cannot be checked: try again once it is running");
|
|
123310
|
+
this.name = "MqttDiscoveryNotReadyError";
|
|
123311
|
+
}
|
|
123312
|
+
};
|
|
123313
|
+
/**
|
|
123314
|
+
* The integrations of the MQTT Discovery addon. Nothing can name a broker only when the registry
|
|
123315
|
+
* ANSWERED that the addon supplies no `integration-instances` AND the addon roster ANSWERED that
|
|
123316
|
+
* it is not installed: then the answer is none. An addon that is installed but has not registered
|
|
123317
|
+
* (booting, switched off) is not-yet-known, and a registry or roster that does not answer, or a
|
|
123318
|
+
* read that fails, is unknown too: all of those throw, so the delete is refused (D49, D315).
|
|
123319
|
+
*/
|
|
123320
|
+
async function listMqttHaInstancesOver(api) {
|
|
123321
|
+
const info = await api.capabilities.getCapability.query({ name: "integration-instances" });
|
|
123322
|
+
if (info === null) throw new Error("the capability registry did not answer");
|
|
123323
|
+
if (!info.providers.includes("provider-mqtt-ha")) {
|
|
123324
|
+
if ((await api.addons.list.query()).some((row) => row.manifest.id === "provider-mqtt-ha")) throw new MqttDiscoveryNotReadyError();
|
|
123325
|
+
return [];
|
|
123326
|
+
}
|
|
123327
|
+
return api.integrationInstances.listInstances.query({ addonId: MQTT_HA_ADDON_ID });
|
|
123328
|
+
}
|
|
123329
|
+
//#endregion
|
|
123330
|
+
//#region src/mqtt-broker-instances.ts
|
|
123331
|
+
/**
|
|
123332
|
+
* The MQTT brokers as `integration-instances` (D1013): one instance per `brokers` record. An
|
|
123333
|
+
* external broker is a URL; an ONBOARD broker (`onboard: true` on the create form) is a CamStack
|
|
123334
|
+
* `aedes` server on this hub: the create allocates its port (or honours the operator's) and the
|
|
123335
|
+
* addon spawns it from the record, the delete tears it down. Removing a broker an MQTT Discovery
|
|
123336
|
+
* integration still names is refused (D1012).
|
|
123337
|
+
*/
|
|
123338
|
+
var MQTT_INSTANCE_KIND = "mqtt";
|
|
123339
|
+
/** The create-form keys that only mean "run an onboard broker"; the record keeps a descriptor. */
|
|
123340
|
+
var ONBOARD_FORM_KEYS = new Set([
|
|
123341
|
+
"onboard",
|
|
123342
|
+
"port",
|
|
123343
|
+
"allowAnonymous"
|
|
123344
|
+
]);
|
|
123345
|
+
var PROBE_LABELS = {
|
|
123346
|
+
connected: "Connected",
|
|
123347
|
+
disconnected: "Disconnected",
|
|
123348
|
+
"auth-failed": "Authentication failed",
|
|
123349
|
+
unreachable: "Unreachable",
|
|
123350
|
+
"tls-error": "TLS error"
|
|
123351
|
+
};
|
|
123352
|
+
/** The instance's health word: connected / auth-failed / unreachable / tls-error, latency, clients. */
|
|
123353
|
+
function mqttHealthOf(body, live, now) {
|
|
123354
|
+
if (body.enabled === false) return { status: {
|
|
123355
|
+
state: "off",
|
|
123356
|
+
label: "Switched off"
|
|
123357
|
+
} };
|
|
123358
|
+
if (body.onboard !== void 0) return live.onboard === null ? {
|
|
123359
|
+
status: {
|
|
123360
|
+
state: "error",
|
|
123361
|
+
label: "Not listening"
|
|
123362
|
+
},
|
|
123363
|
+
lastCheckedAt: now
|
|
123364
|
+
} : {
|
|
123365
|
+
status: {
|
|
123366
|
+
state: "ok",
|
|
123367
|
+
label: "Listening",
|
|
123368
|
+
detail: `${live.onboard.connectedClients} connected clients`
|
|
123369
|
+
},
|
|
123370
|
+
lastCheckedAt: now
|
|
123371
|
+
};
|
|
123372
|
+
const probe = live.probe;
|
|
123373
|
+
if (probe === void 0) return { status: {
|
|
123374
|
+
state: "unknown",
|
|
123375
|
+
label: "Not checked yet"
|
|
123376
|
+
} };
|
|
123377
|
+
const detail = probe.status === "connected" ? probe.latencyMs !== null ? `${probe.latencyMs} ms` : void 0 : probe.error;
|
|
123378
|
+
return {
|
|
123379
|
+
status: {
|
|
123380
|
+
state: probe.status === "connected" ? "ok" : "error",
|
|
123381
|
+
label: PROBE_LABELS[probe.status],
|
|
123382
|
+
...detail !== void 0 ? { detail } : {}
|
|
123383
|
+
},
|
|
123384
|
+
lastCheckedAt: probe.lastCheckedAt
|
|
123385
|
+
};
|
|
123386
|
+
}
|
|
123387
|
+
function withoutOnboardKeys(values) {
|
|
123388
|
+
return Object.fromEntries(Object.entries(values).filter(([key]) => !ONBOARD_FORM_KEYS.has(key)));
|
|
123389
|
+
}
|
|
123390
|
+
function typedText(value) {
|
|
123391
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
123392
|
+
}
|
|
123393
|
+
/** A password the form really typed: not blank, not the redaction token, not the hidden view. */
|
|
123394
|
+
function typedSecret(value) {
|
|
123395
|
+
const text = typedText(value);
|
|
123396
|
+
return text === void 0 || text === "__camstack_redacted__" || isSecretFieldView(text) ? void 0 : text;
|
|
123397
|
+
}
|
|
123398
|
+
function resultOf(probe) {
|
|
123399
|
+
return probe.ok ? {
|
|
123400
|
+
success: true,
|
|
123401
|
+
status: "validated",
|
|
123402
|
+
...probe.latencyMs !== void 0 ? { latencyMs: probe.latencyMs } : {}
|
|
123403
|
+
} : {
|
|
123404
|
+
success: false,
|
|
123405
|
+
status: "rejected",
|
|
123406
|
+
error: probe.error ?? "The broker did not answer"
|
|
123407
|
+
};
|
|
123408
|
+
}
|
|
123409
|
+
function buildMqttBrokerInstances(records, deps) {
|
|
123410
|
+
const instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
|
|
123411
|
+
description: {
|
|
123412
|
+
role: "services",
|
|
123413
|
+
instanceMode: "multiple",
|
|
123414
|
+
kind: "brokers",
|
|
123415
|
+
canTest: true,
|
|
123416
|
+
kinds: [{
|
|
123417
|
+
id: MQTT_INSTANCE_KIND,
|
|
123418
|
+
label: "MQTT broker",
|
|
123419
|
+
functionCap: "broker",
|
|
123420
|
+
brokerKind: "mqtt",
|
|
123421
|
+
canTest: true
|
|
123422
|
+
}]
|
|
123423
|
+
},
|
|
123424
|
+
formSchema: (id) => deps.formSchema(id, false),
|
|
123425
|
+
kindOf: () => MQTT_INSTANCE_KIND,
|
|
123426
|
+
initialBody: (name) => ({
|
|
123427
|
+
name,
|
|
123428
|
+
url: "mqtt://"
|
|
123429
|
+
}),
|
|
123430
|
+
configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
|
|
123431
|
+
health: (body, id) => mqttHealthOf(body, deps.liveState(id), Date.now()),
|
|
123432
|
+
onChanged: deps.onChanged,
|
|
123433
|
+
test: async ({ id, values }) => {
|
|
123434
|
+
if (id === null && isOnboardRequested(values)) return {
|
|
123435
|
+
success: true,
|
|
123436
|
+
status: "validated",
|
|
123437
|
+
detail: "A free port is picked when the broker is created"
|
|
123438
|
+
};
|
|
123439
|
+
const saved = id === null ? null : await instances.readResolved(id);
|
|
123440
|
+
if (saved?.body.onboard !== void 0) return deps.onboardListening(saved.id) ? {
|
|
123441
|
+
success: true,
|
|
123442
|
+
status: "validated",
|
|
123443
|
+
detail: `Listening on port ${saved.body.onboard.port}`
|
|
123444
|
+
} : {
|
|
123445
|
+
success: false,
|
|
123446
|
+
status: "rejected",
|
|
123447
|
+
error: "The onboard broker is not listening"
|
|
123448
|
+
};
|
|
123449
|
+
const url = typedText(values["url"]) ?? saved?.body.url;
|
|
123450
|
+
if (url === void 0 || url === "mqtt://") return {
|
|
123451
|
+
success: false,
|
|
123452
|
+
status: "rejected",
|
|
123453
|
+
error: "Enter the broker URL first"
|
|
123454
|
+
};
|
|
123455
|
+
const username = typedText(values["username"]) ?? saved?.body.username;
|
|
123456
|
+
const password = typedSecret(values["password"]) ?? saved?.body.password;
|
|
123457
|
+
return resultOf(await deps.probe({
|
|
123458
|
+
url,
|
|
123459
|
+
...username !== void 0 ? { username } : {},
|
|
123460
|
+
...password !== void 0 ? { password } : {}
|
|
123461
|
+
}));
|
|
123462
|
+
}
|
|
123463
|
+
});
|
|
123464
|
+
const base = instances.toProvider();
|
|
123465
|
+
/** Port, descriptor and URL of an onboard broker the form asked for. */
|
|
123466
|
+
const onboardValues = async (id, name, values) => {
|
|
123467
|
+
const built = buildOnboardEntry({
|
|
123468
|
+
id,
|
|
123469
|
+
name,
|
|
123470
|
+
port: readOnboardPort(values) ?? await deps.freePort(),
|
|
123471
|
+
settings: values
|
|
123472
|
+
});
|
|
123473
|
+
return {
|
|
123474
|
+
...withoutOnboardKeys(values),
|
|
123475
|
+
url: built.url,
|
|
123476
|
+
onboard: built.onboard
|
|
123477
|
+
};
|
|
123478
|
+
};
|
|
123479
|
+
return guardBrokerDelete({
|
|
123480
|
+
...base,
|
|
123481
|
+
instanceFormSchema: async ({ id }) => {
|
|
123482
|
+
const saved = id === null ? null : await instances.getResolved(id);
|
|
123483
|
+
return deps.formSchema(id, saved?.onboard !== void 0);
|
|
123484
|
+
},
|
|
123485
|
+
createInstance: async (input) => {
|
|
123486
|
+
const values = input.values ?? {};
|
|
123487
|
+
if (!isOnboardRequested(values)) return base.createInstance({
|
|
123488
|
+
...input,
|
|
123489
|
+
values: withoutOnboardKeys(values)
|
|
123490
|
+
});
|
|
123491
|
+
const id = input.id ?? randomUUID();
|
|
123492
|
+
return base.createInstance({
|
|
123493
|
+
...input,
|
|
123494
|
+
id,
|
|
123495
|
+
values: await onboardValues(id, input.name, values)
|
|
123496
|
+
});
|
|
123497
|
+
},
|
|
123498
|
+
updateInstance: async (input) => {
|
|
123499
|
+
if (!isOnboardRequested(input.values)) return base.updateInstance({
|
|
123500
|
+
...input,
|
|
123501
|
+
values: withoutOnboardKeys(input.values)
|
|
123502
|
+
});
|
|
123503
|
+
const current = await instances.getResolved(input.id);
|
|
123504
|
+
if (current === null) throw new Error(`mqtt-broker: broker '${input.id}' not found`);
|
|
123505
|
+
if (current.onboard !== void 0) return base.updateInstance({
|
|
123506
|
+
...input,
|
|
123507
|
+
values: withoutOnboardKeys(input.values)
|
|
123508
|
+
});
|
|
123509
|
+
return base.updateInstance({
|
|
123510
|
+
...input,
|
|
123511
|
+
values: await onboardValues(input.id, input.name ?? current.name, input.values)
|
|
123512
|
+
});
|
|
123513
|
+
}
|
|
123514
|
+
}, () => mqttBrokerUsers(deps.listMqttHaInstances));
|
|
123515
|
+
}
|
|
123516
|
+
//#endregion
|
|
123133
123517
|
//#region src/migrations/brokers-to-records.ts
|
|
123134
123518
|
/**
|
|
123135
123519
|
* The external MQTT brokers live in the `brokers` records (D985, D996); the old addon-settings
|
|
@@ -123219,46 +123603,163 @@ async function planDropBrokersKey(ports) {
|
|
|
123219
123603
|
}] };
|
|
123220
123604
|
}
|
|
123221
123605
|
//#endregion
|
|
123606
|
+
//#region src/migrations/embedded-to-onboard.ts
|
|
123607
|
+
/**
|
|
123608
|
+
* The legacy single embedded broker lived in FLAT addon-settings keys (`embeddedEnabled`,
|
|
123609
|
+
* `embeddedPort`, ...). It is an ONBOARD broker record now, so the instance list, the pickers,
|
|
123610
|
+
* the test and the delete all see it (D1013). Two migrations of the catalogue, in this order:
|
|
123611
|
+
*
|
|
123612
|
+
* 1. `embeddedToOnboardRecord` (procedure): a legacy broker (a stopped one as a switched-off
|
|
123613
|
+
* record) becomes a `brokers` record
|
|
123614
|
+
* under the SAME id (`embeddedId`, else the `'embedded'` id the old code derived), so a
|
|
123615
|
+
* consumer that stored that id still resolves. It never overwrites a record of that id.
|
|
123616
|
+
* 2. `planDropEmbeddedKeys` (plan): the flat keys are deleted from the canonical
|
|
123617
|
+
* `addon-settings` rows, and only while the record that replaces them exists and carries the
|
|
123618
|
+
* password the legacy key held. `embeddedClients` / `embeddedAcl` are not flat broker keys
|
|
123619
|
+
* and stay.
|
|
123620
|
+
*/
|
|
123621
|
+
/** The flat keys that described the legacy embedded broker. */
|
|
123622
|
+
var LEGACY_EMBEDDED_KEYS = [
|
|
123623
|
+
"embeddedEnabled",
|
|
123624
|
+
"embeddedId",
|
|
123625
|
+
"embeddedPort",
|
|
123626
|
+
"embeddedAllowAnonymous",
|
|
123627
|
+
"embeddedUsername",
|
|
123628
|
+
"embeddedPassword"
|
|
123629
|
+
];
|
|
123630
|
+
var LEGACY_EMBEDDED_NAME = "Embedded broker";
|
|
123631
|
+
function onboardBodyOf(legacy) {
|
|
123632
|
+
return {
|
|
123633
|
+
name: LEGACY_EMBEDDED_NAME,
|
|
123634
|
+
url: `mqtt://127.0.0.1:${legacy.port}`,
|
|
123635
|
+
...legacy.username !== void 0 ? { username: legacy.username } : {},
|
|
123636
|
+
...legacy.password !== void 0 ? { password: legacy.password } : {},
|
|
123637
|
+
onboard: {
|
|
123638
|
+
port: legacy.port,
|
|
123639
|
+
allowAnonymous: legacy.allowAnonymous
|
|
123640
|
+
},
|
|
123641
|
+
...legacy.enabled ? {} : { enabled: false }
|
|
123642
|
+
};
|
|
123643
|
+
}
|
|
123644
|
+
async function embeddedToOnboardRecord(ports) {
|
|
123645
|
+
const legacy = await ports.readLegacyEmbedded();
|
|
123646
|
+
if (legacy === null) return {
|
|
123647
|
+
outcome: "already-done",
|
|
123648
|
+
summary: { migrated: 0 }
|
|
123649
|
+
};
|
|
123650
|
+
if ((await ports.readRecords()).has(legacy.id)) return {
|
|
123651
|
+
outcome: "already-done",
|
|
123652
|
+
summary: { migrated: 0 }
|
|
123653
|
+
};
|
|
123654
|
+
await ports.putOnboardRecord(legacy.id, onboardBodyOf(legacy));
|
|
123655
|
+
return {
|
|
123656
|
+
outcome: "done",
|
|
123657
|
+
summary: { migrated: 1 }
|
|
123658
|
+
};
|
|
123659
|
+
}
|
|
123660
|
+
async function planDropEmbeddedKeys(ports) {
|
|
123661
|
+
const raw = await ports.readRawEmbeddedKeys();
|
|
123662
|
+
const present = LEGACY_EMBEDDED_KEYS.filter((key) => key in raw);
|
|
123663
|
+
if (present.length === 0) return { edits: [] };
|
|
123664
|
+
const refuse = (reason) => {
|
|
123665
|
+
ports.warn(`mqtt-broker migration: the legacy embedded keys are kept: ${reason}`);
|
|
123666
|
+
throw new Error(`the legacy embedded broker keys are not deleted: ${reason}`);
|
|
123667
|
+
};
|
|
123668
|
+
const legacy = await ports.readLegacyEmbedded();
|
|
123669
|
+
if (legacy !== null) {
|
|
123670
|
+
const record = (await ports.readRecords()).get(legacy.id);
|
|
123671
|
+
if (record === void 0) return refuse(`the record '${legacy.id}' does not exist`);
|
|
123672
|
+
if ((legacy.password ?? "").length > 0 && !record.hasPassword) return refuse(`the record '${legacy.id}' holds no password`);
|
|
123673
|
+
}
|
|
123674
|
+
return { edits: [{
|
|
123675
|
+
op: "deleteKeys",
|
|
123676
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
123677
|
+
keys: present.map((key) => `${MQTT_BROKER_ADDON_ID}.${key}`),
|
|
123678
|
+
precondition: preconditionOf(Object.fromEntries(present.map((key) => [`${MQTT_BROKER_ADDON_ID}.${key}`, {
|
|
123679
|
+
addonId: MQTT_BROKER_ADDON_ID,
|
|
123680
|
+
key,
|
|
123681
|
+
value: raw[key]
|
|
123682
|
+
}])))
|
|
123683
|
+
}] };
|
|
123684
|
+
}
|
|
123685
|
+
//#endregion
|
|
123222
123686
|
//#region src/migrations/index.ts
|
|
123223
123687
|
var BROKERS_TO_RECORDS_ID = "mqtt-broker/2026-11-03-brokers-settings-to-records";
|
|
123224
123688
|
var DROP_BROKERS_KEY_ID = "mqtt-broker/2026-11-03-drop-brokers-key";
|
|
123689
|
+
var EMBEDDED_TO_ONBOARD_ID = "mqtt-broker/2026-11-05-embedded-broker-to-onboard-record";
|
|
123690
|
+
var DROP_EMBEDDED_KEYS_ID = "mqtt-broker/2026-11-05-drop-embedded-keys";
|
|
123225
123691
|
var FIXTURE_MARK = "a setting key the migration does not know";
|
|
123226
123692
|
/**
|
|
123227
123693
|
* The `mqtt-broker` catalogue (D987), offered to the engine when the addon initialises. Both
|
|
123228
123694
|
* entries are `tolerate`: the embedded broker and every record keep working while a legacy key
|
|
123229
123695
|
* stays where it is.
|
|
123230
123696
|
*/
|
|
123231
|
-
var ADDON_MIGRATIONS = [
|
|
123232
|
-
|
|
123233
|
-
|
|
123234
|
-
|
|
123235
|
-
|
|
123236
|
-
|
|
123237
|
-
|
|
123238
|
-
|
|
123239
|
-
|
|
123240
|
-
|
|
123241
|
-
|
|
123242
|
-
|
|
123243
|
-
|
|
123244
|
-
|
|
123245
|
-
|
|
123246
|
-
|
|
123247
|
-
|
|
123248
|
-
|
|
123249
|
-
|
|
123250
|
-
|
|
123251
|
-
|
|
123252
|
-
|
|
123253
|
-
|
|
123254
|
-
|
|
123255
|
-
|
|
123256
|
-
|
|
123257
|
-
|
|
123258
|
-
|
|
123259
|
-
|
|
123260
|
-
|
|
123261
|
-
}
|
|
123697
|
+
var ADDON_MIGRATIONS = [
|
|
123698
|
+
{
|
|
123699
|
+
meta: {
|
|
123700
|
+
id: BROKERS_TO_RECORDS_ID,
|
|
123701
|
+
owner: "mqtt-broker",
|
|
123702
|
+
level: "addon-state",
|
|
123703
|
+
seq: 1,
|
|
123704
|
+
dependsOn: [],
|
|
123705
|
+
store: "settings",
|
|
123706
|
+
scope: "cluster",
|
|
123707
|
+
onFailure: "tolerate",
|
|
123708
|
+
bodyKind: "procedure",
|
|
123709
|
+
description: "Check that every external broker of the old settings list is a brokers record; never deletes",
|
|
123710
|
+
fixtureMark: FIXTURE_MARK
|
|
123711
|
+
},
|
|
123712
|
+
procedure: verifyBrokersAreRecords
|
|
123713
|
+
},
|
|
123714
|
+
{
|
|
123715
|
+
meta: {
|
|
123716
|
+
id: DROP_BROKERS_KEY_ID,
|
|
123717
|
+
owner: "mqtt-broker",
|
|
123718
|
+
level: "addon-state",
|
|
123719
|
+
seq: 2,
|
|
123720
|
+
dependsOn: [BROKERS_TO_RECORDS_ID],
|
|
123721
|
+
store: "settings",
|
|
123722
|
+
scope: "cluster",
|
|
123723
|
+
onFailure: "tolerate",
|
|
123724
|
+
bodyKind: "plan",
|
|
123725
|
+
description: "Delete the legacy brokers settings key once every broker is a record",
|
|
123726
|
+
fixtureMark: FIXTURE_MARK
|
|
123727
|
+
},
|
|
123728
|
+
plan: planDropBrokersKey
|
|
123729
|
+
},
|
|
123730
|
+
{
|
|
123731
|
+
meta: {
|
|
123732
|
+
id: EMBEDDED_TO_ONBOARD_ID,
|
|
123733
|
+
owner: "mqtt-broker",
|
|
123734
|
+
level: "addon-state",
|
|
123735
|
+
seq: 3,
|
|
123736
|
+
dependsOn: [DROP_BROKERS_KEY_ID],
|
|
123737
|
+
store: "settings",
|
|
123738
|
+
scope: "cluster",
|
|
123739
|
+
onFailure: "tolerate",
|
|
123740
|
+
bodyKind: "procedure",
|
|
123741
|
+
description: "The legacy embedded broker (flat embedded* keys) becomes an onboard brokers record under the same id; never overwrites a record",
|
|
123742
|
+
fixtureMark: FIXTURE_MARK
|
|
123743
|
+
},
|
|
123744
|
+
procedure: embeddedToOnboardRecord
|
|
123745
|
+
},
|
|
123746
|
+
{
|
|
123747
|
+
meta: {
|
|
123748
|
+
id: DROP_EMBEDDED_KEYS_ID,
|
|
123749
|
+
owner: "mqtt-broker",
|
|
123750
|
+
level: "addon-state",
|
|
123751
|
+
seq: 4,
|
|
123752
|
+
dependsOn: [EMBEDDED_TO_ONBOARD_ID],
|
|
123753
|
+
store: "settings",
|
|
123754
|
+
scope: "cluster",
|
|
123755
|
+
onFailure: "tolerate",
|
|
123756
|
+
bodyKind: "plan",
|
|
123757
|
+
description: "Delete the flat embedded broker keys once the onboard record holds them",
|
|
123758
|
+
fixtureMark: FIXTURE_MARK
|
|
123759
|
+
},
|
|
123760
|
+
plan: planDropEmbeddedKeys
|
|
123761
|
+
}
|
|
123762
|
+
];
|
|
123262
123763
|
//#endregion
|
|
123263
123764
|
//#region src/mqtt-entry-record-kinds.ts
|
|
123264
123765
|
var aclAccessSchema = _enum([
|
|
@@ -123460,129 +123961,31 @@ var MqttAclStore = class MqttAclStore {
|
|
|
123460
123961
|
* The addon has NO standalone admin page. All configuration — external
|
|
123461
123962
|
* broker entries, the embedded `aedes` broker, embedded credentials,
|
|
123462
123963
|
* and the topic ACL — is exposed through the standard
|
|
123463
|
-
* `
|
|
123964
|
+
* `clusterSettingsSchema()` and rendered automatically inside the
|
|
123464
123965
|
* Integrations → Brokers tab's docked Settings panel, like every other
|
|
123465
123966
|
* integration-type addon.
|
|
123466
123967
|
*
|
|
123467
123968
|
* Lifecycle:
|
|
123468
123969
|
* - External broker entries are persisted in `this.activeExternalBrokers`.
|
|
123469
|
-
* - The embedded broker is
|
|
123470
|
-
*
|
|
123471
|
-
*
|
|
123472
|
-
* reassembles them into an `EmbeddedBrokerEntry`.
|
|
123473
|
-
* - On boot we start the embedded broker when `embeddedEnabled`.
|
|
123474
|
-
* - `addBroker` / `removeBroker` / `startEmbeddedBroker` /
|
|
123475
|
-
* `stopEmbeddedBroker` mutate the config and the runtime state.
|
|
123970
|
+
* - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
|
|
123971
|
+
* `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
|
|
123972
|
+
* - `addBroker` / `removeBroker` mutate the records and the runtime state.
|
|
123476
123973
|
* - `testConnection` does a one-shot `mqtt.connect` race against a 3s
|
|
123477
123974
|
* timeout AND refreshes the probe cache for the broker.
|
|
123478
123975
|
*/
|
|
123479
123976
|
/** bcrypt cost factor — exported for tests. */
|
|
123480
123977
|
var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
|
|
123481
123978
|
var PROBE_INTERVAL_MS = 3e4;
|
|
123482
|
-
var DEFAULT_EMBEDDED_PORT = 1883;
|
|
123483
|
-
/**
|
|
123484
|
-
* Per-kind broker creation form for kind='mqtt', rendered by the unified
|
|
123485
|
-
* Broker manager / create picker (via `broker.getSettingsSchema`). PURE
|
|
123486
|
-
* DATA — emits the flat `{ url, username, password, clientIdPrefix }`
|
|
123487
|
-
* shape `broker.add`'s `settings` payload consumes. Mirrors the external
|
|
123488
|
-
* broker fields `addBroker` accepts.
|
|
123489
|
-
*/
|
|
123490
|
-
var mqttBrokerSettingsSchema = { sections: [{
|
|
123491
|
-
id: "mqtt-broker-connection",
|
|
123492
|
-
title: "MQTT connection",
|
|
123493
|
-
titleKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.title",
|
|
123494
|
-
columns: 1,
|
|
123495
|
-
fields: [
|
|
123496
|
-
{
|
|
123497
|
-
type: "boolean",
|
|
123498
|
-
key: "onboard",
|
|
123499
|
-
label: "Run an onboard broker",
|
|
123500
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.label",
|
|
123501
|
-
description: "Spin up a CamStack-hosted aedes broker on this hub instead of connecting to an external one. Each onboard broker gets its own dedicated server + port.",
|
|
123502
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.description",
|
|
123503
|
-
default: false
|
|
123504
|
-
},
|
|
123505
|
-
{
|
|
123506
|
-
type: "number",
|
|
123507
|
-
key: "port",
|
|
123508
|
-
label: "Port",
|
|
123509
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.label",
|
|
123510
|
-
placeholder: "(auto)",
|
|
123511
|
-
description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
|
|
123512
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.description",
|
|
123513
|
-
showWhen: {
|
|
123514
|
-
field: "onboard",
|
|
123515
|
-
equals: true
|
|
123516
|
-
}
|
|
123517
|
-
},
|
|
123518
|
-
{
|
|
123519
|
-
type: "boolean",
|
|
123520
|
-
key: "allowAnonymous",
|
|
123521
|
-
label: "Allow anonymous",
|
|
123522
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.label",
|
|
123523
|
-
description: "Permit clients to connect without a username/password.",
|
|
123524
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.description",
|
|
123525
|
-
default: false,
|
|
123526
|
-
showWhen: {
|
|
123527
|
-
field: "onboard",
|
|
123528
|
-
equals: true
|
|
123529
|
-
}
|
|
123530
|
-
},
|
|
123531
|
-
{
|
|
123532
|
-
type: "text",
|
|
123533
|
-
key: "url",
|
|
123534
|
-
label: "Broker URL",
|
|
123535
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.url.label",
|
|
123536
|
-
placeholder: "mqtt://localhost:1883",
|
|
123537
|
-
showWhen: {
|
|
123538
|
-
field: "onboard",
|
|
123539
|
-
equals: false
|
|
123540
|
-
}
|
|
123541
|
-
},
|
|
123542
|
-
{
|
|
123543
|
-
type: "text",
|
|
123544
|
-
key: "username",
|
|
123545
|
-
label: "Username",
|
|
123546
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.username.label",
|
|
123547
|
-
placeholder: "(optional)"
|
|
123548
|
-
},
|
|
123549
|
-
{
|
|
123550
|
-
type: "password",
|
|
123551
|
-
key: "password",
|
|
123552
|
-
label: "Password",
|
|
123553
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.password.label",
|
|
123554
|
-
showToggle: true
|
|
123555
|
-
},
|
|
123556
|
-
{
|
|
123557
|
-
type: "text",
|
|
123558
|
-
key: "clientIdPrefix",
|
|
123559
|
-
label: "Client ID prefix",
|
|
123560
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.clientIdPrefix.label",
|
|
123561
|
-
placeholder: "(optional)",
|
|
123562
|
-
showWhen: {
|
|
123563
|
-
field: "onboard",
|
|
123564
|
-
equals: false
|
|
123565
|
-
}
|
|
123566
|
-
}
|
|
123567
|
-
]
|
|
123568
|
-
}] };
|
|
123569
123979
|
var DEFAULT_CONFIG = {
|
|
123570
|
-
embeddedEnabled: false,
|
|
123571
|
-
embeddedId: "",
|
|
123572
|
-
embeddedPort: DEFAULT_EMBEDDED_PORT,
|
|
123573
|
-
embeddedAllowAnonymous: false,
|
|
123574
|
-
embeddedUsername: "",
|
|
123575
|
-
embeddedPassword: "",
|
|
123576
123980
|
embeddedClients: [],
|
|
123577
123981
|
embeddedAcl: []
|
|
123578
123982
|
};
|
|
123579
123983
|
var MqttBrokerAddon = class extends BaseAddon {
|
|
123580
|
-
|
|
123581
|
-
/** Per-entry onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
|
|
123984
|
+
/** Per-record onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
|
|
123582
123985
|
onboardRuntimes = /* @__PURE__ */ new Map();
|
|
123583
123986
|
probeCache = null;
|
|
123584
123987
|
brokerStore = null;
|
|
123585
|
-
|
|
123988
|
+
brokerInstances = null;
|
|
123586
123989
|
clientStore = null;
|
|
123587
123990
|
aclStore = null;
|
|
123588
123991
|
/** The external broker rows: the `brokers` records, secrets resolved. */
|
|
@@ -123596,6 +123999,20 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123596
123999
|
if (this.brokerStore === null) throw new Error("mqtt-broker: the broker records are not open");
|
|
123597
124000
|
return this.brokerStore;
|
|
123598
124001
|
}
|
|
124002
|
+
/** What the addon knows of a broker right now: its onboard server or its last probe. */
|
|
124003
|
+
liveState(id) {
|
|
124004
|
+
const running = this.onboardRuntimes.get(id);
|
|
124005
|
+
const probe = this.probeCache?.get(id);
|
|
124006
|
+
return {
|
|
124007
|
+
onboard: running === void 0 ? null : { connectedClients: running.aedes.connectedClients },
|
|
124008
|
+
...probe !== void 0 ? { probe: {
|
|
124009
|
+
status: probe.status,
|
|
124010
|
+
latencyMs: probe.latencyMs,
|
|
124011
|
+
...probe.error !== void 0 ? { error: probe.error } : {},
|
|
124012
|
+
lastCheckedAt: probe.lastCheckedAt
|
|
124013
|
+
} } : {}
|
|
124014
|
+
};
|
|
124015
|
+
}
|
|
123599
124016
|
async persistExternalBrokers(entries) {
|
|
123600
124017
|
this.activeExternalBrokers = [...entries];
|
|
123601
124018
|
await this.requireBrokerStore().replaceAll(entries);
|
|
@@ -123645,19 +124062,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123645
124062
|
this.brokerStore = MqttBrokerStore.of(records);
|
|
123646
124063
|
this.clientStore = MqttClientStore.of(records);
|
|
123647
124064
|
this.aclStore = MqttAclStore.of(records);
|
|
123648
|
-
this.
|
|
123649
|
-
|
|
123650
|
-
|
|
123651
|
-
|
|
123652
|
-
|
|
123653
|
-
|
|
123654
|
-
|
|
123655
|
-
formSchema: () => this.brokerForm(),
|
|
123656
|
-
initialBody: (name) => ({
|
|
123657
|
-
name,
|
|
123658
|
-
url: "mqtt://"
|
|
123659
|
-
}),
|
|
123660
|
-
configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
|
|
124065
|
+
this.brokerInstances = buildMqttBrokerInstances(records, {
|
|
124066
|
+
formSchema: (id, onboard) => this.brokerForm(id, onboard),
|
|
124067
|
+
listMqttHaInstances: () => listMqttHaInstancesOver(this.ctx.api),
|
|
124068
|
+
freePort: () => findFreePort(),
|
|
124069
|
+
probe: (cfg) => this.probeMqtt(cfg),
|
|
124070
|
+
onboardListening: (id) => this.onboardRuntimes.has(id),
|
|
124071
|
+
liveState: (id) => this.liveState(id),
|
|
123661
124072
|
onChanged: async () => {
|
|
123662
124073
|
await this.refreshExternalBrokers();
|
|
123663
124074
|
}
|
|
@@ -123686,29 +124097,9 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123686
124097
|
});
|
|
123687
124098
|
this.probeCache.setBrokersSource(() => this.collectExternalProbeInputs());
|
|
123688
124099
|
this.probeCache.start();
|
|
123689
|
-
const persistedEmbedded = this.deriveEmbeddedEntry();
|
|
123690
|
-
if (persistedEmbedded) try {
|
|
123691
|
-
await this.spawnEmbedded(persistedEmbedded);
|
|
123692
|
-
this.ctx.logger.info("embedded mqtt broker restarted", {
|
|
123693
|
-
meta: { port: persistedEmbedded.port },
|
|
123694
|
-
tags: {
|
|
123695
|
-
topic: "mqtt-broker",
|
|
123696
|
-
phase: "embedded-restarted"
|
|
123697
|
-
}
|
|
123698
|
-
});
|
|
123699
|
-
} catch (err) {
|
|
123700
|
-
this.ctx.logger.warn("embedded mqtt broker failed to restart", {
|
|
123701
|
-
meta: { error: errMsg(err) },
|
|
123702
|
-
tags: {
|
|
123703
|
-
topic: "mqtt-broker",
|
|
123704
|
-
phase: "embedded-restart-failed"
|
|
123705
|
-
}
|
|
123706
|
-
});
|
|
123707
|
-
}
|
|
123708
124100
|
await this.reconcileOnboardRuntimes();
|
|
123709
124101
|
this.ctx.addDisposer(async () => {
|
|
123710
124102
|
await this.probeCache?.stop();
|
|
123711
|
-
await this.shutdownEmbedded().catch(() => void 0);
|
|
123712
124103
|
await this.shutdownAllOnboard();
|
|
123713
124104
|
});
|
|
123714
124105
|
const provider = {
|
|
@@ -123720,11 +124111,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123720
124111
|
startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
|
|
123721
124112
|
stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
|
|
123722
124113
|
getStatus: async () => ({
|
|
123723
|
-
brokerCount: this.activeExternalBrokers.length
|
|
123724
|
-
embeddedRunning: this.
|
|
124114
|
+
brokerCount: this.activeExternalBrokers.length,
|
|
124115
|
+
embeddedRunning: this.onboardRuntimes.size > 0
|
|
123725
124116
|
})
|
|
123726
124117
|
};
|
|
123727
124118
|
const unifiedProvider = this.buildUnifiedBrokerProvider();
|
|
124119
|
+
const brokerInstances = this.brokerInstances;
|
|
124120
|
+
if (brokerInstances === null) throw new Error("mqtt-broker: the broker instances are not built");
|
|
123728
124121
|
return [
|
|
123729
124122
|
{
|
|
123730
124123
|
capability: mqttBrokerCapability,
|
|
@@ -123736,111 +124129,21 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123736
124129
|
},
|
|
123737
124130
|
{
|
|
123738
124131
|
capability: integrationInstancesCapability,
|
|
123739
|
-
provider:
|
|
124132
|
+
provider: brokerInstances
|
|
123740
124133
|
}
|
|
123741
124134
|
];
|
|
123742
124135
|
}
|
|
124136
|
+
/**
|
|
124137
|
+
* The bus-only `broker` cap (D1013). The brokers themselves are the `integration-instances`
|
|
124138
|
+
* provider; this one only answers the config read and the status.
|
|
124139
|
+
*/
|
|
123743
124140
|
buildUnifiedBrokerProvider() {
|
|
123744
|
-
const
|
|
123745
|
-
const ownerAddonId = this.ctx.id;
|
|
123746
|
-
const adapt = (info) => ({
|
|
123747
|
-
id: info.id,
|
|
123748
|
-
addonId: ownerAddonId,
|
|
123749
|
-
name: info.name,
|
|
123750
|
-
kind: KIND,
|
|
123751
|
-
status: mapMqttStatus(info.status),
|
|
123752
|
-
info: {
|
|
123753
|
-
...info.latencyMs !== null ? { latencyMs: info.latencyMs } : {},
|
|
123754
|
-
...info.kind === "embedded" ? {
|
|
123755
|
-
embedded: true,
|
|
123756
|
-
connectedClients: info.connectedClients ?? 0
|
|
123757
|
-
} : {}
|
|
123758
|
-
},
|
|
123759
|
-
lastCheckedAt: info.lastCheckedAt ?? null,
|
|
123760
|
-
error: info.error ?? null
|
|
123761
|
-
});
|
|
123762
|
-
const owns = (id) => {
|
|
123763
|
-
if (this.activeExternalBrokers.some((b) => b.id === id)) return true;
|
|
123764
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123765
|
-
return embedded !== null && embedded.id === id;
|
|
123766
|
-
};
|
|
124141
|
+
const owns = (id) => this.activeExternalBrokers.some((b) => b.id === id);
|
|
123767
124142
|
return {
|
|
123768
|
-
list: async ({ kind }) => {
|
|
123769
|
-
if (kind && kind !== KIND) return [];
|
|
123770
|
-
return (await this.listBrokers()).map(adapt);
|
|
123771
|
-
},
|
|
123772
|
-
get: async ({ id }) => {
|
|
123773
|
-
const match = (await this.listBrokers()).find((b) => b.id === id);
|
|
123774
|
-
return match ? adapt(match) : null;
|
|
123775
|
-
},
|
|
123776
|
-
listProviders: async () => [{
|
|
123777
|
-
addonId: ownerAddonId,
|
|
123778
|
-
kinds: [{
|
|
123779
|
-
kind: KIND,
|
|
123780
|
-
label: "MQTT"
|
|
123781
|
-
}]
|
|
123782
|
-
}],
|
|
123783
|
-
add: async ({ kind, name, settings, id }) => {
|
|
123784
|
-
if (kind !== KIND) throw new Error(`mqtt-broker: only kind '${KIND}' is handled here (got '${kind}')`);
|
|
123785
|
-
if (id !== void 0) throw new Error("mqtt-broker: adopting an existing broker id is not supported by this provider");
|
|
123786
|
-
if (isOnboardRequested(settings)) return this.addOnboardBroker(name, settings);
|
|
123787
|
-
const url = typeof settings["url"] === "string" ? settings["url"] : "";
|
|
123788
|
-
if (!url) throw new Error("mqtt-broker.add: settings.url is required");
|
|
123789
|
-
const input = {
|
|
123790
|
-
name,
|
|
123791
|
-
url,
|
|
123792
|
-
...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
|
|
123793
|
-
...typeof settings["password"] === "string" ? { password: settings["password"] } : {},
|
|
123794
|
-
...typeof settings["clientIdPrefix"] === "string" ? { clientIdPrefix: settings["clientIdPrefix"] } : {}
|
|
123795
|
-
};
|
|
123796
|
-
return this.addBroker(input);
|
|
123797
|
-
},
|
|
123798
|
-
remove: async ({ id }) => {
|
|
123799
|
-
if (!owns(id)) return;
|
|
123800
|
-
await this.removeBroker(id);
|
|
123801
|
-
},
|
|
123802
|
-
testConnection: async ({ id }) => {
|
|
123803
|
-
if (!owns(id)) return {
|
|
123804
|
-
ok: false,
|
|
123805
|
-
error: "unknown broker"
|
|
123806
|
-
};
|
|
123807
|
-
return this.testConnection(id);
|
|
123808
|
-
},
|
|
123809
|
-
getSettings: async ({ id }) => {
|
|
123810
|
-
if (!owns(id)) return null;
|
|
123811
|
-
return this.getBrokerConfig(id);
|
|
123812
|
-
},
|
|
123813
|
-
setSettings: async () => {
|
|
123814
|
-
throw new Error("mqtt-broker.setSettings: not supported — use remove + add to replace a broker");
|
|
123815
|
-
},
|
|
123816
124143
|
getBrokerConfig: async ({ id }) => {
|
|
123817
124144
|
if (!owns(id)) return null;
|
|
123818
124145
|
return this.getBrokerConfig(id);
|
|
123819
124146
|
},
|
|
123820
|
-
getSettingsSchema: async ({ kind }) => {
|
|
123821
|
-
if (kind !== KIND) return null;
|
|
123822
|
-
return mqttBrokerSettingsSchema;
|
|
123823
|
-
},
|
|
123824
|
-
testSettings: async ({ kind, settings }) => {
|
|
123825
|
-
if (kind !== KIND) return {
|
|
123826
|
-
ok: false,
|
|
123827
|
-
error: `unsupported kind: ${kind}`
|
|
123828
|
-
};
|
|
123829
|
-
if (isOnboardRequested(settings)) return {
|
|
123830
|
-
ok: true,
|
|
123831
|
-
latencyMs: 0
|
|
123832
|
-
};
|
|
123833
|
-
const url = typeof settings["url"] === "string" ? settings["url"] : "";
|
|
123834
|
-
if (!url) return {
|
|
123835
|
-
ok: false,
|
|
123836
|
-
error: "settings.url is required"
|
|
123837
|
-
};
|
|
123838
|
-
return this.probeMqtt({
|
|
123839
|
-
url,
|
|
123840
|
-
...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
|
|
123841
|
-
...typeof settings["password"] === "string" ? { password: settings["password"] } : {}
|
|
123842
|
-
});
|
|
123843
|
-
},
|
|
123844
124147
|
publish: async () => {
|
|
123845
124148
|
throw new Error("mqtt-broker.publish: not supported — fetch getBrokerConfig + open your own mqtt.js client (see \"NOT a pub/sub proxy\" in mqtt-broker.cap.ts)");
|
|
123846
124149
|
},
|
|
@@ -123854,7 +124157,7 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123854
124157
|
getStatus: async () => {
|
|
123855
124158
|
const all = await this.listBrokers();
|
|
123856
124159
|
return {
|
|
123857
|
-
brokerCount: this.activeExternalBrokers.length
|
|
124160
|
+
brokerCount: this.activeExternalBrokers.length,
|
|
123858
124161
|
connectedCount: all.filter((b) => b.status === "connected").length
|
|
123859
124162
|
};
|
|
123860
124163
|
}
|
|
@@ -123862,35 +124165,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123862
124165
|
}
|
|
123863
124166
|
async onShutdown() {
|
|
123864
124167
|
await this.probeCache?.stop();
|
|
123865
|
-
await this.shutdownEmbedded().catch(() => void 0);
|
|
123866
124168
|
await this.shutdownAllOnboard();
|
|
123867
124169
|
}
|
|
123868
124170
|
async onConfigChanged() {
|
|
123869
124171
|
this.activeExternalBrokers = await this.requireBrokerStore().listResolved();
|
|
123870
124172
|
await this.syncEmbeddedEntriesFromConfig();
|
|
123871
124173
|
await this.reconcileOnboardRuntimes();
|
|
123872
|
-
|
|
123873
|
-
const running = this.embeddedRuntime?.entry ?? null;
|
|
123874
|
-
if (!desired && running) {
|
|
123875
|
-
await this.shutdownEmbedded();
|
|
123876
|
-
return;
|
|
123877
|
-
}
|
|
123878
|
-
if (desired && !running) {
|
|
123879
|
-
await this.spawnEmbedded(desired).catch((err) => {
|
|
123880
|
-
this.ctx.logger.warn("embedded mqtt broker spawn (on config change) failed", { meta: { error: errMsg(err) } });
|
|
123881
|
-
});
|
|
123882
|
-
return;
|
|
123883
|
-
}
|
|
123884
|
-
if (desired && running) {
|
|
123885
|
-
if (requiresEmbeddedRespawn(desired, running)) {
|
|
123886
|
-
await this.shutdownEmbedded();
|
|
123887
|
-
await this.spawnEmbedded(desired).catch((err) => {
|
|
123888
|
-
this.ctx.logger.warn("embedded mqtt broker respawn failed", { meta: { error: errMsg(err) } });
|
|
123889
|
-
});
|
|
123890
|
-
return;
|
|
123891
|
-
}
|
|
123892
|
-
this.reconcileEmbeddedClients();
|
|
123893
|
-
}
|
|
124174
|
+
this.reconcileEmbeddedClients();
|
|
123894
124175
|
}
|
|
123895
124176
|
async listBrokers() {
|
|
123896
124177
|
const out = [];
|
|
@@ -123924,22 +124205,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123924
124205
|
};
|
|
123925
124206
|
out.push(info);
|
|
123926
124207
|
}
|
|
123927
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123928
|
-
if (embedded) {
|
|
123929
|
-
const running = this.embeddedRuntime;
|
|
123930
|
-
const status = running !== null ? "connected" : "disconnected";
|
|
123931
|
-
const info = {
|
|
123932
|
-
id: embedded.id,
|
|
123933
|
-
name: embedded.name,
|
|
123934
|
-
url: `mqtt://127.0.0.1:${embedded.port}`,
|
|
123935
|
-
kind: "embedded",
|
|
123936
|
-
status,
|
|
123937
|
-
latencyMs: null,
|
|
123938
|
-
...running ? { connectedClients: running.aedes.connectedClients } : {},
|
|
123939
|
-
lastCheckedAt: Date.now()
|
|
123940
|
-
};
|
|
123941
|
-
out.push(info);
|
|
123942
|
-
}
|
|
123943
124208
|
return out;
|
|
123944
124209
|
}
|
|
123945
124210
|
getBrokerConfig(id) {
|
|
@@ -123954,15 +124219,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123954
124219
|
...external.clientIdPrefix !== void 0 ? { clientIdPrefix: external.clientIdPrefix } : {}
|
|
123955
124220
|
};
|
|
123956
124221
|
}
|
|
123957
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123958
|
-
if (embedded && embedded.id === id) {
|
|
123959
|
-
const password = this.storedPassword(embedded.password);
|
|
123960
|
-
return {
|
|
123961
|
-
url: `mqtt://127.0.0.1:${embedded.port}`,
|
|
123962
|
-
...embedded.username !== void 0 ? { username: embedded.username } : {},
|
|
123963
|
-
...password !== void 0 ? { password } : {}
|
|
123964
|
-
};
|
|
123965
|
-
}
|
|
123966
124222
|
throw new Error(`mqtt-broker: broker '${id}' not found`);
|
|
123967
124223
|
}
|
|
123968
124224
|
async addBroker(input) {
|
|
@@ -123983,44 +124239,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123983
124239
|
} });
|
|
123984
124240
|
return { id };
|
|
123985
124241
|
}
|
|
123986
|
-
/**
|
|
123987
|
-
* Create a per-entry ONBOARD broker from `broker.add` settings: allocate
|
|
123988
|
-
* (or honour) a port, persist an `ExternalBrokerEntry` carrying an
|
|
123989
|
-
* `onboard` descriptor, then spawn its dedicated `aedes` server.
|
|
123990
|
-
*/
|
|
123991
|
-
async addOnboardBroker(name, settings) {
|
|
123992
|
-
const id = randomUUID();
|
|
123993
|
-
const port = readOnboardPort(settings) ?? await findFreePort();
|
|
123994
|
-
const built = buildOnboardEntry({
|
|
123995
|
-
id,
|
|
123996
|
-
name,
|
|
123997
|
-
port,
|
|
123998
|
-
settings
|
|
123999
|
-
});
|
|
124000
|
-
const entry = {
|
|
124001
|
-
id: built.id,
|
|
124002
|
-
name: built.name,
|
|
124003
|
-
url: built.url,
|
|
124004
|
-
...built.username !== void 0 ? { username: built.username } : {},
|
|
124005
|
-
...built.password !== void 0 ? { password: built.password } : {},
|
|
124006
|
-
onboard: built.onboard
|
|
124007
|
-
};
|
|
124008
|
-
const next = [...this.activeExternalBrokers, entry];
|
|
124009
|
-
await this.persistExternalBrokers(next);
|
|
124010
|
-
const spawn = this.onboardSpawnEntry(entry);
|
|
124011
|
-
if (spawn) await this.spawnOnboard(spawn);
|
|
124012
|
-
this.ctx.logger.info("onboard mqtt broker added", {
|
|
124013
|
-
meta: {
|
|
124014
|
-
id,
|
|
124015
|
-
port
|
|
124016
|
-
},
|
|
124017
|
-
tags: {
|
|
124018
|
-
topic: "mqtt-broker",
|
|
124019
|
-
phase: "onboard-started"
|
|
124020
|
-
}
|
|
124021
|
-
});
|
|
124022
|
-
return { id };
|
|
124023
|
-
}
|
|
124024
124242
|
async removeBroker(id) {
|
|
124025
124243
|
const removed = this.activeExternalBrokers.find((b) => b.id === id);
|
|
124026
124244
|
if (removed) {
|
|
@@ -124031,13 +124249,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124031
124249
|
this.ctx.logger.info("mqtt broker removed", { meta: { id } });
|
|
124032
124250
|
return;
|
|
124033
124251
|
}
|
|
124034
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
124035
|
-
if (embedded && embedded.id === id) {
|
|
124036
|
-
await this.shutdownEmbedded();
|
|
124037
|
-
await this.updateGlobalSettings({ embeddedEnabled: false });
|
|
124038
|
-
this.ctx.logger.info("embedded mqtt broker removed", { meta: { id } });
|
|
124039
|
-
return;
|
|
124040
|
-
}
|
|
124041
124252
|
}
|
|
124042
124253
|
async testConnection(id) {
|
|
124043
124254
|
let cfg;
|
|
@@ -124056,7 +124267,7 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124056
124267
|
* One-shot connectivity probe against an MQTT endpoint built from a
|
|
124057
124268
|
* flat config (`{url, username?, password?}`). Shared by the
|
|
124058
124269
|
* id-keyed `testConnection` (existing broker) and the pre-creation
|
|
124059
|
-
* `
|
|
124270
|
+
* the instance test (`testInstance`) of a broker not saved yet (the typed values).
|
|
124060
124271
|
* Never throws — every failure resolves to `{ ok: false, error }`.
|
|
124061
124272
|
*/
|
|
124062
124273
|
async probeMqtt(cfg) {
|
|
@@ -124101,30 +124312,46 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124101
124312
|
});
|
|
124102
124313
|
});
|
|
124103
124314
|
}
|
|
124315
|
+
/** The `brokers` records, for the two legacy cap methods that create or switch off one. */
|
|
124316
|
+
brokerRecordHandle() {
|
|
124317
|
+
const handle = this.ctx.records?.kind(mqttBrokersEntryKind);
|
|
124318
|
+
if (handle === void 0) throw new Error("mqtt-broker: the broker records are not open");
|
|
124319
|
+
return handle;
|
|
124320
|
+
}
|
|
124321
|
+
/** Create an ONBOARD broker record; the reconcile then spawns its `aedes` server. */
|
|
124104
124322
|
async startEmbeddedBroker(input) {
|
|
124105
|
-
|
|
124106
|
-
const
|
|
124107
|
-
const entry = {
|
|
124323
|
+
const id = randomUUID();
|
|
124324
|
+
const built = buildOnboardEntry({
|
|
124108
124325
|
id,
|
|
124109
|
-
name:
|
|
124326
|
+
name: LEGACY_EMBEDDED_NAME,
|
|
124110
124327
|
port: input.port,
|
|
124111
|
-
|
|
124112
|
-
|
|
124113
|
-
|
|
124114
|
-
|
|
124115
|
-
|
|
124116
|
-
await this.updateGlobalSettings({
|
|
124117
|
-
embeddedEnabled: true,
|
|
124118
|
-
embeddedId: id,
|
|
124119
|
-
embeddedPort: input.port,
|
|
124120
|
-
embeddedAllowAnonymous: input.allowAnonymous,
|
|
124121
|
-
embeddedUsername: input.username ?? "",
|
|
124122
|
-
embeddedPassword: input.password ?? ""
|
|
124328
|
+
settings: {
|
|
124329
|
+
allowAnonymous: input.allowAnonymous,
|
|
124330
|
+
...input.username !== void 0 ? { username: input.username } : {},
|
|
124331
|
+
...input.password !== void 0 ? { password: input.password } : {}
|
|
124332
|
+
}
|
|
124123
124333
|
});
|
|
124334
|
+
const handle = this.brokerRecordHandle();
|
|
124335
|
+
await handle.put({
|
|
124336
|
+
id,
|
|
124337
|
+
data: mqttBrokersEntryKind.schema.parse({
|
|
124338
|
+
name: built.name,
|
|
124339
|
+
url: built.url,
|
|
124340
|
+
...built.username !== void 0 ? { username: built.username } : {},
|
|
124341
|
+
...built.password !== void 0 ? { password: built.password } : {},
|
|
124342
|
+
onboard: built.onboard
|
|
124343
|
+
})
|
|
124344
|
+
}, { ifRev: 0 });
|
|
124345
|
+
await this.refreshExternalBrokers();
|
|
124346
|
+
if (!this.onboardRuntimes.has(id)) {
|
|
124347
|
+
await handle.delete(id);
|
|
124348
|
+
await this.refreshExternalBrokers();
|
|
124349
|
+
throw new Error(`embedded broker could not listen on port ${input.port}`);
|
|
124350
|
+
}
|
|
124124
124351
|
this.ctx.logger.info("embedded mqtt broker started", {
|
|
124125
124352
|
meta: {
|
|
124126
124353
|
id,
|
|
124127
|
-
port:
|
|
124354
|
+
port: input.port
|
|
124128
124355
|
},
|
|
124129
124356
|
tags: {
|
|
124130
124357
|
topic: "mqtt-broker",
|
|
@@ -124146,20 +124373,22 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124146
124373
|
});
|
|
124147
124374
|
return {
|
|
124148
124375
|
id,
|
|
124149
|
-
url:
|
|
124376
|
+
url: built.url
|
|
124150
124377
|
};
|
|
124151
124378
|
}
|
|
124379
|
+
/** Switch an onboard broker off (its record stays, so it can be switched on again). */
|
|
124152
124380
|
async stopEmbeddedBroker(id) {
|
|
124153
|
-
|
|
124154
|
-
await
|
|
124155
|
-
|
|
124381
|
+
const handle = this.brokerRecordHandle();
|
|
124382
|
+
const row = await handle.get(id);
|
|
124383
|
+
if (row === null || row.data.onboard === void 0) return;
|
|
124384
|
+
await handle.patch(id, { enabled: false });
|
|
124385
|
+
await this.refreshExternalBrokers();
|
|
124156
124386
|
this.ctx.logger.info("embedded mqtt broker stopped", { meta: { id } });
|
|
124157
124387
|
}
|
|
124158
124388
|
/**
|
|
124159
124389
|
* Build + listen an `aedes` server for `entry` and RETURN the runtime
|
|
124160
|
-
* without assigning it to any field
|
|
124161
|
-
*
|
|
124162
|
-
* brokers ({@link spawnOnboard}).
|
|
124390
|
+
* without assigning it to any field (the onboard brokers keep theirs in
|
|
124391
|
+
* {@link spawnOnboard}).
|
|
124163
124392
|
*/
|
|
124164
124393
|
async spawnAedes(entry) {
|
|
124165
124394
|
const aedes = new import_aedes.default();
|
|
@@ -124198,15 +124427,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124198
124427
|
aedes.close(() => resolve());
|
|
124199
124428
|
});
|
|
124200
124429
|
}
|
|
124201
|
-
async spawnEmbedded(entry) {
|
|
124202
|
-
this.embeddedRuntime = await this.spawnAedes(entry);
|
|
124203
|
-
}
|
|
124204
|
-
async shutdownEmbedded() {
|
|
124205
|
-
if (!this.embeddedRuntime) return;
|
|
124206
|
-
const runtime = this.embeddedRuntime;
|
|
124207
|
-
this.embeddedRuntime = null;
|
|
124208
|
-
await this.closeRuntime(runtime);
|
|
124209
|
-
}
|
|
124210
124430
|
/** Spawn a dedicated onboard `aedes` server for an onboard entry. */
|
|
124211
124431
|
async spawnOnboard(entry) {
|
|
124212
124432
|
const runtime = await this.spawnAedes(entry);
|
|
@@ -124343,8 +124563,9 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124343
124563
|
* (or whose username was removed entirely) get force-closed.
|
|
124344
124564
|
*/
|
|
124345
124565
|
reconcileEmbeddedClients() {
|
|
124346
|
-
const runtime
|
|
124347
|
-
|
|
124566
|
+
for (const runtime of this.onboardRuntimes.values()) this.reconcileRuntimeClients(runtime);
|
|
124567
|
+
}
|
|
124568
|
+
reconcileRuntimeClients(runtime) {
|
|
124348
124569
|
const prevSnapshot = runtime.credentialsSnapshot;
|
|
124349
124570
|
const nextSnapshot = this.buildCredentialsSnapshot();
|
|
124350
124571
|
const affectedUsers = [];
|
|
@@ -124383,26 +124604,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124383
124604
|
for (const c of this.activeEmbeddedClients) m.set(c.username, c.passwordHash);
|
|
124384
124605
|
return m;
|
|
124385
124606
|
}
|
|
124386
|
-
/**
|
|
124387
|
-
* Reassemble the internal {@link EmbeddedBrokerEntry} from the flat
|
|
124388
|
-
* `embedded*` config keys the settings form writes. Returns `null`
|
|
124389
|
-
* when the embedded broker is disabled — that's the signal
|
|
124390
|
-
* `onConfigChanged` / `onInitialize` use to decide whether to spawn
|
|
124391
|
-
* or shut down the `aedes` instance.
|
|
124392
|
-
*/
|
|
124393
|
-
deriveEmbeddedEntry() {
|
|
124394
|
-
if (!this.config.embeddedEnabled) return null;
|
|
124395
|
-
const username = this.config.embeddedUsername;
|
|
124396
|
-
const password = this.config.embeddedPassword;
|
|
124397
|
-
return {
|
|
124398
|
-
id: this.config.embeddedId || "embedded",
|
|
124399
|
-
name: "Embedded broker",
|
|
124400
|
-
port: this.config.embeddedPort,
|
|
124401
|
-
allowAnonymous: this.config.embeddedAllowAnonymous,
|
|
124402
|
-
...username.length > 0 ? { username } : {},
|
|
124403
|
-
...password.length > 0 ? { password } : {}
|
|
124404
|
-
};
|
|
124405
|
-
}
|
|
124406
124607
|
collectExternalProbeInputs() {
|
|
124407
124608
|
const out = [];
|
|
124408
124609
|
for (const b of this.activeExternalBrokers) {
|
|
@@ -124439,19 +124640,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124439
124640
|
* here (no migration code ships): the operator's one-off `mqtt` step does.
|
|
124440
124641
|
*/
|
|
124441
124642
|
reportRetiredCiphertext() {
|
|
124442
|
-
const paths = retiredCiphertextPaths({
|
|
124443
|
-
brokers: this.activeExternalBrokers,
|
|
124444
|
-
embeddedPassword: this.config.embeddedPassword
|
|
124445
|
-
});
|
|
124643
|
+
const paths = retiredCiphertextPaths({ brokers: this.activeExternalBrokers });
|
|
124446
124644
|
if (paths.length === 0) return;
|
|
124447
124645
|
const unreadable = [];
|
|
124448
124646
|
const values = [...this.activeExternalBrokers.map((b) => ({
|
|
124449
124647
|
id: `brokers[${b.id}].password`,
|
|
124450
124648
|
v: b.password
|
|
124451
|
-
}))
|
|
124452
|
-
id: "embeddedPassword",
|
|
124453
|
-
v: this.config.embeddedPassword
|
|
124454
|
-
}];
|
|
124649
|
+
}))];
|
|
124455
124650
|
for (const { id, v } of values) try {
|
|
124456
124651
|
this.storedPassword(v);
|
|
124457
124652
|
} catch (err) {
|
|
@@ -124475,13 +124670,79 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124475
124670
|
}
|
|
124476
124671
|
});
|
|
124477
124672
|
}
|
|
124478
|
-
|
|
124673
|
+
/**
|
|
124674
|
+
* The broker form. The create form (`id` null) offers "run an onboard broker" (CamStack hosts an
|
|
124675
|
+
* `aedes` server on this hub: a port, anonymous access) or an external URL; an existing onboard
|
|
124676
|
+
* broker edits only its credentials, since its address is the one it was given.
|
|
124677
|
+
*/
|
|
124678
|
+
brokerForm(id, onboard) {
|
|
124679
|
+
const credentials = [{
|
|
124680
|
+
type: "text",
|
|
124681
|
+
key: "username",
|
|
124682
|
+
label: "Username",
|
|
124683
|
+
labelKey: "mqtt-broker:fullSettings.brokers.username.label"
|
|
124684
|
+
}, {
|
|
124685
|
+
type: "password",
|
|
124686
|
+
key: "password",
|
|
124687
|
+
label: "Password",
|
|
124688
|
+
labelKey: "mqtt-broker:fullSettings.brokers.password.label",
|
|
124689
|
+
showToggle: true
|
|
124690
|
+
}];
|
|
124691
|
+
if (onboard) return this.schema({ sections: [{
|
|
124692
|
+
id: "broker",
|
|
124693
|
+
title: "Onboard broker",
|
|
124694
|
+
description: "A CamStack-hosted broker on this hub. Its port was chosen when it was created.",
|
|
124695
|
+
columns: 1,
|
|
124696
|
+
fields: credentials
|
|
124697
|
+
}] });
|
|
124698
|
+
const creating = id === null;
|
|
124699
|
+
const external = {
|
|
124700
|
+
field: "onboard",
|
|
124701
|
+
equals: false
|
|
124702
|
+
};
|
|
124479
124703
|
return this.schema({ sections: [{
|
|
124480
124704
|
id: "broker",
|
|
124481
|
-
title: "External broker",
|
|
124482
|
-
description: "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
|
|
124705
|
+
title: creating ? "MQTT broker" : "External broker",
|
|
124706
|
+
description: creating ? "Run a broker on this hub, or connect to one running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain." : "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
|
|
124483
124707
|
columns: 1,
|
|
124484
124708
|
fields: [
|
|
124709
|
+
...creating ? [
|
|
124710
|
+
{
|
|
124711
|
+
type: "boolean",
|
|
124712
|
+
key: "onboard",
|
|
124713
|
+
label: "Run an onboard broker",
|
|
124714
|
+
labelKey: "mqtt-broker:fullSettings.brokers.onboard.label",
|
|
124715
|
+
description: "Spin up a CamStack-hosted aedes broker on this hub instead of connecting to an external one. Each onboard broker gets its own dedicated server + port.",
|
|
124716
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.onboard.description",
|
|
124717
|
+
default: false
|
|
124718
|
+
},
|
|
124719
|
+
{
|
|
124720
|
+
type: "number",
|
|
124721
|
+
key: "port",
|
|
124722
|
+
label: "Port",
|
|
124723
|
+
labelKey: "mqtt-broker:fullSettings.brokers.port.label",
|
|
124724
|
+
placeholder: "(auto)",
|
|
124725
|
+
description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
|
|
124726
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.port.description",
|
|
124727
|
+
showWhen: {
|
|
124728
|
+
field: "onboard",
|
|
124729
|
+
equals: true
|
|
124730
|
+
}
|
|
124731
|
+
},
|
|
124732
|
+
{
|
|
124733
|
+
type: "boolean",
|
|
124734
|
+
key: "allowAnonymous",
|
|
124735
|
+
label: "Allow anonymous",
|
|
124736
|
+
labelKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.label",
|
|
124737
|
+
description: "Permit clients to connect without a username/password.",
|
|
124738
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.description",
|
|
124739
|
+
default: false,
|
|
124740
|
+
showWhen: {
|
|
124741
|
+
field: "onboard",
|
|
124742
|
+
equals: true
|
|
124743
|
+
}
|
|
124744
|
+
}
|
|
124745
|
+
] : [],
|
|
124485
124746
|
{
|
|
124486
124747
|
type: "text",
|
|
124487
124748
|
key: "url",
|
|
@@ -124489,26 +124750,15 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124489
124750
|
labelKey: "mqtt-broker:fullSettings.brokers.url.label",
|
|
124490
124751
|
description: "mqtt://, mqtts://, ws://, wss://",
|
|
124491
124752
|
descriptionKey: "mqtt-broker:fullSettings.brokers.url.description",
|
|
124492
|
-
required: true
|
|
124493
|
-
},
|
|
124494
|
-
{
|
|
124495
|
-
type: "text",
|
|
124496
|
-
key: "username",
|
|
124497
|
-
label: "Username",
|
|
124498
|
-
labelKey: "mqtt-broker:fullSettings.brokers.username.label"
|
|
124499
|
-
},
|
|
124500
|
-
{
|
|
124501
|
-
type: "password",
|
|
124502
|
-
key: "password",
|
|
124503
|
-
label: "Password",
|
|
124504
|
-
labelKey: "mqtt-broker:fullSettings.brokers.password.label",
|
|
124505
|
-
showToggle: true
|
|
124753
|
+
...creating ? { showWhen: external } : { required: true }
|
|
124506
124754
|
},
|
|
124755
|
+
...credentials,
|
|
124507
124756
|
{
|
|
124508
124757
|
type: "text",
|
|
124509
124758
|
key: "clientIdPrefix",
|
|
124510
124759
|
label: "Client id prefix",
|
|
124511
|
-
labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label"
|
|
124760
|
+
labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label",
|
|
124761
|
+
...creating ? { showWhen: external } : {}
|
|
124512
124762
|
}
|
|
124513
124763
|
]
|
|
124514
124764
|
}] });
|
|
@@ -124519,6 +124769,41 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124519
124769
|
if (handle === void 0) throw new Error("records are not available");
|
|
124520
124770
|
return {
|
|
124521
124771
|
readRawBrokers: async () => (await this.ctx.settings?.readAddonStore())?.["brokers"],
|
|
124772
|
+
readLegacyEmbedded: async () => {
|
|
124773
|
+
const raw = await this.ctx.settings?.readAddonStore() ?? {};
|
|
124774
|
+
const secrets = this.ctx.secrets;
|
|
124775
|
+
const { value } = secrets === void 0 ? { value: raw } : await resolveSecretMarkers(raw, secrets, {
|
|
124776
|
+
unresolved: "keep-marker",
|
|
124777
|
+
ownedBy: secrets.owner
|
|
124778
|
+
});
|
|
124779
|
+
const text = (key) => {
|
|
124780
|
+
const v = value[key];
|
|
124781
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
124782
|
+
};
|
|
124783
|
+
const port = value["embeddedPort"];
|
|
124784
|
+
const username = text("embeddedUsername");
|
|
124785
|
+
const password = this.storedPassword(text("embeddedPassword"));
|
|
124786
|
+
const enabled = value["embeddedEnabled"] === true;
|
|
124787
|
+
if (!(enabled || text("embeddedId") !== void 0 || username !== void 0 || password !== void 0)) return null;
|
|
124788
|
+
return {
|
|
124789
|
+
enabled,
|
|
124790
|
+
id: text("embeddedId") ?? "embedded",
|
|
124791
|
+
port: typeof port === "number" && Number.isInteger(port) ? port : 1883,
|
|
124792
|
+
allowAnonymous: value["embeddedAllowAnonymous"] === true,
|
|
124793
|
+
...username !== void 0 ? { username } : {},
|
|
124794
|
+
...password !== void 0 ? { password } : {}
|
|
124795
|
+
};
|
|
124796
|
+
},
|
|
124797
|
+
readRawEmbeddedKeys: async () => {
|
|
124798
|
+
const raw = await this.ctx.settings?.readAddonStore() ?? {};
|
|
124799
|
+
return Object.fromEntries(LEGACY_EMBEDDED_KEYS.filter((key) => key in raw).map((key) => [key, raw[key]]));
|
|
124800
|
+
},
|
|
124801
|
+
putOnboardRecord: async (id, body) => {
|
|
124802
|
+
await handle.put({
|
|
124803
|
+
id,
|
|
124804
|
+
data: mqttBrokersEntryKind.schema.parse(body)
|
|
124805
|
+
}, { ifRev: 0 });
|
|
124806
|
+
},
|
|
124522
124807
|
readRecords: async () => {
|
|
124523
124808
|
const page = await handle.query({ limit: 1e3 });
|
|
124524
124809
|
return new Map(page.records.map((r) => [r.id, { hasPassword: (r.data.password ?? "").length > 0 }]));
|
|
@@ -124531,184 +124816,109 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124531
124816
|
}
|
|
124532
124817
|
};
|
|
124533
124818
|
}
|
|
124819
|
+
scopedSettingsCapabilities() {
|
|
124820
|
+
return ["mqtt-broker", "broker"];
|
|
124821
|
+
}
|
|
124534
124822
|
/**
|
|
124535
|
-
* The
|
|
124823
|
+
* The credentials and the ACL of the onboard brokers. The brokers are records, listed
|
|
124536
124824
|
* on the Brokers page and edited as integration instances (`brokerForm`).
|
|
124537
124825
|
*/
|
|
124538
|
-
|
|
124539
|
-
|
|
124540
|
-
|
|
124541
|
-
|
|
124542
|
-
|
|
124543
|
-
|
|
124544
|
-
|
|
124545
|
-
|
|
124546
|
-
|
|
124547
|
-
|
|
124548
|
-
|
|
124549
|
-
|
|
124550
|
-
|
|
124551
|
-
|
|
124552
|
-
|
|
124553
|
-
|
|
124554
|
-
|
|
124555
|
-
|
|
124556
|
-
|
|
124557
|
-
|
|
124558
|
-
|
|
124559
|
-
|
|
124560
|
-
|
|
124561
|
-
|
|
124562
|
-
|
|
124563
|
-
|
|
124564
|
-
|
|
124565
|
-
|
|
124566
|
-
|
|
124567
|
-
|
|
124568
|
-
|
|
124569
|
-
|
|
124570
|
-
|
|
124571
|
-
|
|
124572
|
-
|
|
124573
|
-
|
|
124574
|
-
|
|
124575
|
-
|
|
124576
|
-
|
|
124577
|
-
|
|
124578
|
-
|
|
124579
|
-
|
|
124580
|
-
|
|
124581
|
-
|
|
124582
|
-
|
|
124583
|
-
|
|
124584
|
-
|
|
124585
|
-
|
|
124586
|
-
|
|
124587
|
-
|
|
124588
|
-
|
|
124589
|
-
|
|
124590
|
-
|
|
124591
|
-
|
|
124592
|
-
|
|
124593
|
-
|
|
124594
|
-
|
|
124595
|
-
showWhen: {
|
|
124596
|
-
field: "embeddedEnabled",
|
|
124597
|
-
equals: true
|
|
124598
|
-
}
|
|
124599
|
-
}),
|
|
124600
|
-
this.field({
|
|
124601
|
-
type: "password",
|
|
124602
|
-
key: "embeddedPassword",
|
|
124603
|
-
label: "Shared password",
|
|
124604
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.label",
|
|
124605
|
-
description: "Kept in the keychain. Paired with the shared username above.",
|
|
124606
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.description",
|
|
124607
|
-
showToggle: true,
|
|
124608
|
-
default: "",
|
|
124609
|
-
showWhen: {
|
|
124610
|
-
field: "embeddedEnabled",
|
|
124611
|
-
equals: true
|
|
124612
|
-
}
|
|
124613
|
-
})
|
|
124614
|
-
]
|
|
124615
|
-
},
|
|
124616
|
-
{
|
|
124617
|
-
id: "mqtt-broker-embedded-clients",
|
|
124618
|
-
title: "Embedded broker — credentials",
|
|
124619
|
-
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
|
|
124620
|
-
description: "Per-client username + bcrypt password hash for the embedded aedes broker. Generate the hash with `npx bcrypt-cli` (or any bcrypt tool, cost factor 10) and paste it here.",
|
|
124621
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
|
|
124622
|
-
columns: 1,
|
|
124623
|
-
fields: [{
|
|
124624
|
-
type: "editable-array",
|
|
124625
|
-
key: "embeddedClients",
|
|
124626
|
-
label: "Clients",
|
|
124627
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
|
|
124628
|
-
emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
|
|
124629
|
-
addLabel: "Add client",
|
|
124630
|
-
rowTitleTemplate: "{username}",
|
|
124631
|
-
defaultItem: {
|
|
124632
|
-
username: "",
|
|
124633
|
-
passwordHash: ""
|
|
124634
|
-
},
|
|
124635
|
-
itemFields: [{
|
|
124826
|
+
async clusterSettingsSchema(cap) {
|
|
124827
|
+
if (cap !== "mqtt-broker" && cap !== "broker") return null;
|
|
124828
|
+
return this.schema({ sections: [{
|
|
124829
|
+
id: "mqtt-broker-embedded-clients",
|
|
124830
|
+
title: "Embedded broker — credentials",
|
|
124831
|
+
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
|
|
124832
|
+
description: "Per-client username + bcrypt password hash for the embedded aedes broker. Generate the hash with `npx bcrypt-cli` (or any bcrypt tool, cost factor 10) and paste it here.",
|
|
124833
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
|
|
124834
|
+
columns: 1,
|
|
124835
|
+
fields: [{
|
|
124836
|
+
type: "editable-array",
|
|
124837
|
+
key: "embeddedClients",
|
|
124838
|
+
label: "Clients",
|
|
124839
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
|
|
124840
|
+
emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
|
|
124841
|
+
addLabel: "Add client",
|
|
124842
|
+
rowTitleTemplate: "{username}",
|
|
124843
|
+
defaultItem: {
|
|
124844
|
+
username: "",
|
|
124845
|
+
passwordHash: ""
|
|
124846
|
+
},
|
|
124847
|
+
itemFields: [{
|
|
124848
|
+
type: "text",
|
|
124849
|
+
key: "username",
|
|
124850
|
+
label: "Username",
|
|
124851
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.username.label"
|
|
124852
|
+
}, {
|
|
124853
|
+
type: "text",
|
|
124854
|
+
key: "passwordHash",
|
|
124855
|
+
label: "bcrypt hash",
|
|
124856
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.label",
|
|
124857
|
+
description: "bcrypt hash of the password (cost factor 10).",
|
|
124858
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.description"
|
|
124859
|
+
}]
|
|
124860
|
+
}]
|
|
124861
|
+
}, {
|
|
124862
|
+
id: "mqtt-broker-embedded-acl",
|
|
124863
|
+
title: "Embedded broker — topic ACL",
|
|
124864
|
+
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
|
|
124865
|
+
description: "Per-user pub/sub allow-list. With zero rules the broker is fully open to authenticated clients. With at least one rule, every user needs at least one rule that matches the topic.",
|
|
124866
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
|
|
124867
|
+
columns: 1,
|
|
124868
|
+
fields: [{
|
|
124869
|
+
type: "editable-array",
|
|
124870
|
+
key: "embeddedAcl",
|
|
124871
|
+
label: "Rules",
|
|
124872
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
|
|
124873
|
+
emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
|
|
124874
|
+
addLabel: "Add rule",
|
|
124875
|
+
rowTitleTemplate: "{username} → {topicPattern} ({access})",
|
|
124876
|
+
defaultItem: {
|
|
124877
|
+
username: "",
|
|
124878
|
+
topicPattern: "#",
|
|
124879
|
+
access: "both"
|
|
124880
|
+
},
|
|
124881
|
+
itemFields: [
|
|
124882
|
+
{
|
|
124636
124883
|
type: "text",
|
|
124637
124884
|
key: "username",
|
|
124638
124885
|
label: "Username",
|
|
124639
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124640
|
-
},
|
|
124886
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.username.label"
|
|
124887
|
+
},
|
|
124888
|
+
{
|
|
124641
124889
|
type: "text",
|
|
124642
|
-
key: "
|
|
124643
|
-
label: "
|
|
124644
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124645
|
-
description: "
|
|
124646
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124647
|
-
}]
|
|
124648
|
-
}]
|
|
124649
|
-
},
|
|
124650
|
-
{
|
|
124651
|
-
id: "mqtt-broker-embedded-acl",
|
|
124652
|
-
title: "Embedded broker — topic ACL",
|
|
124653
|
-
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
|
|
124654
|
-
description: "Per-user pub/sub allow-list. With zero rules the broker is fully open to authenticated clients. With at least one rule, every user needs at least one rule that matches the topic.",
|
|
124655
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
|
|
124656
|
-
columns: 1,
|
|
124657
|
-
fields: [{
|
|
124658
|
-
type: "editable-array",
|
|
124659
|
-
key: "embeddedAcl",
|
|
124660
|
-
label: "Rules",
|
|
124661
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
|
|
124662
|
-
emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
|
|
124663
|
-
addLabel: "Add rule",
|
|
124664
|
-
rowTitleTemplate: "{username} → {topicPattern} ({access})",
|
|
124665
|
-
defaultItem: {
|
|
124666
|
-
username: "",
|
|
124667
|
-
topicPattern: "#",
|
|
124668
|
-
access: "both"
|
|
124890
|
+
key: "topicPattern",
|
|
124891
|
+
label: "Topic pattern",
|
|
124892
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.label",
|
|
124893
|
+
description: "MQTT wildcards: `+` = single level, `#` = multi-level.",
|
|
124894
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.description"
|
|
124669
124895
|
},
|
|
124670
|
-
|
|
124671
|
-
|
|
124672
|
-
|
|
124673
|
-
|
|
124674
|
-
|
|
124675
|
-
|
|
124676
|
-
|
|
124677
|
-
|
|
124678
|
-
|
|
124679
|
-
|
|
124680
|
-
|
|
124681
|
-
|
|
124682
|
-
|
|
124683
|
-
|
|
124684
|
-
|
|
124685
|
-
|
|
124686
|
-
|
|
124687
|
-
|
|
124688
|
-
|
|
124689
|
-
|
|
124690
|
-
|
|
124691
|
-
|
|
124692
|
-
|
|
124693
|
-
|
|
124694
|
-
|
|
124695
|
-
|
|
124696
|
-
{
|
|
124697
|
-
value: "pub",
|
|
124698
|
-
label: "Publish only",
|
|
124699
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
|
|
124700
|
-
},
|
|
124701
|
-
{
|
|
124702
|
-
value: "both",
|
|
124703
|
-
label: "Subscribe + publish",
|
|
124704
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
|
|
124705
|
-
}
|
|
124706
|
-
]
|
|
124707
|
-
}
|
|
124708
|
-
]
|
|
124709
|
-
}]
|
|
124710
|
-
}
|
|
124711
|
-
] });
|
|
124896
|
+
{
|
|
124897
|
+
type: "select",
|
|
124898
|
+
key: "access",
|
|
124899
|
+
label: "Access",
|
|
124900
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.label",
|
|
124901
|
+
options: [
|
|
124902
|
+
{
|
|
124903
|
+
value: "sub",
|
|
124904
|
+
label: "Subscribe only",
|
|
124905
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_sub.label"
|
|
124906
|
+
},
|
|
124907
|
+
{
|
|
124908
|
+
value: "pub",
|
|
124909
|
+
label: "Publish only",
|
|
124910
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
|
|
124911
|
+
},
|
|
124912
|
+
{
|
|
124913
|
+
value: "both",
|
|
124914
|
+
label: "Subscribe + publish",
|
|
124915
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
|
|
124916
|
+
}
|
|
124917
|
+
]
|
|
124918
|
+
}
|
|
124919
|
+
]
|
|
124920
|
+
}]
|
|
124921
|
+
}] });
|
|
124712
124922
|
}
|
|
124713
124923
|
};
|
|
124714
124924
|
/**
|
|
@@ -124738,13 +124948,6 @@ function bcryptCompareSafe(plain, hash) {
|
|
|
124738
124948
|
}
|
|
124739
124949
|
}
|
|
124740
124950
|
/**
|
|
124741
|
-
* Decide whether a config update to the embedded broker requires a full
|
|
124742
|
-
* restart (port change) or can be hot-reloaded (credential / ACL change).
|
|
124743
|
-
*/
|
|
124744
|
-
function requiresEmbeddedRespawn(desired, running) {
|
|
124745
|
-
return desired.id !== running.id || desired.port !== running.port || desired.allowAnonymous !== running.allowAnonymous || desired.username !== running.username || desired.password !== running.password;
|
|
124746
|
-
}
|
|
124747
|
-
/**
|
|
124748
124951
|
* Read the `username` field off an aedes Client without leaking a cast.
|
|
124749
124952
|
* aedes attaches the username at runtime but its `.d.ts` doesn't carry
|
|
124750
124953
|
* it — we narrow with a small structural check.
|
|
@@ -124797,15 +125000,5 @@ async function findFreePort() {
|
|
|
124797
125000
|
});
|
|
124798
125001
|
});
|
|
124799
125002
|
}
|
|
124800
|
-
function mapMqttStatus(s) {
|
|
124801
|
-
switch (s) {
|
|
124802
|
-
case "connected": return "connected";
|
|
124803
|
-
case "disconnected": return "disconnected";
|
|
124804
|
-
case "auth-failed": return "auth-failed";
|
|
124805
|
-
case "unreachable": return "unreachable";
|
|
124806
|
-
case "tls-error": return "error";
|
|
124807
|
-
default: return "error";
|
|
124808
|
-
}
|
|
124809
|
-
}
|
|
124810
125003
|
//#endregion
|
|
124811
125004
|
export { MQTT_EMBEDDED_BCRYPT_ROUNDS, MqttBrokerAddon, MqttBrokerAddon as default };
|