@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
|
@@ -6028,7 +6028,7 @@ var ZodIssueCode = {
|
|
|
6028
6028
|
var ZodFirstPartyTypeKind;
|
|
6029
6029
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6030
6030
|
//#endregion
|
|
6031
|
-
//#region ../types/dist/sleep-
|
|
6031
|
+
//#region ../types/dist/sleep-BX_MqzUe.mjs
|
|
6032
6032
|
/**
|
|
6033
6033
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6034
6034
|
* window to float samples (D455).
|
|
@@ -6628,7 +6628,7 @@ var NOT_RUN = {
|
|
|
6628
6628
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6629
6629
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6630
6630
|
*/
|
|
6631
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6631
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6632
6632
|
const first = migrations[0];
|
|
6633
6633
|
if (first === void 0) return NOT_RUN;
|
|
6634
6634
|
const owner = first.meta.owner;
|
|
@@ -6657,7 +6657,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6657
6657
|
else if (status === "blocked") blocked.push(id);
|
|
6658
6658
|
else skipped.push(id);
|
|
6659
6659
|
};
|
|
6660
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6660
6661
|
for (const step of declaration.steps) {
|
|
6662
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6661
6663
|
const bound = byId.get(step.id);
|
|
6662
6664
|
if (bound === void 0 || step.action === "skip") {
|
|
6663
6665
|
skipped.push(step.id);
|
|
@@ -6835,6 +6837,12 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6835
6837
|
}
|
|
6836
6838
|
return out;
|
|
6837
6839
|
}
|
|
6840
|
+
/** Every scope, in the order the guard and the docs list them. */
|
|
6841
|
+
var SCOPED_SETTINGS_SCOPES = [
|
|
6842
|
+
"cluster",
|
|
6843
|
+
"device-defaults",
|
|
6844
|
+
"node"
|
|
6845
|
+
];
|
|
6838
6846
|
/**
|
|
6839
6847
|
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6840
6848
|
*
|
|
@@ -7649,6 +7657,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
|
|
|
7649
7657
|
/** Coalesces a burst of `keychain.changed` events into one re-resolve. */
|
|
7650
7658
|
var SECRET_CHANGE_DEBOUNCE_MS = 250;
|
|
7651
7659
|
/**
|
|
7660
|
+
* Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
|
|
7661
|
+
* place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
|
|
7662
|
+
* through `api` (guard `check-migration-ports-surface`).
|
|
7663
|
+
*/
|
|
7664
|
+
function migrationSet(catalogue, bindPorts) {
|
|
7665
|
+
return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
|
|
7666
|
+
}
|
|
7667
|
+
/** A mixed array is a mistake: the other kind would be dropped without a word. */
|
|
7668
|
+
function refuseMixed(form, kept, dropped) {
|
|
7669
|
+
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`);
|
|
7670
|
+
}
|
|
7671
|
+
function catalogueOf(list) {
|
|
7672
|
+
const kept = list.filter((entry) => !("execute" in entry));
|
|
7673
|
+
refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
|
|
7674
|
+
return kept;
|
|
7675
|
+
}
|
|
7676
|
+
function boundOf(list) {
|
|
7677
|
+
const kept = list.filter((entry) => "execute" in entry);
|
|
7678
|
+
refuseMixed("bound migrations", kept.length, list.length - kept.length);
|
|
7679
|
+
return kept;
|
|
7680
|
+
}
|
|
7681
|
+
/**
|
|
7652
7682
|
* Base class for CamStack addons. Eliminates settings boilerplate:
|
|
7653
7683
|
*
|
|
7654
7684
|
* - Typed `config` property with automatic resolution from store + defaults
|
|
@@ -7726,6 +7756,13 @@ var BaseAddon = class {
|
|
|
7726
7756
|
*/
|
|
7727
7757
|
secretStatePaths = [];
|
|
7728
7758
|
_addonSecretTree = null;
|
|
7759
|
+
/**
|
|
7760
|
+
* D986 M4: the scoped schemas this addon declared, resolved once per capability
|
|
7761
|
+
* (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
|
|
7762
|
+
* see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
|
|
7763
|
+
*/
|
|
7764
|
+
_scopedByCap = /* @__PURE__ */ new Map();
|
|
7765
|
+
_nodeKeysByCap = /* @__PURE__ */ new Map();
|
|
7729
7766
|
_deviceSecretTree = null;
|
|
7730
7767
|
/** Refs the last resolved config pointed at — a change to one re-resolves. */
|
|
7731
7768
|
_configSecretRefs = /* @__PURE__ */ new Set();
|
|
@@ -7795,6 +7832,7 @@ var BaseAddon = class {
|
|
|
7795
7832
|
this._ctx = context;
|
|
7796
7833
|
this._secretSettings = null;
|
|
7797
7834
|
}
|
|
7835
|
+
await this.loadScopedSchemas();
|
|
7798
7836
|
await this.resolveConfig();
|
|
7799
7837
|
if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
|
|
7800
7838
|
if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
|
|
@@ -7804,27 +7842,30 @@ var BaseAddon = class {
|
|
|
7804
7842
|
const normalized = normalizeAddonInitResult(result);
|
|
7805
7843
|
const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
|
|
7806
7844
|
this._registeredCapNames = providers.map((p) => p.capability.name);
|
|
7845
|
+
await this.loadScopedSchemas();
|
|
7807
7846
|
return normalized;
|
|
7808
7847
|
}
|
|
7848
|
+
async runMigrations(first, second, third) {
|
|
7849
|
+
if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
|
|
7850
|
+
return this.runBound(boundOf(first), second);
|
|
7851
|
+
}
|
|
7809
7852
|
/**
|
|
7810
|
-
*
|
|
7811
|
-
*
|
|
7812
|
-
*
|
|
7813
|
-
* restored devices — with the catalogue bound to the addon's ports
|
|
7814
|
-
* (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
|
|
7815
|
-
*
|
|
7816
|
-
* An engine that cannot be reached or refuses the catalogue is logged and runs
|
|
7817
|
-
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7818
|
-
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7819
|
-
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7853
|
+
* Several catalogues of ONE owner in one declaration (the engine replaces an owner's
|
|
7854
|
+
* catalogue on every declare, so two calls would each judge the other's ids "newer").
|
|
7855
|
+
* Build each entry with `migrationSet(catalogue, (api) => ports)`.
|
|
7820
7856
|
*/
|
|
7821
|
-
async
|
|
7857
|
+
async runMigrationSets(sets, options) {
|
|
7858
|
+
const api = this.ctx.migrationApi;
|
|
7859
|
+
if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
|
|
7860
|
+
return this.runBound(sets.flatMap((set) => set.bind(api)), options);
|
|
7861
|
+
}
|
|
7862
|
+
async runBound(migrations, options) {
|
|
7822
7863
|
const api = this.ctx.api.migrations;
|
|
7823
7864
|
const report = await runAddonMigrations({
|
|
7824
7865
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7825
7866
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7826
7867
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7827
|
-
}, migrations, this.ctx.logger);
|
|
7868
|
+
}, migrations, this.ctx.logger, options);
|
|
7828
7869
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7829
7870
|
return report;
|
|
7830
7871
|
}
|
|
@@ -7888,6 +7929,16 @@ var BaseAddon = class {
|
|
|
7888
7929
|
schema(schema) {
|
|
7889
7930
|
return schema;
|
|
7890
7931
|
}
|
|
7932
|
+
/**
|
|
7933
|
+
* The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
|
|
7934
|
+
* {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
|
|
7935
|
+
* `onInitialize`, when the providers it registers are not known yet: the first config resolve
|
|
7936
|
+
* must already know which keys are per node. Names the providers register at init are added
|
|
7937
|
+
* once known.
|
|
7938
|
+
*/
|
|
7939
|
+
scopedSettingsCapabilities() {
|
|
7940
|
+
return [];
|
|
7941
|
+
}
|
|
7891
7942
|
/** Override to provide global-level settings UI schema. */
|
|
7892
7943
|
globalSettingsSchema(_cap) {
|
|
7893
7944
|
return null;
|
|
@@ -7931,7 +7982,8 @@ var BaseAddon = class {
|
|
|
7931
7982
|
return [];
|
|
7932
7983
|
}
|
|
7933
7984
|
async getGlobalSettings(overlay, cap, nodeId) {
|
|
7934
|
-
|
|
7985
|
+
await this.loadScopedSchemas(cap);
|
|
7986
|
+
const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
|
|
7935
7987
|
if (!schema) return { sections: [] };
|
|
7936
7988
|
const projected = await this.readRawProjectedStore(nodeId, cap);
|
|
7937
7989
|
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
|
|
@@ -7966,10 +8018,9 @@ var BaseAddon = class {
|
|
|
7966
8018
|
*
|
|
7967
8019
|
* Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
|
|
7968
8020
|
* needs its connection fields, the default integration the addon's own sections. It is the
|
|
7969
|
-
* SHAPE of the form only — the values live
|
|
7970
|
-
* unchanged (`integrations.setSettings` for an account integration).
|
|
8021
|
+
* SHAPE of the form only — the values live on the instance record.
|
|
7971
8022
|
*/
|
|
7972
|
-
async
|
|
8023
|
+
async integrationFormSchema(_instance) {
|
|
7973
8024
|
return null;
|
|
7974
8025
|
}
|
|
7975
8026
|
/**
|
|
@@ -8015,7 +8066,7 @@ var BaseAddon = class {
|
|
|
8015
8066
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8016
8067
|
*/
|
|
8017
8068
|
async getIntegrationFormSchema(instance, nodeId) {
|
|
8018
|
-
const scoped = await this.
|
|
8069
|
+
const scoped = await this.integrationFormSchema(instance);
|
|
8019
8070
|
if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
|
|
8020
8071
|
return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
|
|
8021
8072
|
}
|
|
@@ -8063,6 +8114,7 @@ var BaseAddon = class {
|
|
|
8063
8114
|
* resolved blob back is a no-op for every unchanged secret.
|
|
8064
8115
|
*/
|
|
8065
8116
|
async resolveGlobalStore(nodeId, cap) {
|
|
8117
|
+
await this.loadScopedSchemas(cap);
|
|
8066
8118
|
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
8067
8119
|
return this.projectForNode(raw, nodeId, cap);
|
|
8068
8120
|
}
|
|
@@ -8073,6 +8125,7 @@ var BaseAddon = class {
|
|
|
8073
8125
|
}
|
|
8074
8126
|
/** The PROJECTED store with its markers — the form hydrate only (D798). */
|
|
8075
8127
|
async readRawProjectedStore(nodeId, cap) {
|
|
8128
|
+
await this.loadScopedSchemas(cap);
|
|
8076
8129
|
const raw = await this.rawSettings()?.readAddonStore() ?? {};
|
|
8077
8130
|
return this.projectForNode(raw, nodeId, cap);
|
|
8078
8131
|
}
|
|
@@ -8175,6 +8228,7 @@ var BaseAddon = class {
|
|
|
8175
8228
|
await this.applyGlobalPatch(patch, nodeId);
|
|
8176
8229
|
}
|
|
8177
8230
|
async applyGlobalPatch(barePatch, nodeId) {
|
|
8231
|
+
await this.loadScopedSchemas();
|
|
8178
8232
|
const keys = this.perNodeKeys();
|
|
8179
8233
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
8180
8234
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
@@ -8188,7 +8242,8 @@ var BaseAddon = class {
|
|
|
8188
8242
|
await this.resolveConfig();
|
|
8189
8243
|
await this.onConfigChanged();
|
|
8190
8244
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
8191
|
-
|
|
8245
|
+
const legacy = this.globalSettingsSchema();
|
|
8246
|
+
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
8192
8247
|
}
|
|
8193
8248
|
/**
|
|
8194
8249
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -8196,14 +8251,67 @@ var BaseAddon = class {
|
|
|
8196
8251
|
* declarations). Empty set ⇒ every per-node code path is bypassed and the
|
|
8197
8252
|
* settings API behaves exactly like the legacy node-agnostic one.
|
|
8198
8253
|
*/
|
|
8199
|
-
|
|
8254
|
+
/**
|
|
8255
|
+
* Load the scoped schemas of every capability this addon declares (the override and the
|
|
8256
|
+
* providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
|
|
8257
|
+
* derived from the previous set. Idempotent.
|
|
8258
|
+
*/
|
|
8259
|
+
async loadScopedSchemas(extraCap) {
|
|
8260
|
+
const caps = new Set([
|
|
8261
|
+
...this.scopedSettingsCapabilities(),
|
|
8262
|
+
...this._registeredCapNames,
|
|
8263
|
+
...extraCap === void 0 ? [] : [extraCap]
|
|
8264
|
+
]);
|
|
8265
|
+
let added = false;
|
|
8266
|
+
for (const cap of caps) {
|
|
8267
|
+
if (this._scopedByCap.has(cap)) continue;
|
|
8268
|
+
const loaded = [];
|
|
8269
|
+
for (const scope of SCOPED_SETTINGS_SCOPES) {
|
|
8270
|
+
const schema = await this.getScopedSettingsSchema(scope, cap);
|
|
8271
|
+
if (schema !== null) loaded.push({
|
|
8272
|
+
scope,
|
|
8273
|
+
schema
|
|
8274
|
+
});
|
|
8275
|
+
}
|
|
8276
|
+
this._scopedByCap.set(cap, loaded);
|
|
8277
|
+
added = true;
|
|
8278
|
+
}
|
|
8279
|
+
if (!added) return;
|
|
8280
|
+
this._nodeKeysByCap = /* @__PURE__ */ new Map();
|
|
8281
|
+
this._addonSecretTree = null;
|
|
8282
|
+
}
|
|
8283
|
+
/**
|
|
8284
|
+
* The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
|
|
8285
|
+
* sections of its scoped schemas, so dropping the old hook does not blank the form.
|
|
8286
|
+
*/
|
|
8287
|
+
scopedFormSchema(cap) {
|
|
8288
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8289
|
+
for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
|
|
8290
|
+
return byId.size === 0 ? null : { sections: [...byId.values()] };
|
|
8291
|
+
}
|
|
8292
|
+
/** Every loaded scoped schema of `cap`, or of every loaded capability. */
|
|
8293
|
+
loadedScopedSchemas(cap) {
|
|
8294
|
+
if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
|
|
8295
|
+
return [...this._scopedByCap.values()].flat();
|
|
8296
|
+
}
|
|
8297
|
+
/**
|
|
8298
|
+
* The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
|
|
8299
|
+
* the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
|
|
8300
|
+
* per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
|
|
8301
|
+
* per-node code path is bypassed and the settings API behaves like the node-agnostic one.
|
|
8302
|
+
* Call {@link loadScopedSchemas} first (the async seams do).
|
|
8303
|
+
*/
|
|
8200
8304
|
perNodeKeys(cap) {
|
|
8201
8305
|
const cacheKey = cap ?? "";
|
|
8202
|
-
const cached = this.
|
|
8306
|
+
const cached = this._nodeKeysByCap.get(cacheKey);
|
|
8203
8307
|
if (cached) return cached;
|
|
8204
|
-
const
|
|
8205
|
-
const keys = new Set(
|
|
8206
|
-
this.
|
|
8308
|
+
const legacy = this.globalSettingsSchema(cap);
|
|
8309
|
+
const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
|
|
8310
|
+
for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
|
|
8311
|
+
if (scope !== "node") continue;
|
|
8312
|
+
for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
|
|
8313
|
+
}
|
|
8314
|
+
this._nodeKeysByCap.set(cacheKey, keys);
|
|
8207
8315
|
return keys;
|
|
8208
8316
|
}
|
|
8209
8317
|
/**
|
|
@@ -8213,10 +8321,9 @@ var BaseAddon = class {
|
|
|
8213
8321
|
* time to return its response before the addon is torn down and
|
|
8214
8322
|
* re-initialised by `AddonRegistryService.restartAddon`.
|
|
8215
8323
|
*/
|
|
8216
|
-
maybeAutoRestart(patchKeys,
|
|
8217
|
-
if (!schema) return;
|
|
8324
|
+
maybeAutoRestart(patchKeys, schemas) {
|
|
8218
8325
|
const restartKeys = /* @__PURE__ */ new Set();
|
|
8219
|
-
for (const section of schema.sections) for (const field of section.fields) {
|
|
8326
|
+
for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
|
|
8220
8327
|
if (field.type === "separator" || field.type === "info") continue;
|
|
8221
8328
|
if (field.requiresRestart) restartKeys.add(field.key);
|
|
8222
8329
|
}
|
|
@@ -8412,12 +8519,14 @@ var BaseAddon = class {
|
|
|
8412
8519
|
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
8413
8520
|
*/
|
|
8414
8521
|
async getDeclaredSettingsKeys() {
|
|
8522
|
+
await this.loadScopedSchemas();
|
|
8415
8523
|
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
8416
8524
|
}
|
|
8417
8525
|
declaredKeySet() {
|
|
8418
8526
|
const keys = new Set(Object.keys(this.defaults));
|
|
8419
8527
|
const schema = this.globalSettingsSchema();
|
|
8420
8528
|
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
8529
|
+
for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
8421
8530
|
for (const key of this._declaredStateKeys) keys.add(key);
|
|
8422
8531
|
for (const path of this.secretStatePaths) {
|
|
8423
8532
|
const root = path.split(/[.[]/, 1)[0];
|
|
@@ -8513,7 +8622,11 @@ var BaseAddon = class {
|
|
|
8513
8622
|
}
|
|
8514
8623
|
/** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
|
|
8515
8624
|
addonSecretTree() {
|
|
8516
|
-
this._addonSecretTree
|
|
8625
|
+
if (this._addonSecretTree === null) {
|
|
8626
|
+
const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
|
|
8627
|
+
for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
|
|
8628
|
+
this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
|
|
8629
|
+
}
|
|
8517
8630
|
return this._addonSecretTree;
|
|
8518
8631
|
}
|
|
8519
8632
|
deviceSecretTree() {
|
|
@@ -8763,6 +8876,23 @@ function dropPerNodeFields(fields) {
|
|
|
8763
8876
|
}
|
|
8764
8877
|
return kept;
|
|
8765
8878
|
}
|
|
8879
|
+
/** The keys of the value-holding fields, recursing into layout containers. */
|
|
8880
|
+
function collectValueFieldKeys(fields) {
|
|
8881
|
+
const collected = [];
|
|
8882
|
+
for (const field of fields) {
|
|
8883
|
+
if (field.type === "group") {
|
|
8884
|
+
collected.push(...collectValueFieldKeys(field.fields));
|
|
8885
|
+
continue;
|
|
8886
|
+
}
|
|
8887
|
+
if (field.type === "sub-tabs") {
|
|
8888
|
+
for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
|
|
8889
|
+
continue;
|
|
8890
|
+
}
|
|
8891
|
+
if (field.type === "separator" || field.type === "info") continue;
|
|
8892
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
8893
|
+
}
|
|
8894
|
+
return collected;
|
|
8895
|
+
}
|
|
8766
8896
|
function collectFieldKeys(fields) {
|
|
8767
8897
|
const collected = [];
|
|
8768
8898
|
for (const field of fields) {
|
|
@@ -9289,6 +9419,7 @@ function defineEntryKind(name, spec) {
|
|
|
9289
9419
|
class: "config",
|
|
9290
9420
|
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9291
9421
|
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9422
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9292
9423
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9293
9424
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9294
9425
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
@@ -11954,7 +12085,7 @@ object({
|
|
|
11954
12085
|
params: record(string(), union([string(), number()])).optional()
|
|
11955
12086
|
});
|
|
11956
12087
|
//#endregion
|
|
11957
|
-
//#region ../types/dist/composition-
|
|
12088
|
+
//#region ../types/dist/composition-pOHbTzRB.mjs
|
|
11958
12089
|
/**
|
|
11959
12090
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11960
12091
|
*
|
|
@@ -13435,7 +13566,7 @@ var RecordIntegrationInstances = class {
|
|
|
13435
13566
|
return record === null ? null : this.viewOf(record);
|
|
13436
13567
|
};
|
|
13437
13568
|
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13438
|
-
createInstance = async ({ id, name, kind }) => {
|
|
13569
|
+
createInstance = async ({ id, name, kind, values }) => {
|
|
13439
13570
|
const minted = id ?? this.hooks.mintId?.({
|
|
13440
13571
|
name,
|
|
13441
13572
|
...kind !== void 0 ? { kind } : {}
|
|
@@ -13445,10 +13576,20 @@ var RecordIntegrationInstances = class {
|
|
|
13445
13576
|
id: minted,
|
|
13446
13577
|
data: this.hooks.initialBody(name, kind)
|
|
13447
13578
|
}, { ifRev: 0 });
|
|
13579
|
+
const patch = this.patchOf(values ?? {});
|
|
13580
|
+
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
13448
13581
|
await this.hooks.onChanged?.(minted, "created");
|
|
13449
13582
|
return { id: minted };
|
|
13450
13583
|
};
|
|
13451
13584
|
updateInstance = async ({ id, name, values }) => {
|
|
13585
|
+
const patch = this.patchOf(values);
|
|
13586
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13587
|
+
await this.handle.patch(id, patch);
|
|
13588
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13589
|
+
return { ok: true };
|
|
13590
|
+
};
|
|
13591
|
+
/** The record patch a form `values` object means: secrets kept, cleared or replaced. */
|
|
13592
|
+
patchOf(values) {
|
|
13452
13593
|
const patch = {};
|
|
13453
13594
|
for (const [key, value] of Object.entries(values)) {
|
|
13454
13595
|
if (value === void 0) continue;
|
|
@@ -13459,11 +13600,8 @@ var RecordIntegrationInstances = class {
|
|
|
13459
13600
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13460
13601
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13461
13602
|
}
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
await this.hooks.onChanged?.(id, "updated");
|
|
13465
|
-
return { ok: true };
|
|
13466
|
-
};
|
|
13603
|
+
return patch;
|
|
13604
|
+
}
|
|
13467
13605
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13468
13606
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13469
13607
|
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
@@ -14545,7 +14683,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14545
14683
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14546
14684
|
* catalogue lists the id (a newer database).
|
|
14547
14685
|
*/
|
|
14548
|
-
meta: MigrationMetaSchema.optional()
|
|
14686
|
+
meta: MigrationMetaSchema.optional(),
|
|
14687
|
+
/**
|
|
14688
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14689
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14690
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14691
|
+
*/
|
|
14692
|
+
ownerNotLoaded: boolean().optional()
|
|
14549
14693
|
});
|
|
14550
14694
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14551
14695
|
var MigrationPendingEntrySchema = object({
|
|
@@ -15666,6 +15810,39 @@ var UnstampedEventMediaCountSchema = object({
|
|
|
15666
15810
|
/** Sum across both, or `null` when either lane could not be counted. */
|
|
15667
15811
|
total: number().int().nonnegative().nullable()
|
|
15668
15812
|
}).nullable();
|
|
15813
|
+
/**
|
|
15814
|
+
* Where the stored event media sits, per storage location: how many media rows
|
|
15815
|
+
* and how many bytes carry each location id, how many carry none (legacy,
|
|
15816
|
+
* resolved to the default `eventMedia` location), and how many name a location
|
|
15817
|
+
* that was not asked about (a deleted or never-listed one).
|
|
15818
|
+
*
|
|
15819
|
+
* One covering-index aggregate per asked location plus the NULL one, and one
|
|
15820
|
+
* unfiltered total that scans the covering index: O(rows), not a walk of the
|
|
15821
|
+
* rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
|
|
15822
|
+
* it is a deleted location only when every location id was asked.
|
|
15823
|
+
* `null` is "could not be measured", never zero.
|
|
15824
|
+
*/
|
|
15825
|
+
var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
|
|
15826
|
+
var MediaLocationCensusSchema = object({
|
|
15827
|
+
locations: array(object({
|
|
15828
|
+
locationId: string(),
|
|
15829
|
+
rows: number().int().nonnegative(),
|
|
15830
|
+
bytes: number().nonnegative()
|
|
15831
|
+
})).readonly(),
|
|
15832
|
+
unstamped: object({
|
|
15833
|
+
rows: number().int().nonnegative(),
|
|
15834
|
+
bytes: number().nonnegative()
|
|
15835
|
+
}),
|
|
15836
|
+
/** Rows stamped with a location id that was not in the asked list. */
|
|
15837
|
+
unlisted: object({
|
|
15838
|
+
rows: number().int().nonnegative(),
|
|
15839
|
+
bytes: number().nonnegative()
|
|
15840
|
+
}),
|
|
15841
|
+
total: object({
|
|
15842
|
+
rows: number().int().nonnegative(),
|
|
15843
|
+
bytes: number().nonnegative()
|
|
15844
|
+
})
|
|
15845
|
+
});
|
|
15669
15846
|
var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
|
|
15670
15847
|
/** The independently selectable logical storage classes — every class
|
|
15671
15848
|
* `storage.listLocationDeclarations` reports, so an operator never meets a
|
|
@@ -46186,52 +46363,7 @@ DeviceType.Light, method(object({
|
|
|
46186
46363
|
level: number().min(0).max(100),
|
|
46187
46364
|
lastChangedAt: number()
|
|
46188
46365
|
});
|
|
46189
|
-
|
|
46190
|
-
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
46191
|
-
*
|
|
46192
|
-
* The cap models any kind-tagged message broker the user wants to
|
|
46193
|
-
* register and that other addons might consume. The first two kinds
|
|
46194
|
-
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
46195
|
-
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
46196
|
-
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
46197
|
-
* the same surface.
|
|
46198
|
-
*
|
|
46199
|
-
* Why one cap, not one cap per kind:
|
|
46200
|
-
* - The integrations page wants a single table of "every broker"
|
|
46201
|
-
* across kinds; a unified cap drives it without join logic.
|
|
46202
|
-
* - Generic admin operations (add / remove / test / status) are
|
|
46203
|
-
* identical across kinds — duplicating them per cap was busywork.
|
|
46204
|
-
* - The `network-access` cap precedent: same interface, many
|
|
46205
|
-
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
46206
|
-
*
|
|
46207
|
-
* What stays kind-specific:
|
|
46208
|
-
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
46209
|
-
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
46210
|
-
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
46211
|
-
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
46212
|
-
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
46213
|
-
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
46214
|
-
* without rebreaking the cap signature.
|
|
46215
|
-
* - `add` `settings` payload (different fields per kind, validated by
|
|
46216
|
-
* the kind-specific provider).
|
|
46217
|
-
*
|
|
46218
|
-
* Bidirectionality:
|
|
46219
|
-
* - `publish` is RPC — synchronous result (provider-defined).
|
|
46220
|
-
* - `subscribe` returns a subscription handle; the actual message
|
|
46221
|
-
* stream flows over the typed event-bus as `broker.message` events
|
|
46222
|
-
* keyed by `(brokerId, subscriptionId)`. Consumers filter in their
|
|
46223
|
-
* event handler. Subscriptions persist across reconnects — the
|
|
46224
|
-
* provider re-subscribes upstream after a transport drop.
|
|
46225
|
-
*
|
|
46226
|
-
* Why event-bus push (not a subscription RPC stream):
|
|
46227
|
-
* - Matches the rest of CamStack's D8 contract — events for telemetry,
|
|
46228
|
-
* RPC for loss-is-a-bug. Broker messages are telemetry (a single
|
|
46229
|
-
* drop is recoverable via `getState({key})`).
|
|
46230
|
-
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
46231
|
-
* - Cross-process: events already route through `$event-bus`; no
|
|
46232
|
-
* per-broker wire-shaping needed.
|
|
46233
|
-
*/
|
|
46234
|
-
var BrokerStatusEnum = _enum([
|
|
46366
|
+
_enum([
|
|
46235
46367
|
"connected",
|
|
46236
46368
|
"disconnected",
|
|
46237
46369
|
"connecting",
|
|
@@ -46239,141 +46371,12 @@ var BrokerStatusEnum = _enum([
|
|
|
46239
46371
|
"unreachable",
|
|
46240
46372
|
"error"
|
|
46241
46373
|
]);
|
|
46242
|
-
var BrokerInfoSchema = object({
|
|
46243
|
-
/** Stable broker id. Persisted; survives addon restarts. */
|
|
46244
|
-
id: string(),
|
|
46245
|
-
/** Addon id of the provider that OWNS this broker.
|
|
46246
|
-
*
|
|
46247
|
-
* The `broker` cap is a system-scoped collection: several addons
|
|
46248
|
-
* register a `broker` provider, each owning a DISJOINT set of
|
|
46249
|
-
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
46250
|
-
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
46251
|
-
* device belongs to one integration. The admin UI threads this id
|
|
46252
|
-
* back as the `{ addonId }` system-collection selector on every
|
|
46253
|
-
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
46254
|
-
* `testConnection`), so the call routes to the OWNING provider
|
|
46255
|
-
* instead of defaulting to the first-registered one. */
|
|
46256
|
-
addonId: string(),
|
|
46257
|
-
/** Human-readable name (operator-chosen at add-time). */
|
|
46258
|
-
name: string(),
|
|
46259
|
-
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
46260
|
-
kind: string(),
|
|
46261
|
-
status: BrokerStatusEnum,
|
|
46262
|
-
/** Free-form provider-specific info (HA version, MQTT broker
|
|
46263
|
-
* flavour, latency, mTLS active, …). */
|
|
46264
|
-
info: record(string(), unknown()),
|
|
46265
|
-
/** Ms epoch of the last connection probe / status update. */
|
|
46266
|
-
lastCheckedAt: number().nullable(),
|
|
46267
|
-
/** Last error message, when `status` indicates failure. */
|
|
46268
|
-
error: string().nullable()
|
|
46269
|
-
});
|
|
46270
46374
|
var RegistryStatusSchema = object({
|
|
46271
46375
|
brokerCount: number().int().nonnegative(),
|
|
46272
46376
|
connectedCount: number().int().nonnegative()
|
|
46273
46377
|
});
|
|
46274
|
-
/**
|
|
46275
|
-
* One entry per `broker` provider — which addon provides which broker
|
|
46276
|
-
* kind(s). The unified create picker reads this to know, for each
|
|
46277
|
-
* (addonId, kind) pair the operator can pick, where to route the
|
|
46278
|
-
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
46279
|
-
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
46280
|
-
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
46281
|
-
* offers a kind whose creation form can't be rendered).
|
|
46282
|
-
*/
|
|
46283
|
-
var BrokerProviderInfoSchema = object({
|
|
46284
|
-
/** Addon id of the `broker` provider this entry describes. */
|
|
46285
|
-
addonId: string(),
|
|
46286
|
-
/** Broker kinds this provider can create, with a display label. */
|
|
46287
|
-
kinds: array(object({
|
|
46288
|
-
kind: string(),
|
|
46289
|
-
label: string(),
|
|
46290
|
-
/**
|
|
46291
|
-
* Relative hub path to start an interactive account link (OAuth popup).
|
|
46292
|
-
* May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
|
|
46293
|
-
*/
|
|
46294
|
-
interactiveConnectPath: string().min(1).optional(),
|
|
46295
|
-
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
46296
|
-
interactiveDisconnectPath: string().min(1).optional(),
|
|
46297
|
-
/**
|
|
46298
|
-
* The kind signs in through a server-held interactive session instead of
|
|
46299
|
-
* (or before) typed credentials. `basePath` is a hub-relative route set
|
|
46300
|
-
* served by the owning addon (`/addon/<id>/...`) speaking the sign-in
|
|
46301
|
-
* contract: `POST {basePath}/start` (body: the form's settings) answers
|
|
46302
|
-
* `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
|
|
46303
|
-
* `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
|
|
46304
|
-
* 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
|
|
46305
|
-
* (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
|
|
46306
|
-
* existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
|
|
46307
|
-
* (`{ sessionId }`) drops the session. The dialog that edits a broker
|
|
46308
|
-
* offers it as the default path; the fields the session fills are
|
|
46309
|
-
* marked `filledBySignIn` in the kind's settings schema.
|
|
46310
|
-
*
|
|
46311
|
-
* `methods` lists the ways the session can sign in, recommended first:
|
|
46312
|
-
* `'device-code'` adds `POST {basePath}/code/start` (body: the form's
|
|
46313
|
-
* settings) answering `{ sessionId, userCode, verificationUrl,
|
|
46314
|
-
* expiresAt }` — the operator types `userCode` at `verificationUrl` on
|
|
46315
|
-
* any device, and the same `status` / `commit` / `cancel` routes follow
|
|
46316
|
-
* (status then carries `method`, `userCode`, `verificationUrl`,
|
|
46317
|
-
* `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
|
|
46318
|
-
* `'proxy'` is the `{basePath}/start` flow above. Absent means
|
|
46319
|
-
* `['proxy']`.
|
|
46320
|
-
*/
|
|
46321
|
-
signIn: object({
|
|
46322
|
-
basePath: string().min(1),
|
|
46323
|
-
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
46324
|
-
}).optional()
|
|
46325
|
-
}))
|
|
46326
|
-
});
|
|
46327
|
-
var ListInputSchema = object({
|
|
46328
|
-
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
46329
|
-
* only HA brokers. Omit to list every broker. */
|
|
46330
|
-
kind: string().optional() });
|
|
46331
46378
|
var GetInputSchema = object({ id: string() });
|
|
46332
|
-
var AddInputSchema = object({
|
|
46333
|
-
kind: string().min(1),
|
|
46334
|
-
name: string().min(1),
|
|
46335
|
-
/**
|
|
46336
|
-
* ADOPT an existing id instead of minting a new one.
|
|
46337
|
-
*
|
|
46338
|
-
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
46339
|
-
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
46340
|
-
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
46341
|
-
* bound to a broker that no longer exists. Re-entering the password under the
|
|
46342
|
-
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
46343
|
-
* password.
|
|
46344
|
-
*
|
|
46345
|
-
* A provider MUST refuse an id that is already in use — adopting a live
|
|
46346
|
-
* broker's id would silently take it over.
|
|
46347
|
-
*/
|
|
46348
|
-
id: string().min(1).optional(),
|
|
46349
|
-
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
46350
|
-
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
46351
|
-
* branch on receipt — invalid shape rejects the add. */
|
|
46352
|
-
settings: record(string(), unknown())
|
|
46353
|
-
});
|
|
46354
|
-
var AddResultSchema = object({ id: string() });
|
|
46355
|
-
var RemoveInputSchema = object({ id: string() });
|
|
46356
|
-
var TestConnectionResultSchema = discriminatedUnion("ok", [object({
|
|
46357
|
-
ok: literal(true),
|
|
46358
|
-
latencyMs: number().nonnegative()
|
|
46359
|
-
}), object({
|
|
46360
|
-
ok: literal(false),
|
|
46361
|
-
error: string()
|
|
46362
|
-
})]);
|
|
46363
46379
|
var SettingsRecordSchema$1 = record(string(), unknown());
|
|
46364
|
-
var SettingsSchemaInputSchema = object({ kind: string() });
|
|
46365
|
-
var TestSettingsInputSchema = object({
|
|
46366
|
-
kind: string(),
|
|
46367
|
-
settings: SettingsRecordSchema$1
|
|
46368
|
-
});
|
|
46369
|
-
var TestSettingsResultSchema = discriminatedUnion("ok", [object({
|
|
46370
|
-
ok: literal(true),
|
|
46371
|
-
latencyMs: number().nonnegative().optional()
|
|
46372
|
-
}).strict(), object({
|
|
46373
|
-
ok: literal(false),
|
|
46374
|
-
error: string()
|
|
46375
|
-
})]);
|
|
46376
|
-
var SettingsSchemaResultSchema = unknown().nullable();
|
|
46377
46380
|
var PublishInputSchema = object({
|
|
46378
46381
|
brokerId: string(),
|
|
46379
46382
|
/** Kind-specific routing target.
|
|
@@ -46417,51 +46420,12 @@ var brokerCapability = {
|
|
|
46417
46420
|
kind: "poll"
|
|
46418
46421
|
},
|
|
46419
46422
|
methods: {
|
|
46420
|
-
list: method(ListInputSchema, array(BrokerInfoSchema)),
|
|
46421
|
-
get: method(GetInputSchema, BrokerInfoSchema.nullable()),
|
|
46422
|
-
/** Enumerate which addon provides which broker kind(s) for the
|
|
46423
|
-
* unified create picker. The auto-mount fans this array across
|
|
46424
|
-
* every registered `broker` provider (array-output method), so the
|
|
46425
|
-
* picker sees every kind from every provider in one call. */
|
|
46426
|
-
listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
46427
|
-
add: method(AddInputSchema, AddResultSchema, {
|
|
46428
|
-
kind: "mutation",
|
|
46429
|
-
auth: "admin"
|
|
46430
|
-
}),
|
|
46431
|
-
remove: method(RemoveInputSchema, _void(), {
|
|
46432
|
-
kind: "mutation",
|
|
46433
|
-
auth: "admin"
|
|
46434
|
-
}),
|
|
46435
|
-
testConnection: method(GetInputSchema, TestConnectionResultSchema, {
|
|
46436
|
-
kind: "mutation",
|
|
46437
|
-
auth: "admin"
|
|
46438
|
-
}),
|
|
46439
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
46440
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
46441
|
-
* when the broker id is unknown to the provider (the collection
|
|
46442
|
-
* fallback may route a foreign id to the first provider). */
|
|
46443
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46444
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
46445
|
-
* provider validates the shape and applies the change (reconnects
|
|
46446
|
-
* if credentials changed). */
|
|
46447
|
-
setSettings: method(object({
|
|
46448
|
-
id: string(),
|
|
46449
|
-
settings: SettingsRecordSchema$1
|
|
46450
|
-
}), _void(), {
|
|
46451
|
-
kind: "mutation",
|
|
46452
|
-
auth: "admin"
|
|
46453
|
-
}),
|
|
46454
46423
|
/** Returns the kind-specific connection config the consumer needs
|
|
46455
46424
|
* to open its own client (MQTT pattern: `{url, username, password,
|
|
46456
46425
|
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
46457
46426
|
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
46458
46427
|
* Returns `null` when the broker id is unknown to the provider. */
|
|
46459
46428
|
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46460
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
46461
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
46462
|
-
kind: "mutation",
|
|
46463
|
-
auth: "admin"
|
|
46464
|
-
}),
|
|
46465
46429
|
publish: method(PublishInputSchema, unknown(), {
|
|
46466
46430
|
kind: "mutation",
|
|
46467
46431
|
auth: "admin"
|
|
@@ -49085,6 +49049,9 @@ var RunningProposalEvidenceSchema = object({
|
|
|
49085
49049
|
runsFound: number().int().nonnegative(),
|
|
49086
49050
|
pausesFound: number().int().nonnegative(),
|
|
49087
49051
|
longestPauseMs: number().nonnegative(),
|
|
49052
|
+
/** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
|
|
49053
|
+
pulsesFound: number().int().nonnegative(),
|
|
49054
|
+
longestPulseMs: number().nonnegative(),
|
|
49088
49055
|
idleFloorW: number(),
|
|
49089
49056
|
noiseCeilingW: number(),
|
|
49090
49057
|
plateauW: number(),
|
|
@@ -50531,7 +50498,14 @@ var DiscoveryCandidateSchema = object({
|
|
|
50531
50498
|
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
50532
50499
|
parentStableId: string().optional(),
|
|
50533
50500
|
/** `false` = shown for context but not pickable. Default true. */
|
|
50534
|
-
selectable: boolean().optional()
|
|
50501
|
+
selectable: boolean().optional(),
|
|
50502
|
+
/**
|
|
50503
|
+
* Keys of the provider's creation form (`getChildCreationSchema`) this candidate
|
|
50504
|
+
* cannot be adopted without — a key for an encrypted board, a pairing code. The
|
|
50505
|
+
* admin asks for exactly these, on the creation form prefilled with
|
|
50506
|
+
* `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
|
|
50507
|
+
*/
|
|
50508
|
+
needsParams: array(string()).optional()
|
|
50535
50509
|
});
|
|
50536
50510
|
/**
|
|
50537
50511
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -50590,12 +50564,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
|
|
|
50590
50564
|
}), array(DiscoveryCandidateSchema), {
|
|
50591
50565
|
kind: "mutation",
|
|
50592
50566
|
auth: "admin"
|
|
50593
|
-
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50567
|
+
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50568
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50569
|
+
integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
|
|
50570
|
+
candidate: DiscoveryCandidateSchema,
|
|
50571
|
+
/** The integration the operator adopts into; a provider with several instances needs it. */
|
|
50572
|
+
integrationId: string().optional()
|
|
50573
|
+
}), DeviceSummarySchema, {
|
|
50594
50574
|
kind: "mutation",
|
|
50595
50575
|
auth: "admin"
|
|
50596
|
-
}), method(object({}), boolean()), method(object({
|
|
50576
|
+
}), method(object({}), boolean()), method(object({
|
|
50597
50577
|
type: _enum(DeviceType),
|
|
50598
|
-
|
|
50578
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50579
|
+
integrationId: string().optional()
|
|
50580
|
+
}), CreationSchemaOutputSchema), method(object({
|
|
50581
|
+
type: _enum(DeviceType),
|
|
50582
|
+
config: record(string(), unknown()),
|
|
50583
|
+
/** The integration the operator adds the device to; a provider with several instances needs it. */
|
|
50584
|
+
integrationId: string().optional()
|
|
50599
50585
|
}), DeviceSummarySchema, {
|
|
50600
50586
|
kind: "mutation",
|
|
50601
50587
|
auth: "admin"
|
|
@@ -51305,7 +51291,10 @@ method(object({
|
|
|
51305
51291
|
auth: "admin"
|
|
51306
51292
|
}), method(object({
|
|
51307
51293
|
addonId: string(),
|
|
51308
|
-
type: _enum(DeviceType)
|
|
51294
|
+
type: _enum(DeviceType),
|
|
51295
|
+
/** The integration the form is for; a provider whose integrations differ (a hub
|
|
51296
|
+
* integration takes no manual add) answers per integration. Absent = the addon's own. */
|
|
51297
|
+
integrationId: string().optional()
|
|
51309
51298
|
}), unknown().nullable()), method(object({
|
|
51310
51299
|
addonId: string(),
|
|
51311
51300
|
type: _enum(DeviceType),
|
|
@@ -51389,7 +51378,11 @@ method(object({
|
|
|
51389
51378
|
kind: "mutation",
|
|
51390
51379
|
auth: "admin",
|
|
51391
51380
|
timeoutMs: 12e4
|
|
51392
|
-
}), method(object({
|
|
51381
|
+
}), method(object({
|
|
51382
|
+
addonId: string(),
|
|
51383
|
+
/** The integration the Add-Device dialog is open on; absent = the addon's own. */
|
|
51384
|
+
integrationId: string().optional()
|
|
51385
|
+
}), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
51393
51386
|
deviceId: number(),
|
|
51394
51387
|
key: string(),
|
|
51395
51388
|
value: unknown()
|
|
@@ -53985,7 +53978,7 @@ var TestConnectionStatusEnum = _enum([
|
|
|
53985
53978
|
"inconclusive",
|
|
53986
53979
|
"unsupported"
|
|
53987
53980
|
]);
|
|
53988
|
-
var TestConnectionResultSchema
|
|
53981
|
+
var TestConnectionResultSchema = object({
|
|
53989
53982
|
/** True ONLY for `validated`. Never true for a test that did not run. */
|
|
53990
53983
|
success: boolean(),
|
|
53991
53984
|
error: string().optional(),
|
|
@@ -54006,11 +53999,7 @@ var CreateIntegrationInputSchema = object({
|
|
|
54006
53999
|
info: record(string(), unknown()).optional(),
|
|
54007
54000
|
/** `stableId` of the discovery candidate the form was seeded from (D984). */
|
|
54008
54001
|
originStableId: string().optional(),
|
|
54009
|
-
settings: record(string(), unknown()).optional()
|
|
54010
|
-
/** Keys of `settings` the form's schema declares secret (by type) — kept in
|
|
54011
|
-
* the keychain, not the table (D787). */
|
|
54012
|
-
secretKeys: array(string()).optional(),
|
|
54013
|
-
skipRestart: boolean().optional()
|
|
54002
|
+
settings: record(string(), unknown()).optional()
|
|
54014
54003
|
});
|
|
54015
54004
|
var UpdateIntegrationInputSchema = object({
|
|
54016
54005
|
id: string(),
|
|
@@ -54031,20 +54020,10 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54031
54020
|
}), method(object({ id: string() }), DeleteIntegrationResultSchema, {
|
|
54032
54021
|
kind: "mutation",
|
|
54033
54022
|
auth: "admin"
|
|
54034
|
-
}), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
|
|
54035
|
-
id: string(),
|
|
54036
|
-
settings: record(string(), unknown()),
|
|
54037
|
-
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
54038
|
-
secretKeys: array(string()).optional(),
|
|
54039
|
-
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
54040
|
-
skipRestart: boolean().optional()
|
|
54041
|
-
}), object({ success: boolean() }), {
|
|
54042
|
-
kind: "mutation",
|
|
54043
|
-
auth: "admin"
|
|
54044
54023
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
54045
54024
|
addonId: string(),
|
|
54046
54025
|
settings: record(string(), unknown())
|
|
54047
|
-
}), TestConnectionResultSchema
|
|
54026
|
+
}), TestConnectionResultSchema, {
|
|
54048
54027
|
kind: "mutation",
|
|
54049
54028
|
auth: "admin"
|
|
54050
54029
|
});
|
|
@@ -54071,6 +54050,14 @@ var ConfigSchemaPassthrough$3 = custom();
|
|
|
54071
54050
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54072
54051
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54073
54052
|
/**
|
|
54053
|
+
* What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
|
|
54054
|
+
* instance is an integration that brings devices (the registry owns its on/off switch);
|
|
54055
|
+
* `services`: a connection configured from the addon's own function page, never listed on
|
|
54056
|
+
* Integrations.
|
|
54057
|
+
* It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
|
|
54058
|
+
*/
|
|
54059
|
+
var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
|
|
54060
|
+
/**
|
|
54074
54061
|
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54075
54062
|
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54076
54063
|
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
@@ -54081,6 +54068,22 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54081
54068
|
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54082
54069
|
id: string().min(1),
|
|
54083
54070
|
label: string().min(1),
|
|
54071
|
+
/**
|
|
54072
|
+
* The function capability this kind is configured for (`broker`, `smtp-provider`,
|
|
54073
|
+
* `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
|
|
54074
|
+
* provider with more than one section must name it on every kind.
|
|
54075
|
+
*/
|
|
54076
|
+
functionCap: string().min(1),
|
|
54077
|
+
/**
|
|
54078
|
+
* For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
|
|
54079
|
+
* (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
|
|
54080
|
+
* finds the instances whose kind names it; absent takes the kind `id`.
|
|
54081
|
+
*/
|
|
54082
|
+
brokerKind: string().min(1).optional(),
|
|
54083
|
+
/** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
|
|
54084
|
+
role: IntegrationInstanceRoleSchema.optional(),
|
|
54085
|
+
/** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
|
|
54086
|
+
canTest: boolean().optional(),
|
|
54084
54087
|
/** The kind signs in through a server-held interactive session. */
|
|
54085
54088
|
signIn: object({
|
|
54086
54089
|
basePath: string().min(1),
|
|
@@ -54093,11 +54096,7 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54093
54096
|
});
|
|
54094
54097
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54095
54098
|
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54096
|
-
role:
|
|
54097
|
-
"devices",
|
|
54098
|
-
"services",
|
|
54099
|
-
"both"
|
|
54100
|
-
]),
|
|
54099
|
+
role: IntegrationInstanceRoleSchema,
|
|
54101
54100
|
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54102
54101
|
discovery: object({
|
|
54103
54102
|
mode: _enum([
|
|
@@ -54172,7 +54171,11 @@ var integrationInstancesCapability = {
|
|
|
54172
54171
|
methods: {
|
|
54173
54172
|
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54174
54173
|
/** The form; `id: null` is the create form. */
|
|
54175
|
-
instanceFormSchema: method(object({
|
|
54174
|
+
instanceFormSchema: method(object({
|
|
54175
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54176
|
+
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
54177
|
+
kind: string().min(1).optional()
|
|
54178
|
+
}), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54176
54179
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54177
54180
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54178
54181
|
/**
|
|
@@ -54182,7 +54185,9 @@ var integrationInstancesCapability = {
|
|
|
54182
54185
|
createInstance: method(object({
|
|
54183
54186
|
id: IntegrationInstanceIdSchema.optional(),
|
|
54184
54187
|
name: string().min(1),
|
|
54185
|
-
kind: string().min(1).optional()
|
|
54188
|
+
kind: string().min(1).optional(),
|
|
54189
|
+
/** Form values written with the record (a link picked in the create dialog). */
|
|
54190
|
+
values: record(string(), unknown()).optional()
|
|
54186
54191
|
}), object({ id: string() }), {
|
|
54187
54192
|
kind: "mutation",
|
|
54188
54193
|
auth: "admin"
|
|
@@ -54209,8 +54214,10 @@ var integrationInstancesCapability = {
|
|
|
54209
54214
|
}),
|
|
54210
54215
|
testInstance: optionalMethod(object({
|
|
54211
54216
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
54212
|
-
values: record(string(), unknown())
|
|
54213
|
-
|
|
54217
|
+
values: record(string(), unknown()),
|
|
54218
|
+
/** The kind to test when `id` is null and the provider has several. */
|
|
54219
|
+
kind: string().min(1).optional()
|
|
54220
|
+
}), TestConnectionResultSchema, {
|
|
54214
54221
|
kind: "mutation",
|
|
54215
54222
|
auth: "admin"
|
|
54216
54223
|
})
|
|
@@ -56715,6 +56722,380 @@ var SystemMetricsSchema = object({
|
|
|
56715
56722
|
gpuMemoryPercent: number().optional()
|
|
56716
56723
|
});
|
|
56717
56724
|
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);
|
|
56725
|
+
/**
|
|
56726
|
+
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
56727
|
+
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
56728
|
+
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
56729
|
+
*
|
|
56730
|
+
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
56731
|
+
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
56732
|
+
* that with the addon id closed over). It is deliberately NOT in any input
|
|
56733
|
+
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
56734
|
+
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
56735
|
+
* addons go through the owner's capability.
|
|
56736
|
+
*
|
|
56737
|
+
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
56738
|
+
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
56739
|
+
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
56740
|
+
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
56741
|
+
* row's own ref.
|
|
56742
|
+
*
|
|
56743
|
+
* Internal: addons reach it through `ctx.records`, never directly.
|
|
56744
|
+
*/
|
|
56745
|
+
var RecordIdSchema = string().min(1).max(256);
|
|
56746
|
+
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
56747
|
+
var RecordBodySchema = record(string(), unknown());
|
|
56748
|
+
var RecordInputSchema = object({
|
|
56749
|
+
id: RecordIdSchema,
|
|
56750
|
+
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
56751
|
+
deviceId: number().int().optional(),
|
|
56752
|
+
/** The domain clock, ms: observed / created / opened. */
|
|
56753
|
+
at: number().int().optional(),
|
|
56754
|
+
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
56755
|
+
expiresAt: number().int().optional(),
|
|
56756
|
+
data: RecordBodySchema
|
|
56757
|
+
});
|
|
56758
|
+
var RecordViewSchema = object({
|
|
56759
|
+
id: string(),
|
|
56760
|
+
deviceId: number().int().nullable(),
|
|
56761
|
+
at: number().nullable(),
|
|
56762
|
+
expiresAt: number().nullable(),
|
|
56763
|
+
rev: number().int(),
|
|
56764
|
+
updatedAt: number(),
|
|
56765
|
+
data: RecordBodySchema
|
|
56766
|
+
});
|
|
56767
|
+
/** Declared index fields only. Anything else does not compile, by design. */
|
|
56768
|
+
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
56769
|
+
string(),
|
|
56770
|
+
number(),
|
|
56771
|
+
boolean(),
|
|
56772
|
+
object({ in: array(union([
|
|
56773
|
+
string(),
|
|
56774
|
+
number(),
|
|
56775
|
+
boolean()
|
|
56776
|
+
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
56777
|
+
object({
|
|
56778
|
+
gt: union([string(), number()]).optional(),
|
|
56779
|
+
gte: union([string(), number()]).optional(),
|
|
56780
|
+
lt: union([string(), number()]).optional(),
|
|
56781
|
+
lte: union([string(), number()]).optional()
|
|
56782
|
+
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
56783
|
+
]));
|
|
56784
|
+
var KindShape = { kind: RecordKindNameSchema };
|
|
56785
|
+
var TimeWindowShape = {
|
|
56786
|
+
deviceId: number().int().optional(),
|
|
56787
|
+
atFrom: number().optional(),
|
|
56788
|
+
atTo: number().optional()
|
|
56789
|
+
};
|
|
56790
|
+
var RecordQueryInputSchema = object({
|
|
56791
|
+
...KindShape,
|
|
56792
|
+
...TimeWindowShape,
|
|
56793
|
+
where: RecordWhereSchema.optional(),
|
|
56794
|
+
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
56795
|
+
order: _enum(["at", "id"]).optional(),
|
|
56796
|
+
desc: boolean().optional(),
|
|
56797
|
+
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
56798
|
+
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
56799
|
+
cursor: string().min(1).optional()
|
|
56800
|
+
});
|
|
56801
|
+
var RecordQueryResultSchema = object({
|
|
56802
|
+
records: array(RecordViewSchema),
|
|
56803
|
+
nextCursor: string().nullable()
|
|
56804
|
+
});
|
|
56805
|
+
var RecordKindStateSchema = _enum([
|
|
56806
|
+
"created",
|
|
56807
|
+
"unchanged",
|
|
56808
|
+
"updated"
|
|
56809
|
+
]);
|
|
56810
|
+
var DeclareKindResultSchema = object({
|
|
56811
|
+
kindId: number().int(),
|
|
56812
|
+
state: RecordKindStateSchema,
|
|
56813
|
+
createdIndexes: array(string())
|
|
56814
|
+
});
|
|
56815
|
+
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
56816
|
+
var SecretRefsList = array(string());
|
|
56817
|
+
/**
|
|
56818
|
+
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
56819
|
+
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
56820
|
+
*/
|
|
56821
|
+
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
56822
|
+
object({
|
|
56823
|
+
op: literal("put"),
|
|
56824
|
+
kind: RecordKindNameSchema,
|
|
56825
|
+
record: RecordInputSchema,
|
|
56826
|
+
ifRev: number().int().min(0).optional()
|
|
56827
|
+
}),
|
|
56828
|
+
object({
|
|
56829
|
+
op: literal("patch"),
|
|
56830
|
+
kind: RecordKindNameSchema,
|
|
56831
|
+
id: RecordIdSchema,
|
|
56832
|
+
data: RecordBodySchema,
|
|
56833
|
+
at: number().int().nullable().optional(),
|
|
56834
|
+
expiresAt: number().int().nullable().optional(),
|
|
56835
|
+
ifRev: number().int().min(1).optional()
|
|
56836
|
+
}),
|
|
56837
|
+
object({
|
|
56838
|
+
op: literal("delete"),
|
|
56839
|
+
kind: RecordKindNameSchema,
|
|
56840
|
+
id: RecordIdSchema
|
|
56841
|
+
})
|
|
56842
|
+
]);
|
|
56843
|
+
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
56844
|
+
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
56845
|
+
kind: "mutation",
|
|
56846
|
+
auth: "admin"
|
|
56847
|
+
}), method(object({
|
|
56848
|
+
...KindShape,
|
|
56849
|
+
id: RecordIdSchema
|
|
56850
|
+
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
56851
|
+
...KindShape,
|
|
56852
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56853
|
+
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
56854
|
+
...KindShape,
|
|
56855
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56856
|
+
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
56857
|
+
...KindShape,
|
|
56858
|
+
record: RecordInputSchema,
|
|
56859
|
+
/** Compare-and-set. `0` means "the row must not exist". */
|
|
56860
|
+
ifRev: number().int().min(0).optional()
|
|
56861
|
+
}), object({
|
|
56862
|
+
rev: number().int(),
|
|
56863
|
+
releasedRefs: SecretRefsList
|
|
56864
|
+
}), {
|
|
56865
|
+
kind: "mutation",
|
|
56866
|
+
auth: "admin"
|
|
56867
|
+
}), method(object({
|
|
56868
|
+
...KindShape,
|
|
56869
|
+
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56870
|
+
}), object({
|
|
56871
|
+
count: number().int(),
|
|
56872
|
+
releasedRefs: SecretRefsList
|
|
56873
|
+
}), {
|
|
56874
|
+
kind: "mutation",
|
|
56875
|
+
auth: "admin"
|
|
56876
|
+
}), method(object({
|
|
56877
|
+
...KindShape,
|
|
56878
|
+
id: RecordIdSchema,
|
|
56879
|
+
data: RecordBodySchema,
|
|
56880
|
+
at: number().int().nullable().optional(),
|
|
56881
|
+
expiresAt: number().int().nullable().optional(),
|
|
56882
|
+
ifRev: number().int().min(1).optional()
|
|
56883
|
+
}), object({
|
|
56884
|
+
rev: number().int(),
|
|
56885
|
+
releasedRefs: SecretRefsList
|
|
56886
|
+
}), {
|
|
56887
|
+
kind: "mutation",
|
|
56888
|
+
auth: "admin"
|
|
56889
|
+
}), method(object({
|
|
56890
|
+
...KindShape,
|
|
56891
|
+
id: RecordIdSchema
|
|
56892
|
+
}), object({
|
|
56893
|
+
deleted: boolean(),
|
|
56894
|
+
secretRefs: SecretRefsList
|
|
56895
|
+
}), {
|
|
56896
|
+
kind: "mutation",
|
|
56897
|
+
auth: "admin"
|
|
56898
|
+
}), method(object({
|
|
56899
|
+
...KindShape,
|
|
56900
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56901
|
+
}), object({
|
|
56902
|
+
deleted: number().int(),
|
|
56903
|
+
secretRefs: SecretRefsList
|
|
56904
|
+
}), {
|
|
56905
|
+
kind: "mutation",
|
|
56906
|
+
auth: "admin"
|
|
56907
|
+
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
56908
|
+
...KindShape,
|
|
56909
|
+
...TimeWindowShape,
|
|
56910
|
+
where: RecordWhereSchema.optional()
|
|
56911
|
+
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
56912
|
+
...KindShape,
|
|
56913
|
+
deviceId: number().int().optional(),
|
|
56914
|
+
from: number(),
|
|
56915
|
+
to: number(),
|
|
56916
|
+
bucketMs: number().int().positive()
|
|
56917
|
+
}), object({ buckets: array(object({
|
|
56918
|
+
bucketStart: number(),
|
|
56919
|
+
count: number().int()
|
|
56920
|
+
})) }), { auth: "admin" }), method(object({
|
|
56921
|
+
...KindShape,
|
|
56922
|
+
op: _enum([
|
|
56923
|
+
"sum",
|
|
56924
|
+
"min",
|
|
56925
|
+
"max",
|
|
56926
|
+
"count"
|
|
56927
|
+
]),
|
|
56928
|
+
field: RecordFieldPathSchema.optional(),
|
|
56929
|
+
groupBy: RecordFieldPathSchema.optional(),
|
|
56930
|
+
where: RecordWhereSchema.optional()
|
|
56931
|
+
}), object({ rows: array(object({
|
|
56932
|
+
group: union([
|
|
56933
|
+
string(),
|
|
56934
|
+
number(),
|
|
56935
|
+
_null()
|
|
56936
|
+
]),
|
|
56937
|
+
value: number().nullable()
|
|
56938
|
+
})) }), { auth: "admin" }), method(object({
|
|
56939
|
+
...KindShape,
|
|
56940
|
+
deviceId: number().int().optional(),
|
|
56941
|
+
atBefore: number().optional(),
|
|
56942
|
+
where: RecordWhereSchema.optional()
|
|
56943
|
+
}), object({
|
|
56944
|
+
deleted: number().int(),
|
|
56945
|
+
secretRefs: SecretRefsList
|
|
56946
|
+
}), {
|
|
56947
|
+
kind: "mutation",
|
|
56948
|
+
auth: "admin"
|
|
56949
|
+
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
56950
|
+
results: array(object({
|
|
56951
|
+
rev: number().int().nullable(),
|
|
56952
|
+
deleted: boolean()
|
|
56953
|
+
})),
|
|
56954
|
+
releasedRefs: SecretRefsList,
|
|
56955
|
+
secretRefs: SecretRefsList
|
|
56956
|
+
}), {
|
|
56957
|
+
kind: "mutation",
|
|
56958
|
+
auth: "admin"
|
|
56959
|
+
}), method(object({
|
|
56960
|
+
deviceId: number().int(),
|
|
56961
|
+
kinds: array(RecordKindNameSchema).optional()
|
|
56962
|
+
}), object({
|
|
56963
|
+
deleted: record(string(), number().int()),
|
|
56964
|
+
secretRefs: SecretRefsList
|
|
56965
|
+
}), {
|
|
56966
|
+
kind: "mutation",
|
|
56967
|
+
auth: "admin"
|
|
56968
|
+
}), method(object(KindShape), object({ id: number().int() }), {
|
|
56969
|
+
kind: "mutation",
|
|
56970
|
+
auth: "admin"
|
|
56971
|
+
});
|
|
56972
|
+
/**
|
|
56973
|
+
* The wire shapes of the migration API, version 1 (planning#447).
|
|
56974
|
+
*
|
|
56975
|
+
* A migration is code that outlives the server it was written against: an addon at
|
|
56976
|
+
* version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
|
|
56977
|
+
* the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
|
|
56978
|
+
* is the contract of every method of it, one Zod pair per method.
|
|
56979
|
+
*
|
|
56980
|
+
* APPEND-ONLY. A released method is never removed and never changes meaning: an input
|
|
56981
|
+
* field is never removed or made required, an enum never narrows, an output field is
|
|
56982
|
+
* never removed. A change that cannot be expressed that way is `migration-api.v2`, next
|
|
56983
|
+
* to this file. `scripts/check-migration-api-append-only.ts` holds the line against
|
|
56984
|
+
* `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
|
|
56985
|
+
* `it('<id>'` in `migration-api.contract.spec.ts`.
|
|
56986
|
+
*/
|
|
56987
|
+
var Rows = record(string(), unknown());
|
|
56988
|
+
var IntegrationId = object({ integrationId: string().min(1) });
|
|
56989
|
+
var RecordId = object({ id: string().min(1) });
|
|
56990
|
+
var RecordRowSchema = object({
|
|
56991
|
+
id: string(),
|
|
56992
|
+
deviceId: number().int().nullable(),
|
|
56993
|
+
at: number().nullable(),
|
|
56994
|
+
expiresAt: number().nullable(),
|
|
56995
|
+
rev: number().int(),
|
|
56996
|
+
updatedAt: number(),
|
|
56997
|
+
data: Rows
|
|
56998
|
+
});
|
|
56999
|
+
var RecordPutShape = {
|
|
57000
|
+
id: string().min(1),
|
|
57001
|
+
deviceId: number().int().optional(),
|
|
57002
|
+
at: number().int().optional(),
|
|
57003
|
+
expiresAt: number().int().optional(),
|
|
57004
|
+
data: Rows
|
|
57005
|
+
};
|
|
57006
|
+
var MigrationIntegrationSchema = object({
|
|
57007
|
+
id: string(),
|
|
57008
|
+
addonId: string(),
|
|
57009
|
+
name: string(),
|
|
57010
|
+
info: Rows,
|
|
57011
|
+
originStableId: string().optional()
|
|
57012
|
+
});
|
|
57013
|
+
var MigrationDeviceSchema = object({
|
|
57014
|
+
id: number().int(),
|
|
57015
|
+
integrationId: string().nullable().optional(),
|
|
57016
|
+
/** The owning device; null or absent for a root. */
|
|
57017
|
+
parentDeviceId: number().int().nullable().optional(),
|
|
57018
|
+
type: string().optional(),
|
|
57019
|
+
name: string().optional(),
|
|
57020
|
+
stableId: string().optional()
|
|
57021
|
+
});
|
|
57022
|
+
/** What the engine says about the migration API it serves: lets an addon notice an older server. */
|
|
57023
|
+
var MigrationApiInfoSchema = object({
|
|
57024
|
+
version: number().int(),
|
|
57025
|
+
methods: array(string())
|
|
57026
|
+
});
|
|
57027
|
+
object({
|
|
57028
|
+
message: string(),
|
|
57029
|
+
meta: Rows.optional(),
|
|
57030
|
+
/** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
|
|
57031
|
+
tags: object({ deviceId: number().int().optional() }).optional()
|
|
57032
|
+
});
|
|
57033
|
+
object({
|
|
57034
|
+
owner: string().min(1),
|
|
57035
|
+
metas: array(MigrationMetaSchema)
|
|
57036
|
+
}), object({
|
|
57037
|
+
bootId: string(),
|
|
57038
|
+
snapshot: _enum([
|
|
57039
|
+
"none",
|
|
57040
|
+
"written",
|
|
57041
|
+
"refused"
|
|
57042
|
+
]),
|
|
57043
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57044
|
+
/** The migration API the engine serves; absent from a server that predates it. */
|
|
57045
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
57046
|
+
}), object({ id: string().min(1) }), object({
|
|
57047
|
+
proceed: boolean(),
|
|
57048
|
+
state: union([literal("pending"), MigrationStatusSchema]),
|
|
57049
|
+
reason: string().nullable()
|
|
57050
|
+
}), object({
|
|
57051
|
+
id: string().min(1),
|
|
57052
|
+
result: MigrationOwnerResultSchema
|
|
57053
|
+
}), object({
|
|
57054
|
+
status: MigrationStatusSchema,
|
|
57055
|
+
error: string().nullable(),
|
|
57056
|
+
rowsChanged: number().int().nonnegative()
|
|
57057
|
+
}), object({
|
|
57058
|
+
rows: number().int().nonnegative(),
|
|
57059
|
+
secrets: number().int().nonnegative()
|
|
57060
|
+
}), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
|
|
57061
|
+
present: boolean(),
|
|
57062
|
+
removed: Rows
|
|
57063
|
+
}), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
|
|
57064
|
+
...RecordPutShape,
|
|
57065
|
+
ifRev: number().int().optional()
|
|
57066
|
+
}), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
|
|
57067
|
+
partial: Rows,
|
|
57068
|
+
ifRev: number().int().optional()
|
|
57069
|
+
}), object({ rev: number().int() }), object({ deleted: boolean() }), object({
|
|
57070
|
+
deviceId: number().int().optional(),
|
|
57071
|
+
atFrom: number().optional(),
|
|
57072
|
+
atTo: number().optional(),
|
|
57073
|
+
where: RecordWhereSchema.optional(),
|
|
57074
|
+
limit: number().int().min(1).optional(),
|
|
57075
|
+
cursor: string().min(1).optional(),
|
|
57076
|
+
resolveSecrets: boolean().optional()
|
|
57077
|
+
}), object({
|
|
57078
|
+
records: array(RecordRowSchema),
|
|
57079
|
+
nextCursor: string().nullable()
|
|
57080
|
+
}), object({
|
|
57081
|
+
deviceId: number().int().optional(),
|
|
57082
|
+
atBefore: number().optional(),
|
|
57083
|
+
where: RecordWhereSchema.optional()
|
|
57084
|
+
}), 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({
|
|
57085
|
+
ref: string().min(1),
|
|
57086
|
+
value: string()
|
|
57087
|
+
}), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
|
|
57088
|
+
addonId: string().min(1),
|
|
57089
|
+
integrationId: string().min(1).optional()
|
|
57090
|
+
}), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
|
|
57091
|
+
id: number().int(),
|
|
57092
|
+
integrationId: string().nullable(),
|
|
57093
|
+
/** Whether the device carries any stored config; its content is not offered. */
|
|
57094
|
+
hasConfig: boolean()
|
|
57095
|
+
}).nullable(), object({
|
|
57096
|
+
deviceId: number().int(),
|
|
57097
|
+
integrationId: string().min(1)
|
|
57098
|
+
}), object({}), object({}), object({ nodeId: string() }), object({}), object({});
|
|
56718
57099
|
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({
|
|
56719
57100
|
id: string().min(1),
|
|
56720
57101
|
error: string().min(1)
|
|
@@ -56744,7 +57125,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56744
57125
|
"written",
|
|
56745
57126
|
"refused"
|
|
56746
57127
|
]),
|
|
56747
|
-
steps: array(MigrationOwnerStepSchema)
|
|
57128
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57129
|
+
/** The migration API this engine serves (additive: a server that predates it omits it). */
|
|
57130
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
56748
57131
|
}), {
|
|
56749
57132
|
kind: "mutation",
|
|
56750
57133
|
auth: "admin"
|
|
@@ -56765,6 +57148,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56765
57148
|
}), {
|
|
56766
57149
|
kind: "mutation",
|
|
56767
57150
|
auth: "admin"
|
|
57151
|
+
}), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
|
|
57152
|
+
rows: number().int().nonnegative(),
|
|
57153
|
+
secrets: number().int().nonnegative()
|
|
57154
|
+
}), {
|
|
57155
|
+
kind: "mutation",
|
|
57156
|
+
auth: "admin"
|
|
57157
|
+
}), method(object({
|
|
57158
|
+
integrationId: string().min(1),
|
|
57159
|
+
patch: record(string(), unknown())
|
|
57160
|
+
}), object({ written: boolean() }), {
|
|
57161
|
+
kind: "mutation",
|
|
57162
|
+
auth: "admin"
|
|
57163
|
+
}), method(object({
|
|
57164
|
+
integrationId: string().min(1),
|
|
57165
|
+
keys: array(string().min(1))
|
|
57166
|
+
}), object({
|
|
57167
|
+
present: boolean(),
|
|
57168
|
+
removed: record(string(), unknown())
|
|
57169
|
+
}), {
|
|
57170
|
+
kind: "mutation",
|
|
57171
|
+
auth: "admin"
|
|
56768
57172
|
});
|
|
56769
57173
|
method(object({
|
|
56770
57174
|
sourceUrl: string(),
|
|
@@ -59390,7 +59794,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
59390
59794
|
}), method(RelocateMediaInputSchema, object({ jobId: string() }), {
|
|
59391
59795
|
kind: "mutation",
|
|
59392
59796
|
auth: "admin"
|
|
59393
|
-
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(
|
|
59797
|
+
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
|
|
59798
|
+
kind: "query",
|
|
59799
|
+
auth: "admin"
|
|
59800
|
+
}), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
|
|
59394
59801
|
kind: "query",
|
|
59395
59802
|
auth: "admin"
|
|
59396
59803
|
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
@@ -61797,7 +62204,7 @@ var BrokerStatusSchema$1 = _enum([
|
|
|
61797
62204
|
"unreachable",
|
|
61798
62205
|
"tls-error"
|
|
61799
62206
|
]);
|
|
61800
|
-
var BrokerInfoSchema
|
|
62207
|
+
var BrokerInfoSchema = object({
|
|
61801
62208
|
id: string(),
|
|
61802
62209
|
name: string(),
|
|
61803
62210
|
url: string(),
|
|
@@ -61870,7 +62277,7 @@ var mqttBrokerCapability = {
|
|
|
61870
62277
|
kind: "poll"
|
|
61871
62278
|
},
|
|
61872
62279
|
methods: {
|
|
61873
|
-
listBrokers: method(_void(), array(BrokerInfoSchema
|
|
62280
|
+
listBrokers: method(_void(), array(BrokerInfoSchema)),
|
|
61874
62281
|
getBrokerConfig: method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }),
|
|
61875
62282
|
addBroker: method(AddBrokerInputSchema, AddBrokerResultSchema, {
|
|
61876
62283
|
kind: "mutation",
|
|
@@ -68197,253 +68604,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
|
|
|
68197
68604
|
auth: "admin"
|
|
68198
68605
|
});
|
|
68199
68606
|
/**
|
|
68200
|
-
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
68201
|
-
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
68202
|
-
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
68203
|
-
*
|
|
68204
|
-
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
68205
|
-
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
68206
|
-
* that with the addon id closed over). It is deliberately NOT in any input
|
|
68207
|
-
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
68208
|
-
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
68209
|
-
* addons go through the owner's capability.
|
|
68210
|
-
*
|
|
68211
|
-
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
68212
|
-
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
68213
|
-
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
68214
|
-
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
68215
|
-
* row's own ref.
|
|
68216
|
-
*
|
|
68217
|
-
* Internal: addons reach it through `ctx.records`, never directly.
|
|
68218
|
-
*/
|
|
68219
|
-
var RecordIdSchema = string().min(1).max(256);
|
|
68220
|
-
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
68221
|
-
var RecordBodySchema = record(string(), unknown());
|
|
68222
|
-
var RecordInputSchema = object({
|
|
68223
|
-
id: RecordIdSchema,
|
|
68224
|
-
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
68225
|
-
deviceId: number().int().optional(),
|
|
68226
|
-
/** The domain clock, ms: observed / created / opened. */
|
|
68227
|
-
at: number().int().optional(),
|
|
68228
|
-
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
68229
|
-
expiresAt: number().int().optional(),
|
|
68230
|
-
data: RecordBodySchema
|
|
68231
|
-
});
|
|
68232
|
-
var RecordViewSchema = object({
|
|
68233
|
-
id: string(),
|
|
68234
|
-
deviceId: number().int().nullable(),
|
|
68235
|
-
at: number().nullable(),
|
|
68236
|
-
expiresAt: number().nullable(),
|
|
68237
|
-
rev: number().int(),
|
|
68238
|
-
updatedAt: number(),
|
|
68239
|
-
data: RecordBodySchema
|
|
68240
|
-
});
|
|
68241
|
-
/** Declared index fields only. Anything else does not compile, by design. */
|
|
68242
|
-
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
68243
|
-
string(),
|
|
68244
|
-
number(),
|
|
68245
|
-
boolean(),
|
|
68246
|
-
object({ in: array(union([
|
|
68247
|
-
string(),
|
|
68248
|
-
number(),
|
|
68249
|
-
boolean()
|
|
68250
|
-
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
68251
|
-
object({
|
|
68252
|
-
gt: union([string(), number()]).optional(),
|
|
68253
|
-
gte: union([string(), number()]).optional(),
|
|
68254
|
-
lt: union([string(), number()]).optional(),
|
|
68255
|
-
lte: union([string(), number()]).optional()
|
|
68256
|
-
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
68257
|
-
]));
|
|
68258
|
-
var KindShape = { kind: RecordKindNameSchema };
|
|
68259
|
-
var TimeWindowShape = {
|
|
68260
|
-
deviceId: number().int().optional(),
|
|
68261
|
-
atFrom: number().optional(),
|
|
68262
|
-
atTo: number().optional()
|
|
68263
|
-
};
|
|
68264
|
-
var RecordQueryInputSchema = object({
|
|
68265
|
-
...KindShape,
|
|
68266
|
-
...TimeWindowShape,
|
|
68267
|
-
where: RecordWhereSchema.optional(),
|
|
68268
|
-
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
68269
|
-
order: _enum(["at", "id"]).optional(),
|
|
68270
|
-
desc: boolean().optional(),
|
|
68271
|
-
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
68272
|
-
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
68273
|
-
cursor: string().min(1).optional()
|
|
68274
|
-
});
|
|
68275
|
-
var RecordQueryResultSchema = object({
|
|
68276
|
-
records: array(RecordViewSchema),
|
|
68277
|
-
nextCursor: string().nullable()
|
|
68278
|
-
});
|
|
68279
|
-
var RecordKindStateSchema = _enum([
|
|
68280
|
-
"created",
|
|
68281
|
-
"unchanged",
|
|
68282
|
-
"updated"
|
|
68283
|
-
]);
|
|
68284
|
-
var DeclareKindResultSchema = object({
|
|
68285
|
-
kindId: number().int(),
|
|
68286
|
-
state: RecordKindStateSchema,
|
|
68287
|
-
createdIndexes: array(string())
|
|
68288
|
-
});
|
|
68289
|
-
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
68290
|
-
var SecretRefsList = array(string());
|
|
68291
|
-
/**
|
|
68292
|
-
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
68293
|
-
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
68294
|
-
*/
|
|
68295
|
-
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
68296
|
-
object({
|
|
68297
|
-
op: literal("put"),
|
|
68298
|
-
kind: RecordKindNameSchema,
|
|
68299
|
-
record: RecordInputSchema,
|
|
68300
|
-
ifRev: number().int().min(0).optional()
|
|
68301
|
-
}),
|
|
68302
|
-
object({
|
|
68303
|
-
op: literal("patch"),
|
|
68304
|
-
kind: RecordKindNameSchema,
|
|
68305
|
-
id: RecordIdSchema,
|
|
68306
|
-
data: RecordBodySchema,
|
|
68307
|
-
at: number().int().nullable().optional(),
|
|
68308
|
-
expiresAt: number().int().nullable().optional(),
|
|
68309
|
-
ifRev: number().int().min(1).optional()
|
|
68310
|
-
}),
|
|
68311
|
-
object({
|
|
68312
|
-
op: literal("delete"),
|
|
68313
|
-
kind: RecordKindNameSchema,
|
|
68314
|
-
id: RecordIdSchema
|
|
68315
|
-
})
|
|
68316
|
-
]);
|
|
68317
|
-
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
68318
|
-
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
68319
|
-
kind: "mutation",
|
|
68320
|
-
auth: "admin"
|
|
68321
|
-
}), method(object({
|
|
68322
|
-
...KindShape,
|
|
68323
|
-
id: RecordIdSchema
|
|
68324
|
-
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
68325
|
-
...KindShape,
|
|
68326
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68327
|
-
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
68328
|
-
...KindShape,
|
|
68329
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68330
|
-
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
68331
|
-
...KindShape,
|
|
68332
|
-
record: RecordInputSchema,
|
|
68333
|
-
/** Compare-and-set. `0` means "the row must not exist". */
|
|
68334
|
-
ifRev: number().int().min(0).optional()
|
|
68335
|
-
}), object({
|
|
68336
|
-
rev: number().int(),
|
|
68337
|
-
releasedRefs: SecretRefsList
|
|
68338
|
-
}), {
|
|
68339
|
-
kind: "mutation",
|
|
68340
|
-
auth: "admin"
|
|
68341
|
-
}), method(object({
|
|
68342
|
-
...KindShape,
|
|
68343
|
-
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68344
|
-
}), object({
|
|
68345
|
-
count: number().int(),
|
|
68346
|
-
releasedRefs: SecretRefsList
|
|
68347
|
-
}), {
|
|
68348
|
-
kind: "mutation",
|
|
68349
|
-
auth: "admin"
|
|
68350
|
-
}), method(object({
|
|
68351
|
-
...KindShape,
|
|
68352
|
-
id: RecordIdSchema,
|
|
68353
|
-
data: RecordBodySchema,
|
|
68354
|
-
at: number().int().nullable().optional(),
|
|
68355
|
-
expiresAt: number().int().nullable().optional(),
|
|
68356
|
-
ifRev: number().int().min(1).optional()
|
|
68357
|
-
}), object({
|
|
68358
|
-
rev: number().int(),
|
|
68359
|
-
releasedRefs: SecretRefsList
|
|
68360
|
-
}), {
|
|
68361
|
-
kind: "mutation",
|
|
68362
|
-
auth: "admin"
|
|
68363
|
-
}), method(object({
|
|
68364
|
-
...KindShape,
|
|
68365
|
-
id: RecordIdSchema
|
|
68366
|
-
}), object({
|
|
68367
|
-
deleted: boolean(),
|
|
68368
|
-
secretRefs: SecretRefsList
|
|
68369
|
-
}), {
|
|
68370
|
-
kind: "mutation",
|
|
68371
|
-
auth: "admin"
|
|
68372
|
-
}), method(object({
|
|
68373
|
-
...KindShape,
|
|
68374
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68375
|
-
}), object({
|
|
68376
|
-
deleted: number().int(),
|
|
68377
|
-
secretRefs: SecretRefsList
|
|
68378
|
-
}), {
|
|
68379
|
-
kind: "mutation",
|
|
68380
|
-
auth: "admin"
|
|
68381
|
-
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
68382
|
-
...KindShape,
|
|
68383
|
-
...TimeWindowShape,
|
|
68384
|
-
where: RecordWhereSchema.optional()
|
|
68385
|
-
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
68386
|
-
...KindShape,
|
|
68387
|
-
deviceId: number().int().optional(),
|
|
68388
|
-
from: number(),
|
|
68389
|
-
to: number(),
|
|
68390
|
-
bucketMs: number().int().positive()
|
|
68391
|
-
}), object({ buckets: array(object({
|
|
68392
|
-
bucketStart: number(),
|
|
68393
|
-
count: number().int()
|
|
68394
|
-
})) }), { auth: "admin" }), method(object({
|
|
68395
|
-
...KindShape,
|
|
68396
|
-
op: _enum([
|
|
68397
|
-
"sum",
|
|
68398
|
-
"min",
|
|
68399
|
-
"max",
|
|
68400
|
-
"count"
|
|
68401
|
-
]),
|
|
68402
|
-
field: RecordFieldPathSchema.optional(),
|
|
68403
|
-
groupBy: RecordFieldPathSchema.optional(),
|
|
68404
|
-
where: RecordWhereSchema.optional()
|
|
68405
|
-
}), object({ rows: array(object({
|
|
68406
|
-
group: union([
|
|
68407
|
-
string(),
|
|
68408
|
-
number(),
|
|
68409
|
-
_null()
|
|
68410
|
-
]),
|
|
68411
|
-
value: number().nullable()
|
|
68412
|
-
})) }), { auth: "admin" }), method(object({
|
|
68413
|
-
...KindShape,
|
|
68414
|
-
deviceId: number().int().optional(),
|
|
68415
|
-
atBefore: number().optional(),
|
|
68416
|
-
where: RecordWhereSchema.optional()
|
|
68417
|
-
}), object({
|
|
68418
|
-
deleted: number().int(),
|
|
68419
|
-
secretRefs: SecretRefsList
|
|
68420
|
-
}), {
|
|
68421
|
-
kind: "mutation",
|
|
68422
|
-
auth: "admin"
|
|
68423
|
-
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
68424
|
-
results: array(object({
|
|
68425
|
-
rev: number().int().nullable(),
|
|
68426
|
-
deleted: boolean()
|
|
68427
|
-
})),
|
|
68428
|
-
releasedRefs: SecretRefsList,
|
|
68429
|
-
secretRefs: SecretRefsList
|
|
68430
|
-
}), {
|
|
68431
|
-
kind: "mutation",
|
|
68432
|
-
auth: "admin"
|
|
68433
|
-
}), method(object({
|
|
68434
|
-
deviceId: number().int(),
|
|
68435
|
-
kinds: array(RecordKindNameSchema).optional()
|
|
68436
|
-
}), object({
|
|
68437
|
-
deleted: record(string(), number().int()),
|
|
68438
|
-
secretRefs: SecretRefsList
|
|
68439
|
-
}), {
|
|
68440
|
-
kind: "mutation",
|
|
68441
|
-
auth: "admin"
|
|
68442
|
-
}), method(object(KindShape), object({ id: number().int() }), {
|
|
68443
|
-
kind: "mutation",
|
|
68444
|
-
auth: "admin"
|
|
68445
|
-
});
|
|
68446
|
-
/**
|
|
68447
68607
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
68448
68608
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
68449
68609
|
* declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
|
|
@@ -75945,11 +76105,15 @@ discriminatedUnion("kind", [object({
|
|
|
75945
76105
|
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
75946
76106
|
* near F. A device that is not near one level most of the time has no floor.
|
|
75947
76107
|
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
75948
|
-
* 3.
|
|
75949
|
-
*
|
|
76108
|
+
* 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
|
|
76109
|
+
* display waking): it is neither a run nor a pause. Among the remaining
|
|
76110
|
+
* episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
|
|
76111
|
+
* two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
|
|
76112
|
+
* a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
|
|
76113
|
+
* is no run.
|
|
75950
76114
|
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
75951
|
-
* grid a person would type; stop dwell covers the longest pause
|
|
75952
|
-
* covers the longest standby blip above T.
|
|
76115
|
+
* grid a person would type; stop dwell covers the longest pause and the
|
|
76116
|
+
* kind's floor; start dwell covers the longest pulse and standby blip above T.
|
|
75953
76117
|
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
75954
76118
|
* run must hold exactly one true interval and nothing may flap outside.
|
|
75955
76119
|
*/
|
|
@@ -75960,6 +76124,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
|
75960
76124
|
30 * SECOND_MS;
|
|
75961
76125
|
60 * SECOND_MS;
|
|
75962
76126
|
20 * MINUTE_MS$1;
|
|
76127
|
+
90 * SECOND_MS;
|
|
76128
|
+
30 * MINUTE_MS$1;
|
|
76129
|
+
10 * MINUTE_MS$1;
|
|
76130
|
+
DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
|
|
75963
76131
|
10 * SECOND_MS;
|
|
75964
76132
|
10 * SECOND_MS;
|
|
75965
76133
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
@@ -76978,20 +77146,6 @@ Object.freeze({
|
|
|
76978
77146
|
access: "create",
|
|
76979
77147
|
role: "viewer"
|
|
76980
77148
|
},
|
|
76981
|
-
"broker.add": {
|
|
76982
|
-
capName: "broker",
|
|
76983
|
-
capScope: "system",
|
|
76984
|
-
addonId: null,
|
|
76985
|
-
access: "create",
|
|
76986
|
-
role: "admin"
|
|
76987
|
-
},
|
|
76988
|
-
"broker.get": {
|
|
76989
|
-
capName: "broker",
|
|
76990
|
-
capScope: "system",
|
|
76991
|
-
addonId: null,
|
|
76992
|
-
access: "view",
|
|
76993
|
-
role: "admin"
|
|
76994
|
-
},
|
|
76995
77149
|
"broker.getBrokerConfig": {
|
|
76996
77150
|
capName: "broker",
|
|
76997
77151
|
capScope: "system",
|
|
@@ -76999,20 +77153,6 @@ Object.freeze({
|
|
|
76999
77153
|
access: "view",
|
|
77000
77154
|
role: "admin"
|
|
77001
77155
|
},
|
|
77002
|
-
"broker.getSettings": {
|
|
77003
|
-
capName: "broker",
|
|
77004
|
-
capScope: "system",
|
|
77005
|
-
addonId: null,
|
|
77006
|
-
access: "view",
|
|
77007
|
-
role: "admin"
|
|
77008
|
-
},
|
|
77009
|
-
"broker.getSettingsSchema": {
|
|
77010
|
-
capName: "broker",
|
|
77011
|
-
capScope: "system",
|
|
77012
|
-
addonId: null,
|
|
77013
|
-
access: "view",
|
|
77014
|
-
role: "admin"
|
|
77015
|
-
},
|
|
77016
77156
|
"broker.getState": {
|
|
77017
77157
|
capName: "broker",
|
|
77018
77158
|
capScope: "system",
|
|
@@ -77027,20 +77167,6 @@ Object.freeze({
|
|
|
77027
77167
|
access: "view",
|
|
77028
77168
|
role: "admin"
|
|
77029
77169
|
},
|
|
77030
|
-
"broker.list": {
|
|
77031
|
-
capName: "broker",
|
|
77032
|
-
capScope: "system",
|
|
77033
|
-
addonId: null,
|
|
77034
|
-
access: "view",
|
|
77035
|
-
role: "admin"
|
|
77036
|
-
},
|
|
77037
|
-
"broker.listProviders": {
|
|
77038
|
-
capName: "broker",
|
|
77039
|
-
capScope: "system",
|
|
77040
|
-
addonId: null,
|
|
77041
|
-
access: "view",
|
|
77042
|
-
role: "admin"
|
|
77043
|
-
},
|
|
77044
77170
|
"broker.publish": {
|
|
77045
77171
|
capName: "broker",
|
|
77046
77172
|
capScope: "system",
|
|
@@ -77048,20 +77174,6 @@ Object.freeze({
|
|
|
77048
77174
|
access: "create",
|
|
77049
77175
|
role: "admin"
|
|
77050
77176
|
},
|
|
77051
|
-
"broker.remove": {
|
|
77052
|
-
capName: "broker",
|
|
77053
|
-
capScope: "system",
|
|
77054
|
-
addonId: null,
|
|
77055
|
-
access: "delete",
|
|
77056
|
-
role: "admin"
|
|
77057
|
-
},
|
|
77058
|
-
"broker.setSettings": {
|
|
77059
|
-
capName: "broker",
|
|
77060
|
-
capScope: "system",
|
|
77061
|
-
addonId: null,
|
|
77062
|
-
access: "create",
|
|
77063
|
-
role: "admin"
|
|
77064
|
-
},
|
|
77065
77177
|
"broker.subscribe": {
|
|
77066
77178
|
capName: "broker",
|
|
77067
77179
|
capScope: "system",
|
|
@@ -77069,20 +77181,6 @@ Object.freeze({
|
|
|
77069
77181
|
access: "create",
|
|
77070
77182
|
role: "admin"
|
|
77071
77183
|
},
|
|
77072
|
-
"broker.testConnection": {
|
|
77073
|
-
capName: "broker",
|
|
77074
|
-
capScope: "system",
|
|
77075
|
-
addonId: null,
|
|
77076
|
-
access: "create",
|
|
77077
|
-
role: "admin"
|
|
77078
|
-
},
|
|
77079
|
-
"broker.testSettings": {
|
|
77080
|
-
capName: "broker",
|
|
77081
|
-
capScope: "system",
|
|
77082
|
-
addonId: null,
|
|
77083
|
-
access: "create",
|
|
77084
|
-
role: "admin"
|
|
77085
|
-
},
|
|
77086
77184
|
"broker.unsubscribe": {
|
|
77087
77185
|
capName: "broker",
|
|
77088
77186
|
capScope: "system",
|
|
@@ -79715,13 +79813,6 @@ Object.freeze({
|
|
|
79715
79813
|
access: "view",
|
|
79716
79814
|
role: "admin"
|
|
79717
79815
|
},
|
|
79718
|
-
"integrations.getSettings": {
|
|
79719
|
-
capName: "integrations",
|
|
79720
|
-
capScope: "system",
|
|
79721
|
-
addonId: null,
|
|
79722
|
-
access: "view",
|
|
79723
|
-
role: "admin"
|
|
79724
|
-
},
|
|
79725
79816
|
"integrations.list": {
|
|
79726
79817
|
capName: "integrations",
|
|
79727
79818
|
capScope: "system",
|
|
@@ -79729,13 +79820,6 @@ Object.freeze({
|
|
|
79729
79820
|
access: "view",
|
|
79730
79821
|
role: "admin"
|
|
79731
79822
|
},
|
|
79732
|
-
"integrations.setSettings": {
|
|
79733
|
-
capName: "integrations",
|
|
79734
|
-
capScope: "system",
|
|
79735
|
-
addonId: null,
|
|
79736
|
-
access: "create",
|
|
79737
|
-
role: "admin"
|
|
79738
|
-
},
|
|
79739
79823
|
"integrations.testConnection": {
|
|
79740
79824
|
capName: "integrations",
|
|
79741
79825
|
capScope: "system",
|
|
@@ -80730,6 +80814,13 @@ Object.freeze({
|
|
|
80730
80814
|
access: "view",
|
|
80731
80815
|
role: "admin"
|
|
80732
80816
|
},
|
|
80817
|
+
"migrations.clearLegacyIntegrationRows": {
|
|
80818
|
+
capName: "migrations",
|
|
80819
|
+
capScope: "system",
|
|
80820
|
+
addonId: null,
|
|
80821
|
+
access: "delete",
|
|
80822
|
+
role: "admin"
|
|
80823
|
+
},
|
|
80733
80824
|
"migrations.declareOwner": {
|
|
80734
80825
|
capName: "migrations",
|
|
80735
80826
|
capScope: "system",
|
|
@@ -80737,6 +80828,13 @@ Object.freeze({
|
|
|
80737
80828
|
access: "create",
|
|
80738
80829
|
role: "admin"
|
|
80739
80830
|
},
|
|
80831
|
+
"migrations.deleteLegacyIntegrationRowKeys": {
|
|
80832
|
+
capName: "migrations",
|
|
80833
|
+
capScope: "system",
|
|
80834
|
+
addonId: null,
|
|
80835
|
+
access: "delete",
|
|
80836
|
+
role: "admin"
|
|
80837
|
+
},
|
|
80740
80838
|
"migrations.dryRun": {
|
|
80741
80839
|
capName: "migrations",
|
|
80742
80840
|
capScope: "system",
|
|
@@ -80772,6 +80870,20 @@ Object.freeze({
|
|
|
80772
80870
|
access: "create",
|
|
80773
80871
|
role: "admin"
|
|
80774
80872
|
},
|
|
80873
|
+
"migrations.patchLegacyIntegrationRows": {
|
|
80874
|
+
capName: "migrations",
|
|
80875
|
+
capScope: "system",
|
|
80876
|
+
addonId: null,
|
|
80877
|
+
access: "create",
|
|
80878
|
+
role: "admin"
|
|
80879
|
+
},
|
|
80880
|
+
"migrations.readLegacyIntegrationRows": {
|
|
80881
|
+
capName: "migrations",
|
|
80882
|
+
capScope: "system",
|
|
80883
|
+
addonId: null,
|
|
80884
|
+
access: "view",
|
|
80885
|
+
role: "admin"
|
|
80886
|
+
},
|
|
80775
80887
|
"migrations.restorePreMigration": {
|
|
80776
80888
|
capName: "migrations",
|
|
80777
80889
|
capScope: "system",
|
|
@@ -82032,6 +82144,13 @@ Object.freeze({
|
|
|
82032
82144
|
access: "view",
|
|
82033
82145
|
role: "viewer"
|
|
82034
82146
|
},
|
|
82147
|
+
"pipelineAnalytics.getMediaLocationCensus": {
|
|
82148
|
+
capName: "pipeline-analytics",
|
|
82149
|
+
capScope: "device",
|
|
82150
|
+
addonId: null,
|
|
82151
|
+
access: "view",
|
|
82152
|
+
role: "admin"
|
|
82153
|
+
},
|
|
82035
82154
|
"pipelineAnalytics.getMediaReclaimStatus": {
|
|
82036
82155
|
capName: "pipeline-analytics",
|
|
82037
82156
|
capScope: "device",
|
|
@@ -90973,6 +91092,48 @@ object({
|
|
|
90973
91092
|
delay: number().nonnegative().optional(),
|
|
90974
91093
|
commands: record(string(), unknown())
|
|
90975
91094
|
});
|
|
91095
|
+
var BrokerInUseError = class extends Error {
|
|
91096
|
+
brokerId;
|
|
91097
|
+
users;
|
|
91098
|
+
name = "BrokerInUseError";
|
|
91099
|
+
constructor(brokerId, users) {
|
|
91100
|
+
super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
|
|
91101
|
+
this.brokerId = brokerId;
|
|
91102
|
+
this.users = users;
|
|
91103
|
+
}
|
|
91104
|
+
};
|
|
91105
|
+
/** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
|
|
91106
|
+
async function assertBrokerUnused(brokerId, finders) {
|
|
91107
|
+
const users = [];
|
|
91108
|
+
for (const finder of finders) users.push(...await finder.find(brokerId));
|
|
91109
|
+
if (users.length > 0) throw new BrokerInUseError(brokerId, users);
|
|
91110
|
+
}
|
|
91111
|
+
/**
|
|
91112
|
+
* Users reached through another addon's listed rows (an addon cannot read a record kind it does
|
|
91113
|
+
* not own): every row whose `field` holds the broker id. `list` is a cap call the owner serves.
|
|
91114
|
+
*/
|
|
91115
|
+
function fieldUserFinder(spec) {
|
|
91116
|
+
return { find: async (brokerId) => (await spec.list()).filter((row) => row.formValues[spec.field] === brokerId).map((row) => ({
|
|
91117
|
+
kind: spec.kind,
|
|
91118
|
+
id: row.id,
|
|
91119
|
+
...row.name !== void 0 ? { name: row.name } : {}
|
|
91120
|
+
})) };
|
|
91121
|
+
}
|
|
91122
|
+
/**
|
|
91123
|
+
* An account provider whose `deleteInstance` is refused while anything still uses the account
|
|
91124
|
+
* (D1012). The account is an `integration-instances` kind (D1013), so the refusal lives on the
|
|
91125
|
+
* delete the instances page calls, not on a separate broker cap.
|
|
91126
|
+
*/
|
|
91127
|
+
function guardBrokerDelete(base, users) {
|
|
91128
|
+
return {
|
|
91129
|
+
...base,
|
|
91130
|
+
deleteInstance: async (input) => {
|
|
91131
|
+
await assertBrokerUnused(input.id, users());
|
|
91132
|
+
return base.deleteInstance(input);
|
|
91133
|
+
}
|
|
91134
|
+
};
|
|
91135
|
+
}
|
|
91136
|
+
entryBody({ brokerId: string() });
|
|
90976
91137
|
//#endregion
|
|
90977
91138
|
//#region ../../node_modules/xtend/immutable.js
|
|
90978
91139
|
var require_immutable = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -123017,7 +123178,7 @@ function readOnboardPort(settings) {
|
|
|
123017
123178
|
return raw;
|
|
123018
123179
|
}
|
|
123019
123180
|
/**
|
|
123020
|
-
* Build an onboard {@link BuiltOnboardEntry} from
|
|
123181
|
+
* Build an onboard {@link BuiltOnboardEntry} from the create form's values.
|
|
123021
123182
|
* Pure — the port is pre-resolved by the caller (auto-allocation is a
|
|
123022
123183
|
* side-effecting concern kept out of this helper), so the helper performs no
|
|
123023
123184
|
* I/O of its own.
|
|
@@ -123103,7 +123264,7 @@ function resolveStoredPassword(value, legacy) {
|
|
|
123103
123264
|
function retiredCiphertextPaths(config) {
|
|
123104
123265
|
const out = [];
|
|
123105
123266
|
for (const b of config.brokers) if (b.password?.startsWith("enc:v1:") === true) out.push(`brokers[${b.id}].password`);
|
|
123106
|
-
if (config.embeddedPassword
|
|
123267
|
+
if (config.embeddedPassword?.startsWith("enc:v1:") === true) out.push("embeddedPassword");
|
|
123107
123268
|
return out;
|
|
123108
123269
|
}
|
|
123109
123270
|
//#endregion
|
|
@@ -123137,6 +123298,229 @@ var mqttBrokersEntryKind = defineEntryKind("brokers", {
|
|
|
123137
123298
|
writeTrigger: "an operator adds or removes an external MQTT broker entry"
|
|
123138
123299
|
});
|
|
123139
123300
|
//#endregion
|
|
123301
|
+
//#region src/mqtt-broker-users.ts
|
|
123302
|
+
/** The addon whose integrations name an MQTT broker, and the field they keep that id in. */
|
|
123303
|
+
var MQTT_HA_ADDON_ID = "provider-mqtt-ha";
|
|
123304
|
+
var MQTT_HA_BROKER_FIELD = "mqttBrokerId";
|
|
123305
|
+
/** The finders for everything that still points at an MQTT broker. */
|
|
123306
|
+
function mqttBrokerUsers(listMqttHaInstances) {
|
|
123307
|
+
return [fieldUserFinder({
|
|
123308
|
+
kind: "integration",
|
|
123309
|
+
field: MQTT_HA_BROKER_FIELD,
|
|
123310
|
+
list: listMqttHaInstances
|
|
123311
|
+
})];
|
|
123312
|
+
}
|
|
123313
|
+
/** MQTT Discovery is installed but has not registered as a provider: not known, NOT absent (D315). */
|
|
123314
|
+
var MqttDiscoveryNotReadyError = class extends Error {
|
|
123315
|
+
constructor() {
|
|
123316
|
+
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");
|
|
123317
|
+
this.name = "MqttDiscoveryNotReadyError";
|
|
123318
|
+
}
|
|
123319
|
+
};
|
|
123320
|
+
/**
|
|
123321
|
+
* The integrations of the MQTT Discovery addon. Nothing can name a broker only when the registry
|
|
123322
|
+
* ANSWERED that the addon supplies no `integration-instances` AND the addon roster ANSWERED that
|
|
123323
|
+
* it is not installed: then the answer is none. An addon that is installed but has not registered
|
|
123324
|
+
* (booting, switched off) is not-yet-known, and a registry or roster that does not answer, or a
|
|
123325
|
+
* read that fails, is unknown too: all of those throw, so the delete is refused (D49, D315).
|
|
123326
|
+
*/
|
|
123327
|
+
async function listMqttHaInstancesOver(api) {
|
|
123328
|
+
const info = await api.capabilities.getCapability.query({ name: "integration-instances" });
|
|
123329
|
+
if (info === null) throw new Error("the capability registry did not answer");
|
|
123330
|
+
if (!info.providers.includes("provider-mqtt-ha")) {
|
|
123331
|
+
if ((await api.addons.list.query()).some((row) => row.manifest.id === "provider-mqtt-ha")) throw new MqttDiscoveryNotReadyError();
|
|
123332
|
+
return [];
|
|
123333
|
+
}
|
|
123334
|
+
return api.integrationInstances.listInstances.query({ addonId: MQTT_HA_ADDON_ID });
|
|
123335
|
+
}
|
|
123336
|
+
//#endregion
|
|
123337
|
+
//#region src/mqtt-broker-instances.ts
|
|
123338
|
+
/**
|
|
123339
|
+
* The MQTT brokers as `integration-instances` (D1013): one instance per `brokers` record. An
|
|
123340
|
+
* external broker is a URL; an ONBOARD broker (`onboard: true` on the create form) is a CamStack
|
|
123341
|
+
* `aedes` server on this hub: the create allocates its port (or honours the operator's) and the
|
|
123342
|
+
* addon spawns it from the record, the delete tears it down. Removing a broker an MQTT Discovery
|
|
123343
|
+
* integration still names is refused (D1012).
|
|
123344
|
+
*/
|
|
123345
|
+
var MQTT_INSTANCE_KIND = "mqtt";
|
|
123346
|
+
/** The create-form keys that only mean "run an onboard broker"; the record keeps a descriptor. */
|
|
123347
|
+
var ONBOARD_FORM_KEYS = new Set([
|
|
123348
|
+
"onboard",
|
|
123349
|
+
"port",
|
|
123350
|
+
"allowAnonymous"
|
|
123351
|
+
]);
|
|
123352
|
+
var PROBE_LABELS = {
|
|
123353
|
+
connected: "Connected",
|
|
123354
|
+
disconnected: "Disconnected",
|
|
123355
|
+
"auth-failed": "Authentication failed",
|
|
123356
|
+
unreachable: "Unreachable",
|
|
123357
|
+
"tls-error": "TLS error"
|
|
123358
|
+
};
|
|
123359
|
+
/** The instance's health word: connected / auth-failed / unreachable / tls-error, latency, clients. */
|
|
123360
|
+
function mqttHealthOf(body, live, now) {
|
|
123361
|
+
if (body.enabled === false) return { status: {
|
|
123362
|
+
state: "off",
|
|
123363
|
+
label: "Switched off"
|
|
123364
|
+
} };
|
|
123365
|
+
if (body.onboard !== void 0) return live.onboard === null ? {
|
|
123366
|
+
status: {
|
|
123367
|
+
state: "error",
|
|
123368
|
+
label: "Not listening"
|
|
123369
|
+
},
|
|
123370
|
+
lastCheckedAt: now
|
|
123371
|
+
} : {
|
|
123372
|
+
status: {
|
|
123373
|
+
state: "ok",
|
|
123374
|
+
label: "Listening",
|
|
123375
|
+
detail: `${live.onboard.connectedClients} connected clients`
|
|
123376
|
+
},
|
|
123377
|
+
lastCheckedAt: now
|
|
123378
|
+
};
|
|
123379
|
+
const probe = live.probe;
|
|
123380
|
+
if (probe === void 0) return { status: {
|
|
123381
|
+
state: "unknown",
|
|
123382
|
+
label: "Not checked yet"
|
|
123383
|
+
} };
|
|
123384
|
+
const detail = probe.status === "connected" ? probe.latencyMs !== null ? `${probe.latencyMs} ms` : void 0 : probe.error;
|
|
123385
|
+
return {
|
|
123386
|
+
status: {
|
|
123387
|
+
state: probe.status === "connected" ? "ok" : "error",
|
|
123388
|
+
label: PROBE_LABELS[probe.status],
|
|
123389
|
+
...detail !== void 0 ? { detail } : {}
|
|
123390
|
+
},
|
|
123391
|
+
lastCheckedAt: probe.lastCheckedAt
|
|
123392
|
+
};
|
|
123393
|
+
}
|
|
123394
|
+
function withoutOnboardKeys(values) {
|
|
123395
|
+
return Object.fromEntries(Object.entries(values).filter(([key]) => !ONBOARD_FORM_KEYS.has(key)));
|
|
123396
|
+
}
|
|
123397
|
+
function typedText(value) {
|
|
123398
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
123399
|
+
}
|
|
123400
|
+
/** A password the form really typed: not blank, not the redaction token, not the hidden view. */
|
|
123401
|
+
function typedSecret(value) {
|
|
123402
|
+
const text = typedText(value);
|
|
123403
|
+
return text === void 0 || text === "__camstack_redacted__" || isSecretFieldView(text) ? void 0 : text;
|
|
123404
|
+
}
|
|
123405
|
+
function resultOf(probe) {
|
|
123406
|
+
return probe.ok ? {
|
|
123407
|
+
success: true,
|
|
123408
|
+
status: "validated",
|
|
123409
|
+
...probe.latencyMs !== void 0 ? { latencyMs: probe.latencyMs } : {}
|
|
123410
|
+
} : {
|
|
123411
|
+
success: false,
|
|
123412
|
+
status: "rejected",
|
|
123413
|
+
error: probe.error ?? "The broker did not answer"
|
|
123414
|
+
};
|
|
123415
|
+
}
|
|
123416
|
+
function buildMqttBrokerInstances(records, deps) {
|
|
123417
|
+
const instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
|
|
123418
|
+
description: {
|
|
123419
|
+
role: "services",
|
|
123420
|
+
instanceMode: "multiple",
|
|
123421
|
+
kind: "brokers",
|
|
123422
|
+
canTest: true,
|
|
123423
|
+
kinds: [{
|
|
123424
|
+
id: MQTT_INSTANCE_KIND,
|
|
123425
|
+
label: "MQTT broker",
|
|
123426
|
+
functionCap: "broker",
|
|
123427
|
+
brokerKind: "mqtt",
|
|
123428
|
+
canTest: true
|
|
123429
|
+
}]
|
|
123430
|
+
},
|
|
123431
|
+
formSchema: (id) => deps.formSchema(id, false),
|
|
123432
|
+
kindOf: () => MQTT_INSTANCE_KIND,
|
|
123433
|
+
initialBody: (name) => ({
|
|
123434
|
+
name,
|
|
123435
|
+
url: "mqtt://"
|
|
123436
|
+
}),
|
|
123437
|
+
configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
|
|
123438
|
+
health: (body, id) => mqttHealthOf(body, deps.liveState(id), Date.now()),
|
|
123439
|
+
onChanged: deps.onChanged,
|
|
123440
|
+
test: async ({ id, values }) => {
|
|
123441
|
+
if (id === null && isOnboardRequested(values)) return {
|
|
123442
|
+
success: true,
|
|
123443
|
+
status: "validated",
|
|
123444
|
+
detail: "A free port is picked when the broker is created"
|
|
123445
|
+
};
|
|
123446
|
+
const saved = id === null ? null : await instances.readResolved(id);
|
|
123447
|
+
if (saved?.body.onboard !== void 0) return deps.onboardListening(saved.id) ? {
|
|
123448
|
+
success: true,
|
|
123449
|
+
status: "validated",
|
|
123450
|
+
detail: `Listening on port ${saved.body.onboard.port}`
|
|
123451
|
+
} : {
|
|
123452
|
+
success: false,
|
|
123453
|
+
status: "rejected",
|
|
123454
|
+
error: "The onboard broker is not listening"
|
|
123455
|
+
};
|
|
123456
|
+
const url = typedText(values["url"]) ?? saved?.body.url;
|
|
123457
|
+
if (url === void 0 || url === "mqtt://") return {
|
|
123458
|
+
success: false,
|
|
123459
|
+
status: "rejected",
|
|
123460
|
+
error: "Enter the broker URL first"
|
|
123461
|
+
};
|
|
123462
|
+
const username = typedText(values["username"]) ?? saved?.body.username;
|
|
123463
|
+
const password = typedSecret(values["password"]) ?? saved?.body.password;
|
|
123464
|
+
return resultOf(await deps.probe({
|
|
123465
|
+
url,
|
|
123466
|
+
...username !== void 0 ? { username } : {},
|
|
123467
|
+
...password !== void 0 ? { password } : {}
|
|
123468
|
+
}));
|
|
123469
|
+
}
|
|
123470
|
+
});
|
|
123471
|
+
const base = instances.toProvider();
|
|
123472
|
+
/** Port, descriptor and URL of an onboard broker the form asked for. */
|
|
123473
|
+
const onboardValues = async (id, name, values) => {
|
|
123474
|
+
const built = buildOnboardEntry({
|
|
123475
|
+
id,
|
|
123476
|
+
name,
|
|
123477
|
+
port: readOnboardPort(values) ?? await deps.freePort(),
|
|
123478
|
+
settings: values
|
|
123479
|
+
});
|
|
123480
|
+
return {
|
|
123481
|
+
...withoutOnboardKeys(values),
|
|
123482
|
+
url: built.url,
|
|
123483
|
+
onboard: built.onboard
|
|
123484
|
+
};
|
|
123485
|
+
};
|
|
123486
|
+
return guardBrokerDelete({
|
|
123487
|
+
...base,
|
|
123488
|
+
instanceFormSchema: async ({ id }) => {
|
|
123489
|
+
const saved = id === null ? null : await instances.getResolved(id);
|
|
123490
|
+
return deps.formSchema(id, saved?.onboard !== void 0);
|
|
123491
|
+
},
|
|
123492
|
+
createInstance: async (input) => {
|
|
123493
|
+
const values = input.values ?? {};
|
|
123494
|
+
if (!isOnboardRequested(values)) return base.createInstance({
|
|
123495
|
+
...input,
|
|
123496
|
+
values: withoutOnboardKeys(values)
|
|
123497
|
+
});
|
|
123498
|
+
const id = input.id ?? (0, node_crypto.randomUUID)();
|
|
123499
|
+
return base.createInstance({
|
|
123500
|
+
...input,
|
|
123501
|
+
id,
|
|
123502
|
+
values: await onboardValues(id, input.name, values)
|
|
123503
|
+
});
|
|
123504
|
+
},
|
|
123505
|
+
updateInstance: async (input) => {
|
|
123506
|
+
if (!isOnboardRequested(input.values)) return base.updateInstance({
|
|
123507
|
+
...input,
|
|
123508
|
+
values: withoutOnboardKeys(input.values)
|
|
123509
|
+
});
|
|
123510
|
+
const current = await instances.getResolved(input.id);
|
|
123511
|
+
if (current === null) throw new Error(`mqtt-broker: broker '${input.id}' not found`);
|
|
123512
|
+
if (current.onboard !== void 0) return base.updateInstance({
|
|
123513
|
+
...input,
|
|
123514
|
+
values: withoutOnboardKeys(input.values)
|
|
123515
|
+
});
|
|
123516
|
+
return base.updateInstance({
|
|
123517
|
+
...input,
|
|
123518
|
+
values: await onboardValues(input.id, input.name ?? current.name, input.values)
|
|
123519
|
+
});
|
|
123520
|
+
}
|
|
123521
|
+
}, () => mqttBrokerUsers(deps.listMqttHaInstances));
|
|
123522
|
+
}
|
|
123523
|
+
//#endregion
|
|
123140
123524
|
//#region src/migrations/brokers-to-records.ts
|
|
123141
123525
|
/**
|
|
123142
123526
|
* The external MQTT brokers live in the `brokers` records (D985, D996); the old addon-settings
|
|
@@ -123226,46 +123610,163 @@ async function planDropBrokersKey(ports) {
|
|
|
123226
123610
|
}] };
|
|
123227
123611
|
}
|
|
123228
123612
|
//#endregion
|
|
123613
|
+
//#region src/migrations/embedded-to-onboard.ts
|
|
123614
|
+
/**
|
|
123615
|
+
* The legacy single embedded broker lived in FLAT addon-settings keys (`embeddedEnabled`,
|
|
123616
|
+
* `embeddedPort`, ...). It is an ONBOARD broker record now, so the instance list, the pickers,
|
|
123617
|
+
* the test and the delete all see it (D1013). Two migrations of the catalogue, in this order:
|
|
123618
|
+
*
|
|
123619
|
+
* 1. `embeddedToOnboardRecord` (procedure): a legacy broker (a stopped one as a switched-off
|
|
123620
|
+
* record) becomes a `brokers` record
|
|
123621
|
+
* under the SAME id (`embeddedId`, else the `'embedded'` id the old code derived), so a
|
|
123622
|
+
* consumer that stored that id still resolves. It never overwrites a record of that id.
|
|
123623
|
+
* 2. `planDropEmbeddedKeys` (plan): the flat keys are deleted from the canonical
|
|
123624
|
+
* `addon-settings` rows, and only while the record that replaces them exists and carries the
|
|
123625
|
+
* password the legacy key held. `embeddedClients` / `embeddedAcl` are not flat broker keys
|
|
123626
|
+
* and stay.
|
|
123627
|
+
*/
|
|
123628
|
+
/** The flat keys that described the legacy embedded broker. */
|
|
123629
|
+
var LEGACY_EMBEDDED_KEYS = [
|
|
123630
|
+
"embeddedEnabled",
|
|
123631
|
+
"embeddedId",
|
|
123632
|
+
"embeddedPort",
|
|
123633
|
+
"embeddedAllowAnonymous",
|
|
123634
|
+
"embeddedUsername",
|
|
123635
|
+
"embeddedPassword"
|
|
123636
|
+
];
|
|
123637
|
+
var LEGACY_EMBEDDED_NAME = "Embedded broker";
|
|
123638
|
+
function onboardBodyOf(legacy) {
|
|
123639
|
+
return {
|
|
123640
|
+
name: LEGACY_EMBEDDED_NAME,
|
|
123641
|
+
url: `mqtt://127.0.0.1:${legacy.port}`,
|
|
123642
|
+
...legacy.username !== void 0 ? { username: legacy.username } : {},
|
|
123643
|
+
...legacy.password !== void 0 ? { password: legacy.password } : {},
|
|
123644
|
+
onboard: {
|
|
123645
|
+
port: legacy.port,
|
|
123646
|
+
allowAnonymous: legacy.allowAnonymous
|
|
123647
|
+
},
|
|
123648
|
+
...legacy.enabled ? {} : { enabled: false }
|
|
123649
|
+
};
|
|
123650
|
+
}
|
|
123651
|
+
async function embeddedToOnboardRecord(ports) {
|
|
123652
|
+
const legacy = await ports.readLegacyEmbedded();
|
|
123653
|
+
if (legacy === null) return {
|
|
123654
|
+
outcome: "already-done",
|
|
123655
|
+
summary: { migrated: 0 }
|
|
123656
|
+
};
|
|
123657
|
+
if ((await ports.readRecords()).has(legacy.id)) return {
|
|
123658
|
+
outcome: "already-done",
|
|
123659
|
+
summary: { migrated: 0 }
|
|
123660
|
+
};
|
|
123661
|
+
await ports.putOnboardRecord(legacy.id, onboardBodyOf(legacy));
|
|
123662
|
+
return {
|
|
123663
|
+
outcome: "done",
|
|
123664
|
+
summary: { migrated: 1 }
|
|
123665
|
+
};
|
|
123666
|
+
}
|
|
123667
|
+
async function planDropEmbeddedKeys(ports) {
|
|
123668
|
+
const raw = await ports.readRawEmbeddedKeys();
|
|
123669
|
+
const present = LEGACY_EMBEDDED_KEYS.filter((key) => key in raw);
|
|
123670
|
+
if (present.length === 0) return { edits: [] };
|
|
123671
|
+
const refuse = (reason) => {
|
|
123672
|
+
ports.warn(`mqtt-broker migration: the legacy embedded keys are kept: ${reason}`);
|
|
123673
|
+
throw new Error(`the legacy embedded broker keys are not deleted: ${reason}`);
|
|
123674
|
+
};
|
|
123675
|
+
const legacy = await ports.readLegacyEmbedded();
|
|
123676
|
+
if (legacy !== null) {
|
|
123677
|
+
const record = (await ports.readRecords()).get(legacy.id);
|
|
123678
|
+
if (record === void 0) return refuse(`the record '${legacy.id}' does not exist`);
|
|
123679
|
+
if ((legacy.password ?? "").length > 0 && !record.hasPassword) return refuse(`the record '${legacy.id}' holds no password`);
|
|
123680
|
+
}
|
|
123681
|
+
return { edits: [{
|
|
123682
|
+
op: "deleteKeys",
|
|
123683
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
123684
|
+
keys: present.map((key) => `${MQTT_BROKER_ADDON_ID}.${key}`),
|
|
123685
|
+
precondition: preconditionOf(Object.fromEntries(present.map((key) => [`${MQTT_BROKER_ADDON_ID}.${key}`, {
|
|
123686
|
+
addonId: MQTT_BROKER_ADDON_ID,
|
|
123687
|
+
key,
|
|
123688
|
+
value: raw[key]
|
|
123689
|
+
}])))
|
|
123690
|
+
}] };
|
|
123691
|
+
}
|
|
123692
|
+
//#endregion
|
|
123229
123693
|
//#region src/migrations/index.ts
|
|
123230
123694
|
var BROKERS_TO_RECORDS_ID = "mqtt-broker/2026-11-03-brokers-settings-to-records";
|
|
123231
123695
|
var DROP_BROKERS_KEY_ID = "mqtt-broker/2026-11-03-drop-brokers-key";
|
|
123696
|
+
var EMBEDDED_TO_ONBOARD_ID = "mqtt-broker/2026-11-05-embedded-broker-to-onboard-record";
|
|
123697
|
+
var DROP_EMBEDDED_KEYS_ID = "mqtt-broker/2026-11-05-drop-embedded-keys";
|
|
123232
123698
|
var FIXTURE_MARK = "a setting key the migration does not know";
|
|
123233
123699
|
/**
|
|
123234
123700
|
* The `mqtt-broker` catalogue (D987), offered to the engine when the addon initialises. Both
|
|
123235
123701
|
* entries are `tolerate`: the embedded broker and every record keep working while a legacy key
|
|
123236
123702
|
* stays where it is.
|
|
123237
123703
|
*/
|
|
123238
|
-
var ADDON_MIGRATIONS = [
|
|
123239
|
-
|
|
123240
|
-
|
|
123241
|
-
|
|
123242
|
-
|
|
123243
|
-
|
|
123244
|
-
|
|
123245
|
-
|
|
123246
|
-
|
|
123247
|
-
|
|
123248
|
-
|
|
123249
|
-
|
|
123250
|
-
|
|
123251
|
-
|
|
123252
|
-
|
|
123253
|
-
|
|
123254
|
-
|
|
123255
|
-
|
|
123256
|
-
|
|
123257
|
-
|
|
123258
|
-
|
|
123259
|
-
|
|
123260
|
-
|
|
123261
|
-
|
|
123262
|
-
|
|
123263
|
-
|
|
123264
|
-
|
|
123265
|
-
|
|
123266
|
-
|
|
123267
|
-
|
|
123268
|
-
}
|
|
123704
|
+
var ADDON_MIGRATIONS = [
|
|
123705
|
+
{
|
|
123706
|
+
meta: {
|
|
123707
|
+
id: BROKERS_TO_RECORDS_ID,
|
|
123708
|
+
owner: "mqtt-broker",
|
|
123709
|
+
level: "addon-state",
|
|
123710
|
+
seq: 1,
|
|
123711
|
+
dependsOn: [],
|
|
123712
|
+
store: "settings",
|
|
123713
|
+
scope: "cluster",
|
|
123714
|
+
onFailure: "tolerate",
|
|
123715
|
+
bodyKind: "procedure",
|
|
123716
|
+
description: "Check that every external broker of the old settings list is a brokers record; never deletes",
|
|
123717
|
+
fixtureMark: FIXTURE_MARK
|
|
123718
|
+
},
|
|
123719
|
+
procedure: verifyBrokersAreRecords
|
|
123720
|
+
},
|
|
123721
|
+
{
|
|
123722
|
+
meta: {
|
|
123723
|
+
id: DROP_BROKERS_KEY_ID,
|
|
123724
|
+
owner: "mqtt-broker",
|
|
123725
|
+
level: "addon-state",
|
|
123726
|
+
seq: 2,
|
|
123727
|
+
dependsOn: [BROKERS_TO_RECORDS_ID],
|
|
123728
|
+
store: "settings",
|
|
123729
|
+
scope: "cluster",
|
|
123730
|
+
onFailure: "tolerate",
|
|
123731
|
+
bodyKind: "plan",
|
|
123732
|
+
description: "Delete the legacy brokers settings key once every broker is a record",
|
|
123733
|
+
fixtureMark: FIXTURE_MARK
|
|
123734
|
+
},
|
|
123735
|
+
plan: planDropBrokersKey
|
|
123736
|
+
},
|
|
123737
|
+
{
|
|
123738
|
+
meta: {
|
|
123739
|
+
id: EMBEDDED_TO_ONBOARD_ID,
|
|
123740
|
+
owner: "mqtt-broker",
|
|
123741
|
+
level: "addon-state",
|
|
123742
|
+
seq: 3,
|
|
123743
|
+
dependsOn: [DROP_BROKERS_KEY_ID],
|
|
123744
|
+
store: "settings",
|
|
123745
|
+
scope: "cluster",
|
|
123746
|
+
onFailure: "tolerate",
|
|
123747
|
+
bodyKind: "procedure",
|
|
123748
|
+
description: "The legacy embedded broker (flat embedded* keys) becomes an onboard brokers record under the same id; never overwrites a record",
|
|
123749
|
+
fixtureMark: FIXTURE_MARK
|
|
123750
|
+
},
|
|
123751
|
+
procedure: embeddedToOnboardRecord
|
|
123752
|
+
},
|
|
123753
|
+
{
|
|
123754
|
+
meta: {
|
|
123755
|
+
id: DROP_EMBEDDED_KEYS_ID,
|
|
123756
|
+
owner: "mqtt-broker",
|
|
123757
|
+
level: "addon-state",
|
|
123758
|
+
seq: 4,
|
|
123759
|
+
dependsOn: [EMBEDDED_TO_ONBOARD_ID],
|
|
123760
|
+
store: "settings",
|
|
123761
|
+
scope: "cluster",
|
|
123762
|
+
onFailure: "tolerate",
|
|
123763
|
+
bodyKind: "plan",
|
|
123764
|
+
description: "Delete the flat embedded broker keys once the onboard record holds them",
|
|
123765
|
+
fixtureMark: FIXTURE_MARK
|
|
123766
|
+
},
|
|
123767
|
+
plan: planDropEmbeddedKeys
|
|
123768
|
+
}
|
|
123769
|
+
];
|
|
123269
123770
|
//#endregion
|
|
123270
123771
|
//#region src/mqtt-entry-record-kinds.ts
|
|
123271
123772
|
var aclAccessSchema = _enum([
|
|
@@ -123467,129 +123968,31 @@ var MqttAclStore = class MqttAclStore {
|
|
|
123467
123968
|
* The addon has NO standalone admin page. All configuration — external
|
|
123468
123969
|
* broker entries, the embedded `aedes` broker, embedded credentials,
|
|
123469
123970
|
* and the topic ACL — is exposed through the standard
|
|
123470
|
-
* `
|
|
123971
|
+
* `clusterSettingsSchema()` and rendered automatically inside the
|
|
123471
123972
|
* Integrations → Brokers tab's docked Settings panel, like every other
|
|
123472
123973
|
* integration-type addon.
|
|
123473
123974
|
*
|
|
123474
123975
|
* Lifecycle:
|
|
123475
123976
|
* - External broker entries are persisted in `this.activeExternalBrokers`.
|
|
123476
|
-
* - The embedded broker is
|
|
123477
|
-
*
|
|
123478
|
-
*
|
|
123479
|
-
* reassembles them into an `EmbeddedBrokerEntry`.
|
|
123480
|
-
* - On boot we start the embedded broker when `embeddedEnabled`.
|
|
123481
|
-
* - `addBroker` / `removeBroker` / `startEmbeddedBroker` /
|
|
123482
|
-
* `stopEmbeddedBroker` mutate the config and the runtime state.
|
|
123977
|
+
* - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
|
|
123978
|
+
* `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
|
|
123979
|
+
* - `addBroker` / `removeBroker` mutate the records and the runtime state.
|
|
123483
123980
|
* - `testConnection` does a one-shot `mqtt.connect` race against a 3s
|
|
123484
123981
|
* timeout AND refreshes the probe cache for the broker.
|
|
123485
123982
|
*/
|
|
123486
123983
|
/** bcrypt cost factor — exported for tests. */
|
|
123487
123984
|
var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
|
|
123488
123985
|
var PROBE_INTERVAL_MS = 3e4;
|
|
123489
|
-
var DEFAULT_EMBEDDED_PORT = 1883;
|
|
123490
|
-
/**
|
|
123491
|
-
* Per-kind broker creation form for kind='mqtt', rendered by the unified
|
|
123492
|
-
* Broker manager / create picker (via `broker.getSettingsSchema`). PURE
|
|
123493
|
-
* DATA — emits the flat `{ url, username, password, clientIdPrefix }`
|
|
123494
|
-
* shape `broker.add`'s `settings` payload consumes. Mirrors the external
|
|
123495
|
-
* broker fields `addBroker` accepts.
|
|
123496
|
-
*/
|
|
123497
|
-
var mqttBrokerSettingsSchema = { sections: [{
|
|
123498
|
-
id: "mqtt-broker-connection",
|
|
123499
|
-
title: "MQTT connection",
|
|
123500
|
-
titleKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.title",
|
|
123501
|
-
columns: 1,
|
|
123502
|
-
fields: [
|
|
123503
|
-
{
|
|
123504
|
-
type: "boolean",
|
|
123505
|
-
key: "onboard",
|
|
123506
|
-
label: "Run an onboard broker",
|
|
123507
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.label",
|
|
123508
|
-
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.",
|
|
123509
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.description",
|
|
123510
|
-
default: false
|
|
123511
|
-
},
|
|
123512
|
-
{
|
|
123513
|
-
type: "number",
|
|
123514
|
-
key: "port",
|
|
123515
|
-
label: "Port",
|
|
123516
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.label",
|
|
123517
|
-
placeholder: "(auto)",
|
|
123518
|
-
description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
|
|
123519
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.description",
|
|
123520
|
-
showWhen: {
|
|
123521
|
-
field: "onboard",
|
|
123522
|
-
equals: true
|
|
123523
|
-
}
|
|
123524
|
-
},
|
|
123525
|
-
{
|
|
123526
|
-
type: "boolean",
|
|
123527
|
-
key: "allowAnonymous",
|
|
123528
|
-
label: "Allow anonymous",
|
|
123529
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.label",
|
|
123530
|
-
description: "Permit clients to connect without a username/password.",
|
|
123531
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.description",
|
|
123532
|
-
default: false,
|
|
123533
|
-
showWhen: {
|
|
123534
|
-
field: "onboard",
|
|
123535
|
-
equals: true
|
|
123536
|
-
}
|
|
123537
|
-
},
|
|
123538
|
-
{
|
|
123539
|
-
type: "text",
|
|
123540
|
-
key: "url",
|
|
123541
|
-
label: "Broker URL",
|
|
123542
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.url.label",
|
|
123543
|
-
placeholder: "mqtt://localhost:1883",
|
|
123544
|
-
showWhen: {
|
|
123545
|
-
field: "onboard",
|
|
123546
|
-
equals: false
|
|
123547
|
-
}
|
|
123548
|
-
},
|
|
123549
|
-
{
|
|
123550
|
-
type: "text",
|
|
123551
|
-
key: "username",
|
|
123552
|
-
label: "Username",
|
|
123553
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.username.label",
|
|
123554
|
-
placeholder: "(optional)"
|
|
123555
|
-
},
|
|
123556
|
-
{
|
|
123557
|
-
type: "password",
|
|
123558
|
-
key: "password",
|
|
123559
|
-
label: "Password",
|
|
123560
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.password.label",
|
|
123561
|
-
showToggle: true
|
|
123562
|
-
},
|
|
123563
|
-
{
|
|
123564
|
-
type: "text",
|
|
123565
|
-
key: "clientIdPrefix",
|
|
123566
|
-
label: "Client ID prefix",
|
|
123567
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.clientIdPrefix.label",
|
|
123568
|
-
placeholder: "(optional)",
|
|
123569
|
-
showWhen: {
|
|
123570
|
-
field: "onboard",
|
|
123571
|
-
equals: false
|
|
123572
|
-
}
|
|
123573
|
-
}
|
|
123574
|
-
]
|
|
123575
|
-
}] };
|
|
123576
123986
|
var DEFAULT_CONFIG = {
|
|
123577
|
-
embeddedEnabled: false,
|
|
123578
|
-
embeddedId: "",
|
|
123579
|
-
embeddedPort: DEFAULT_EMBEDDED_PORT,
|
|
123580
|
-
embeddedAllowAnonymous: false,
|
|
123581
|
-
embeddedUsername: "",
|
|
123582
|
-
embeddedPassword: "",
|
|
123583
123987
|
embeddedClients: [],
|
|
123584
123988
|
embeddedAcl: []
|
|
123585
123989
|
};
|
|
123586
123990
|
var MqttBrokerAddon = class extends BaseAddon {
|
|
123587
|
-
|
|
123588
|
-
/** Per-entry onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
|
|
123991
|
+
/** Per-record onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
|
|
123589
123992
|
onboardRuntimes = /* @__PURE__ */ new Map();
|
|
123590
123993
|
probeCache = null;
|
|
123591
123994
|
brokerStore = null;
|
|
123592
|
-
|
|
123995
|
+
brokerInstances = null;
|
|
123593
123996
|
clientStore = null;
|
|
123594
123997
|
aclStore = null;
|
|
123595
123998
|
/** The external broker rows: the `brokers` records, secrets resolved. */
|
|
@@ -123603,6 +124006,20 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123603
124006
|
if (this.brokerStore === null) throw new Error("mqtt-broker: the broker records are not open");
|
|
123604
124007
|
return this.brokerStore;
|
|
123605
124008
|
}
|
|
124009
|
+
/** What the addon knows of a broker right now: its onboard server or its last probe. */
|
|
124010
|
+
liveState(id) {
|
|
124011
|
+
const running = this.onboardRuntimes.get(id);
|
|
124012
|
+
const probe = this.probeCache?.get(id);
|
|
124013
|
+
return {
|
|
124014
|
+
onboard: running === void 0 ? null : { connectedClients: running.aedes.connectedClients },
|
|
124015
|
+
...probe !== void 0 ? { probe: {
|
|
124016
|
+
status: probe.status,
|
|
124017
|
+
latencyMs: probe.latencyMs,
|
|
124018
|
+
...probe.error !== void 0 ? { error: probe.error } : {},
|
|
124019
|
+
lastCheckedAt: probe.lastCheckedAt
|
|
124020
|
+
} } : {}
|
|
124021
|
+
};
|
|
124022
|
+
}
|
|
123606
124023
|
async persistExternalBrokers(entries) {
|
|
123607
124024
|
this.activeExternalBrokers = [...entries];
|
|
123608
124025
|
await this.requireBrokerStore().replaceAll(entries);
|
|
@@ -123652,19 +124069,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123652
124069
|
this.brokerStore = MqttBrokerStore.of(records);
|
|
123653
124070
|
this.clientStore = MqttClientStore.of(records);
|
|
123654
124071
|
this.aclStore = MqttAclStore.of(records);
|
|
123655
|
-
this.
|
|
123656
|
-
|
|
123657
|
-
|
|
123658
|
-
|
|
123659
|
-
|
|
123660
|
-
|
|
123661
|
-
|
|
123662
|
-
formSchema: () => this.brokerForm(),
|
|
123663
|
-
initialBody: (name) => ({
|
|
123664
|
-
name,
|
|
123665
|
-
url: "mqtt://"
|
|
123666
|
-
}),
|
|
123667
|
-
configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
|
|
124072
|
+
this.brokerInstances = buildMqttBrokerInstances(records, {
|
|
124073
|
+
formSchema: (id, onboard) => this.brokerForm(id, onboard),
|
|
124074
|
+
listMqttHaInstances: () => listMqttHaInstancesOver(this.ctx.api),
|
|
124075
|
+
freePort: () => findFreePort(),
|
|
124076
|
+
probe: (cfg) => this.probeMqtt(cfg),
|
|
124077
|
+
onboardListening: (id) => this.onboardRuntimes.has(id),
|
|
124078
|
+
liveState: (id) => this.liveState(id),
|
|
123668
124079
|
onChanged: async () => {
|
|
123669
124080
|
await this.refreshExternalBrokers();
|
|
123670
124081
|
}
|
|
@@ -123693,29 +124104,9 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123693
124104
|
});
|
|
123694
124105
|
this.probeCache.setBrokersSource(() => this.collectExternalProbeInputs());
|
|
123695
124106
|
this.probeCache.start();
|
|
123696
|
-
const persistedEmbedded = this.deriveEmbeddedEntry();
|
|
123697
|
-
if (persistedEmbedded) try {
|
|
123698
|
-
await this.spawnEmbedded(persistedEmbedded);
|
|
123699
|
-
this.ctx.logger.info("embedded mqtt broker restarted", {
|
|
123700
|
-
meta: { port: persistedEmbedded.port },
|
|
123701
|
-
tags: {
|
|
123702
|
-
topic: "mqtt-broker",
|
|
123703
|
-
phase: "embedded-restarted"
|
|
123704
|
-
}
|
|
123705
|
-
});
|
|
123706
|
-
} catch (err) {
|
|
123707
|
-
this.ctx.logger.warn("embedded mqtt broker failed to restart", {
|
|
123708
|
-
meta: { error: errMsg(err) },
|
|
123709
|
-
tags: {
|
|
123710
|
-
topic: "mqtt-broker",
|
|
123711
|
-
phase: "embedded-restart-failed"
|
|
123712
|
-
}
|
|
123713
|
-
});
|
|
123714
|
-
}
|
|
123715
124107
|
await this.reconcileOnboardRuntimes();
|
|
123716
124108
|
this.ctx.addDisposer(async () => {
|
|
123717
124109
|
await this.probeCache?.stop();
|
|
123718
|
-
await this.shutdownEmbedded().catch(() => void 0);
|
|
123719
124110
|
await this.shutdownAllOnboard();
|
|
123720
124111
|
});
|
|
123721
124112
|
const provider = {
|
|
@@ -123727,11 +124118,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123727
124118
|
startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
|
|
123728
124119
|
stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
|
|
123729
124120
|
getStatus: async () => ({
|
|
123730
|
-
brokerCount: this.activeExternalBrokers.length
|
|
123731
|
-
embeddedRunning: this.
|
|
124121
|
+
brokerCount: this.activeExternalBrokers.length,
|
|
124122
|
+
embeddedRunning: this.onboardRuntimes.size > 0
|
|
123732
124123
|
})
|
|
123733
124124
|
};
|
|
123734
124125
|
const unifiedProvider = this.buildUnifiedBrokerProvider();
|
|
124126
|
+
const brokerInstances = this.brokerInstances;
|
|
124127
|
+
if (brokerInstances === null) throw new Error("mqtt-broker: the broker instances are not built");
|
|
123735
124128
|
return [
|
|
123736
124129
|
{
|
|
123737
124130
|
capability: mqttBrokerCapability,
|
|
@@ -123743,111 +124136,21 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123743
124136
|
},
|
|
123744
124137
|
{
|
|
123745
124138
|
capability: integrationInstancesCapability,
|
|
123746
|
-
provider:
|
|
124139
|
+
provider: brokerInstances
|
|
123747
124140
|
}
|
|
123748
124141
|
];
|
|
123749
124142
|
}
|
|
124143
|
+
/**
|
|
124144
|
+
* The bus-only `broker` cap (D1013). The brokers themselves are the `integration-instances`
|
|
124145
|
+
* provider; this one only answers the config read and the status.
|
|
124146
|
+
*/
|
|
123750
124147
|
buildUnifiedBrokerProvider() {
|
|
123751
|
-
const
|
|
123752
|
-
const ownerAddonId = this.ctx.id;
|
|
123753
|
-
const adapt = (info) => ({
|
|
123754
|
-
id: info.id,
|
|
123755
|
-
addonId: ownerAddonId,
|
|
123756
|
-
name: info.name,
|
|
123757
|
-
kind: KIND,
|
|
123758
|
-
status: mapMqttStatus(info.status),
|
|
123759
|
-
info: {
|
|
123760
|
-
...info.latencyMs !== null ? { latencyMs: info.latencyMs } : {},
|
|
123761
|
-
...info.kind === "embedded" ? {
|
|
123762
|
-
embedded: true,
|
|
123763
|
-
connectedClients: info.connectedClients ?? 0
|
|
123764
|
-
} : {}
|
|
123765
|
-
},
|
|
123766
|
-
lastCheckedAt: info.lastCheckedAt ?? null,
|
|
123767
|
-
error: info.error ?? null
|
|
123768
|
-
});
|
|
123769
|
-
const owns = (id) => {
|
|
123770
|
-
if (this.activeExternalBrokers.some((b) => b.id === id)) return true;
|
|
123771
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123772
|
-
return embedded !== null && embedded.id === id;
|
|
123773
|
-
};
|
|
124148
|
+
const owns = (id) => this.activeExternalBrokers.some((b) => b.id === id);
|
|
123774
124149
|
return {
|
|
123775
|
-
list: async ({ kind }) => {
|
|
123776
|
-
if (kind && kind !== KIND) return [];
|
|
123777
|
-
return (await this.listBrokers()).map(adapt);
|
|
123778
|
-
},
|
|
123779
|
-
get: async ({ id }) => {
|
|
123780
|
-
const match = (await this.listBrokers()).find((b) => b.id === id);
|
|
123781
|
-
return match ? adapt(match) : null;
|
|
123782
|
-
},
|
|
123783
|
-
listProviders: async () => [{
|
|
123784
|
-
addonId: ownerAddonId,
|
|
123785
|
-
kinds: [{
|
|
123786
|
-
kind: KIND,
|
|
123787
|
-
label: "MQTT"
|
|
123788
|
-
}]
|
|
123789
|
-
}],
|
|
123790
|
-
add: async ({ kind, name, settings, id }) => {
|
|
123791
|
-
if (kind !== KIND) throw new Error(`mqtt-broker: only kind '${KIND}' is handled here (got '${kind}')`);
|
|
123792
|
-
if (id !== void 0) throw new Error("mqtt-broker: adopting an existing broker id is not supported by this provider");
|
|
123793
|
-
if (isOnboardRequested(settings)) return this.addOnboardBroker(name, settings);
|
|
123794
|
-
const url = typeof settings["url"] === "string" ? settings["url"] : "";
|
|
123795
|
-
if (!url) throw new Error("mqtt-broker.add: settings.url is required");
|
|
123796
|
-
const input = {
|
|
123797
|
-
name,
|
|
123798
|
-
url,
|
|
123799
|
-
...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
|
|
123800
|
-
...typeof settings["password"] === "string" ? { password: settings["password"] } : {},
|
|
123801
|
-
...typeof settings["clientIdPrefix"] === "string" ? { clientIdPrefix: settings["clientIdPrefix"] } : {}
|
|
123802
|
-
};
|
|
123803
|
-
return this.addBroker(input);
|
|
123804
|
-
},
|
|
123805
|
-
remove: async ({ id }) => {
|
|
123806
|
-
if (!owns(id)) return;
|
|
123807
|
-
await this.removeBroker(id);
|
|
123808
|
-
},
|
|
123809
|
-
testConnection: async ({ id }) => {
|
|
123810
|
-
if (!owns(id)) return {
|
|
123811
|
-
ok: false,
|
|
123812
|
-
error: "unknown broker"
|
|
123813
|
-
};
|
|
123814
|
-
return this.testConnection(id);
|
|
123815
|
-
},
|
|
123816
|
-
getSettings: async ({ id }) => {
|
|
123817
|
-
if (!owns(id)) return null;
|
|
123818
|
-
return this.getBrokerConfig(id);
|
|
123819
|
-
},
|
|
123820
|
-
setSettings: async () => {
|
|
123821
|
-
throw new Error("mqtt-broker.setSettings: not supported — use remove + add to replace a broker");
|
|
123822
|
-
},
|
|
123823
124150
|
getBrokerConfig: async ({ id }) => {
|
|
123824
124151
|
if (!owns(id)) return null;
|
|
123825
124152
|
return this.getBrokerConfig(id);
|
|
123826
124153
|
},
|
|
123827
|
-
getSettingsSchema: async ({ kind }) => {
|
|
123828
|
-
if (kind !== KIND) return null;
|
|
123829
|
-
return mqttBrokerSettingsSchema;
|
|
123830
|
-
},
|
|
123831
|
-
testSettings: async ({ kind, settings }) => {
|
|
123832
|
-
if (kind !== KIND) return {
|
|
123833
|
-
ok: false,
|
|
123834
|
-
error: `unsupported kind: ${kind}`
|
|
123835
|
-
};
|
|
123836
|
-
if (isOnboardRequested(settings)) return {
|
|
123837
|
-
ok: true,
|
|
123838
|
-
latencyMs: 0
|
|
123839
|
-
};
|
|
123840
|
-
const url = typeof settings["url"] === "string" ? settings["url"] : "";
|
|
123841
|
-
if (!url) return {
|
|
123842
|
-
ok: false,
|
|
123843
|
-
error: "settings.url is required"
|
|
123844
|
-
};
|
|
123845
|
-
return this.probeMqtt({
|
|
123846
|
-
url,
|
|
123847
|
-
...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
|
|
123848
|
-
...typeof settings["password"] === "string" ? { password: settings["password"] } : {}
|
|
123849
|
-
});
|
|
123850
|
-
},
|
|
123851
124154
|
publish: async () => {
|
|
123852
124155
|
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)");
|
|
123853
124156
|
},
|
|
@@ -123861,7 +124164,7 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123861
124164
|
getStatus: async () => {
|
|
123862
124165
|
const all = await this.listBrokers();
|
|
123863
124166
|
return {
|
|
123864
|
-
brokerCount: this.activeExternalBrokers.length
|
|
124167
|
+
brokerCount: this.activeExternalBrokers.length,
|
|
123865
124168
|
connectedCount: all.filter((b) => b.status === "connected").length
|
|
123866
124169
|
};
|
|
123867
124170
|
}
|
|
@@ -123869,35 +124172,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123869
124172
|
}
|
|
123870
124173
|
async onShutdown() {
|
|
123871
124174
|
await this.probeCache?.stop();
|
|
123872
|
-
await this.shutdownEmbedded().catch(() => void 0);
|
|
123873
124175
|
await this.shutdownAllOnboard();
|
|
123874
124176
|
}
|
|
123875
124177
|
async onConfigChanged() {
|
|
123876
124178
|
this.activeExternalBrokers = await this.requireBrokerStore().listResolved();
|
|
123877
124179
|
await this.syncEmbeddedEntriesFromConfig();
|
|
123878
124180
|
await this.reconcileOnboardRuntimes();
|
|
123879
|
-
|
|
123880
|
-
const running = this.embeddedRuntime?.entry ?? null;
|
|
123881
|
-
if (!desired && running) {
|
|
123882
|
-
await this.shutdownEmbedded();
|
|
123883
|
-
return;
|
|
123884
|
-
}
|
|
123885
|
-
if (desired && !running) {
|
|
123886
|
-
await this.spawnEmbedded(desired).catch((err) => {
|
|
123887
|
-
this.ctx.logger.warn("embedded mqtt broker spawn (on config change) failed", { meta: { error: errMsg(err) } });
|
|
123888
|
-
});
|
|
123889
|
-
return;
|
|
123890
|
-
}
|
|
123891
|
-
if (desired && running) {
|
|
123892
|
-
if (requiresEmbeddedRespawn(desired, running)) {
|
|
123893
|
-
await this.shutdownEmbedded();
|
|
123894
|
-
await this.spawnEmbedded(desired).catch((err) => {
|
|
123895
|
-
this.ctx.logger.warn("embedded mqtt broker respawn failed", { meta: { error: errMsg(err) } });
|
|
123896
|
-
});
|
|
123897
|
-
return;
|
|
123898
|
-
}
|
|
123899
|
-
this.reconcileEmbeddedClients();
|
|
123900
|
-
}
|
|
124181
|
+
this.reconcileEmbeddedClients();
|
|
123901
124182
|
}
|
|
123902
124183
|
async listBrokers() {
|
|
123903
124184
|
const out = [];
|
|
@@ -123931,22 +124212,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123931
124212
|
};
|
|
123932
124213
|
out.push(info);
|
|
123933
124214
|
}
|
|
123934
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123935
|
-
if (embedded) {
|
|
123936
|
-
const running = this.embeddedRuntime;
|
|
123937
|
-
const status = running !== null ? "connected" : "disconnected";
|
|
123938
|
-
const info = {
|
|
123939
|
-
id: embedded.id,
|
|
123940
|
-
name: embedded.name,
|
|
123941
|
-
url: `mqtt://127.0.0.1:${embedded.port}`,
|
|
123942
|
-
kind: "embedded",
|
|
123943
|
-
status,
|
|
123944
|
-
latencyMs: null,
|
|
123945
|
-
...running ? { connectedClients: running.aedes.connectedClients } : {},
|
|
123946
|
-
lastCheckedAt: Date.now()
|
|
123947
|
-
};
|
|
123948
|
-
out.push(info);
|
|
123949
|
-
}
|
|
123950
124215
|
return out;
|
|
123951
124216
|
}
|
|
123952
124217
|
getBrokerConfig(id) {
|
|
@@ -123961,15 +124226,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123961
124226
|
...external.clientIdPrefix !== void 0 ? { clientIdPrefix: external.clientIdPrefix } : {}
|
|
123962
124227
|
};
|
|
123963
124228
|
}
|
|
123964
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123965
|
-
if (embedded && embedded.id === id) {
|
|
123966
|
-
const password = this.storedPassword(embedded.password);
|
|
123967
|
-
return {
|
|
123968
|
-
url: `mqtt://127.0.0.1:${embedded.port}`,
|
|
123969
|
-
...embedded.username !== void 0 ? { username: embedded.username } : {},
|
|
123970
|
-
...password !== void 0 ? { password } : {}
|
|
123971
|
-
};
|
|
123972
|
-
}
|
|
123973
124229
|
throw new Error(`mqtt-broker: broker '${id}' not found`);
|
|
123974
124230
|
}
|
|
123975
124231
|
async addBroker(input) {
|
|
@@ -123990,44 +124246,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123990
124246
|
} });
|
|
123991
124247
|
return { id };
|
|
123992
124248
|
}
|
|
123993
|
-
/**
|
|
123994
|
-
* Create a per-entry ONBOARD broker from `broker.add` settings: allocate
|
|
123995
|
-
* (or honour) a port, persist an `ExternalBrokerEntry` carrying an
|
|
123996
|
-
* `onboard` descriptor, then spawn its dedicated `aedes` server.
|
|
123997
|
-
*/
|
|
123998
|
-
async addOnboardBroker(name, settings) {
|
|
123999
|
-
const id = (0, node_crypto.randomUUID)();
|
|
124000
|
-
const port = readOnboardPort(settings) ?? await findFreePort();
|
|
124001
|
-
const built = buildOnboardEntry({
|
|
124002
|
-
id,
|
|
124003
|
-
name,
|
|
124004
|
-
port,
|
|
124005
|
-
settings
|
|
124006
|
-
});
|
|
124007
|
-
const entry = {
|
|
124008
|
-
id: built.id,
|
|
124009
|
-
name: built.name,
|
|
124010
|
-
url: built.url,
|
|
124011
|
-
...built.username !== void 0 ? { username: built.username } : {},
|
|
124012
|
-
...built.password !== void 0 ? { password: built.password } : {},
|
|
124013
|
-
onboard: built.onboard
|
|
124014
|
-
};
|
|
124015
|
-
const next = [...this.activeExternalBrokers, entry];
|
|
124016
|
-
await this.persistExternalBrokers(next);
|
|
124017
|
-
const spawn = this.onboardSpawnEntry(entry);
|
|
124018
|
-
if (spawn) await this.spawnOnboard(spawn);
|
|
124019
|
-
this.ctx.logger.info("onboard mqtt broker added", {
|
|
124020
|
-
meta: {
|
|
124021
|
-
id,
|
|
124022
|
-
port
|
|
124023
|
-
},
|
|
124024
|
-
tags: {
|
|
124025
|
-
topic: "mqtt-broker",
|
|
124026
|
-
phase: "onboard-started"
|
|
124027
|
-
}
|
|
124028
|
-
});
|
|
124029
|
-
return { id };
|
|
124030
|
-
}
|
|
124031
124249
|
async removeBroker(id) {
|
|
124032
124250
|
const removed = this.activeExternalBrokers.find((b) => b.id === id);
|
|
124033
124251
|
if (removed) {
|
|
@@ -124038,13 +124256,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124038
124256
|
this.ctx.logger.info("mqtt broker removed", { meta: { id } });
|
|
124039
124257
|
return;
|
|
124040
124258
|
}
|
|
124041
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
124042
|
-
if (embedded && embedded.id === id) {
|
|
124043
|
-
await this.shutdownEmbedded();
|
|
124044
|
-
await this.updateGlobalSettings({ embeddedEnabled: false });
|
|
124045
|
-
this.ctx.logger.info("embedded mqtt broker removed", { meta: { id } });
|
|
124046
|
-
return;
|
|
124047
|
-
}
|
|
124048
124259
|
}
|
|
124049
124260
|
async testConnection(id) {
|
|
124050
124261
|
let cfg;
|
|
@@ -124063,7 +124274,7 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124063
124274
|
* One-shot connectivity probe against an MQTT endpoint built from a
|
|
124064
124275
|
* flat config (`{url, username?, password?}`). Shared by the
|
|
124065
124276
|
* id-keyed `testConnection` (existing broker) and the pre-creation
|
|
124066
|
-
* `
|
|
124277
|
+
* the instance test (`testInstance`) of a broker not saved yet (the typed values).
|
|
124067
124278
|
* Never throws — every failure resolves to `{ ok: false, error }`.
|
|
124068
124279
|
*/
|
|
124069
124280
|
async probeMqtt(cfg) {
|
|
@@ -124108,30 +124319,46 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124108
124319
|
});
|
|
124109
124320
|
});
|
|
124110
124321
|
}
|
|
124322
|
+
/** The `brokers` records, for the two legacy cap methods that create or switch off one. */
|
|
124323
|
+
brokerRecordHandle() {
|
|
124324
|
+
const handle = this.ctx.records?.kind(mqttBrokersEntryKind);
|
|
124325
|
+
if (handle === void 0) throw new Error("mqtt-broker: the broker records are not open");
|
|
124326
|
+
return handle;
|
|
124327
|
+
}
|
|
124328
|
+
/** Create an ONBOARD broker record; the reconcile then spawns its `aedes` server. */
|
|
124111
124329
|
async startEmbeddedBroker(input) {
|
|
124112
|
-
|
|
124113
|
-
const
|
|
124114
|
-
const entry = {
|
|
124330
|
+
const id = (0, node_crypto.randomUUID)();
|
|
124331
|
+
const built = buildOnboardEntry({
|
|
124115
124332
|
id,
|
|
124116
|
-
name:
|
|
124333
|
+
name: LEGACY_EMBEDDED_NAME,
|
|
124117
124334
|
port: input.port,
|
|
124118
|
-
|
|
124119
|
-
|
|
124120
|
-
|
|
124121
|
-
|
|
124122
|
-
|
|
124123
|
-
await this.updateGlobalSettings({
|
|
124124
|
-
embeddedEnabled: true,
|
|
124125
|
-
embeddedId: id,
|
|
124126
|
-
embeddedPort: input.port,
|
|
124127
|
-
embeddedAllowAnonymous: input.allowAnonymous,
|
|
124128
|
-
embeddedUsername: input.username ?? "",
|
|
124129
|
-
embeddedPassword: input.password ?? ""
|
|
124335
|
+
settings: {
|
|
124336
|
+
allowAnonymous: input.allowAnonymous,
|
|
124337
|
+
...input.username !== void 0 ? { username: input.username } : {},
|
|
124338
|
+
...input.password !== void 0 ? { password: input.password } : {}
|
|
124339
|
+
}
|
|
124130
124340
|
});
|
|
124341
|
+
const handle = this.brokerRecordHandle();
|
|
124342
|
+
await handle.put({
|
|
124343
|
+
id,
|
|
124344
|
+
data: mqttBrokersEntryKind.schema.parse({
|
|
124345
|
+
name: built.name,
|
|
124346
|
+
url: built.url,
|
|
124347
|
+
...built.username !== void 0 ? { username: built.username } : {},
|
|
124348
|
+
...built.password !== void 0 ? { password: built.password } : {},
|
|
124349
|
+
onboard: built.onboard
|
|
124350
|
+
})
|
|
124351
|
+
}, { ifRev: 0 });
|
|
124352
|
+
await this.refreshExternalBrokers();
|
|
124353
|
+
if (!this.onboardRuntimes.has(id)) {
|
|
124354
|
+
await handle.delete(id);
|
|
124355
|
+
await this.refreshExternalBrokers();
|
|
124356
|
+
throw new Error(`embedded broker could not listen on port ${input.port}`);
|
|
124357
|
+
}
|
|
124131
124358
|
this.ctx.logger.info("embedded mqtt broker started", {
|
|
124132
124359
|
meta: {
|
|
124133
124360
|
id,
|
|
124134
|
-
port:
|
|
124361
|
+
port: input.port
|
|
124135
124362
|
},
|
|
124136
124363
|
tags: {
|
|
124137
124364
|
topic: "mqtt-broker",
|
|
@@ -124153,20 +124380,22 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124153
124380
|
});
|
|
124154
124381
|
return {
|
|
124155
124382
|
id,
|
|
124156
|
-
url:
|
|
124383
|
+
url: built.url
|
|
124157
124384
|
};
|
|
124158
124385
|
}
|
|
124386
|
+
/** Switch an onboard broker off (its record stays, so it can be switched on again). */
|
|
124159
124387
|
async stopEmbeddedBroker(id) {
|
|
124160
|
-
|
|
124161
|
-
await
|
|
124162
|
-
|
|
124388
|
+
const handle = this.brokerRecordHandle();
|
|
124389
|
+
const row = await handle.get(id);
|
|
124390
|
+
if (row === null || row.data.onboard === void 0) return;
|
|
124391
|
+
await handle.patch(id, { enabled: false });
|
|
124392
|
+
await this.refreshExternalBrokers();
|
|
124163
124393
|
this.ctx.logger.info("embedded mqtt broker stopped", { meta: { id } });
|
|
124164
124394
|
}
|
|
124165
124395
|
/**
|
|
124166
124396
|
* Build + listen an `aedes` server for `entry` and RETURN the runtime
|
|
124167
|
-
* without assigning it to any field
|
|
124168
|
-
*
|
|
124169
|
-
* brokers ({@link spawnOnboard}).
|
|
124397
|
+
* without assigning it to any field (the onboard brokers keep theirs in
|
|
124398
|
+
* {@link spawnOnboard}).
|
|
124170
124399
|
*/
|
|
124171
124400
|
async spawnAedes(entry) {
|
|
124172
124401
|
const aedes = new import_aedes.default();
|
|
@@ -124205,15 +124434,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124205
124434
|
aedes.close(() => resolve());
|
|
124206
124435
|
});
|
|
124207
124436
|
}
|
|
124208
|
-
async spawnEmbedded(entry) {
|
|
124209
|
-
this.embeddedRuntime = await this.spawnAedes(entry);
|
|
124210
|
-
}
|
|
124211
|
-
async shutdownEmbedded() {
|
|
124212
|
-
if (!this.embeddedRuntime) return;
|
|
124213
|
-
const runtime = this.embeddedRuntime;
|
|
124214
|
-
this.embeddedRuntime = null;
|
|
124215
|
-
await this.closeRuntime(runtime);
|
|
124216
|
-
}
|
|
124217
124437
|
/** Spawn a dedicated onboard `aedes` server for an onboard entry. */
|
|
124218
124438
|
async spawnOnboard(entry) {
|
|
124219
124439
|
const runtime = await this.spawnAedes(entry);
|
|
@@ -124350,8 +124570,9 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124350
124570
|
* (or whose username was removed entirely) get force-closed.
|
|
124351
124571
|
*/
|
|
124352
124572
|
reconcileEmbeddedClients() {
|
|
124353
|
-
const runtime
|
|
124354
|
-
|
|
124573
|
+
for (const runtime of this.onboardRuntimes.values()) this.reconcileRuntimeClients(runtime);
|
|
124574
|
+
}
|
|
124575
|
+
reconcileRuntimeClients(runtime) {
|
|
124355
124576
|
const prevSnapshot = runtime.credentialsSnapshot;
|
|
124356
124577
|
const nextSnapshot = this.buildCredentialsSnapshot();
|
|
124357
124578
|
const affectedUsers = [];
|
|
@@ -124390,26 +124611,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124390
124611
|
for (const c of this.activeEmbeddedClients) m.set(c.username, c.passwordHash);
|
|
124391
124612
|
return m;
|
|
124392
124613
|
}
|
|
124393
|
-
/**
|
|
124394
|
-
* Reassemble the internal {@link EmbeddedBrokerEntry} from the flat
|
|
124395
|
-
* `embedded*` config keys the settings form writes. Returns `null`
|
|
124396
|
-
* when the embedded broker is disabled — that's the signal
|
|
124397
|
-
* `onConfigChanged` / `onInitialize` use to decide whether to spawn
|
|
124398
|
-
* or shut down the `aedes` instance.
|
|
124399
|
-
*/
|
|
124400
|
-
deriveEmbeddedEntry() {
|
|
124401
|
-
if (!this.config.embeddedEnabled) return null;
|
|
124402
|
-
const username = this.config.embeddedUsername;
|
|
124403
|
-
const password = this.config.embeddedPassword;
|
|
124404
|
-
return {
|
|
124405
|
-
id: this.config.embeddedId || "embedded",
|
|
124406
|
-
name: "Embedded broker",
|
|
124407
|
-
port: this.config.embeddedPort,
|
|
124408
|
-
allowAnonymous: this.config.embeddedAllowAnonymous,
|
|
124409
|
-
...username.length > 0 ? { username } : {},
|
|
124410
|
-
...password.length > 0 ? { password } : {}
|
|
124411
|
-
};
|
|
124412
|
-
}
|
|
124413
124614
|
collectExternalProbeInputs() {
|
|
124414
124615
|
const out = [];
|
|
124415
124616
|
for (const b of this.activeExternalBrokers) {
|
|
@@ -124446,19 +124647,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124446
124647
|
* here (no migration code ships): the operator's one-off `mqtt` step does.
|
|
124447
124648
|
*/
|
|
124448
124649
|
reportRetiredCiphertext() {
|
|
124449
|
-
const paths = retiredCiphertextPaths({
|
|
124450
|
-
brokers: this.activeExternalBrokers,
|
|
124451
|
-
embeddedPassword: this.config.embeddedPassword
|
|
124452
|
-
});
|
|
124650
|
+
const paths = retiredCiphertextPaths({ brokers: this.activeExternalBrokers });
|
|
124453
124651
|
if (paths.length === 0) return;
|
|
124454
124652
|
const unreadable = [];
|
|
124455
124653
|
const values = [...this.activeExternalBrokers.map((b) => ({
|
|
124456
124654
|
id: `brokers[${b.id}].password`,
|
|
124457
124655
|
v: b.password
|
|
124458
|
-
}))
|
|
124459
|
-
id: "embeddedPassword",
|
|
124460
|
-
v: this.config.embeddedPassword
|
|
124461
|
-
}];
|
|
124656
|
+
}))];
|
|
124462
124657
|
for (const { id, v } of values) try {
|
|
124463
124658
|
this.storedPassword(v);
|
|
124464
124659
|
} catch (err) {
|
|
@@ -124482,13 +124677,79 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124482
124677
|
}
|
|
124483
124678
|
});
|
|
124484
124679
|
}
|
|
124485
|
-
|
|
124680
|
+
/**
|
|
124681
|
+
* The broker form. The create form (`id` null) offers "run an onboard broker" (CamStack hosts an
|
|
124682
|
+
* `aedes` server on this hub: a port, anonymous access) or an external URL; an existing onboard
|
|
124683
|
+
* broker edits only its credentials, since its address is the one it was given.
|
|
124684
|
+
*/
|
|
124685
|
+
brokerForm(id, onboard) {
|
|
124686
|
+
const credentials = [{
|
|
124687
|
+
type: "text",
|
|
124688
|
+
key: "username",
|
|
124689
|
+
label: "Username",
|
|
124690
|
+
labelKey: "mqtt-broker:fullSettings.brokers.username.label"
|
|
124691
|
+
}, {
|
|
124692
|
+
type: "password",
|
|
124693
|
+
key: "password",
|
|
124694
|
+
label: "Password",
|
|
124695
|
+
labelKey: "mqtt-broker:fullSettings.brokers.password.label",
|
|
124696
|
+
showToggle: true
|
|
124697
|
+
}];
|
|
124698
|
+
if (onboard) return this.schema({ sections: [{
|
|
124699
|
+
id: "broker",
|
|
124700
|
+
title: "Onboard broker",
|
|
124701
|
+
description: "A CamStack-hosted broker on this hub. Its port was chosen when it was created.",
|
|
124702
|
+
columns: 1,
|
|
124703
|
+
fields: credentials
|
|
124704
|
+
}] });
|
|
124705
|
+
const creating = id === null;
|
|
124706
|
+
const external = {
|
|
124707
|
+
field: "onboard",
|
|
124708
|
+
equals: false
|
|
124709
|
+
};
|
|
124486
124710
|
return this.schema({ sections: [{
|
|
124487
124711
|
id: "broker",
|
|
124488
|
-
title: "External broker",
|
|
124489
|
-
description: "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
|
|
124712
|
+
title: creating ? "MQTT broker" : "External broker",
|
|
124713
|
+
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.",
|
|
124490
124714
|
columns: 1,
|
|
124491
124715
|
fields: [
|
|
124716
|
+
...creating ? [
|
|
124717
|
+
{
|
|
124718
|
+
type: "boolean",
|
|
124719
|
+
key: "onboard",
|
|
124720
|
+
label: "Run an onboard broker",
|
|
124721
|
+
labelKey: "mqtt-broker:fullSettings.brokers.onboard.label",
|
|
124722
|
+
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.",
|
|
124723
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.onboard.description",
|
|
124724
|
+
default: false
|
|
124725
|
+
},
|
|
124726
|
+
{
|
|
124727
|
+
type: "number",
|
|
124728
|
+
key: "port",
|
|
124729
|
+
label: "Port",
|
|
124730
|
+
labelKey: "mqtt-broker:fullSettings.brokers.port.label",
|
|
124731
|
+
placeholder: "(auto)",
|
|
124732
|
+
description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
|
|
124733
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.port.description",
|
|
124734
|
+
showWhen: {
|
|
124735
|
+
field: "onboard",
|
|
124736
|
+
equals: true
|
|
124737
|
+
}
|
|
124738
|
+
},
|
|
124739
|
+
{
|
|
124740
|
+
type: "boolean",
|
|
124741
|
+
key: "allowAnonymous",
|
|
124742
|
+
label: "Allow anonymous",
|
|
124743
|
+
labelKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.label",
|
|
124744
|
+
description: "Permit clients to connect without a username/password.",
|
|
124745
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.description",
|
|
124746
|
+
default: false,
|
|
124747
|
+
showWhen: {
|
|
124748
|
+
field: "onboard",
|
|
124749
|
+
equals: true
|
|
124750
|
+
}
|
|
124751
|
+
}
|
|
124752
|
+
] : [],
|
|
124492
124753
|
{
|
|
124493
124754
|
type: "text",
|
|
124494
124755
|
key: "url",
|
|
@@ -124496,26 +124757,15 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124496
124757
|
labelKey: "mqtt-broker:fullSettings.brokers.url.label",
|
|
124497
124758
|
description: "mqtt://, mqtts://, ws://, wss://",
|
|
124498
124759
|
descriptionKey: "mqtt-broker:fullSettings.brokers.url.description",
|
|
124499
|
-
required: true
|
|
124500
|
-
},
|
|
124501
|
-
{
|
|
124502
|
-
type: "text",
|
|
124503
|
-
key: "username",
|
|
124504
|
-
label: "Username",
|
|
124505
|
-
labelKey: "mqtt-broker:fullSettings.brokers.username.label"
|
|
124506
|
-
},
|
|
124507
|
-
{
|
|
124508
|
-
type: "password",
|
|
124509
|
-
key: "password",
|
|
124510
|
-
label: "Password",
|
|
124511
|
-
labelKey: "mqtt-broker:fullSettings.brokers.password.label",
|
|
124512
|
-
showToggle: true
|
|
124760
|
+
...creating ? { showWhen: external } : { required: true }
|
|
124513
124761
|
},
|
|
124762
|
+
...credentials,
|
|
124514
124763
|
{
|
|
124515
124764
|
type: "text",
|
|
124516
124765
|
key: "clientIdPrefix",
|
|
124517
124766
|
label: "Client id prefix",
|
|
124518
|
-
labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label"
|
|
124767
|
+
labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label",
|
|
124768
|
+
...creating ? { showWhen: external } : {}
|
|
124519
124769
|
}
|
|
124520
124770
|
]
|
|
124521
124771
|
}] });
|
|
@@ -124526,6 +124776,41 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124526
124776
|
if (handle === void 0) throw new Error("records are not available");
|
|
124527
124777
|
return {
|
|
124528
124778
|
readRawBrokers: async () => (await this.ctx.settings?.readAddonStore())?.["brokers"],
|
|
124779
|
+
readLegacyEmbedded: async () => {
|
|
124780
|
+
const raw = await this.ctx.settings?.readAddonStore() ?? {};
|
|
124781
|
+
const secrets = this.ctx.secrets;
|
|
124782
|
+
const { value } = secrets === void 0 ? { value: raw } : await resolveSecretMarkers(raw, secrets, {
|
|
124783
|
+
unresolved: "keep-marker",
|
|
124784
|
+
ownedBy: secrets.owner
|
|
124785
|
+
});
|
|
124786
|
+
const text = (key) => {
|
|
124787
|
+
const v = value[key];
|
|
124788
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
124789
|
+
};
|
|
124790
|
+
const port = value["embeddedPort"];
|
|
124791
|
+
const username = text("embeddedUsername");
|
|
124792
|
+
const password = this.storedPassword(text("embeddedPassword"));
|
|
124793
|
+
const enabled = value["embeddedEnabled"] === true;
|
|
124794
|
+
if (!(enabled || text("embeddedId") !== void 0 || username !== void 0 || password !== void 0)) return null;
|
|
124795
|
+
return {
|
|
124796
|
+
enabled,
|
|
124797
|
+
id: text("embeddedId") ?? "embedded",
|
|
124798
|
+
port: typeof port === "number" && Number.isInteger(port) ? port : 1883,
|
|
124799
|
+
allowAnonymous: value["embeddedAllowAnonymous"] === true,
|
|
124800
|
+
...username !== void 0 ? { username } : {},
|
|
124801
|
+
...password !== void 0 ? { password } : {}
|
|
124802
|
+
};
|
|
124803
|
+
},
|
|
124804
|
+
readRawEmbeddedKeys: async () => {
|
|
124805
|
+
const raw = await this.ctx.settings?.readAddonStore() ?? {};
|
|
124806
|
+
return Object.fromEntries(LEGACY_EMBEDDED_KEYS.filter((key) => key in raw).map((key) => [key, raw[key]]));
|
|
124807
|
+
},
|
|
124808
|
+
putOnboardRecord: async (id, body) => {
|
|
124809
|
+
await handle.put({
|
|
124810
|
+
id,
|
|
124811
|
+
data: mqttBrokersEntryKind.schema.parse(body)
|
|
124812
|
+
}, { ifRev: 0 });
|
|
124813
|
+
},
|
|
124529
124814
|
readRecords: async () => {
|
|
124530
124815
|
const page = await handle.query({ limit: 1e3 });
|
|
124531
124816
|
return new Map(page.records.map((r) => [r.id, { hasPassword: (r.data.password ?? "").length > 0 }]));
|
|
@@ -124538,184 +124823,109 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124538
124823
|
}
|
|
124539
124824
|
};
|
|
124540
124825
|
}
|
|
124826
|
+
scopedSettingsCapabilities() {
|
|
124827
|
+
return ["mqtt-broker", "broker"];
|
|
124828
|
+
}
|
|
124541
124829
|
/**
|
|
124542
|
-
* The
|
|
124830
|
+
* The credentials and the ACL of the onboard brokers. The brokers are records, listed
|
|
124543
124831
|
* on the Brokers page and edited as integration instances (`brokerForm`).
|
|
124544
124832
|
*/
|
|
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
|
-
|
|
124596
|
-
|
|
124597
|
-
|
|
124598
|
-
|
|
124599
|
-
|
|
124600
|
-
|
|
124601
|
-
|
|
124602
|
-
showWhen: {
|
|
124603
|
-
field: "embeddedEnabled",
|
|
124604
|
-
equals: true
|
|
124605
|
-
}
|
|
124606
|
-
}),
|
|
124607
|
-
this.field({
|
|
124608
|
-
type: "password",
|
|
124609
|
-
key: "embeddedPassword",
|
|
124610
|
-
label: "Shared password",
|
|
124611
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.label",
|
|
124612
|
-
description: "Kept in the keychain. Paired with the shared username above.",
|
|
124613
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.description",
|
|
124614
|
-
showToggle: true,
|
|
124615
|
-
default: "",
|
|
124616
|
-
showWhen: {
|
|
124617
|
-
field: "embeddedEnabled",
|
|
124618
|
-
equals: true
|
|
124619
|
-
}
|
|
124620
|
-
})
|
|
124621
|
-
]
|
|
124622
|
-
},
|
|
124623
|
-
{
|
|
124624
|
-
id: "mqtt-broker-embedded-clients",
|
|
124625
|
-
title: "Embedded broker — credentials",
|
|
124626
|
-
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
|
|
124627
|
-
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.",
|
|
124628
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
|
|
124629
|
-
columns: 1,
|
|
124630
|
-
fields: [{
|
|
124631
|
-
type: "editable-array",
|
|
124632
|
-
key: "embeddedClients",
|
|
124633
|
-
label: "Clients",
|
|
124634
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
|
|
124635
|
-
emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
|
|
124636
|
-
addLabel: "Add client",
|
|
124637
|
-
rowTitleTemplate: "{username}",
|
|
124638
|
-
defaultItem: {
|
|
124639
|
-
username: "",
|
|
124640
|
-
passwordHash: ""
|
|
124641
|
-
},
|
|
124642
|
-
itemFields: [{
|
|
124833
|
+
async clusterSettingsSchema(cap) {
|
|
124834
|
+
if (cap !== "mqtt-broker" && cap !== "broker") return null;
|
|
124835
|
+
return this.schema({ sections: [{
|
|
124836
|
+
id: "mqtt-broker-embedded-clients",
|
|
124837
|
+
title: "Embedded broker — credentials",
|
|
124838
|
+
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
|
|
124839
|
+
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.",
|
|
124840
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
|
|
124841
|
+
columns: 1,
|
|
124842
|
+
fields: [{
|
|
124843
|
+
type: "editable-array",
|
|
124844
|
+
key: "embeddedClients",
|
|
124845
|
+
label: "Clients",
|
|
124846
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
|
|
124847
|
+
emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
|
|
124848
|
+
addLabel: "Add client",
|
|
124849
|
+
rowTitleTemplate: "{username}",
|
|
124850
|
+
defaultItem: {
|
|
124851
|
+
username: "",
|
|
124852
|
+
passwordHash: ""
|
|
124853
|
+
},
|
|
124854
|
+
itemFields: [{
|
|
124855
|
+
type: "text",
|
|
124856
|
+
key: "username",
|
|
124857
|
+
label: "Username",
|
|
124858
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.username.label"
|
|
124859
|
+
}, {
|
|
124860
|
+
type: "text",
|
|
124861
|
+
key: "passwordHash",
|
|
124862
|
+
label: "bcrypt hash",
|
|
124863
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.label",
|
|
124864
|
+
description: "bcrypt hash of the password (cost factor 10).",
|
|
124865
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.description"
|
|
124866
|
+
}]
|
|
124867
|
+
}]
|
|
124868
|
+
}, {
|
|
124869
|
+
id: "mqtt-broker-embedded-acl",
|
|
124870
|
+
title: "Embedded broker — topic ACL",
|
|
124871
|
+
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
|
|
124872
|
+
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.",
|
|
124873
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
|
|
124874
|
+
columns: 1,
|
|
124875
|
+
fields: [{
|
|
124876
|
+
type: "editable-array",
|
|
124877
|
+
key: "embeddedAcl",
|
|
124878
|
+
label: "Rules",
|
|
124879
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
|
|
124880
|
+
emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
|
|
124881
|
+
addLabel: "Add rule",
|
|
124882
|
+
rowTitleTemplate: "{username} → {topicPattern} ({access})",
|
|
124883
|
+
defaultItem: {
|
|
124884
|
+
username: "",
|
|
124885
|
+
topicPattern: "#",
|
|
124886
|
+
access: "both"
|
|
124887
|
+
},
|
|
124888
|
+
itemFields: [
|
|
124889
|
+
{
|
|
124643
124890
|
type: "text",
|
|
124644
124891
|
key: "username",
|
|
124645
124892
|
label: "Username",
|
|
124646
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124647
|
-
},
|
|
124893
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.username.label"
|
|
124894
|
+
},
|
|
124895
|
+
{
|
|
124648
124896
|
type: "text",
|
|
124649
|
-
key: "
|
|
124650
|
-
label: "
|
|
124651
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124652
|
-
description: "
|
|
124653
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124654
|
-
}]
|
|
124655
|
-
}]
|
|
124656
|
-
},
|
|
124657
|
-
{
|
|
124658
|
-
id: "mqtt-broker-embedded-acl",
|
|
124659
|
-
title: "Embedded broker — topic ACL",
|
|
124660
|
-
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
|
|
124661
|
-
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.",
|
|
124662
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
|
|
124663
|
-
columns: 1,
|
|
124664
|
-
fields: [{
|
|
124665
|
-
type: "editable-array",
|
|
124666
|
-
key: "embeddedAcl",
|
|
124667
|
-
label: "Rules",
|
|
124668
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
|
|
124669
|
-
emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
|
|
124670
|
-
addLabel: "Add rule",
|
|
124671
|
-
rowTitleTemplate: "{username} → {topicPattern} ({access})",
|
|
124672
|
-
defaultItem: {
|
|
124673
|
-
username: "",
|
|
124674
|
-
topicPattern: "#",
|
|
124675
|
-
access: "both"
|
|
124897
|
+
key: "topicPattern",
|
|
124898
|
+
label: "Topic pattern",
|
|
124899
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.label",
|
|
124900
|
+
description: "MQTT wildcards: `+` = single level, `#` = multi-level.",
|
|
124901
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.description"
|
|
124676
124902
|
},
|
|
124677
|
-
|
|
124678
|
-
|
|
124679
|
-
|
|
124680
|
-
|
|
124681
|
-
|
|
124682
|
-
|
|
124683
|
-
|
|
124684
|
-
|
|
124685
|
-
|
|
124686
|
-
|
|
124687
|
-
|
|
124688
|
-
|
|
124689
|
-
|
|
124690
|
-
|
|
124691
|
-
|
|
124692
|
-
|
|
124693
|
-
|
|
124694
|
-
|
|
124695
|
-
|
|
124696
|
-
|
|
124697
|
-
|
|
124698
|
-
|
|
124699
|
-
|
|
124700
|
-
|
|
124701
|
-
|
|
124702
|
-
|
|
124703
|
-
{
|
|
124704
|
-
value: "pub",
|
|
124705
|
-
label: "Publish only",
|
|
124706
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
|
|
124707
|
-
},
|
|
124708
|
-
{
|
|
124709
|
-
value: "both",
|
|
124710
|
-
label: "Subscribe + publish",
|
|
124711
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
|
|
124712
|
-
}
|
|
124713
|
-
]
|
|
124714
|
-
}
|
|
124715
|
-
]
|
|
124716
|
-
}]
|
|
124717
|
-
}
|
|
124718
|
-
] });
|
|
124903
|
+
{
|
|
124904
|
+
type: "select",
|
|
124905
|
+
key: "access",
|
|
124906
|
+
label: "Access",
|
|
124907
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.label",
|
|
124908
|
+
options: [
|
|
124909
|
+
{
|
|
124910
|
+
value: "sub",
|
|
124911
|
+
label: "Subscribe only",
|
|
124912
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_sub.label"
|
|
124913
|
+
},
|
|
124914
|
+
{
|
|
124915
|
+
value: "pub",
|
|
124916
|
+
label: "Publish only",
|
|
124917
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
|
|
124918
|
+
},
|
|
124919
|
+
{
|
|
124920
|
+
value: "both",
|
|
124921
|
+
label: "Subscribe + publish",
|
|
124922
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
|
|
124923
|
+
}
|
|
124924
|
+
]
|
|
124925
|
+
}
|
|
124926
|
+
]
|
|
124927
|
+
}]
|
|
124928
|
+
}] });
|
|
124719
124929
|
}
|
|
124720
124930
|
};
|
|
124721
124931
|
/**
|
|
@@ -124745,13 +124955,6 @@ function bcryptCompareSafe(plain, hash) {
|
|
|
124745
124955
|
}
|
|
124746
124956
|
}
|
|
124747
124957
|
/**
|
|
124748
|
-
* Decide whether a config update to the embedded broker requires a full
|
|
124749
|
-
* restart (port change) or can be hot-reloaded (credential / ACL change).
|
|
124750
|
-
*/
|
|
124751
|
-
function requiresEmbeddedRespawn(desired, running) {
|
|
124752
|
-
return desired.id !== running.id || desired.port !== running.port || desired.allowAnonymous !== running.allowAnonymous || desired.username !== running.username || desired.password !== running.password;
|
|
124753
|
-
}
|
|
124754
|
-
/**
|
|
124755
124958
|
* Read the `username` field off an aedes Client without leaking a cast.
|
|
124756
124959
|
* aedes attaches the username at runtime but its `.d.ts` doesn't carry
|
|
124757
124960
|
* it — we narrow with a small structural check.
|
|
@@ -124804,16 +125007,6 @@ async function findFreePort() {
|
|
|
124804
125007
|
});
|
|
124805
125008
|
});
|
|
124806
125009
|
}
|
|
124807
|
-
function mapMqttStatus(s) {
|
|
124808
|
-
switch (s) {
|
|
124809
|
-
case "connected": return "connected";
|
|
124810
|
-
case "disconnected": return "disconnected";
|
|
124811
|
-
case "auth-failed": return "auth-failed";
|
|
124812
|
-
case "unreachable": return "unreachable";
|
|
124813
|
-
case "tls-error": return "error";
|
|
124814
|
-
default: return "error";
|
|
124815
|
-
}
|
|
124816
|
-
}
|
|
124817
125010
|
//#endregion
|
|
124818
125011
|
exports.MQTT_EMBEDDED_BCRYPT_ROUNDS = MQTT_EMBEDDED_BCRYPT_ROUNDS;
|
|
124819
125012
|
exports.MqttBrokerAddon = MqttBrokerAddon;
|