@camstack/addon-mqtt-broker 1.2.151 → 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 +11 -0
- package/dist/mqtt-broker.addon.js +1243 -1203
- package/dist/mqtt-broker.addon.mjs +1243 -1203
- package/i18n/en.json +6 -23
- package/i18n/it.json +6 -23
- package/package.json +2 -2
|
@@ -6021,7 +6021,7 @@ var ZodIssueCode = {
|
|
|
6021
6021
|
var ZodFirstPartyTypeKind;
|
|
6022
6022
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6023
6023
|
//#endregion
|
|
6024
|
-
//#region ../types/dist/sleep-
|
|
6024
|
+
//#region ../types/dist/sleep-BX_MqzUe.mjs
|
|
6025
6025
|
/**
|
|
6026
6026
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6027
6027
|
* window to float samples (D455).
|
|
@@ -7650,6 +7650,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
|
|
|
7650
7650
|
/** Coalesces a burst of `keychain.changed` events into one re-resolve. */
|
|
7651
7651
|
var SECRET_CHANGE_DEBOUNCE_MS = 250;
|
|
7652
7652
|
/**
|
|
7653
|
+
* Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
|
|
7654
|
+
* place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
|
|
7655
|
+
* through `api` (guard `check-migration-ports-surface`).
|
|
7656
|
+
*/
|
|
7657
|
+
function migrationSet(catalogue, bindPorts) {
|
|
7658
|
+
return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
|
|
7659
|
+
}
|
|
7660
|
+
/** A mixed array is a mistake: the other kind would be dropped without a word. */
|
|
7661
|
+
function refuseMixed(form, kept, dropped) {
|
|
7662
|
+
if (dropped > 0) throw new Error(`runMigrations(${form}): ${dropped} of ${kept + dropped} entries are of the other kind and would be dropped; pass one kind per call`);
|
|
7663
|
+
}
|
|
7664
|
+
function catalogueOf(list) {
|
|
7665
|
+
const kept = list.filter((entry) => !("execute" in entry));
|
|
7666
|
+
refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
|
|
7667
|
+
return kept;
|
|
7668
|
+
}
|
|
7669
|
+
function boundOf(list) {
|
|
7670
|
+
const kept = list.filter((entry) => "execute" in entry);
|
|
7671
|
+
refuseMixed("bound migrations", kept.length, list.length - kept.length);
|
|
7672
|
+
return kept;
|
|
7673
|
+
}
|
|
7674
|
+
/**
|
|
7653
7675
|
* Base class for CamStack addons. Eliminates settings boilerplate:
|
|
7654
7676
|
*
|
|
7655
7677
|
* - Typed `config` property with automatic resolution from store + defaults
|
|
@@ -7816,23 +7838,21 @@ var BaseAddon = class {
|
|
|
7816
7838
|
await this.loadScopedSchemas();
|
|
7817
7839
|
return normalized;
|
|
7818
7840
|
}
|
|
7841
|
+
async runMigrations(first, second, third) {
|
|
7842
|
+
if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
|
|
7843
|
+
return this.runBound(boundOf(first), second);
|
|
7844
|
+
}
|
|
7819
7845
|
/**
|
|
7820
|
-
*
|
|
7821
|
-
*
|
|
7822
|
-
*
|
|
7823
|
-
* restored devices — with the catalogue bound to the addon's ports
|
|
7824
|
-
* (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
|
|
7825
|
-
*
|
|
7826
|
-
* An engine that cannot be reached or refuses the catalogue is logged and runs
|
|
7827
|
-
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7828
|
-
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7829
|
-
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7830
|
-
*
|
|
7831
|
-
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7832
|
-
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7833
|
-
* omitted applied ids "newer").
|
|
7846
|
+
* Several catalogues of ONE owner in one declaration (the engine replaces an owner's
|
|
7847
|
+
* catalogue on every declare, so two calls would each judge the other's ids "newer").
|
|
7848
|
+
* Build each entry with `migrationSet(catalogue, (api) => ports)`.
|
|
7834
7849
|
*/
|
|
7835
|
-
async
|
|
7850
|
+
async runMigrationSets(sets, options) {
|
|
7851
|
+
const api = this.ctx.migrationApi;
|
|
7852
|
+
if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
|
|
7853
|
+
return this.runBound(sets.flatMap((set) => set.bind(api)), options);
|
|
7854
|
+
}
|
|
7855
|
+
async runBound(migrations, options) {
|
|
7836
7856
|
const api = this.ctx.api.migrations;
|
|
7837
7857
|
const report = await runAddonMigrations({
|
|
7838
7858
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
@@ -13539,7 +13559,7 @@ var RecordIntegrationInstances = class {
|
|
|
13539
13559
|
return record === null ? null : this.viewOf(record);
|
|
13540
13560
|
};
|
|
13541
13561
|
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13542
|
-
createInstance = async ({ id, name, kind }) => {
|
|
13562
|
+
createInstance = async ({ id, name, kind, values }) => {
|
|
13543
13563
|
const minted = id ?? this.hooks.mintId?.({
|
|
13544
13564
|
name,
|
|
13545
13565
|
...kind !== void 0 ? { kind } : {}
|
|
@@ -13549,10 +13569,20 @@ var RecordIntegrationInstances = class {
|
|
|
13549
13569
|
id: minted,
|
|
13550
13570
|
data: this.hooks.initialBody(name, kind)
|
|
13551
13571
|
}, { ifRev: 0 });
|
|
13572
|
+
const patch = this.patchOf(values ?? {});
|
|
13573
|
+
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
13552
13574
|
await this.hooks.onChanged?.(minted, "created");
|
|
13553
13575
|
return { id: minted };
|
|
13554
13576
|
};
|
|
13555
13577
|
updateInstance = async ({ id, name, values }) => {
|
|
13578
|
+
const patch = this.patchOf(values);
|
|
13579
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13580
|
+
await this.handle.patch(id, patch);
|
|
13581
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13582
|
+
return { ok: true };
|
|
13583
|
+
};
|
|
13584
|
+
/** The record patch a form `values` object means: secrets kept, cleared or replaced. */
|
|
13585
|
+
patchOf(values) {
|
|
13556
13586
|
const patch = {};
|
|
13557
13587
|
for (const [key, value] of Object.entries(values)) {
|
|
13558
13588
|
if (value === void 0) continue;
|
|
@@ -13563,11 +13593,8 @@ var RecordIntegrationInstances = class {
|
|
|
13563
13593
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13564
13594
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13565
13595
|
}
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
await this.hooks.onChanged?.(id, "updated");
|
|
13569
|
-
return { ok: true };
|
|
13570
|
-
};
|
|
13596
|
+
return patch;
|
|
13597
|
+
}
|
|
13571
13598
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13572
13599
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13573
13600
|
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
@@ -15776,6 +15803,39 @@ var UnstampedEventMediaCountSchema = object({
|
|
|
15776
15803
|
/** Sum across both, or `null` when either lane could not be counted. */
|
|
15777
15804
|
total: number().int().nonnegative().nullable()
|
|
15778
15805
|
}).nullable();
|
|
15806
|
+
/**
|
|
15807
|
+
* Where the stored event media sits, per storage location: how many media rows
|
|
15808
|
+
* and how many bytes carry each location id, how many carry none (legacy,
|
|
15809
|
+
* resolved to the default `eventMedia` location), and how many name a location
|
|
15810
|
+
* that was not asked about (a deleted or never-listed one).
|
|
15811
|
+
*
|
|
15812
|
+
* One covering-index aggregate per asked location plus the NULL one, and one
|
|
15813
|
+
* unfiltered total that scans the covering index: O(rows), not a walk of the
|
|
15814
|
+
* rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
|
|
15815
|
+
* it is a deleted location only when every location id was asked.
|
|
15816
|
+
* `null` is "could not be measured", never zero.
|
|
15817
|
+
*/
|
|
15818
|
+
var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
|
|
15819
|
+
var MediaLocationCensusSchema = object({
|
|
15820
|
+
locations: array(object({
|
|
15821
|
+
locationId: string(),
|
|
15822
|
+
rows: number().int().nonnegative(),
|
|
15823
|
+
bytes: number().nonnegative()
|
|
15824
|
+
})).readonly(),
|
|
15825
|
+
unstamped: object({
|
|
15826
|
+
rows: number().int().nonnegative(),
|
|
15827
|
+
bytes: number().nonnegative()
|
|
15828
|
+
}),
|
|
15829
|
+
/** Rows stamped with a location id that was not in the asked list. */
|
|
15830
|
+
unlisted: object({
|
|
15831
|
+
rows: number().int().nonnegative(),
|
|
15832
|
+
bytes: number().nonnegative()
|
|
15833
|
+
}),
|
|
15834
|
+
total: object({
|
|
15835
|
+
rows: number().int().nonnegative(),
|
|
15836
|
+
bytes: number().nonnegative()
|
|
15837
|
+
})
|
|
15838
|
+
});
|
|
15779
15839
|
var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
|
|
15780
15840
|
/** The independently selectable logical storage classes — every class
|
|
15781
15841
|
* `storage.listLocationDeclarations` reports, so an operator never meets a
|
|
@@ -46296,52 +46356,7 @@ DeviceType.Light, method(object({
|
|
|
46296
46356
|
level: number().min(0).max(100),
|
|
46297
46357
|
lastChangedAt: number()
|
|
46298
46358
|
});
|
|
46299
|
-
|
|
46300
|
-
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
46301
|
-
*
|
|
46302
|
-
* The cap models any kind-tagged message broker the user wants to
|
|
46303
|
-
* register and that other addons might consume. The first two kinds
|
|
46304
|
-
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
46305
|
-
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
46306
|
-
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
46307
|
-
* the same surface.
|
|
46308
|
-
*
|
|
46309
|
-
* Why one cap, not one cap per kind:
|
|
46310
|
-
* - The integrations page wants a single table of "every broker"
|
|
46311
|
-
* across kinds; a unified cap drives it without join logic.
|
|
46312
|
-
* - Generic admin operations (add / remove / test / status) are
|
|
46313
|
-
* identical across kinds — duplicating them per cap was busywork.
|
|
46314
|
-
* - The `network-access` cap precedent: same interface, many
|
|
46315
|
-
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
46316
|
-
*
|
|
46317
|
-
* What stays kind-specific:
|
|
46318
|
-
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
46319
|
-
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
46320
|
-
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
46321
|
-
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
46322
|
-
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
46323
|
-
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
46324
|
-
* without rebreaking the cap signature.
|
|
46325
|
-
* - `add` `settings` payload (different fields per kind, validated by
|
|
46326
|
-
* the kind-specific provider).
|
|
46327
|
-
*
|
|
46328
|
-
* Bidirectionality:
|
|
46329
|
-
* - `publish` is RPC — synchronous result (provider-defined).
|
|
46330
|
-
* - `subscribe` returns a subscription handle; the actual message
|
|
46331
|
-
* stream flows over the typed event-bus as `broker.message` events
|
|
46332
|
-
* keyed by `(brokerId, subscriptionId)`. Consumers filter in their
|
|
46333
|
-
* event handler. Subscriptions persist across reconnects — the
|
|
46334
|
-
* provider re-subscribes upstream after a transport drop.
|
|
46335
|
-
*
|
|
46336
|
-
* Why event-bus push (not a subscription RPC stream):
|
|
46337
|
-
* - Matches the rest of CamStack's D8 contract — events for telemetry,
|
|
46338
|
-
* RPC for loss-is-a-bug. Broker messages are telemetry (a single
|
|
46339
|
-
* drop is recoverable via `getState({key})`).
|
|
46340
|
-
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
46341
|
-
* - Cross-process: events already route through `$event-bus`; no
|
|
46342
|
-
* per-broker wire-shaping needed.
|
|
46343
|
-
*/
|
|
46344
|
-
var BrokerStatusEnum = _enum([
|
|
46359
|
+
_enum([
|
|
46345
46360
|
"connected",
|
|
46346
46361
|
"disconnected",
|
|
46347
46362
|
"connecting",
|
|
@@ -46349,141 +46364,12 @@ var BrokerStatusEnum = _enum([
|
|
|
46349
46364
|
"unreachable",
|
|
46350
46365
|
"error"
|
|
46351
46366
|
]);
|
|
46352
|
-
var BrokerInfoSchema = object({
|
|
46353
|
-
/** Stable broker id. Persisted; survives addon restarts. */
|
|
46354
|
-
id: string(),
|
|
46355
|
-
/** Addon id of the provider that OWNS this broker.
|
|
46356
|
-
*
|
|
46357
|
-
* The `broker` cap is a system-scoped collection: several addons
|
|
46358
|
-
* register a `broker` provider, each owning a DISJOINT set of
|
|
46359
|
-
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
46360
|
-
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
46361
|
-
* device belongs to one integration. The admin UI threads this id
|
|
46362
|
-
* back as the `{ addonId }` system-collection selector on every
|
|
46363
|
-
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
46364
|
-
* `testConnection`), so the call routes to the OWNING provider
|
|
46365
|
-
* instead of defaulting to the first-registered one. */
|
|
46366
|
-
addonId: string(),
|
|
46367
|
-
/** Human-readable name (operator-chosen at add-time). */
|
|
46368
|
-
name: string(),
|
|
46369
|
-
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
46370
|
-
kind: string(),
|
|
46371
|
-
status: BrokerStatusEnum,
|
|
46372
|
-
/** Free-form provider-specific info (HA version, MQTT broker
|
|
46373
|
-
* flavour, latency, mTLS active, …). */
|
|
46374
|
-
info: record(string(), unknown()),
|
|
46375
|
-
/** Ms epoch of the last connection probe / status update. */
|
|
46376
|
-
lastCheckedAt: number().nullable(),
|
|
46377
|
-
/** Last error message, when `status` indicates failure. */
|
|
46378
|
-
error: string().nullable()
|
|
46379
|
-
});
|
|
46380
46367
|
var RegistryStatusSchema = object({
|
|
46381
46368
|
brokerCount: number().int().nonnegative(),
|
|
46382
46369
|
connectedCount: number().int().nonnegative()
|
|
46383
46370
|
});
|
|
46384
|
-
/**
|
|
46385
|
-
* One entry per `broker` provider — which addon provides which broker
|
|
46386
|
-
* kind(s). The unified create picker reads this to know, for each
|
|
46387
|
-
* (addonId, kind) pair the operator can pick, where to route the
|
|
46388
|
-
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
46389
|
-
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
46390
|
-
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
46391
|
-
* offers a kind whose creation form can't be rendered).
|
|
46392
|
-
*/
|
|
46393
|
-
var BrokerProviderInfoSchema = object({
|
|
46394
|
-
/** Addon id of the `broker` provider this entry describes. */
|
|
46395
|
-
addonId: string(),
|
|
46396
|
-
/** Broker kinds this provider can create, with a display label. */
|
|
46397
|
-
kinds: array(object({
|
|
46398
|
-
kind: string(),
|
|
46399
|
-
label: string(),
|
|
46400
|
-
/**
|
|
46401
|
-
* Relative hub path to start an interactive account link (OAuth popup).
|
|
46402
|
-
* May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
|
|
46403
|
-
*/
|
|
46404
|
-
interactiveConnectPath: string().min(1).optional(),
|
|
46405
|
-
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
46406
|
-
interactiveDisconnectPath: string().min(1).optional(),
|
|
46407
|
-
/**
|
|
46408
|
-
* The kind signs in through a server-held interactive session instead of
|
|
46409
|
-
* (or before) typed credentials. `basePath` is a hub-relative route set
|
|
46410
|
-
* served by the owning addon (`/addon/<id>/...`) speaking the sign-in
|
|
46411
|
-
* contract: `POST {basePath}/start` (body: the form's settings) answers
|
|
46412
|
-
* `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
|
|
46413
|
-
* `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
|
|
46414
|
-
* 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
|
|
46415
|
-
* (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
|
|
46416
|
-
* existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
|
|
46417
|
-
* (`{ sessionId }`) drops the session. The dialog that edits a broker
|
|
46418
|
-
* offers it as the default path; the fields the session fills are
|
|
46419
|
-
* marked `filledBySignIn` in the kind's settings schema.
|
|
46420
|
-
*
|
|
46421
|
-
* `methods` lists the ways the session can sign in, recommended first:
|
|
46422
|
-
* `'device-code'` adds `POST {basePath}/code/start` (body: the form's
|
|
46423
|
-
* settings) answering `{ sessionId, userCode, verificationUrl,
|
|
46424
|
-
* expiresAt }` — the operator types `userCode` at `verificationUrl` on
|
|
46425
|
-
* any device, and the same `status` / `commit` / `cancel` routes follow
|
|
46426
|
-
* (status then carries `method`, `userCode`, `verificationUrl`,
|
|
46427
|
-
* `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
|
|
46428
|
-
* `'proxy'` is the `{basePath}/start` flow above. Absent means
|
|
46429
|
-
* `['proxy']`.
|
|
46430
|
-
*/
|
|
46431
|
-
signIn: object({
|
|
46432
|
-
basePath: string().min(1),
|
|
46433
|
-
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
46434
|
-
}).optional()
|
|
46435
|
-
}))
|
|
46436
|
-
});
|
|
46437
|
-
var ListInputSchema = object({
|
|
46438
|
-
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
46439
|
-
* only HA brokers. Omit to list every broker. */
|
|
46440
|
-
kind: string().optional() });
|
|
46441
46371
|
var GetInputSchema = object({ id: string() });
|
|
46442
|
-
var AddInputSchema = object({
|
|
46443
|
-
kind: string().min(1),
|
|
46444
|
-
name: string().min(1),
|
|
46445
|
-
/**
|
|
46446
|
-
* ADOPT an existing id instead of minting a new one.
|
|
46447
|
-
*
|
|
46448
|
-
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
46449
|
-
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
46450
|
-
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
46451
|
-
* bound to a broker that no longer exists. Re-entering the password under the
|
|
46452
|
-
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
46453
|
-
* password.
|
|
46454
|
-
*
|
|
46455
|
-
* A provider MUST refuse an id that is already in use — adopting a live
|
|
46456
|
-
* broker's id would silently take it over.
|
|
46457
|
-
*/
|
|
46458
|
-
id: string().min(1).optional(),
|
|
46459
|
-
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
46460
|
-
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
46461
|
-
* branch on receipt — invalid shape rejects the add. */
|
|
46462
|
-
settings: record(string(), unknown())
|
|
46463
|
-
});
|
|
46464
|
-
var AddResultSchema = object({ id: string() });
|
|
46465
|
-
var RemoveInputSchema = object({ id: string() });
|
|
46466
|
-
var TestConnectionResultSchema = discriminatedUnion("ok", [object({
|
|
46467
|
-
ok: literal(true),
|
|
46468
|
-
latencyMs: number().nonnegative()
|
|
46469
|
-
}), object({
|
|
46470
|
-
ok: literal(false),
|
|
46471
|
-
error: string()
|
|
46472
|
-
})]);
|
|
46473
46372
|
var SettingsRecordSchema$1 = record(string(), unknown());
|
|
46474
|
-
var SettingsSchemaInputSchema = object({ kind: string() });
|
|
46475
|
-
var TestSettingsInputSchema = object({
|
|
46476
|
-
kind: string(),
|
|
46477
|
-
settings: SettingsRecordSchema$1
|
|
46478
|
-
});
|
|
46479
|
-
var TestSettingsResultSchema = discriminatedUnion("ok", [object({
|
|
46480
|
-
ok: literal(true),
|
|
46481
|
-
latencyMs: number().nonnegative().optional()
|
|
46482
|
-
}).strict(), object({
|
|
46483
|
-
ok: literal(false),
|
|
46484
|
-
error: string()
|
|
46485
|
-
})]);
|
|
46486
|
-
var SettingsSchemaResultSchema = unknown().nullable();
|
|
46487
46373
|
var PublishInputSchema = object({
|
|
46488
46374
|
brokerId: string(),
|
|
46489
46375
|
/** Kind-specific routing target.
|
|
@@ -46527,51 +46413,12 @@ var brokerCapability = {
|
|
|
46527
46413
|
kind: "poll"
|
|
46528
46414
|
},
|
|
46529
46415
|
methods: {
|
|
46530
|
-
list: method(ListInputSchema, array(BrokerInfoSchema)),
|
|
46531
|
-
get: method(GetInputSchema, BrokerInfoSchema.nullable()),
|
|
46532
|
-
/** Enumerate which addon provides which broker kind(s) for the
|
|
46533
|
-
* unified create picker. The auto-mount fans this array across
|
|
46534
|
-
* every registered `broker` provider (array-output method), so the
|
|
46535
|
-
* picker sees every kind from every provider in one call. */
|
|
46536
|
-
listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
46537
|
-
add: method(AddInputSchema, AddResultSchema, {
|
|
46538
|
-
kind: "mutation",
|
|
46539
|
-
auth: "admin"
|
|
46540
|
-
}),
|
|
46541
|
-
remove: method(RemoveInputSchema, _void(), {
|
|
46542
|
-
kind: "mutation",
|
|
46543
|
-
auth: "admin"
|
|
46544
|
-
}),
|
|
46545
|
-
testConnection: method(GetInputSchema, TestConnectionResultSchema, {
|
|
46546
|
-
kind: "mutation",
|
|
46547
|
-
auth: "admin"
|
|
46548
|
-
}),
|
|
46549
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
46550
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
46551
|
-
* when the broker id is unknown to the provider (the collection
|
|
46552
|
-
* fallback may route a foreign id to the first provider). */
|
|
46553
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46554
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
46555
|
-
* provider validates the shape and applies the change (reconnects
|
|
46556
|
-
* if credentials changed). */
|
|
46557
|
-
setSettings: method(object({
|
|
46558
|
-
id: string(),
|
|
46559
|
-
settings: SettingsRecordSchema$1
|
|
46560
|
-
}), _void(), {
|
|
46561
|
-
kind: "mutation",
|
|
46562
|
-
auth: "admin"
|
|
46563
|
-
}),
|
|
46564
46416
|
/** Returns the kind-specific connection config the consumer needs
|
|
46565
46417
|
* to open its own client (MQTT pattern: `{url, username, password,
|
|
46566
46418
|
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
46567
46419
|
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
46568
46420
|
* Returns `null` when the broker id is unknown to the provider. */
|
|
46569
46421
|
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46570
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
46571
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
46572
|
-
kind: "mutation",
|
|
46573
|
-
auth: "admin"
|
|
46574
|
-
}),
|
|
46575
46422
|
publish: method(PublishInputSchema, unknown(), {
|
|
46576
46423
|
kind: "mutation",
|
|
46577
46424
|
auth: "admin"
|
|
@@ -50644,7 +50491,14 @@ var DiscoveryCandidateSchema = object({
|
|
|
50644
50491
|
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
50645
50492
|
parentStableId: string().optional(),
|
|
50646
50493
|
/** `false` = shown for context but not pickable. Default true. */
|
|
50647
|
-
selectable: boolean().optional()
|
|
50494
|
+
selectable: boolean().optional(),
|
|
50495
|
+
/**
|
|
50496
|
+
* Keys of the provider's creation form (`getChildCreationSchema`) this candidate
|
|
50497
|
+
* cannot be adopted without — a key for an encrypted board, a pairing code. The
|
|
50498
|
+
* admin asks for exactly these, on the creation form prefilled with
|
|
50499
|
+
* `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
|
|
50500
|
+
*/
|
|
50501
|
+
needsParams: array(string()).optional()
|
|
50648
50502
|
});
|
|
50649
50503
|
/**
|
|
50650
50504
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -50703,12 +50557,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
|
|
|
50703
50557
|
}), array(DiscoveryCandidateSchema), {
|
|
50704
50558
|
kind: "mutation",
|
|
50705
50559
|
auth: "admin"
|
|
50706
|
-
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50560
|
+
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50561
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50562
|
+
integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
|
|
50563
|
+
candidate: DiscoveryCandidateSchema,
|
|
50564
|
+
/** The integration the operator adopts into; a provider with several instances needs it. */
|
|
50565
|
+
integrationId: string().optional()
|
|
50566
|
+
}), DeviceSummarySchema, {
|
|
50707
50567
|
kind: "mutation",
|
|
50708
50568
|
auth: "admin"
|
|
50709
|
-
}), method(object({}), boolean()), method(object({
|
|
50569
|
+
}), method(object({}), boolean()), method(object({
|
|
50710
50570
|
type: _enum(DeviceType),
|
|
50711
|
-
|
|
50571
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50572
|
+
integrationId: string().optional()
|
|
50573
|
+
}), CreationSchemaOutputSchema), method(object({
|
|
50574
|
+
type: _enum(DeviceType),
|
|
50575
|
+
config: record(string(), unknown()),
|
|
50576
|
+
/** The integration the operator adds the device to; a provider with several instances needs it. */
|
|
50577
|
+
integrationId: string().optional()
|
|
50712
50578
|
}), DeviceSummarySchema, {
|
|
50713
50579
|
kind: "mutation",
|
|
50714
50580
|
auth: "admin"
|
|
@@ -51418,7 +51284,10 @@ method(object({
|
|
|
51418
51284
|
auth: "admin"
|
|
51419
51285
|
}), method(object({
|
|
51420
51286
|
addonId: string(),
|
|
51421
|
-
type: _enum(DeviceType)
|
|
51287
|
+
type: _enum(DeviceType),
|
|
51288
|
+
/** The integration the form is for; a provider whose integrations differ (a hub
|
|
51289
|
+
* integration takes no manual add) answers per integration. Absent = the addon's own. */
|
|
51290
|
+
integrationId: string().optional()
|
|
51422
51291
|
}), unknown().nullable()), method(object({
|
|
51423
51292
|
addonId: string(),
|
|
51424
51293
|
type: _enum(DeviceType),
|
|
@@ -51502,7 +51371,11 @@ method(object({
|
|
|
51502
51371
|
kind: "mutation",
|
|
51503
51372
|
auth: "admin",
|
|
51504
51373
|
timeoutMs: 12e4
|
|
51505
|
-
}), method(object({
|
|
51374
|
+
}), method(object({
|
|
51375
|
+
addonId: string(),
|
|
51376
|
+
/** The integration the Add-Device dialog is open on; absent = the addon's own. */
|
|
51377
|
+
integrationId: string().optional()
|
|
51378
|
+
}), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
51506
51379
|
deviceId: number(),
|
|
51507
51380
|
key: string(),
|
|
51508
51381
|
value: unknown()
|
|
@@ -54098,7 +53971,7 @@ var TestConnectionStatusEnum = _enum([
|
|
|
54098
53971
|
"inconclusive",
|
|
54099
53972
|
"unsupported"
|
|
54100
53973
|
]);
|
|
54101
|
-
var TestConnectionResultSchema
|
|
53974
|
+
var TestConnectionResultSchema = object({
|
|
54102
53975
|
/** True ONLY for `validated`. Never true for a test that did not run. */
|
|
54103
53976
|
success: boolean(),
|
|
54104
53977
|
error: string().optional(),
|
|
@@ -54143,7 +54016,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54143
54016
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
54144
54017
|
addonId: string(),
|
|
54145
54018
|
settings: record(string(), unknown())
|
|
54146
|
-
}), TestConnectionResultSchema
|
|
54019
|
+
}), TestConnectionResultSchema, {
|
|
54147
54020
|
kind: "mutation",
|
|
54148
54021
|
auth: "admin"
|
|
54149
54022
|
});
|
|
@@ -54170,6 +54043,14 @@ var ConfigSchemaPassthrough$3 = custom();
|
|
|
54170
54043
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54171
54044
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54172
54045
|
/**
|
|
54046
|
+
* What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
|
|
54047
|
+
* instance is an integration that brings devices (the registry owns its on/off switch);
|
|
54048
|
+
* `services`: a connection configured from the addon's own function page, never listed on
|
|
54049
|
+
* Integrations.
|
|
54050
|
+
* It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
|
|
54051
|
+
*/
|
|
54052
|
+
var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
|
|
54053
|
+
/**
|
|
54173
54054
|
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54174
54055
|
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54175
54056
|
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
@@ -54180,6 +54061,22 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54180
54061
|
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54181
54062
|
id: string().min(1),
|
|
54182
54063
|
label: string().min(1),
|
|
54064
|
+
/**
|
|
54065
|
+
* The function capability this kind is configured for (`broker`, `smtp-provider`,
|
|
54066
|
+
* `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
|
|
54067
|
+
* provider with more than one section must name it on every kind.
|
|
54068
|
+
*/
|
|
54069
|
+
functionCap: string().min(1),
|
|
54070
|
+
/**
|
|
54071
|
+
* For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
|
|
54072
|
+
* (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
|
|
54073
|
+
* finds the instances whose kind names it; absent takes the kind `id`.
|
|
54074
|
+
*/
|
|
54075
|
+
brokerKind: string().min(1).optional(),
|
|
54076
|
+
/** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
|
|
54077
|
+
role: IntegrationInstanceRoleSchema.optional(),
|
|
54078
|
+
/** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
|
|
54079
|
+
canTest: boolean().optional(),
|
|
54183
54080
|
/** The kind signs in through a server-held interactive session. */
|
|
54184
54081
|
signIn: object({
|
|
54185
54082
|
basePath: string().min(1),
|
|
@@ -54192,11 +54089,7 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54192
54089
|
});
|
|
54193
54090
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54194
54091
|
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54195
|
-
role:
|
|
54196
|
-
"devices",
|
|
54197
|
-
"services",
|
|
54198
|
-
"both"
|
|
54199
|
-
]),
|
|
54092
|
+
role: IntegrationInstanceRoleSchema,
|
|
54200
54093
|
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54201
54094
|
discovery: object({
|
|
54202
54095
|
mode: _enum([
|
|
@@ -54271,7 +54164,11 @@ var integrationInstancesCapability = {
|
|
|
54271
54164
|
methods: {
|
|
54272
54165
|
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54273
54166
|
/** The form; `id: null` is the create form. */
|
|
54274
|
-
instanceFormSchema: method(object({
|
|
54167
|
+
instanceFormSchema: method(object({
|
|
54168
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54169
|
+
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
54170
|
+
kind: string().min(1).optional()
|
|
54171
|
+
}), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54275
54172
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54276
54173
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54277
54174
|
/**
|
|
@@ -54281,7 +54178,9 @@ var integrationInstancesCapability = {
|
|
|
54281
54178
|
createInstance: method(object({
|
|
54282
54179
|
id: IntegrationInstanceIdSchema.optional(),
|
|
54283
54180
|
name: string().min(1),
|
|
54284
|
-
kind: string().min(1).optional()
|
|
54181
|
+
kind: string().min(1).optional(),
|
|
54182
|
+
/** Form values written with the record (a link picked in the create dialog). */
|
|
54183
|
+
values: record(string(), unknown()).optional()
|
|
54285
54184
|
}), object({ id: string() }), {
|
|
54286
54185
|
kind: "mutation",
|
|
54287
54186
|
auth: "admin"
|
|
@@ -54308,8 +54207,10 @@ var integrationInstancesCapability = {
|
|
|
54308
54207
|
}),
|
|
54309
54208
|
testInstance: optionalMethod(object({
|
|
54310
54209
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
54311
|
-
values: record(string(), unknown())
|
|
54312
|
-
|
|
54210
|
+
values: record(string(), unknown()),
|
|
54211
|
+
/** The kind to test when `id` is null and the provider has several. */
|
|
54212
|
+
kind: string().min(1).optional()
|
|
54213
|
+
}), TestConnectionResultSchema, {
|
|
54313
54214
|
kind: "mutation",
|
|
54314
54215
|
auth: "admin"
|
|
54315
54216
|
})
|
|
@@ -56814,6 +56715,380 @@ var SystemMetricsSchema = object({
|
|
|
56814
56715
|
gpuMemoryPercent: number().optional()
|
|
56815
56716
|
});
|
|
56816
56717
|
method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
|
|
56718
|
+
/**
|
|
56719
|
+
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
56720
|
+
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
56721
|
+
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
56722
|
+
*
|
|
56723
|
+
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
56724
|
+
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
56725
|
+
* that with the addon id closed over). It is deliberately NOT in any input
|
|
56726
|
+
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
56727
|
+
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
56728
|
+
* addons go through the owner's capability.
|
|
56729
|
+
*
|
|
56730
|
+
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
56731
|
+
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
56732
|
+
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
56733
|
+
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
56734
|
+
* row's own ref.
|
|
56735
|
+
*
|
|
56736
|
+
* Internal: addons reach it through `ctx.records`, never directly.
|
|
56737
|
+
*/
|
|
56738
|
+
var RecordIdSchema = string().min(1).max(256);
|
|
56739
|
+
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
56740
|
+
var RecordBodySchema = record(string(), unknown());
|
|
56741
|
+
var RecordInputSchema = object({
|
|
56742
|
+
id: RecordIdSchema,
|
|
56743
|
+
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
56744
|
+
deviceId: number().int().optional(),
|
|
56745
|
+
/** The domain clock, ms: observed / created / opened. */
|
|
56746
|
+
at: number().int().optional(),
|
|
56747
|
+
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
56748
|
+
expiresAt: number().int().optional(),
|
|
56749
|
+
data: RecordBodySchema
|
|
56750
|
+
});
|
|
56751
|
+
var RecordViewSchema = object({
|
|
56752
|
+
id: string(),
|
|
56753
|
+
deviceId: number().int().nullable(),
|
|
56754
|
+
at: number().nullable(),
|
|
56755
|
+
expiresAt: number().nullable(),
|
|
56756
|
+
rev: number().int(),
|
|
56757
|
+
updatedAt: number(),
|
|
56758
|
+
data: RecordBodySchema
|
|
56759
|
+
});
|
|
56760
|
+
/** Declared index fields only. Anything else does not compile, by design. */
|
|
56761
|
+
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
56762
|
+
string(),
|
|
56763
|
+
number(),
|
|
56764
|
+
boolean(),
|
|
56765
|
+
object({ in: array(union([
|
|
56766
|
+
string(),
|
|
56767
|
+
number(),
|
|
56768
|
+
boolean()
|
|
56769
|
+
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
56770
|
+
object({
|
|
56771
|
+
gt: union([string(), number()]).optional(),
|
|
56772
|
+
gte: union([string(), number()]).optional(),
|
|
56773
|
+
lt: union([string(), number()]).optional(),
|
|
56774
|
+
lte: union([string(), number()]).optional()
|
|
56775
|
+
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
56776
|
+
]));
|
|
56777
|
+
var KindShape = { kind: RecordKindNameSchema };
|
|
56778
|
+
var TimeWindowShape = {
|
|
56779
|
+
deviceId: number().int().optional(),
|
|
56780
|
+
atFrom: number().optional(),
|
|
56781
|
+
atTo: number().optional()
|
|
56782
|
+
};
|
|
56783
|
+
var RecordQueryInputSchema = object({
|
|
56784
|
+
...KindShape,
|
|
56785
|
+
...TimeWindowShape,
|
|
56786
|
+
where: RecordWhereSchema.optional(),
|
|
56787
|
+
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
56788
|
+
order: _enum(["at", "id"]).optional(),
|
|
56789
|
+
desc: boolean().optional(),
|
|
56790
|
+
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
56791
|
+
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
56792
|
+
cursor: string().min(1).optional()
|
|
56793
|
+
});
|
|
56794
|
+
var RecordQueryResultSchema = object({
|
|
56795
|
+
records: array(RecordViewSchema),
|
|
56796
|
+
nextCursor: string().nullable()
|
|
56797
|
+
});
|
|
56798
|
+
var RecordKindStateSchema = _enum([
|
|
56799
|
+
"created",
|
|
56800
|
+
"unchanged",
|
|
56801
|
+
"updated"
|
|
56802
|
+
]);
|
|
56803
|
+
var DeclareKindResultSchema = object({
|
|
56804
|
+
kindId: number().int(),
|
|
56805
|
+
state: RecordKindStateSchema,
|
|
56806
|
+
createdIndexes: array(string())
|
|
56807
|
+
});
|
|
56808
|
+
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
56809
|
+
var SecretRefsList = array(string());
|
|
56810
|
+
/**
|
|
56811
|
+
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
56812
|
+
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
56813
|
+
*/
|
|
56814
|
+
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
56815
|
+
object({
|
|
56816
|
+
op: literal("put"),
|
|
56817
|
+
kind: RecordKindNameSchema,
|
|
56818
|
+
record: RecordInputSchema,
|
|
56819
|
+
ifRev: number().int().min(0).optional()
|
|
56820
|
+
}),
|
|
56821
|
+
object({
|
|
56822
|
+
op: literal("patch"),
|
|
56823
|
+
kind: RecordKindNameSchema,
|
|
56824
|
+
id: RecordIdSchema,
|
|
56825
|
+
data: RecordBodySchema,
|
|
56826
|
+
at: number().int().nullable().optional(),
|
|
56827
|
+
expiresAt: number().int().nullable().optional(),
|
|
56828
|
+
ifRev: number().int().min(1).optional()
|
|
56829
|
+
}),
|
|
56830
|
+
object({
|
|
56831
|
+
op: literal("delete"),
|
|
56832
|
+
kind: RecordKindNameSchema,
|
|
56833
|
+
id: RecordIdSchema
|
|
56834
|
+
})
|
|
56835
|
+
]);
|
|
56836
|
+
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
56837
|
+
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
56838
|
+
kind: "mutation",
|
|
56839
|
+
auth: "admin"
|
|
56840
|
+
}), method(object({
|
|
56841
|
+
...KindShape,
|
|
56842
|
+
id: RecordIdSchema
|
|
56843
|
+
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
56844
|
+
...KindShape,
|
|
56845
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56846
|
+
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
56847
|
+
...KindShape,
|
|
56848
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56849
|
+
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
56850
|
+
...KindShape,
|
|
56851
|
+
record: RecordInputSchema,
|
|
56852
|
+
/** Compare-and-set. `0` means "the row must not exist". */
|
|
56853
|
+
ifRev: number().int().min(0).optional()
|
|
56854
|
+
}), object({
|
|
56855
|
+
rev: number().int(),
|
|
56856
|
+
releasedRefs: SecretRefsList
|
|
56857
|
+
}), {
|
|
56858
|
+
kind: "mutation",
|
|
56859
|
+
auth: "admin"
|
|
56860
|
+
}), method(object({
|
|
56861
|
+
...KindShape,
|
|
56862
|
+
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56863
|
+
}), object({
|
|
56864
|
+
count: number().int(),
|
|
56865
|
+
releasedRefs: SecretRefsList
|
|
56866
|
+
}), {
|
|
56867
|
+
kind: "mutation",
|
|
56868
|
+
auth: "admin"
|
|
56869
|
+
}), method(object({
|
|
56870
|
+
...KindShape,
|
|
56871
|
+
id: RecordIdSchema,
|
|
56872
|
+
data: RecordBodySchema,
|
|
56873
|
+
at: number().int().nullable().optional(),
|
|
56874
|
+
expiresAt: number().int().nullable().optional(),
|
|
56875
|
+
ifRev: number().int().min(1).optional()
|
|
56876
|
+
}), object({
|
|
56877
|
+
rev: number().int(),
|
|
56878
|
+
releasedRefs: SecretRefsList
|
|
56879
|
+
}), {
|
|
56880
|
+
kind: "mutation",
|
|
56881
|
+
auth: "admin"
|
|
56882
|
+
}), method(object({
|
|
56883
|
+
...KindShape,
|
|
56884
|
+
id: RecordIdSchema
|
|
56885
|
+
}), object({
|
|
56886
|
+
deleted: boolean(),
|
|
56887
|
+
secretRefs: SecretRefsList
|
|
56888
|
+
}), {
|
|
56889
|
+
kind: "mutation",
|
|
56890
|
+
auth: "admin"
|
|
56891
|
+
}), method(object({
|
|
56892
|
+
...KindShape,
|
|
56893
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56894
|
+
}), object({
|
|
56895
|
+
deleted: number().int(),
|
|
56896
|
+
secretRefs: SecretRefsList
|
|
56897
|
+
}), {
|
|
56898
|
+
kind: "mutation",
|
|
56899
|
+
auth: "admin"
|
|
56900
|
+
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
56901
|
+
...KindShape,
|
|
56902
|
+
...TimeWindowShape,
|
|
56903
|
+
where: RecordWhereSchema.optional()
|
|
56904
|
+
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
56905
|
+
...KindShape,
|
|
56906
|
+
deviceId: number().int().optional(),
|
|
56907
|
+
from: number(),
|
|
56908
|
+
to: number(),
|
|
56909
|
+
bucketMs: number().int().positive()
|
|
56910
|
+
}), object({ buckets: array(object({
|
|
56911
|
+
bucketStart: number(),
|
|
56912
|
+
count: number().int()
|
|
56913
|
+
})) }), { auth: "admin" }), method(object({
|
|
56914
|
+
...KindShape,
|
|
56915
|
+
op: _enum([
|
|
56916
|
+
"sum",
|
|
56917
|
+
"min",
|
|
56918
|
+
"max",
|
|
56919
|
+
"count"
|
|
56920
|
+
]),
|
|
56921
|
+
field: RecordFieldPathSchema.optional(),
|
|
56922
|
+
groupBy: RecordFieldPathSchema.optional(),
|
|
56923
|
+
where: RecordWhereSchema.optional()
|
|
56924
|
+
}), object({ rows: array(object({
|
|
56925
|
+
group: union([
|
|
56926
|
+
string(),
|
|
56927
|
+
number(),
|
|
56928
|
+
_null()
|
|
56929
|
+
]),
|
|
56930
|
+
value: number().nullable()
|
|
56931
|
+
})) }), { auth: "admin" }), method(object({
|
|
56932
|
+
...KindShape,
|
|
56933
|
+
deviceId: number().int().optional(),
|
|
56934
|
+
atBefore: number().optional(),
|
|
56935
|
+
where: RecordWhereSchema.optional()
|
|
56936
|
+
}), object({
|
|
56937
|
+
deleted: number().int(),
|
|
56938
|
+
secretRefs: SecretRefsList
|
|
56939
|
+
}), {
|
|
56940
|
+
kind: "mutation",
|
|
56941
|
+
auth: "admin"
|
|
56942
|
+
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
56943
|
+
results: array(object({
|
|
56944
|
+
rev: number().int().nullable(),
|
|
56945
|
+
deleted: boolean()
|
|
56946
|
+
})),
|
|
56947
|
+
releasedRefs: SecretRefsList,
|
|
56948
|
+
secretRefs: SecretRefsList
|
|
56949
|
+
}), {
|
|
56950
|
+
kind: "mutation",
|
|
56951
|
+
auth: "admin"
|
|
56952
|
+
}), method(object({
|
|
56953
|
+
deviceId: number().int(),
|
|
56954
|
+
kinds: array(RecordKindNameSchema).optional()
|
|
56955
|
+
}), object({
|
|
56956
|
+
deleted: record(string(), number().int()),
|
|
56957
|
+
secretRefs: SecretRefsList
|
|
56958
|
+
}), {
|
|
56959
|
+
kind: "mutation",
|
|
56960
|
+
auth: "admin"
|
|
56961
|
+
}), method(object(KindShape), object({ id: number().int() }), {
|
|
56962
|
+
kind: "mutation",
|
|
56963
|
+
auth: "admin"
|
|
56964
|
+
});
|
|
56965
|
+
/**
|
|
56966
|
+
* The wire shapes of the migration API, version 1 (planning#447).
|
|
56967
|
+
*
|
|
56968
|
+
* A migration is code that outlives the server it was written against: an addon at
|
|
56969
|
+
* version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
|
|
56970
|
+
* the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
|
|
56971
|
+
* is the contract of every method of it, one Zod pair per method.
|
|
56972
|
+
*
|
|
56973
|
+
* APPEND-ONLY. A released method is never removed and never changes meaning: an input
|
|
56974
|
+
* field is never removed or made required, an enum never narrows, an output field is
|
|
56975
|
+
* never removed. A change that cannot be expressed that way is `migration-api.v2`, next
|
|
56976
|
+
* to this file. `scripts/check-migration-api-append-only.ts` holds the line against
|
|
56977
|
+
* `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
|
|
56978
|
+
* `it('<id>'` in `migration-api.contract.spec.ts`.
|
|
56979
|
+
*/
|
|
56980
|
+
var Rows = record(string(), unknown());
|
|
56981
|
+
var IntegrationId = object({ integrationId: string().min(1) });
|
|
56982
|
+
var RecordId = object({ id: string().min(1) });
|
|
56983
|
+
var RecordRowSchema = object({
|
|
56984
|
+
id: string(),
|
|
56985
|
+
deviceId: number().int().nullable(),
|
|
56986
|
+
at: number().nullable(),
|
|
56987
|
+
expiresAt: number().nullable(),
|
|
56988
|
+
rev: number().int(),
|
|
56989
|
+
updatedAt: number(),
|
|
56990
|
+
data: Rows
|
|
56991
|
+
});
|
|
56992
|
+
var RecordPutShape = {
|
|
56993
|
+
id: string().min(1),
|
|
56994
|
+
deviceId: number().int().optional(),
|
|
56995
|
+
at: number().int().optional(),
|
|
56996
|
+
expiresAt: number().int().optional(),
|
|
56997
|
+
data: Rows
|
|
56998
|
+
};
|
|
56999
|
+
var MigrationIntegrationSchema = object({
|
|
57000
|
+
id: string(),
|
|
57001
|
+
addonId: string(),
|
|
57002
|
+
name: string(),
|
|
57003
|
+
info: Rows,
|
|
57004
|
+
originStableId: string().optional()
|
|
57005
|
+
});
|
|
57006
|
+
var MigrationDeviceSchema = object({
|
|
57007
|
+
id: number().int(),
|
|
57008
|
+
integrationId: string().nullable().optional(),
|
|
57009
|
+
/** The owning device; null or absent for a root. */
|
|
57010
|
+
parentDeviceId: number().int().nullable().optional(),
|
|
57011
|
+
type: string().optional(),
|
|
57012
|
+
name: string().optional(),
|
|
57013
|
+
stableId: string().optional()
|
|
57014
|
+
});
|
|
57015
|
+
/** What the engine says about the migration API it serves: lets an addon notice an older server. */
|
|
57016
|
+
var MigrationApiInfoSchema = object({
|
|
57017
|
+
version: number().int(),
|
|
57018
|
+
methods: array(string())
|
|
57019
|
+
});
|
|
57020
|
+
object({
|
|
57021
|
+
message: string(),
|
|
57022
|
+
meta: Rows.optional(),
|
|
57023
|
+
/** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
|
|
57024
|
+
tags: object({ deviceId: number().int().optional() }).optional()
|
|
57025
|
+
});
|
|
57026
|
+
object({
|
|
57027
|
+
owner: string().min(1),
|
|
57028
|
+
metas: array(MigrationMetaSchema)
|
|
57029
|
+
}), object({
|
|
57030
|
+
bootId: string(),
|
|
57031
|
+
snapshot: _enum([
|
|
57032
|
+
"none",
|
|
57033
|
+
"written",
|
|
57034
|
+
"refused"
|
|
57035
|
+
]),
|
|
57036
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57037
|
+
/** The migration API the engine serves; absent from a server that predates it. */
|
|
57038
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
57039
|
+
}), object({ id: string().min(1) }), object({
|
|
57040
|
+
proceed: boolean(),
|
|
57041
|
+
state: union([literal("pending"), MigrationStatusSchema]),
|
|
57042
|
+
reason: string().nullable()
|
|
57043
|
+
}), object({
|
|
57044
|
+
id: string().min(1),
|
|
57045
|
+
result: MigrationOwnerResultSchema
|
|
57046
|
+
}), object({
|
|
57047
|
+
status: MigrationStatusSchema,
|
|
57048
|
+
error: string().nullable(),
|
|
57049
|
+
rowsChanged: number().int().nonnegative()
|
|
57050
|
+
}), object({
|
|
57051
|
+
rows: number().int().nonnegative(),
|
|
57052
|
+
secrets: number().int().nonnegative()
|
|
57053
|
+
}), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
|
|
57054
|
+
present: boolean(),
|
|
57055
|
+
removed: Rows
|
|
57056
|
+
}), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
|
|
57057
|
+
...RecordPutShape,
|
|
57058
|
+
ifRev: number().int().optional()
|
|
57059
|
+
}), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
|
|
57060
|
+
partial: Rows,
|
|
57061
|
+
ifRev: number().int().optional()
|
|
57062
|
+
}), object({ rev: number().int() }), object({ deleted: boolean() }), object({
|
|
57063
|
+
deviceId: number().int().optional(),
|
|
57064
|
+
atFrom: number().optional(),
|
|
57065
|
+
atTo: number().optional(),
|
|
57066
|
+
where: RecordWhereSchema.optional(),
|
|
57067
|
+
limit: number().int().min(1).optional(),
|
|
57068
|
+
cursor: string().min(1).optional(),
|
|
57069
|
+
resolveSecrets: boolean().optional()
|
|
57070
|
+
}), object({
|
|
57071
|
+
records: array(RecordRowSchema),
|
|
57072
|
+
nextCursor: string().nullable()
|
|
57073
|
+
}), object({
|
|
57074
|
+
deviceId: number().int().optional(),
|
|
57075
|
+
atBefore: number().optional(),
|
|
57076
|
+
where: RecordWhereSchema.optional()
|
|
57077
|
+
}), object({ count: number().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string().min(1)) }), object({ removed: array(string()) }), object({ refs: array(string()) }), object({ values: record(string(), string().nullable()) }), object({
|
|
57078
|
+
ref: string().min(1),
|
|
57079
|
+
value: string()
|
|
57080
|
+
}), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
|
|
57081
|
+
addonId: string().min(1),
|
|
57082
|
+
integrationId: string().min(1).optional()
|
|
57083
|
+
}), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
|
|
57084
|
+
id: number().int(),
|
|
57085
|
+
integrationId: string().nullable(),
|
|
57086
|
+
/** Whether the device carries any stored config; its content is not offered. */
|
|
57087
|
+
hasConfig: boolean()
|
|
57088
|
+
}).nullable(), object({
|
|
57089
|
+
deviceId: number().int(),
|
|
57090
|
+
integrationId: string().min(1)
|
|
57091
|
+
}), object({}), object({}), object({ nodeId: string() }), object({}), object({});
|
|
56817
57092
|
method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
|
|
56818
57093
|
id: string().min(1),
|
|
56819
57094
|
error: string().min(1)
|
|
@@ -56843,7 +57118,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56843
57118
|
"written",
|
|
56844
57119
|
"refused"
|
|
56845
57120
|
]),
|
|
56846
|
-
steps: array(MigrationOwnerStepSchema)
|
|
57121
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57122
|
+
/** The migration API this engine serves (additive: a server that predates it omits it). */
|
|
57123
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
56847
57124
|
}), {
|
|
56848
57125
|
kind: "mutation",
|
|
56849
57126
|
auth: "admin"
|
|
@@ -59510,7 +59787,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
59510
59787
|
}), method(RelocateMediaInputSchema, object({ jobId: string() }), {
|
|
59511
59788
|
kind: "mutation",
|
|
59512
59789
|
auth: "admin"
|
|
59513
|
-
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(
|
|
59790
|
+
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
|
|
59791
|
+
kind: "query",
|
|
59792
|
+
auth: "admin"
|
|
59793
|
+
}), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
|
|
59514
59794
|
kind: "query",
|
|
59515
59795
|
auth: "admin"
|
|
59516
59796
|
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
@@ -61917,7 +62197,7 @@ var BrokerStatusSchema$1 = _enum([
|
|
|
61917
62197
|
"unreachable",
|
|
61918
62198
|
"tls-error"
|
|
61919
62199
|
]);
|
|
61920
|
-
var BrokerInfoSchema
|
|
62200
|
+
var BrokerInfoSchema = object({
|
|
61921
62201
|
id: string(),
|
|
61922
62202
|
name: string(),
|
|
61923
62203
|
url: string(),
|
|
@@ -61990,7 +62270,7 @@ var mqttBrokerCapability = {
|
|
|
61990
62270
|
kind: "poll"
|
|
61991
62271
|
},
|
|
61992
62272
|
methods: {
|
|
61993
|
-
listBrokers: method(_void(), array(BrokerInfoSchema
|
|
62273
|
+
listBrokers: method(_void(), array(BrokerInfoSchema)),
|
|
61994
62274
|
getBrokerConfig: method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }),
|
|
61995
62275
|
addBroker: method(AddBrokerInputSchema, AddBrokerResultSchema, {
|
|
61996
62276
|
kind: "mutation",
|
|
@@ -68317,253 +68597,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
|
|
|
68317
68597
|
auth: "admin"
|
|
68318
68598
|
});
|
|
68319
68599
|
/**
|
|
68320
|
-
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
68321
|
-
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
68322
|
-
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
68323
|
-
*
|
|
68324
|
-
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
68325
|
-
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
68326
|
-
* that with the addon id closed over). It is deliberately NOT in any input
|
|
68327
|
-
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
68328
|
-
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
68329
|
-
* addons go through the owner's capability.
|
|
68330
|
-
*
|
|
68331
|
-
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
68332
|
-
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
68333
|
-
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
68334
|
-
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
68335
|
-
* row's own ref.
|
|
68336
|
-
*
|
|
68337
|
-
* Internal: addons reach it through `ctx.records`, never directly.
|
|
68338
|
-
*/
|
|
68339
|
-
var RecordIdSchema = string().min(1).max(256);
|
|
68340
|
-
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
68341
|
-
var RecordBodySchema = record(string(), unknown());
|
|
68342
|
-
var RecordInputSchema = object({
|
|
68343
|
-
id: RecordIdSchema,
|
|
68344
|
-
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
68345
|
-
deviceId: number().int().optional(),
|
|
68346
|
-
/** The domain clock, ms: observed / created / opened. */
|
|
68347
|
-
at: number().int().optional(),
|
|
68348
|
-
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
68349
|
-
expiresAt: number().int().optional(),
|
|
68350
|
-
data: RecordBodySchema
|
|
68351
|
-
});
|
|
68352
|
-
var RecordViewSchema = object({
|
|
68353
|
-
id: string(),
|
|
68354
|
-
deviceId: number().int().nullable(),
|
|
68355
|
-
at: number().nullable(),
|
|
68356
|
-
expiresAt: number().nullable(),
|
|
68357
|
-
rev: number().int(),
|
|
68358
|
-
updatedAt: number(),
|
|
68359
|
-
data: RecordBodySchema
|
|
68360
|
-
});
|
|
68361
|
-
/** Declared index fields only. Anything else does not compile, by design. */
|
|
68362
|
-
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
68363
|
-
string(),
|
|
68364
|
-
number(),
|
|
68365
|
-
boolean(),
|
|
68366
|
-
object({ in: array(union([
|
|
68367
|
-
string(),
|
|
68368
|
-
number(),
|
|
68369
|
-
boolean()
|
|
68370
|
-
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
68371
|
-
object({
|
|
68372
|
-
gt: union([string(), number()]).optional(),
|
|
68373
|
-
gte: union([string(), number()]).optional(),
|
|
68374
|
-
lt: union([string(), number()]).optional(),
|
|
68375
|
-
lte: union([string(), number()]).optional()
|
|
68376
|
-
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
68377
|
-
]));
|
|
68378
|
-
var KindShape = { kind: RecordKindNameSchema };
|
|
68379
|
-
var TimeWindowShape = {
|
|
68380
|
-
deviceId: number().int().optional(),
|
|
68381
|
-
atFrom: number().optional(),
|
|
68382
|
-
atTo: number().optional()
|
|
68383
|
-
};
|
|
68384
|
-
var RecordQueryInputSchema = object({
|
|
68385
|
-
...KindShape,
|
|
68386
|
-
...TimeWindowShape,
|
|
68387
|
-
where: RecordWhereSchema.optional(),
|
|
68388
|
-
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
68389
|
-
order: _enum(["at", "id"]).optional(),
|
|
68390
|
-
desc: boolean().optional(),
|
|
68391
|
-
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
68392
|
-
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
68393
|
-
cursor: string().min(1).optional()
|
|
68394
|
-
});
|
|
68395
|
-
var RecordQueryResultSchema = object({
|
|
68396
|
-
records: array(RecordViewSchema),
|
|
68397
|
-
nextCursor: string().nullable()
|
|
68398
|
-
});
|
|
68399
|
-
var RecordKindStateSchema = _enum([
|
|
68400
|
-
"created",
|
|
68401
|
-
"unchanged",
|
|
68402
|
-
"updated"
|
|
68403
|
-
]);
|
|
68404
|
-
var DeclareKindResultSchema = object({
|
|
68405
|
-
kindId: number().int(),
|
|
68406
|
-
state: RecordKindStateSchema,
|
|
68407
|
-
createdIndexes: array(string())
|
|
68408
|
-
});
|
|
68409
|
-
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
68410
|
-
var SecretRefsList = array(string());
|
|
68411
|
-
/**
|
|
68412
|
-
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
68413
|
-
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
68414
|
-
*/
|
|
68415
|
-
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
68416
|
-
object({
|
|
68417
|
-
op: literal("put"),
|
|
68418
|
-
kind: RecordKindNameSchema,
|
|
68419
|
-
record: RecordInputSchema,
|
|
68420
|
-
ifRev: number().int().min(0).optional()
|
|
68421
|
-
}),
|
|
68422
|
-
object({
|
|
68423
|
-
op: literal("patch"),
|
|
68424
|
-
kind: RecordKindNameSchema,
|
|
68425
|
-
id: RecordIdSchema,
|
|
68426
|
-
data: RecordBodySchema,
|
|
68427
|
-
at: number().int().nullable().optional(),
|
|
68428
|
-
expiresAt: number().int().nullable().optional(),
|
|
68429
|
-
ifRev: number().int().min(1).optional()
|
|
68430
|
-
}),
|
|
68431
|
-
object({
|
|
68432
|
-
op: literal("delete"),
|
|
68433
|
-
kind: RecordKindNameSchema,
|
|
68434
|
-
id: RecordIdSchema
|
|
68435
|
-
})
|
|
68436
|
-
]);
|
|
68437
|
-
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
68438
|
-
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
68439
|
-
kind: "mutation",
|
|
68440
|
-
auth: "admin"
|
|
68441
|
-
}), method(object({
|
|
68442
|
-
...KindShape,
|
|
68443
|
-
id: RecordIdSchema
|
|
68444
|
-
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
68445
|
-
...KindShape,
|
|
68446
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68447
|
-
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
68448
|
-
...KindShape,
|
|
68449
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68450
|
-
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
68451
|
-
...KindShape,
|
|
68452
|
-
record: RecordInputSchema,
|
|
68453
|
-
/** Compare-and-set. `0` means "the row must not exist". */
|
|
68454
|
-
ifRev: number().int().min(0).optional()
|
|
68455
|
-
}), object({
|
|
68456
|
-
rev: number().int(),
|
|
68457
|
-
releasedRefs: SecretRefsList
|
|
68458
|
-
}), {
|
|
68459
|
-
kind: "mutation",
|
|
68460
|
-
auth: "admin"
|
|
68461
|
-
}), method(object({
|
|
68462
|
-
...KindShape,
|
|
68463
|
-
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68464
|
-
}), object({
|
|
68465
|
-
count: number().int(),
|
|
68466
|
-
releasedRefs: SecretRefsList
|
|
68467
|
-
}), {
|
|
68468
|
-
kind: "mutation",
|
|
68469
|
-
auth: "admin"
|
|
68470
|
-
}), method(object({
|
|
68471
|
-
...KindShape,
|
|
68472
|
-
id: RecordIdSchema,
|
|
68473
|
-
data: RecordBodySchema,
|
|
68474
|
-
at: number().int().nullable().optional(),
|
|
68475
|
-
expiresAt: number().int().nullable().optional(),
|
|
68476
|
-
ifRev: number().int().min(1).optional()
|
|
68477
|
-
}), object({
|
|
68478
|
-
rev: number().int(),
|
|
68479
|
-
releasedRefs: SecretRefsList
|
|
68480
|
-
}), {
|
|
68481
|
-
kind: "mutation",
|
|
68482
|
-
auth: "admin"
|
|
68483
|
-
}), method(object({
|
|
68484
|
-
...KindShape,
|
|
68485
|
-
id: RecordIdSchema
|
|
68486
|
-
}), object({
|
|
68487
|
-
deleted: boolean(),
|
|
68488
|
-
secretRefs: SecretRefsList
|
|
68489
|
-
}), {
|
|
68490
|
-
kind: "mutation",
|
|
68491
|
-
auth: "admin"
|
|
68492
|
-
}), method(object({
|
|
68493
|
-
...KindShape,
|
|
68494
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68495
|
-
}), object({
|
|
68496
|
-
deleted: number().int(),
|
|
68497
|
-
secretRefs: SecretRefsList
|
|
68498
|
-
}), {
|
|
68499
|
-
kind: "mutation",
|
|
68500
|
-
auth: "admin"
|
|
68501
|
-
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
68502
|
-
...KindShape,
|
|
68503
|
-
...TimeWindowShape,
|
|
68504
|
-
where: RecordWhereSchema.optional()
|
|
68505
|
-
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
68506
|
-
...KindShape,
|
|
68507
|
-
deviceId: number().int().optional(),
|
|
68508
|
-
from: number(),
|
|
68509
|
-
to: number(),
|
|
68510
|
-
bucketMs: number().int().positive()
|
|
68511
|
-
}), object({ buckets: array(object({
|
|
68512
|
-
bucketStart: number(),
|
|
68513
|
-
count: number().int()
|
|
68514
|
-
})) }), { auth: "admin" }), method(object({
|
|
68515
|
-
...KindShape,
|
|
68516
|
-
op: _enum([
|
|
68517
|
-
"sum",
|
|
68518
|
-
"min",
|
|
68519
|
-
"max",
|
|
68520
|
-
"count"
|
|
68521
|
-
]),
|
|
68522
|
-
field: RecordFieldPathSchema.optional(),
|
|
68523
|
-
groupBy: RecordFieldPathSchema.optional(),
|
|
68524
|
-
where: RecordWhereSchema.optional()
|
|
68525
|
-
}), object({ rows: array(object({
|
|
68526
|
-
group: union([
|
|
68527
|
-
string(),
|
|
68528
|
-
number(),
|
|
68529
|
-
_null()
|
|
68530
|
-
]),
|
|
68531
|
-
value: number().nullable()
|
|
68532
|
-
})) }), { auth: "admin" }), method(object({
|
|
68533
|
-
...KindShape,
|
|
68534
|
-
deviceId: number().int().optional(),
|
|
68535
|
-
atBefore: number().optional(),
|
|
68536
|
-
where: RecordWhereSchema.optional()
|
|
68537
|
-
}), object({
|
|
68538
|
-
deleted: number().int(),
|
|
68539
|
-
secretRefs: SecretRefsList
|
|
68540
|
-
}), {
|
|
68541
|
-
kind: "mutation",
|
|
68542
|
-
auth: "admin"
|
|
68543
|
-
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
68544
|
-
results: array(object({
|
|
68545
|
-
rev: number().int().nullable(),
|
|
68546
|
-
deleted: boolean()
|
|
68547
|
-
})),
|
|
68548
|
-
releasedRefs: SecretRefsList,
|
|
68549
|
-
secretRefs: SecretRefsList
|
|
68550
|
-
}), {
|
|
68551
|
-
kind: "mutation",
|
|
68552
|
-
auth: "admin"
|
|
68553
|
-
}), method(object({
|
|
68554
|
-
deviceId: number().int(),
|
|
68555
|
-
kinds: array(RecordKindNameSchema).optional()
|
|
68556
|
-
}), object({
|
|
68557
|
-
deleted: record(string(), number().int()),
|
|
68558
|
-
secretRefs: SecretRefsList
|
|
68559
|
-
}), {
|
|
68560
|
-
kind: "mutation",
|
|
68561
|
-
auth: "admin"
|
|
68562
|
-
}), method(object(KindShape), object({ id: number().int() }), {
|
|
68563
|
-
kind: "mutation",
|
|
68564
|
-
auth: "admin"
|
|
68565
|
-
});
|
|
68566
|
-
/**
|
|
68567
68600
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
68568
68601
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
68569
68602
|
* declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
|
|
@@ -77106,20 +77139,6 @@ Object.freeze({
|
|
|
77106
77139
|
access: "create",
|
|
77107
77140
|
role: "viewer"
|
|
77108
77141
|
},
|
|
77109
|
-
"broker.add": {
|
|
77110
|
-
capName: "broker",
|
|
77111
|
-
capScope: "system",
|
|
77112
|
-
addonId: null,
|
|
77113
|
-
access: "create",
|
|
77114
|
-
role: "admin"
|
|
77115
|
-
},
|
|
77116
|
-
"broker.get": {
|
|
77117
|
-
capName: "broker",
|
|
77118
|
-
capScope: "system",
|
|
77119
|
-
addonId: null,
|
|
77120
|
-
access: "view",
|
|
77121
|
-
role: "admin"
|
|
77122
|
-
},
|
|
77123
77142
|
"broker.getBrokerConfig": {
|
|
77124
77143
|
capName: "broker",
|
|
77125
77144
|
capScope: "system",
|
|
@@ -77127,20 +77146,6 @@ Object.freeze({
|
|
|
77127
77146
|
access: "view",
|
|
77128
77147
|
role: "admin"
|
|
77129
77148
|
},
|
|
77130
|
-
"broker.getSettings": {
|
|
77131
|
-
capName: "broker",
|
|
77132
|
-
capScope: "system",
|
|
77133
|
-
addonId: null,
|
|
77134
|
-
access: "view",
|
|
77135
|
-
role: "admin"
|
|
77136
|
-
},
|
|
77137
|
-
"broker.getSettingsSchema": {
|
|
77138
|
-
capName: "broker",
|
|
77139
|
-
capScope: "system",
|
|
77140
|
-
addonId: null,
|
|
77141
|
-
access: "view",
|
|
77142
|
-
role: "admin"
|
|
77143
|
-
},
|
|
77144
77149
|
"broker.getState": {
|
|
77145
77150
|
capName: "broker",
|
|
77146
77151
|
capScope: "system",
|
|
@@ -77155,20 +77160,6 @@ Object.freeze({
|
|
|
77155
77160
|
access: "view",
|
|
77156
77161
|
role: "admin"
|
|
77157
77162
|
},
|
|
77158
|
-
"broker.list": {
|
|
77159
|
-
capName: "broker",
|
|
77160
|
-
capScope: "system",
|
|
77161
|
-
addonId: null,
|
|
77162
|
-
access: "view",
|
|
77163
|
-
role: "admin"
|
|
77164
|
-
},
|
|
77165
|
-
"broker.listProviders": {
|
|
77166
|
-
capName: "broker",
|
|
77167
|
-
capScope: "system",
|
|
77168
|
-
addonId: null,
|
|
77169
|
-
access: "view",
|
|
77170
|
-
role: "admin"
|
|
77171
|
-
},
|
|
77172
77163
|
"broker.publish": {
|
|
77173
77164
|
capName: "broker",
|
|
77174
77165
|
capScope: "system",
|
|
@@ -77176,20 +77167,6 @@ Object.freeze({
|
|
|
77176
77167
|
access: "create",
|
|
77177
77168
|
role: "admin"
|
|
77178
77169
|
},
|
|
77179
|
-
"broker.remove": {
|
|
77180
|
-
capName: "broker",
|
|
77181
|
-
capScope: "system",
|
|
77182
|
-
addonId: null,
|
|
77183
|
-
access: "delete",
|
|
77184
|
-
role: "admin"
|
|
77185
|
-
},
|
|
77186
|
-
"broker.setSettings": {
|
|
77187
|
-
capName: "broker",
|
|
77188
|
-
capScope: "system",
|
|
77189
|
-
addonId: null,
|
|
77190
|
-
access: "create",
|
|
77191
|
-
role: "admin"
|
|
77192
|
-
},
|
|
77193
77170
|
"broker.subscribe": {
|
|
77194
77171
|
capName: "broker",
|
|
77195
77172
|
capScope: "system",
|
|
@@ -77197,20 +77174,6 @@ Object.freeze({
|
|
|
77197
77174
|
access: "create",
|
|
77198
77175
|
role: "admin"
|
|
77199
77176
|
},
|
|
77200
|
-
"broker.testConnection": {
|
|
77201
|
-
capName: "broker",
|
|
77202
|
-
capScope: "system",
|
|
77203
|
-
addonId: null,
|
|
77204
|
-
access: "create",
|
|
77205
|
-
role: "admin"
|
|
77206
|
-
},
|
|
77207
|
-
"broker.testSettings": {
|
|
77208
|
-
capName: "broker",
|
|
77209
|
-
capScope: "system",
|
|
77210
|
-
addonId: null,
|
|
77211
|
-
access: "create",
|
|
77212
|
-
role: "admin"
|
|
77213
|
-
},
|
|
77214
77177
|
"broker.unsubscribe": {
|
|
77215
77178
|
capName: "broker",
|
|
77216
77179
|
capScope: "system",
|
|
@@ -82174,6 +82137,13 @@ Object.freeze({
|
|
|
82174
82137
|
access: "view",
|
|
82175
82138
|
role: "viewer"
|
|
82176
82139
|
},
|
|
82140
|
+
"pipelineAnalytics.getMediaLocationCensus": {
|
|
82141
|
+
capName: "pipeline-analytics",
|
|
82142
|
+
capScope: "device",
|
|
82143
|
+
addonId: null,
|
|
82144
|
+
access: "view",
|
|
82145
|
+
role: "admin"
|
|
82146
|
+
},
|
|
82177
82147
|
"pipelineAnalytics.getMediaReclaimStatus": {
|
|
82178
82148
|
capName: "pipeline-analytics",
|
|
82179
82149
|
capScope: "device",
|
|
@@ -91115,6 +91085,48 @@ object({
|
|
|
91115
91085
|
delay: number().nonnegative().optional(),
|
|
91116
91086
|
commands: record(string(), unknown())
|
|
91117
91087
|
});
|
|
91088
|
+
var BrokerInUseError = class extends Error {
|
|
91089
|
+
brokerId;
|
|
91090
|
+
users;
|
|
91091
|
+
name = "BrokerInUseError";
|
|
91092
|
+
constructor(brokerId, users) {
|
|
91093
|
+
super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
|
|
91094
|
+
this.brokerId = brokerId;
|
|
91095
|
+
this.users = users;
|
|
91096
|
+
}
|
|
91097
|
+
};
|
|
91098
|
+
/** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
|
|
91099
|
+
async function assertBrokerUnused(brokerId, finders) {
|
|
91100
|
+
const users = [];
|
|
91101
|
+
for (const finder of finders) users.push(...await finder.find(brokerId));
|
|
91102
|
+
if (users.length > 0) throw new BrokerInUseError(brokerId, users);
|
|
91103
|
+
}
|
|
91104
|
+
/**
|
|
91105
|
+
* Users reached through another addon's listed rows (an addon cannot read a record kind it does
|
|
91106
|
+
* not own): every row whose `field` holds the broker id. `list` is a cap call the owner serves.
|
|
91107
|
+
*/
|
|
91108
|
+
function fieldUserFinder(spec) {
|
|
91109
|
+
return { find: async (brokerId) => (await spec.list()).filter((row) => row.formValues[spec.field] === brokerId).map((row) => ({
|
|
91110
|
+
kind: spec.kind,
|
|
91111
|
+
id: row.id,
|
|
91112
|
+
...row.name !== void 0 ? { name: row.name } : {}
|
|
91113
|
+
})) };
|
|
91114
|
+
}
|
|
91115
|
+
/**
|
|
91116
|
+
* An account provider whose `deleteInstance` is refused while anything still uses the account
|
|
91117
|
+
* (D1012). The account is an `integration-instances` kind (D1013), so the refusal lives on the
|
|
91118
|
+
* delete the instances page calls, not on a separate broker cap.
|
|
91119
|
+
*/
|
|
91120
|
+
function guardBrokerDelete(base, users) {
|
|
91121
|
+
return {
|
|
91122
|
+
...base,
|
|
91123
|
+
deleteInstance: async (input) => {
|
|
91124
|
+
await assertBrokerUnused(input.id, users());
|
|
91125
|
+
return base.deleteInstance(input);
|
|
91126
|
+
}
|
|
91127
|
+
};
|
|
91128
|
+
}
|
|
91129
|
+
entryBody({ brokerId: string() });
|
|
91118
91130
|
//#endregion
|
|
91119
91131
|
//#region ../../node_modules/xtend/immutable.js
|
|
91120
91132
|
var require_immutable = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -123159,7 +123171,7 @@ function readOnboardPort(settings) {
|
|
|
123159
123171
|
return raw;
|
|
123160
123172
|
}
|
|
123161
123173
|
/**
|
|
123162
|
-
* Build an onboard {@link BuiltOnboardEntry} from
|
|
123174
|
+
* Build an onboard {@link BuiltOnboardEntry} from the create form's values.
|
|
123163
123175
|
* Pure — the port is pre-resolved by the caller (auto-allocation is a
|
|
123164
123176
|
* side-effecting concern kept out of this helper), so the helper performs no
|
|
123165
123177
|
* I/O of its own.
|
|
@@ -123245,7 +123257,7 @@ function resolveStoredPassword(value, legacy) {
|
|
|
123245
123257
|
function retiredCiphertextPaths(config) {
|
|
123246
123258
|
const out = [];
|
|
123247
123259
|
for (const b of config.brokers) if (b.password?.startsWith("enc:v1:") === true) out.push(`brokers[${b.id}].password`);
|
|
123248
|
-
if (config.embeddedPassword
|
|
123260
|
+
if (config.embeddedPassword?.startsWith("enc:v1:") === true) out.push("embeddedPassword");
|
|
123249
123261
|
return out;
|
|
123250
123262
|
}
|
|
123251
123263
|
//#endregion
|
|
@@ -123279,6 +123291,229 @@ var mqttBrokersEntryKind = defineEntryKind("brokers", {
|
|
|
123279
123291
|
writeTrigger: "an operator adds or removes an external MQTT broker entry"
|
|
123280
123292
|
});
|
|
123281
123293
|
//#endregion
|
|
123294
|
+
//#region src/mqtt-broker-users.ts
|
|
123295
|
+
/** The addon whose integrations name an MQTT broker, and the field they keep that id in. */
|
|
123296
|
+
var MQTT_HA_ADDON_ID = "provider-mqtt-ha";
|
|
123297
|
+
var MQTT_HA_BROKER_FIELD = "mqttBrokerId";
|
|
123298
|
+
/** The finders for everything that still points at an MQTT broker. */
|
|
123299
|
+
function mqttBrokerUsers(listMqttHaInstances) {
|
|
123300
|
+
return [fieldUserFinder({
|
|
123301
|
+
kind: "integration",
|
|
123302
|
+
field: MQTT_HA_BROKER_FIELD,
|
|
123303
|
+
list: listMqttHaInstances
|
|
123304
|
+
})];
|
|
123305
|
+
}
|
|
123306
|
+
/** MQTT Discovery is installed but has not registered as a provider: not known, NOT absent (D315). */
|
|
123307
|
+
var MqttDiscoveryNotReadyError = class extends Error {
|
|
123308
|
+
constructor() {
|
|
123309
|
+
super("MQTT Discovery is installed but not answering yet (still starting, or switched off), so its integrations cannot be checked: try again once it is running");
|
|
123310
|
+
this.name = "MqttDiscoveryNotReadyError";
|
|
123311
|
+
}
|
|
123312
|
+
};
|
|
123313
|
+
/**
|
|
123314
|
+
* The integrations of the MQTT Discovery addon. Nothing can name a broker only when the registry
|
|
123315
|
+
* ANSWERED that the addon supplies no `integration-instances` AND the addon roster ANSWERED that
|
|
123316
|
+
* it is not installed: then the answer is none. An addon that is installed but has not registered
|
|
123317
|
+
* (booting, switched off) is not-yet-known, and a registry or roster that does not answer, or a
|
|
123318
|
+
* read that fails, is unknown too: all of those throw, so the delete is refused (D49, D315).
|
|
123319
|
+
*/
|
|
123320
|
+
async function listMqttHaInstancesOver(api) {
|
|
123321
|
+
const info = await api.capabilities.getCapability.query({ name: "integration-instances" });
|
|
123322
|
+
if (info === null) throw new Error("the capability registry did not answer");
|
|
123323
|
+
if (!info.providers.includes("provider-mqtt-ha")) {
|
|
123324
|
+
if ((await api.addons.list.query()).some((row) => row.manifest.id === "provider-mqtt-ha")) throw new MqttDiscoveryNotReadyError();
|
|
123325
|
+
return [];
|
|
123326
|
+
}
|
|
123327
|
+
return api.integrationInstances.listInstances.query({ addonId: MQTT_HA_ADDON_ID });
|
|
123328
|
+
}
|
|
123329
|
+
//#endregion
|
|
123330
|
+
//#region src/mqtt-broker-instances.ts
|
|
123331
|
+
/**
|
|
123332
|
+
* The MQTT brokers as `integration-instances` (D1013): one instance per `brokers` record. An
|
|
123333
|
+
* external broker is a URL; an ONBOARD broker (`onboard: true` on the create form) is a CamStack
|
|
123334
|
+
* `aedes` server on this hub: the create allocates its port (or honours the operator's) and the
|
|
123335
|
+
* addon spawns it from the record, the delete tears it down. Removing a broker an MQTT Discovery
|
|
123336
|
+
* integration still names is refused (D1012).
|
|
123337
|
+
*/
|
|
123338
|
+
var MQTT_INSTANCE_KIND = "mqtt";
|
|
123339
|
+
/** The create-form keys that only mean "run an onboard broker"; the record keeps a descriptor. */
|
|
123340
|
+
var ONBOARD_FORM_KEYS = new Set([
|
|
123341
|
+
"onboard",
|
|
123342
|
+
"port",
|
|
123343
|
+
"allowAnonymous"
|
|
123344
|
+
]);
|
|
123345
|
+
var PROBE_LABELS = {
|
|
123346
|
+
connected: "Connected",
|
|
123347
|
+
disconnected: "Disconnected",
|
|
123348
|
+
"auth-failed": "Authentication failed",
|
|
123349
|
+
unreachable: "Unreachable",
|
|
123350
|
+
"tls-error": "TLS error"
|
|
123351
|
+
};
|
|
123352
|
+
/** The instance's health word: connected / auth-failed / unreachable / tls-error, latency, clients. */
|
|
123353
|
+
function mqttHealthOf(body, live, now) {
|
|
123354
|
+
if (body.enabled === false) return { status: {
|
|
123355
|
+
state: "off",
|
|
123356
|
+
label: "Switched off"
|
|
123357
|
+
} };
|
|
123358
|
+
if (body.onboard !== void 0) return live.onboard === null ? {
|
|
123359
|
+
status: {
|
|
123360
|
+
state: "error",
|
|
123361
|
+
label: "Not listening"
|
|
123362
|
+
},
|
|
123363
|
+
lastCheckedAt: now
|
|
123364
|
+
} : {
|
|
123365
|
+
status: {
|
|
123366
|
+
state: "ok",
|
|
123367
|
+
label: "Listening",
|
|
123368
|
+
detail: `${live.onboard.connectedClients} connected clients`
|
|
123369
|
+
},
|
|
123370
|
+
lastCheckedAt: now
|
|
123371
|
+
};
|
|
123372
|
+
const probe = live.probe;
|
|
123373
|
+
if (probe === void 0) return { status: {
|
|
123374
|
+
state: "unknown",
|
|
123375
|
+
label: "Not checked yet"
|
|
123376
|
+
} };
|
|
123377
|
+
const detail = probe.status === "connected" ? probe.latencyMs !== null ? `${probe.latencyMs} ms` : void 0 : probe.error;
|
|
123378
|
+
return {
|
|
123379
|
+
status: {
|
|
123380
|
+
state: probe.status === "connected" ? "ok" : "error",
|
|
123381
|
+
label: PROBE_LABELS[probe.status],
|
|
123382
|
+
...detail !== void 0 ? { detail } : {}
|
|
123383
|
+
},
|
|
123384
|
+
lastCheckedAt: probe.lastCheckedAt
|
|
123385
|
+
};
|
|
123386
|
+
}
|
|
123387
|
+
function withoutOnboardKeys(values) {
|
|
123388
|
+
return Object.fromEntries(Object.entries(values).filter(([key]) => !ONBOARD_FORM_KEYS.has(key)));
|
|
123389
|
+
}
|
|
123390
|
+
function typedText(value) {
|
|
123391
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
123392
|
+
}
|
|
123393
|
+
/** A password the form really typed: not blank, not the redaction token, not the hidden view. */
|
|
123394
|
+
function typedSecret(value) {
|
|
123395
|
+
const text = typedText(value);
|
|
123396
|
+
return text === void 0 || text === "__camstack_redacted__" || isSecretFieldView(text) ? void 0 : text;
|
|
123397
|
+
}
|
|
123398
|
+
function resultOf(probe) {
|
|
123399
|
+
return probe.ok ? {
|
|
123400
|
+
success: true,
|
|
123401
|
+
status: "validated",
|
|
123402
|
+
...probe.latencyMs !== void 0 ? { latencyMs: probe.latencyMs } : {}
|
|
123403
|
+
} : {
|
|
123404
|
+
success: false,
|
|
123405
|
+
status: "rejected",
|
|
123406
|
+
error: probe.error ?? "The broker did not answer"
|
|
123407
|
+
};
|
|
123408
|
+
}
|
|
123409
|
+
function buildMqttBrokerInstances(records, deps) {
|
|
123410
|
+
const instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
|
|
123411
|
+
description: {
|
|
123412
|
+
role: "services",
|
|
123413
|
+
instanceMode: "multiple",
|
|
123414
|
+
kind: "brokers",
|
|
123415
|
+
canTest: true,
|
|
123416
|
+
kinds: [{
|
|
123417
|
+
id: MQTT_INSTANCE_KIND,
|
|
123418
|
+
label: "MQTT broker",
|
|
123419
|
+
functionCap: "broker",
|
|
123420
|
+
brokerKind: "mqtt",
|
|
123421
|
+
canTest: true
|
|
123422
|
+
}]
|
|
123423
|
+
},
|
|
123424
|
+
formSchema: (id) => deps.formSchema(id, false),
|
|
123425
|
+
kindOf: () => MQTT_INSTANCE_KIND,
|
|
123426
|
+
initialBody: (name) => ({
|
|
123427
|
+
name,
|
|
123428
|
+
url: "mqtt://"
|
|
123429
|
+
}),
|
|
123430
|
+
configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
|
|
123431
|
+
health: (body, id) => mqttHealthOf(body, deps.liveState(id), Date.now()),
|
|
123432
|
+
onChanged: deps.onChanged,
|
|
123433
|
+
test: async ({ id, values }) => {
|
|
123434
|
+
if (id === null && isOnboardRequested(values)) return {
|
|
123435
|
+
success: true,
|
|
123436
|
+
status: "validated",
|
|
123437
|
+
detail: "A free port is picked when the broker is created"
|
|
123438
|
+
};
|
|
123439
|
+
const saved = id === null ? null : await instances.readResolved(id);
|
|
123440
|
+
if (saved?.body.onboard !== void 0) return deps.onboardListening(saved.id) ? {
|
|
123441
|
+
success: true,
|
|
123442
|
+
status: "validated",
|
|
123443
|
+
detail: `Listening on port ${saved.body.onboard.port}`
|
|
123444
|
+
} : {
|
|
123445
|
+
success: false,
|
|
123446
|
+
status: "rejected",
|
|
123447
|
+
error: "The onboard broker is not listening"
|
|
123448
|
+
};
|
|
123449
|
+
const url = typedText(values["url"]) ?? saved?.body.url;
|
|
123450
|
+
if (url === void 0 || url === "mqtt://") return {
|
|
123451
|
+
success: false,
|
|
123452
|
+
status: "rejected",
|
|
123453
|
+
error: "Enter the broker URL first"
|
|
123454
|
+
};
|
|
123455
|
+
const username = typedText(values["username"]) ?? saved?.body.username;
|
|
123456
|
+
const password = typedSecret(values["password"]) ?? saved?.body.password;
|
|
123457
|
+
return resultOf(await deps.probe({
|
|
123458
|
+
url,
|
|
123459
|
+
...username !== void 0 ? { username } : {},
|
|
123460
|
+
...password !== void 0 ? { password } : {}
|
|
123461
|
+
}));
|
|
123462
|
+
}
|
|
123463
|
+
});
|
|
123464
|
+
const base = instances.toProvider();
|
|
123465
|
+
/** Port, descriptor and URL of an onboard broker the form asked for. */
|
|
123466
|
+
const onboardValues = async (id, name, values) => {
|
|
123467
|
+
const built = buildOnboardEntry({
|
|
123468
|
+
id,
|
|
123469
|
+
name,
|
|
123470
|
+
port: readOnboardPort(values) ?? await deps.freePort(),
|
|
123471
|
+
settings: values
|
|
123472
|
+
});
|
|
123473
|
+
return {
|
|
123474
|
+
...withoutOnboardKeys(values),
|
|
123475
|
+
url: built.url,
|
|
123476
|
+
onboard: built.onboard
|
|
123477
|
+
};
|
|
123478
|
+
};
|
|
123479
|
+
return guardBrokerDelete({
|
|
123480
|
+
...base,
|
|
123481
|
+
instanceFormSchema: async ({ id }) => {
|
|
123482
|
+
const saved = id === null ? null : await instances.getResolved(id);
|
|
123483
|
+
return deps.formSchema(id, saved?.onboard !== void 0);
|
|
123484
|
+
},
|
|
123485
|
+
createInstance: async (input) => {
|
|
123486
|
+
const values = input.values ?? {};
|
|
123487
|
+
if (!isOnboardRequested(values)) return base.createInstance({
|
|
123488
|
+
...input,
|
|
123489
|
+
values: withoutOnboardKeys(values)
|
|
123490
|
+
});
|
|
123491
|
+
const id = input.id ?? randomUUID();
|
|
123492
|
+
return base.createInstance({
|
|
123493
|
+
...input,
|
|
123494
|
+
id,
|
|
123495
|
+
values: await onboardValues(id, input.name, values)
|
|
123496
|
+
});
|
|
123497
|
+
},
|
|
123498
|
+
updateInstance: async (input) => {
|
|
123499
|
+
if (!isOnboardRequested(input.values)) return base.updateInstance({
|
|
123500
|
+
...input,
|
|
123501
|
+
values: withoutOnboardKeys(input.values)
|
|
123502
|
+
});
|
|
123503
|
+
const current = await instances.getResolved(input.id);
|
|
123504
|
+
if (current === null) throw new Error(`mqtt-broker: broker '${input.id}' not found`);
|
|
123505
|
+
if (current.onboard !== void 0) return base.updateInstance({
|
|
123506
|
+
...input,
|
|
123507
|
+
values: withoutOnboardKeys(input.values)
|
|
123508
|
+
});
|
|
123509
|
+
return base.updateInstance({
|
|
123510
|
+
...input,
|
|
123511
|
+
values: await onboardValues(input.id, input.name ?? current.name, input.values)
|
|
123512
|
+
});
|
|
123513
|
+
}
|
|
123514
|
+
}, () => mqttBrokerUsers(deps.listMqttHaInstances));
|
|
123515
|
+
}
|
|
123516
|
+
//#endregion
|
|
123282
123517
|
//#region src/migrations/brokers-to-records.ts
|
|
123283
123518
|
/**
|
|
123284
123519
|
* The external MQTT brokers live in the `brokers` records (D985, D996); the old addon-settings
|
|
@@ -123368,46 +123603,163 @@ async function planDropBrokersKey(ports) {
|
|
|
123368
123603
|
}] };
|
|
123369
123604
|
}
|
|
123370
123605
|
//#endregion
|
|
123606
|
+
//#region src/migrations/embedded-to-onboard.ts
|
|
123607
|
+
/**
|
|
123608
|
+
* The legacy single embedded broker lived in FLAT addon-settings keys (`embeddedEnabled`,
|
|
123609
|
+
* `embeddedPort`, ...). It is an ONBOARD broker record now, so the instance list, the pickers,
|
|
123610
|
+
* the test and the delete all see it (D1013). Two migrations of the catalogue, in this order:
|
|
123611
|
+
*
|
|
123612
|
+
* 1. `embeddedToOnboardRecord` (procedure): a legacy broker (a stopped one as a switched-off
|
|
123613
|
+
* record) becomes a `brokers` record
|
|
123614
|
+
* under the SAME id (`embeddedId`, else the `'embedded'` id the old code derived), so a
|
|
123615
|
+
* consumer that stored that id still resolves. It never overwrites a record of that id.
|
|
123616
|
+
* 2. `planDropEmbeddedKeys` (plan): the flat keys are deleted from the canonical
|
|
123617
|
+
* `addon-settings` rows, and only while the record that replaces them exists and carries the
|
|
123618
|
+
* password the legacy key held. `embeddedClients` / `embeddedAcl` are not flat broker keys
|
|
123619
|
+
* and stay.
|
|
123620
|
+
*/
|
|
123621
|
+
/** The flat keys that described the legacy embedded broker. */
|
|
123622
|
+
var LEGACY_EMBEDDED_KEYS = [
|
|
123623
|
+
"embeddedEnabled",
|
|
123624
|
+
"embeddedId",
|
|
123625
|
+
"embeddedPort",
|
|
123626
|
+
"embeddedAllowAnonymous",
|
|
123627
|
+
"embeddedUsername",
|
|
123628
|
+
"embeddedPassword"
|
|
123629
|
+
];
|
|
123630
|
+
var LEGACY_EMBEDDED_NAME = "Embedded broker";
|
|
123631
|
+
function onboardBodyOf(legacy) {
|
|
123632
|
+
return {
|
|
123633
|
+
name: LEGACY_EMBEDDED_NAME,
|
|
123634
|
+
url: `mqtt://127.0.0.1:${legacy.port}`,
|
|
123635
|
+
...legacy.username !== void 0 ? { username: legacy.username } : {},
|
|
123636
|
+
...legacy.password !== void 0 ? { password: legacy.password } : {},
|
|
123637
|
+
onboard: {
|
|
123638
|
+
port: legacy.port,
|
|
123639
|
+
allowAnonymous: legacy.allowAnonymous
|
|
123640
|
+
},
|
|
123641
|
+
...legacy.enabled ? {} : { enabled: false }
|
|
123642
|
+
};
|
|
123643
|
+
}
|
|
123644
|
+
async function embeddedToOnboardRecord(ports) {
|
|
123645
|
+
const legacy = await ports.readLegacyEmbedded();
|
|
123646
|
+
if (legacy === null) return {
|
|
123647
|
+
outcome: "already-done",
|
|
123648
|
+
summary: { migrated: 0 }
|
|
123649
|
+
};
|
|
123650
|
+
if ((await ports.readRecords()).has(legacy.id)) return {
|
|
123651
|
+
outcome: "already-done",
|
|
123652
|
+
summary: { migrated: 0 }
|
|
123653
|
+
};
|
|
123654
|
+
await ports.putOnboardRecord(legacy.id, onboardBodyOf(legacy));
|
|
123655
|
+
return {
|
|
123656
|
+
outcome: "done",
|
|
123657
|
+
summary: { migrated: 1 }
|
|
123658
|
+
};
|
|
123659
|
+
}
|
|
123660
|
+
async function planDropEmbeddedKeys(ports) {
|
|
123661
|
+
const raw = await ports.readRawEmbeddedKeys();
|
|
123662
|
+
const present = LEGACY_EMBEDDED_KEYS.filter((key) => key in raw);
|
|
123663
|
+
if (present.length === 0) return { edits: [] };
|
|
123664
|
+
const refuse = (reason) => {
|
|
123665
|
+
ports.warn(`mqtt-broker migration: the legacy embedded keys are kept: ${reason}`);
|
|
123666
|
+
throw new Error(`the legacy embedded broker keys are not deleted: ${reason}`);
|
|
123667
|
+
};
|
|
123668
|
+
const legacy = await ports.readLegacyEmbedded();
|
|
123669
|
+
if (legacy !== null) {
|
|
123670
|
+
const record = (await ports.readRecords()).get(legacy.id);
|
|
123671
|
+
if (record === void 0) return refuse(`the record '${legacy.id}' does not exist`);
|
|
123672
|
+
if ((legacy.password ?? "").length > 0 && !record.hasPassword) return refuse(`the record '${legacy.id}' holds no password`);
|
|
123673
|
+
}
|
|
123674
|
+
return { edits: [{
|
|
123675
|
+
op: "deleteKeys",
|
|
123676
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
123677
|
+
keys: present.map((key) => `${MQTT_BROKER_ADDON_ID}.${key}`),
|
|
123678
|
+
precondition: preconditionOf(Object.fromEntries(present.map((key) => [`${MQTT_BROKER_ADDON_ID}.${key}`, {
|
|
123679
|
+
addonId: MQTT_BROKER_ADDON_ID,
|
|
123680
|
+
key,
|
|
123681
|
+
value: raw[key]
|
|
123682
|
+
}])))
|
|
123683
|
+
}] };
|
|
123684
|
+
}
|
|
123685
|
+
//#endregion
|
|
123371
123686
|
//#region src/migrations/index.ts
|
|
123372
123687
|
var BROKERS_TO_RECORDS_ID = "mqtt-broker/2026-11-03-brokers-settings-to-records";
|
|
123373
123688
|
var DROP_BROKERS_KEY_ID = "mqtt-broker/2026-11-03-drop-brokers-key";
|
|
123689
|
+
var EMBEDDED_TO_ONBOARD_ID = "mqtt-broker/2026-11-05-embedded-broker-to-onboard-record";
|
|
123690
|
+
var DROP_EMBEDDED_KEYS_ID = "mqtt-broker/2026-11-05-drop-embedded-keys";
|
|
123374
123691
|
var FIXTURE_MARK = "a setting key the migration does not know";
|
|
123375
123692
|
/**
|
|
123376
123693
|
* The `mqtt-broker` catalogue (D987), offered to the engine when the addon initialises. Both
|
|
123377
123694
|
* entries are `tolerate`: the embedded broker and every record keep working while a legacy key
|
|
123378
123695
|
* stays where it is.
|
|
123379
123696
|
*/
|
|
123380
|
-
var ADDON_MIGRATIONS = [
|
|
123381
|
-
|
|
123382
|
-
|
|
123383
|
-
|
|
123384
|
-
|
|
123385
|
-
|
|
123386
|
-
|
|
123387
|
-
|
|
123388
|
-
|
|
123389
|
-
|
|
123390
|
-
|
|
123391
|
-
|
|
123392
|
-
|
|
123393
|
-
|
|
123394
|
-
|
|
123395
|
-
|
|
123396
|
-
|
|
123397
|
-
|
|
123398
|
-
|
|
123399
|
-
|
|
123400
|
-
|
|
123401
|
-
|
|
123402
|
-
|
|
123403
|
-
|
|
123404
|
-
|
|
123405
|
-
|
|
123406
|
-
|
|
123407
|
-
|
|
123408
|
-
|
|
123409
|
-
|
|
123410
|
-
}
|
|
123697
|
+
var ADDON_MIGRATIONS = [
|
|
123698
|
+
{
|
|
123699
|
+
meta: {
|
|
123700
|
+
id: BROKERS_TO_RECORDS_ID,
|
|
123701
|
+
owner: "mqtt-broker",
|
|
123702
|
+
level: "addon-state",
|
|
123703
|
+
seq: 1,
|
|
123704
|
+
dependsOn: [],
|
|
123705
|
+
store: "settings",
|
|
123706
|
+
scope: "cluster",
|
|
123707
|
+
onFailure: "tolerate",
|
|
123708
|
+
bodyKind: "procedure",
|
|
123709
|
+
description: "Check that every external broker of the old settings list is a brokers record; never deletes",
|
|
123710
|
+
fixtureMark: FIXTURE_MARK
|
|
123711
|
+
},
|
|
123712
|
+
procedure: verifyBrokersAreRecords
|
|
123713
|
+
},
|
|
123714
|
+
{
|
|
123715
|
+
meta: {
|
|
123716
|
+
id: DROP_BROKERS_KEY_ID,
|
|
123717
|
+
owner: "mqtt-broker",
|
|
123718
|
+
level: "addon-state",
|
|
123719
|
+
seq: 2,
|
|
123720
|
+
dependsOn: [BROKERS_TO_RECORDS_ID],
|
|
123721
|
+
store: "settings",
|
|
123722
|
+
scope: "cluster",
|
|
123723
|
+
onFailure: "tolerate",
|
|
123724
|
+
bodyKind: "plan",
|
|
123725
|
+
description: "Delete the legacy brokers settings key once every broker is a record",
|
|
123726
|
+
fixtureMark: FIXTURE_MARK
|
|
123727
|
+
},
|
|
123728
|
+
plan: planDropBrokersKey
|
|
123729
|
+
},
|
|
123730
|
+
{
|
|
123731
|
+
meta: {
|
|
123732
|
+
id: EMBEDDED_TO_ONBOARD_ID,
|
|
123733
|
+
owner: "mqtt-broker",
|
|
123734
|
+
level: "addon-state",
|
|
123735
|
+
seq: 3,
|
|
123736
|
+
dependsOn: [DROP_BROKERS_KEY_ID],
|
|
123737
|
+
store: "settings",
|
|
123738
|
+
scope: "cluster",
|
|
123739
|
+
onFailure: "tolerate",
|
|
123740
|
+
bodyKind: "procedure",
|
|
123741
|
+
description: "The legacy embedded broker (flat embedded* keys) becomes an onboard brokers record under the same id; never overwrites a record",
|
|
123742
|
+
fixtureMark: FIXTURE_MARK
|
|
123743
|
+
},
|
|
123744
|
+
procedure: embeddedToOnboardRecord
|
|
123745
|
+
},
|
|
123746
|
+
{
|
|
123747
|
+
meta: {
|
|
123748
|
+
id: DROP_EMBEDDED_KEYS_ID,
|
|
123749
|
+
owner: "mqtt-broker",
|
|
123750
|
+
level: "addon-state",
|
|
123751
|
+
seq: 4,
|
|
123752
|
+
dependsOn: [EMBEDDED_TO_ONBOARD_ID],
|
|
123753
|
+
store: "settings",
|
|
123754
|
+
scope: "cluster",
|
|
123755
|
+
onFailure: "tolerate",
|
|
123756
|
+
bodyKind: "plan",
|
|
123757
|
+
description: "Delete the flat embedded broker keys once the onboard record holds them",
|
|
123758
|
+
fixtureMark: FIXTURE_MARK
|
|
123759
|
+
},
|
|
123760
|
+
plan: planDropEmbeddedKeys
|
|
123761
|
+
}
|
|
123762
|
+
];
|
|
123411
123763
|
//#endregion
|
|
123412
123764
|
//#region src/mqtt-entry-record-kinds.ts
|
|
123413
123765
|
var aclAccessSchema = _enum([
|
|
@@ -123615,123 +123967,25 @@ var MqttAclStore = class MqttAclStore {
|
|
|
123615
123967
|
*
|
|
123616
123968
|
* Lifecycle:
|
|
123617
123969
|
* - External broker entries are persisted in `this.activeExternalBrokers`.
|
|
123618
|
-
* - The embedded broker is
|
|
123619
|
-
*
|
|
123620
|
-
*
|
|
123621
|
-
* reassembles them into an `EmbeddedBrokerEntry`.
|
|
123622
|
-
* - On boot we start the embedded broker when `embeddedEnabled`.
|
|
123623
|
-
* - `addBroker` / `removeBroker` / `startEmbeddedBroker` /
|
|
123624
|
-
* `stopEmbeddedBroker` mutate the config and the runtime state.
|
|
123970
|
+
* - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
|
|
123971
|
+
* `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
|
|
123972
|
+
* - `addBroker` / `removeBroker` mutate the records and the runtime state.
|
|
123625
123973
|
* - `testConnection` does a one-shot `mqtt.connect` race against a 3s
|
|
123626
123974
|
* timeout AND refreshes the probe cache for the broker.
|
|
123627
123975
|
*/
|
|
123628
123976
|
/** bcrypt cost factor — exported for tests. */
|
|
123629
123977
|
var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
|
|
123630
123978
|
var PROBE_INTERVAL_MS = 3e4;
|
|
123631
|
-
var DEFAULT_EMBEDDED_PORT = 1883;
|
|
123632
|
-
/**
|
|
123633
|
-
* Per-kind broker creation form for kind='mqtt', rendered by the unified
|
|
123634
|
-
* Broker manager / create picker (via `broker.getSettingsSchema`). PURE
|
|
123635
|
-
* DATA — emits the flat `{ url, username, password, clientIdPrefix }`
|
|
123636
|
-
* shape `broker.add`'s `settings` payload consumes. Mirrors the external
|
|
123637
|
-
* broker fields `addBroker` accepts.
|
|
123638
|
-
*/
|
|
123639
|
-
var mqttBrokerSettingsSchema = { sections: [{
|
|
123640
|
-
id: "mqtt-broker-connection",
|
|
123641
|
-
title: "MQTT connection",
|
|
123642
|
-
titleKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.title",
|
|
123643
|
-
columns: 1,
|
|
123644
|
-
fields: [
|
|
123645
|
-
{
|
|
123646
|
-
type: "boolean",
|
|
123647
|
-
key: "onboard",
|
|
123648
|
-
label: "Run an onboard broker",
|
|
123649
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.label",
|
|
123650
|
-
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.",
|
|
123651
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.description",
|
|
123652
|
-
default: false
|
|
123653
|
-
},
|
|
123654
|
-
{
|
|
123655
|
-
type: "number",
|
|
123656
|
-
key: "port",
|
|
123657
|
-
label: "Port",
|
|
123658
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.label",
|
|
123659
|
-
placeholder: "(auto)",
|
|
123660
|
-
description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
|
|
123661
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.description",
|
|
123662
|
-
showWhen: {
|
|
123663
|
-
field: "onboard",
|
|
123664
|
-
equals: true
|
|
123665
|
-
}
|
|
123666
|
-
},
|
|
123667
|
-
{
|
|
123668
|
-
type: "boolean",
|
|
123669
|
-
key: "allowAnonymous",
|
|
123670
|
-
label: "Allow anonymous",
|
|
123671
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.label",
|
|
123672
|
-
description: "Permit clients to connect without a username/password.",
|
|
123673
|
-
descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.description",
|
|
123674
|
-
default: false,
|
|
123675
|
-
showWhen: {
|
|
123676
|
-
field: "onboard",
|
|
123677
|
-
equals: true
|
|
123678
|
-
}
|
|
123679
|
-
},
|
|
123680
|
-
{
|
|
123681
|
-
type: "text",
|
|
123682
|
-
key: "url",
|
|
123683
|
-
label: "Broker URL",
|
|
123684
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.url.label",
|
|
123685
|
-
placeholder: "mqtt://localhost:1883",
|
|
123686
|
-
showWhen: {
|
|
123687
|
-
field: "onboard",
|
|
123688
|
-
equals: false
|
|
123689
|
-
}
|
|
123690
|
-
},
|
|
123691
|
-
{
|
|
123692
|
-
type: "text",
|
|
123693
|
-
key: "username",
|
|
123694
|
-
label: "Username",
|
|
123695
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.username.label",
|
|
123696
|
-
placeholder: "(optional)"
|
|
123697
|
-
},
|
|
123698
|
-
{
|
|
123699
|
-
type: "password",
|
|
123700
|
-
key: "password",
|
|
123701
|
-
label: "Password",
|
|
123702
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.password.label",
|
|
123703
|
-
showToggle: true
|
|
123704
|
-
},
|
|
123705
|
-
{
|
|
123706
|
-
type: "text",
|
|
123707
|
-
key: "clientIdPrefix",
|
|
123708
|
-
label: "Client ID prefix",
|
|
123709
|
-
labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.clientIdPrefix.label",
|
|
123710
|
-
placeholder: "(optional)",
|
|
123711
|
-
showWhen: {
|
|
123712
|
-
field: "onboard",
|
|
123713
|
-
equals: false
|
|
123714
|
-
}
|
|
123715
|
-
}
|
|
123716
|
-
]
|
|
123717
|
-
}] };
|
|
123718
123979
|
var DEFAULT_CONFIG = {
|
|
123719
|
-
embeddedEnabled: false,
|
|
123720
|
-
embeddedId: "",
|
|
123721
|
-
embeddedPort: DEFAULT_EMBEDDED_PORT,
|
|
123722
|
-
embeddedAllowAnonymous: false,
|
|
123723
|
-
embeddedUsername: "",
|
|
123724
|
-
embeddedPassword: "",
|
|
123725
123980
|
embeddedClients: [],
|
|
123726
123981
|
embeddedAcl: []
|
|
123727
123982
|
};
|
|
123728
123983
|
var MqttBrokerAddon = class extends BaseAddon {
|
|
123729
|
-
|
|
123730
|
-
/** Per-entry onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
|
|
123984
|
+
/** Per-record onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
|
|
123731
123985
|
onboardRuntimes = /* @__PURE__ */ new Map();
|
|
123732
123986
|
probeCache = null;
|
|
123733
123987
|
brokerStore = null;
|
|
123734
|
-
|
|
123988
|
+
brokerInstances = null;
|
|
123735
123989
|
clientStore = null;
|
|
123736
123990
|
aclStore = null;
|
|
123737
123991
|
/** The external broker rows: the `brokers` records, secrets resolved. */
|
|
@@ -123745,6 +123999,20 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123745
123999
|
if (this.brokerStore === null) throw new Error("mqtt-broker: the broker records are not open");
|
|
123746
124000
|
return this.brokerStore;
|
|
123747
124001
|
}
|
|
124002
|
+
/** What the addon knows of a broker right now: its onboard server or its last probe. */
|
|
124003
|
+
liveState(id) {
|
|
124004
|
+
const running = this.onboardRuntimes.get(id);
|
|
124005
|
+
const probe = this.probeCache?.get(id);
|
|
124006
|
+
return {
|
|
124007
|
+
onboard: running === void 0 ? null : { connectedClients: running.aedes.connectedClients },
|
|
124008
|
+
...probe !== void 0 ? { probe: {
|
|
124009
|
+
status: probe.status,
|
|
124010
|
+
latencyMs: probe.latencyMs,
|
|
124011
|
+
...probe.error !== void 0 ? { error: probe.error } : {},
|
|
124012
|
+
lastCheckedAt: probe.lastCheckedAt
|
|
124013
|
+
} } : {}
|
|
124014
|
+
};
|
|
124015
|
+
}
|
|
123748
124016
|
async persistExternalBrokers(entries) {
|
|
123749
124017
|
this.activeExternalBrokers = [...entries];
|
|
123750
124018
|
await this.requireBrokerStore().replaceAll(entries);
|
|
@@ -123794,19 +124062,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123794
124062
|
this.brokerStore = MqttBrokerStore.of(records);
|
|
123795
124063
|
this.clientStore = MqttClientStore.of(records);
|
|
123796
124064
|
this.aclStore = MqttAclStore.of(records);
|
|
123797
|
-
this.
|
|
123798
|
-
|
|
123799
|
-
|
|
123800
|
-
|
|
123801
|
-
|
|
123802
|
-
|
|
123803
|
-
|
|
123804
|
-
formSchema: () => this.brokerForm(),
|
|
123805
|
-
initialBody: (name) => ({
|
|
123806
|
-
name,
|
|
123807
|
-
url: "mqtt://"
|
|
123808
|
-
}),
|
|
123809
|
-
configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
|
|
124065
|
+
this.brokerInstances = buildMqttBrokerInstances(records, {
|
|
124066
|
+
formSchema: (id, onboard) => this.brokerForm(id, onboard),
|
|
124067
|
+
listMqttHaInstances: () => listMqttHaInstancesOver(this.ctx.api),
|
|
124068
|
+
freePort: () => findFreePort(),
|
|
124069
|
+
probe: (cfg) => this.probeMqtt(cfg),
|
|
124070
|
+
onboardListening: (id) => this.onboardRuntimes.has(id),
|
|
124071
|
+
liveState: (id) => this.liveState(id),
|
|
123810
124072
|
onChanged: async () => {
|
|
123811
124073
|
await this.refreshExternalBrokers();
|
|
123812
124074
|
}
|
|
@@ -123835,29 +124097,9 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123835
124097
|
});
|
|
123836
124098
|
this.probeCache.setBrokersSource(() => this.collectExternalProbeInputs());
|
|
123837
124099
|
this.probeCache.start();
|
|
123838
|
-
const persistedEmbedded = this.deriveEmbeddedEntry();
|
|
123839
|
-
if (persistedEmbedded) try {
|
|
123840
|
-
await this.spawnEmbedded(persistedEmbedded);
|
|
123841
|
-
this.ctx.logger.info("embedded mqtt broker restarted", {
|
|
123842
|
-
meta: { port: persistedEmbedded.port },
|
|
123843
|
-
tags: {
|
|
123844
|
-
topic: "mqtt-broker",
|
|
123845
|
-
phase: "embedded-restarted"
|
|
123846
|
-
}
|
|
123847
|
-
});
|
|
123848
|
-
} catch (err) {
|
|
123849
|
-
this.ctx.logger.warn("embedded mqtt broker failed to restart", {
|
|
123850
|
-
meta: { error: errMsg(err) },
|
|
123851
|
-
tags: {
|
|
123852
|
-
topic: "mqtt-broker",
|
|
123853
|
-
phase: "embedded-restart-failed"
|
|
123854
|
-
}
|
|
123855
|
-
});
|
|
123856
|
-
}
|
|
123857
124100
|
await this.reconcileOnboardRuntimes();
|
|
123858
124101
|
this.ctx.addDisposer(async () => {
|
|
123859
124102
|
await this.probeCache?.stop();
|
|
123860
|
-
await this.shutdownEmbedded().catch(() => void 0);
|
|
123861
124103
|
await this.shutdownAllOnboard();
|
|
123862
124104
|
});
|
|
123863
124105
|
const provider = {
|
|
@@ -123869,11 +124111,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123869
124111
|
startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
|
|
123870
124112
|
stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
|
|
123871
124113
|
getStatus: async () => ({
|
|
123872
|
-
brokerCount: this.activeExternalBrokers.length
|
|
123873
|
-
embeddedRunning: this.
|
|
124114
|
+
brokerCount: this.activeExternalBrokers.length,
|
|
124115
|
+
embeddedRunning: this.onboardRuntimes.size > 0
|
|
123874
124116
|
})
|
|
123875
124117
|
};
|
|
123876
124118
|
const unifiedProvider = this.buildUnifiedBrokerProvider();
|
|
124119
|
+
const brokerInstances = this.brokerInstances;
|
|
124120
|
+
if (brokerInstances === null) throw new Error("mqtt-broker: the broker instances are not built");
|
|
123877
124121
|
return [
|
|
123878
124122
|
{
|
|
123879
124123
|
capability: mqttBrokerCapability,
|
|
@@ -123885,111 +124129,21 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
123885
124129
|
},
|
|
123886
124130
|
{
|
|
123887
124131
|
capability: integrationInstancesCapability,
|
|
123888
|
-
provider:
|
|
124132
|
+
provider: brokerInstances
|
|
123889
124133
|
}
|
|
123890
124134
|
];
|
|
123891
124135
|
}
|
|
124136
|
+
/**
|
|
124137
|
+
* The bus-only `broker` cap (D1013). The brokers themselves are the `integration-instances`
|
|
124138
|
+
* provider; this one only answers the config read and the status.
|
|
124139
|
+
*/
|
|
123892
124140
|
buildUnifiedBrokerProvider() {
|
|
123893
|
-
const
|
|
123894
|
-
const ownerAddonId = this.ctx.id;
|
|
123895
|
-
const adapt = (info) => ({
|
|
123896
|
-
id: info.id,
|
|
123897
|
-
addonId: ownerAddonId,
|
|
123898
|
-
name: info.name,
|
|
123899
|
-
kind: KIND,
|
|
123900
|
-
status: mapMqttStatus(info.status),
|
|
123901
|
-
info: {
|
|
123902
|
-
...info.latencyMs !== null ? { latencyMs: info.latencyMs } : {},
|
|
123903
|
-
...info.kind === "embedded" ? {
|
|
123904
|
-
embedded: true,
|
|
123905
|
-
connectedClients: info.connectedClients ?? 0
|
|
123906
|
-
} : {}
|
|
123907
|
-
},
|
|
123908
|
-
lastCheckedAt: info.lastCheckedAt ?? null,
|
|
123909
|
-
error: info.error ?? null
|
|
123910
|
-
});
|
|
123911
|
-
const owns = (id) => {
|
|
123912
|
-
if (this.activeExternalBrokers.some((b) => b.id === id)) return true;
|
|
123913
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
123914
|
-
return embedded !== null && embedded.id === id;
|
|
123915
|
-
};
|
|
124141
|
+
const owns = (id) => this.activeExternalBrokers.some((b) => b.id === id);
|
|
123916
124142
|
return {
|
|
123917
|
-
list: async ({ kind }) => {
|
|
123918
|
-
if (kind && kind !== KIND) return [];
|
|
123919
|
-
return (await this.listBrokers()).map(adapt);
|
|
123920
|
-
},
|
|
123921
|
-
get: async ({ id }) => {
|
|
123922
|
-
const match = (await this.listBrokers()).find((b) => b.id === id);
|
|
123923
|
-
return match ? adapt(match) : null;
|
|
123924
|
-
},
|
|
123925
|
-
listProviders: async () => [{
|
|
123926
|
-
addonId: ownerAddonId,
|
|
123927
|
-
kinds: [{
|
|
123928
|
-
kind: KIND,
|
|
123929
|
-
label: "MQTT"
|
|
123930
|
-
}]
|
|
123931
|
-
}],
|
|
123932
|
-
add: async ({ kind, name, settings, id }) => {
|
|
123933
|
-
if (kind !== KIND) throw new Error(`mqtt-broker: only kind '${KIND}' is handled here (got '${kind}')`);
|
|
123934
|
-
if (id !== void 0) throw new Error("mqtt-broker: adopting an existing broker id is not supported by this provider");
|
|
123935
|
-
if (isOnboardRequested(settings)) return this.addOnboardBroker(name, settings);
|
|
123936
|
-
const url = typeof settings["url"] === "string" ? settings["url"] : "";
|
|
123937
|
-
if (!url) throw new Error("mqtt-broker.add: settings.url is required");
|
|
123938
|
-
const input = {
|
|
123939
|
-
name,
|
|
123940
|
-
url,
|
|
123941
|
-
...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
|
|
123942
|
-
...typeof settings["password"] === "string" ? { password: settings["password"] } : {},
|
|
123943
|
-
...typeof settings["clientIdPrefix"] === "string" ? { clientIdPrefix: settings["clientIdPrefix"] } : {}
|
|
123944
|
-
};
|
|
123945
|
-
return this.addBroker(input);
|
|
123946
|
-
},
|
|
123947
|
-
remove: async ({ id }) => {
|
|
123948
|
-
if (!owns(id)) return;
|
|
123949
|
-
await this.removeBroker(id);
|
|
123950
|
-
},
|
|
123951
|
-
testConnection: async ({ id }) => {
|
|
123952
|
-
if (!owns(id)) return {
|
|
123953
|
-
ok: false,
|
|
123954
|
-
error: "unknown broker"
|
|
123955
|
-
};
|
|
123956
|
-
return this.testConnection(id);
|
|
123957
|
-
},
|
|
123958
|
-
getSettings: async ({ id }) => {
|
|
123959
|
-
if (!owns(id)) return null;
|
|
123960
|
-
return this.getBrokerConfig(id);
|
|
123961
|
-
},
|
|
123962
|
-
setSettings: async () => {
|
|
123963
|
-
throw new Error("mqtt-broker.setSettings: not supported — use remove + add to replace a broker");
|
|
123964
|
-
},
|
|
123965
124143
|
getBrokerConfig: async ({ id }) => {
|
|
123966
124144
|
if (!owns(id)) return null;
|
|
123967
124145
|
return this.getBrokerConfig(id);
|
|
123968
124146
|
},
|
|
123969
|
-
getSettingsSchema: async ({ kind }) => {
|
|
123970
|
-
if (kind !== KIND) return null;
|
|
123971
|
-
return mqttBrokerSettingsSchema;
|
|
123972
|
-
},
|
|
123973
|
-
testSettings: async ({ kind, settings }) => {
|
|
123974
|
-
if (kind !== KIND) return {
|
|
123975
|
-
ok: false,
|
|
123976
|
-
error: `unsupported kind: ${kind}`
|
|
123977
|
-
};
|
|
123978
|
-
if (isOnboardRequested(settings)) return {
|
|
123979
|
-
ok: true,
|
|
123980
|
-
latencyMs: 0
|
|
123981
|
-
};
|
|
123982
|
-
const url = typeof settings["url"] === "string" ? settings["url"] : "";
|
|
123983
|
-
if (!url) return {
|
|
123984
|
-
ok: false,
|
|
123985
|
-
error: "settings.url is required"
|
|
123986
|
-
};
|
|
123987
|
-
return this.probeMqtt({
|
|
123988
|
-
url,
|
|
123989
|
-
...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
|
|
123990
|
-
...typeof settings["password"] === "string" ? { password: settings["password"] } : {}
|
|
123991
|
-
});
|
|
123992
|
-
},
|
|
123993
124147
|
publish: async () => {
|
|
123994
124148
|
throw new Error("mqtt-broker.publish: not supported — fetch getBrokerConfig + open your own mqtt.js client (see \"NOT a pub/sub proxy\" in mqtt-broker.cap.ts)");
|
|
123995
124149
|
},
|
|
@@ -124003,7 +124157,7 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124003
124157
|
getStatus: async () => {
|
|
124004
124158
|
const all = await this.listBrokers();
|
|
124005
124159
|
return {
|
|
124006
|
-
brokerCount: this.activeExternalBrokers.length
|
|
124160
|
+
brokerCount: this.activeExternalBrokers.length,
|
|
124007
124161
|
connectedCount: all.filter((b) => b.status === "connected").length
|
|
124008
124162
|
};
|
|
124009
124163
|
}
|
|
@@ -124011,35 +124165,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124011
124165
|
}
|
|
124012
124166
|
async onShutdown() {
|
|
124013
124167
|
await this.probeCache?.stop();
|
|
124014
|
-
await this.shutdownEmbedded().catch(() => void 0);
|
|
124015
124168
|
await this.shutdownAllOnboard();
|
|
124016
124169
|
}
|
|
124017
124170
|
async onConfigChanged() {
|
|
124018
124171
|
this.activeExternalBrokers = await this.requireBrokerStore().listResolved();
|
|
124019
124172
|
await this.syncEmbeddedEntriesFromConfig();
|
|
124020
124173
|
await this.reconcileOnboardRuntimes();
|
|
124021
|
-
|
|
124022
|
-
const running = this.embeddedRuntime?.entry ?? null;
|
|
124023
|
-
if (!desired && running) {
|
|
124024
|
-
await this.shutdownEmbedded();
|
|
124025
|
-
return;
|
|
124026
|
-
}
|
|
124027
|
-
if (desired && !running) {
|
|
124028
|
-
await this.spawnEmbedded(desired).catch((err) => {
|
|
124029
|
-
this.ctx.logger.warn("embedded mqtt broker spawn (on config change) failed", { meta: { error: errMsg(err) } });
|
|
124030
|
-
});
|
|
124031
|
-
return;
|
|
124032
|
-
}
|
|
124033
|
-
if (desired && running) {
|
|
124034
|
-
if (requiresEmbeddedRespawn(desired, running)) {
|
|
124035
|
-
await this.shutdownEmbedded();
|
|
124036
|
-
await this.spawnEmbedded(desired).catch((err) => {
|
|
124037
|
-
this.ctx.logger.warn("embedded mqtt broker respawn failed", { meta: { error: errMsg(err) } });
|
|
124038
|
-
});
|
|
124039
|
-
return;
|
|
124040
|
-
}
|
|
124041
|
-
this.reconcileEmbeddedClients();
|
|
124042
|
-
}
|
|
124174
|
+
this.reconcileEmbeddedClients();
|
|
124043
124175
|
}
|
|
124044
124176
|
async listBrokers() {
|
|
124045
124177
|
const out = [];
|
|
@@ -124073,22 +124205,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124073
124205
|
};
|
|
124074
124206
|
out.push(info);
|
|
124075
124207
|
}
|
|
124076
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
124077
|
-
if (embedded) {
|
|
124078
|
-
const running = this.embeddedRuntime;
|
|
124079
|
-
const status = running !== null ? "connected" : "disconnected";
|
|
124080
|
-
const info = {
|
|
124081
|
-
id: embedded.id,
|
|
124082
|
-
name: embedded.name,
|
|
124083
|
-
url: `mqtt://127.0.0.1:${embedded.port}`,
|
|
124084
|
-
kind: "embedded",
|
|
124085
|
-
status,
|
|
124086
|
-
latencyMs: null,
|
|
124087
|
-
...running ? { connectedClients: running.aedes.connectedClients } : {},
|
|
124088
|
-
lastCheckedAt: Date.now()
|
|
124089
|
-
};
|
|
124090
|
-
out.push(info);
|
|
124091
|
-
}
|
|
124092
124208
|
return out;
|
|
124093
124209
|
}
|
|
124094
124210
|
getBrokerConfig(id) {
|
|
@@ -124103,15 +124219,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124103
124219
|
...external.clientIdPrefix !== void 0 ? { clientIdPrefix: external.clientIdPrefix } : {}
|
|
124104
124220
|
};
|
|
124105
124221
|
}
|
|
124106
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
124107
|
-
if (embedded && embedded.id === id) {
|
|
124108
|
-
const password = this.storedPassword(embedded.password);
|
|
124109
|
-
return {
|
|
124110
|
-
url: `mqtt://127.0.0.1:${embedded.port}`,
|
|
124111
|
-
...embedded.username !== void 0 ? { username: embedded.username } : {},
|
|
124112
|
-
...password !== void 0 ? { password } : {}
|
|
124113
|
-
};
|
|
124114
|
-
}
|
|
124115
124222
|
throw new Error(`mqtt-broker: broker '${id}' not found`);
|
|
124116
124223
|
}
|
|
124117
124224
|
async addBroker(input) {
|
|
@@ -124132,44 +124239,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124132
124239
|
} });
|
|
124133
124240
|
return { id };
|
|
124134
124241
|
}
|
|
124135
|
-
/**
|
|
124136
|
-
* Create a per-entry ONBOARD broker from `broker.add` settings: allocate
|
|
124137
|
-
* (or honour) a port, persist an `ExternalBrokerEntry` carrying an
|
|
124138
|
-
* `onboard` descriptor, then spawn its dedicated `aedes` server.
|
|
124139
|
-
*/
|
|
124140
|
-
async addOnboardBroker(name, settings) {
|
|
124141
|
-
const id = randomUUID();
|
|
124142
|
-
const port = readOnboardPort(settings) ?? await findFreePort();
|
|
124143
|
-
const built = buildOnboardEntry({
|
|
124144
|
-
id,
|
|
124145
|
-
name,
|
|
124146
|
-
port,
|
|
124147
|
-
settings
|
|
124148
|
-
});
|
|
124149
|
-
const entry = {
|
|
124150
|
-
id: built.id,
|
|
124151
|
-
name: built.name,
|
|
124152
|
-
url: built.url,
|
|
124153
|
-
...built.username !== void 0 ? { username: built.username } : {},
|
|
124154
|
-
...built.password !== void 0 ? { password: built.password } : {},
|
|
124155
|
-
onboard: built.onboard
|
|
124156
|
-
};
|
|
124157
|
-
const next = [...this.activeExternalBrokers, entry];
|
|
124158
|
-
await this.persistExternalBrokers(next);
|
|
124159
|
-
const spawn = this.onboardSpawnEntry(entry);
|
|
124160
|
-
if (spawn) await this.spawnOnboard(spawn);
|
|
124161
|
-
this.ctx.logger.info("onboard mqtt broker added", {
|
|
124162
|
-
meta: {
|
|
124163
|
-
id,
|
|
124164
|
-
port
|
|
124165
|
-
},
|
|
124166
|
-
tags: {
|
|
124167
|
-
topic: "mqtt-broker",
|
|
124168
|
-
phase: "onboard-started"
|
|
124169
|
-
}
|
|
124170
|
-
});
|
|
124171
|
-
return { id };
|
|
124172
|
-
}
|
|
124173
124242
|
async removeBroker(id) {
|
|
124174
124243
|
const removed = this.activeExternalBrokers.find((b) => b.id === id);
|
|
124175
124244
|
if (removed) {
|
|
@@ -124180,13 +124249,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124180
124249
|
this.ctx.logger.info("mqtt broker removed", { meta: { id } });
|
|
124181
124250
|
return;
|
|
124182
124251
|
}
|
|
124183
|
-
const embedded = this.deriveEmbeddedEntry();
|
|
124184
|
-
if (embedded && embedded.id === id) {
|
|
124185
|
-
await this.shutdownEmbedded();
|
|
124186
|
-
await this.updateGlobalSettings({ embeddedEnabled: false });
|
|
124187
|
-
this.ctx.logger.info("embedded mqtt broker removed", { meta: { id } });
|
|
124188
|
-
return;
|
|
124189
|
-
}
|
|
124190
124252
|
}
|
|
124191
124253
|
async testConnection(id) {
|
|
124192
124254
|
let cfg;
|
|
@@ -124205,7 +124267,7 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124205
124267
|
* One-shot connectivity probe against an MQTT endpoint built from a
|
|
124206
124268
|
* flat config (`{url, username?, password?}`). Shared by the
|
|
124207
124269
|
* id-keyed `testConnection` (existing broker) and the pre-creation
|
|
124208
|
-
* `
|
|
124270
|
+
* the instance test (`testInstance`) of a broker not saved yet (the typed values).
|
|
124209
124271
|
* Never throws — every failure resolves to `{ ok: false, error }`.
|
|
124210
124272
|
*/
|
|
124211
124273
|
async probeMqtt(cfg) {
|
|
@@ -124250,30 +124312,46 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124250
124312
|
});
|
|
124251
124313
|
});
|
|
124252
124314
|
}
|
|
124315
|
+
/** The `brokers` records, for the two legacy cap methods that create or switch off one. */
|
|
124316
|
+
brokerRecordHandle() {
|
|
124317
|
+
const handle = this.ctx.records?.kind(mqttBrokersEntryKind);
|
|
124318
|
+
if (handle === void 0) throw new Error("mqtt-broker: the broker records are not open");
|
|
124319
|
+
return handle;
|
|
124320
|
+
}
|
|
124321
|
+
/** Create an ONBOARD broker record; the reconcile then spawns its `aedes` server. */
|
|
124253
124322
|
async startEmbeddedBroker(input) {
|
|
124254
|
-
|
|
124255
|
-
const
|
|
124256
|
-
const entry = {
|
|
124323
|
+
const id = randomUUID();
|
|
124324
|
+
const built = buildOnboardEntry({
|
|
124257
124325
|
id,
|
|
124258
|
-
name:
|
|
124326
|
+
name: LEGACY_EMBEDDED_NAME,
|
|
124259
124327
|
port: input.port,
|
|
124260
|
-
|
|
124261
|
-
|
|
124262
|
-
|
|
124263
|
-
|
|
124264
|
-
|
|
124265
|
-
await this.updateGlobalSettings({
|
|
124266
|
-
embeddedEnabled: true,
|
|
124267
|
-
embeddedId: id,
|
|
124268
|
-
embeddedPort: input.port,
|
|
124269
|
-
embeddedAllowAnonymous: input.allowAnonymous,
|
|
124270
|
-
embeddedUsername: input.username ?? "",
|
|
124271
|
-
embeddedPassword: input.password ?? ""
|
|
124328
|
+
settings: {
|
|
124329
|
+
allowAnonymous: input.allowAnonymous,
|
|
124330
|
+
...input.username !== void 0 ? { username: input.username } : {},
|
|
124331
|
+
...input.password !== void 0 ? { password: input.password } : {}
|
|
124332
|
+
}
|
|
124272
124333
|
});
|
|
124334
|
+
const handle = this.brokerRecordHandle();
|
|
124335
|
+
await handle.put({
|
|
124336
|
+
id,
|
|
124337
|
+
data: mqttBrokersEntryKind.schema.parse({
|
|
124338
|
+
name: built.name,
|
|
124339
|
+
url: built.url,
|
|
124340
|
+
...built.username !== void 0 ? { username: built.username } : {},
|
|
124341
|
+
...built.password !== void 0 ? { password: built.password } : {},
|
|
124342
|
+
onboard: built.onboard
|
|
124343
|
+
})
|
|
124344
|
+
}, { ifRev: 0 });
|
|
124345
|
+
await this.refreshExternalBrokers();
|
|
124346
|
+
if (!this.onboardRuntimes.has(id)) {
|
|
124347
|
+
await handle.delete(id);
|
|
124348
|
+
await this.refreshExternalBrokers();
|
|
124349
|
+
throw new Error(`embedded broker could not listen on port ${input.port}`);
|
|
124350
|
+
}
|
|
124273
124351
|
this.ctx.logger.info("embedded mqtt broker started", {
|
|
124274
124352
|
meta: {
|
|
124275
124353
|
id,
|
|
124276
|
-
port:
|
|
124354
|
+
port: input.port
|
|
124277
124355
|
},
|
|
124278
124356
|
tags: {
|
|
124279
124357
|
topic: "mqtt-broker",
|
|
@@ -124295,20 +124373,22 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124295
124373
|
});
|
|
124296
124374
|
return {
|
|
124297
124375
|
id,
|
|
124298
|
-
url:
|
|
124376
|
+
url: built.url
|
|
124299
124377
|
};
|
|
124300
124378
|
}
|
|
124379
|
+
/** Switch an onboard broker off (its record stays, so it can be switched on again). */
|
|
124301
124380
|
async stopEmbeddedBroker(id) {
|
|
124302
|
-
|
|
124303
|
-
await
|
|
124304
|
-
|
|
124381
|
+
const handle = this.brokerRecordHandle();
|
|
124382
|
+
const row = await handle.get(id);
|
|
124383
|
+
if (row === null || row.data.onboard === void 0) return;
|
|
124384
|
+
await handle.patch(id, { enabled: false });
|
|
124385
|
+
await this.refreshExternalBrokers();
|
|
124305
124386
|
this.ctx.logger.info("embedded mqtt broker stopped", { meta: { id } });
|
|
124306
124387
|
}
|
|
124307
124388
|
/**
|
|
124308
124389
|
* Build + listen an `aedes` server for `entry` and RETURN the runtime
|
|
124309
|
-
* without assigning it to any field
|
|
124310
|
-
*
|
|
124311
|
-
* brokers ({@link spawnOnboard}).
|
|
124390
|
+
* without assigning it to any field (the onboard brokers keep theirs in
|
|
124391
|
+
* {@link spawnOnboard}).
|
|
124312
124392
|
*/
|
|
124313
124393
|
async spawnAedes(entry) {
|
|
124314
124394
|
const aedes = new import_aedes.default();
|
|
@@ -124347,15 +124427,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124347
124427
|
aedes.close(() => resolve());
|
|
124348
124428
|
});
|
|
124349
124429
|
}
|
|
124350
|
-
async spawnEmbedded(entry) {
|
|
124351
|
-
this.embeddedRuntime = await this.spawnAedes(entry);
|
|
124352
|
-
}
|
|
124353
|
-
async shutdownEmbedded() {
|
|
124354
|
-
if (!this.embeddedRuntime) return;
|
|
124355
|
-
const runtime = this.embeddedRuntime;
|
|
124356
|
-
this.embeddedRuntime = null;
|
|
124357
|
-
await this.closeRuntime(runtime);
|
|
124358
|
-
}
|
|
124359
124430
|
/** Spawn a dedicated onboard `aedes` server for an onboard entry. */
|
|
124360
124431
|
async spawnOnboard(entry) {
|
|
124361
124432
|
const runtime = await this.spawnAedes(entry);
|
|
@@ -124492,8 +124563,9 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124492
124563
|
* (or whose username was removed entirely) get force-closed.
|
|
124493
124564
|
*/
|
|
124494
124565
|
reconcileEmbeddedClients() {
|
|
124495
|
-
const runtime
|
|
124496
|
-
|
|
124566
|
+
for (const runtime of this.onboardRuntimes.values()) this.reconcileRuntimeClients(runtime);
|
|
124567
|
+
}
|
|
124568
|
+
reconcileRuntimeClients(runtime) {
|
|
124497
124569
|
const prevSnapshot = runtime.credentialsSnapshot;
|
|
124498
124570
|
const nextSnapshot = this.buildCredentialsSnapshot();
|
|
124499
124571
|
const affectedUsers = [];
|
|
@@ -124532,26 +124604,6 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124532
124604
|
for (const c of this.activeEmbeddedClients) m.set(c.username, c.passwordHash);
|
|
124533
124605
|
return m;
|
|
124534
124606
|
}
|
|
124535
|
-
/**
|
|
124536
|
-
* Reassemble the internal {@link EmbeddedBrokerEntry} from the flat
|
|
124537
|
-
* `embedded*` config keys the settings form writes. Returns `null`
|
|
124538
|
-
* when the embedded broker is disabled — that's the signal
|
|
124539
|
-
* `onConfigChanged` / `onInitialize` use to decide whether to spawn
|
|
124540
|
-
* or shut down the `aedes` instance.
|
|
124541
|
-
*/
|
|
124542
|
-
deriveEmbeddedEntry() {
|
|
124543
|
-
if (!this.config.embeddedEnabled) return null;
|
|
124544
|
-
const username = this.config.embeddedUsername;
|
|
124545
|
-
const password = this.config.embeddedPassword;
|
|
124546
|
-
return {
|
|
124547
|
-
id: this.config.embeddedId || "embedded",
|
|
124548
|
-
name: "Embedded broker",
|
|
124549
|
-
port: this.config.embeddedPort,
|
|
124550
|
-
allowAnonymous: this.config.embeddedAllowAnonymous,
|
|
124551
|
-
...username.length > 0 ? { username } : {},
|
|
124552
|
-
...password.length > 0 ? { password } : {}
|
|
124553
|
-
};
|
|
124554
|
-
}
|
|
124555
124607
|
collectExternalProbeInputs() {
|
|
124556
124608
|
const out = [];
|
|
124557
124609
|
for (const b of this.activeExternalBrokers) {
|
|
@@ -124588,19 +124640,13 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124588
124640
|
* here (no migration code ships): the operator's one-off `mqtt` step does.
|
|
124589
124641
|
*/
|
|
124590
124642
|
reportRetiredCiphertext() {
|
|
124591
|
-
const paths = retiredCiphertextPaths({
|
|
124592
|
-
brokers: this.activeExternalBrokers,
|
|
124593
|
-
embeddedPassword: this.config.embeddedPassword
|
|
124594
|
-
});
|
|
124643
|
+
const paths = retiredCiphertextPaths({ brokers: this.activeExternalBrokers });
|
|
124595
124644
|
if (paths.length === 0) return;
|
|
124596
124645
|
const unreadable = [];
|
|
124597
124646
|
const values = [...this.activeExternalBrokers.map((b) => ({
|
|
124598
124647
|
id: `brokers[${b.id}].password`,
|
|
124599
124648
|
v: b.password
|
|
124600
|
-
}))
|
|
124601
|
-
id: "embeddedPassword",
|
|
124602
|
-
v: this.config.embeddedPassword
|
|
124603
|
-
}];
|
|
124649
|
+
}))];
|
|
124604
124650
|
for (const { id, v } of values) try {
|
|
124605
124651
|
this.storedPassword(v);
|
|
124606
124652
|
} catch (err) {
|
|
@@ -124624,13 +124670,79 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124624
124670
|
}
|
|
124625
124671
|
});
|
|
124626
124672
|
}
|
|
124627
|
-
|
|
124673
|
+
/**
|
|
124674
|
+
* The broker form. The create form (`id` null) offers "run an onboard broker" (CamStack hosts an
|
|
124675
|
+
* `aedes` server on this hub: a port, anonymous access) or an external URL; an existing onboard
|
|
124676
|
+
* broker edits only its credentials, since its address is the one it was given.
|
|
124677
|
+
*/
|
|
124678
|
+
brokerForm(id, onboard) {
|
|
124679
|
+
const credentials = [{
|
|
124680
|
+
type: "text",
|
|
124681
|
+
key: "username",
|
|
124682
|
+
label: "Username",
|
|
124683
|
+
labelKey: "mqtt-broker:fullSettings.brokers.username.label"
|
|
124684
|
+
}, {
|
|
124685
|
+
type: "password",
|
|
124686
|
+
key: "password",
|
|
124687
|
+
label: "Password",
|
|
124688
|
+
labelKey: "mqtt-broker:fullSettings.brokers.password.label",
|
|
124689
|
+
showToggle: true
|
|
124690
|
+
}];
|
|
124691
|
+
if (onboard) return this.schema({ sections: [{
|
|
124692
|
+
id: "broker",
|
|
124693
|
+
title: "Onboard broker",
|
|
124694
|
+
description: "A CamStack-hosted broker on this hub. Its port was chosen when it was created.",
|
|
124695
|
+
columns: 1,
|
|
124696
|
+
fields: credentials
|
|
124697
|
+
}] });
|
|
124698
|
+
const creating = id === null;
|
|
124699
|
+
const external = {
|
|
124700
|
+
field: "onboard",
|
|
124701
|
+
equals: false
|
|
124702
|
+
};
|
|
124628
124703
|
return this.schema({ sections: [{
|
|
124629
124704
|
id: "broker",
|
|
124630
|
-
title: "External broker",
|
|
124631
|
-
description: "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
|
|
124705
|
+
title: creating ? "MQTT broker" : "External broker",
|
|
124706
|
+
description: creating ? "Run a broker on this hub, or connect to one running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain." : "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
|
|
124632
124707
|
columns: 1,
|
|
124633
124708
|
fields: [
|
|
124709
|
+
...creating ? [
|
|
124710
|
+
{
|
|
124711
|
+
type: "boolean",
|
|
124712
|
+
key: "onboard",
|
|
124713
|
+
label: "Run an onboard broker",
|
|
124714
|
+
labelKey: "mqtt-broker:fullSettings.brokers.onboard.label",
|
|
124715
|
+
description: "Spin up a CamStack-hosted aedes broker on this hub instead of connecting to an external one. Each onboard broker gets its own dedicated server + port.",
|
|
124716
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.onboard.description",
|
|
124717
|
+
default: false
|
|
124718
|
+
},
|
|
124719
|
+
{
|
|
124720
|
+
type: "number",
|
|
124721
|
+
key: "port",
|
|
124722
|
+
label: "Port",
|
|
124723
|
+
labelKey: "mqtt-broker:fullSettings.brokers.port.label",
|
|
124724
|
+
placeholder: "(auto)",
|
|
124725
|
+
description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
|
|
124726
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.port.description",
|
|
124727
|
+
showWhen: {
|
|
124728
|
+
field: "onboard",
|
|
124729
|
+
equals: true
|
|
124730
|
+
}
|
|
124731
|
+
},
|
|
124732
|
+
{
|
|
124733
|
+
type: "boolean",
|
|
124734
|
+
key: "allowAnonymous",
|
|
124735
|
+
label: "Allow anonymous",
|
|
124736
|
+
labelKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.label",
|
|
124737
|
+
description: "Permit clients to connect without a username/password.",
|
|
124738
|
+
descriptionKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.description",
|
|
124739
|
+
default: false,
|
|
124740
|
+
showWhen: {
|
|
124741
|
+
field: "onboard",
|
|
124742
|
+
equals: true
|
|
124743
|
+
}
|
|
124744
|
+
}
|
|
124745
|
+
] : [],
|
|
124634
124746
|
{
|
|
124635
124747
|
type: "text",
|
|
124636
124748
|
key: "url",
|
|
@@ -124638,26 +124750,15 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124638
124750
|
labelKey: "mqtt-broker:fullSettings.brokers.url.label",
|
|
124639
124751
|
description: "mqtt://, mqtts://, ws://, wss://",
|
|
124640
124752
|
descriptionKey: "mqtt-broker:fullSettings.brokers.url.description",
|
|
124641
|
-
required: true
|
|
124642
|
-
},
|
|
124643
|
-
{
|
|
124644
|
-
type: "text",
|
|
124645
|
-
key: "username",
|
|
124646
|
-
label: "Username",
|
|
124647
|
-
labelKey: "mqtt-broker:fullSettings.brokers.username.label"
|
|
124648
|
-
},
|
|
124649
|
-
{
|
|
124650
|
-
type: "password",
|
|
124651
|
-
key: "password",
|
|
124652
|
-
label: "Password",
|
|
124653
|
-
labelKey: "mqtt-broker:fullSettings.brokers.password.label",
|
|
124654
|
-
showToggle: true
|
|
124753
|
+
...creating ? { showWhen: external } : { required: true }
|
|
124655
124754
|
},
|
|
124755
|
+
...credentials,
|
|
124656
124756
|
{
|
|
124657
124757
|
type: "text",
|
|
124658
124758
|
key: "clientIdPrefix",
|
|
124659
124759
|
label: "Client id prefix",
|
|
124660
|
-
labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label"
|
|
124760
|
+
labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label",
|
|
124761
|
+
...creating ? { showWhen: external } : {}
|
|
124661
124762
|
}
|
|
124662
124763
|
]
|
|
124663
124764
|
}] });
|
|
@@ -124668,6 +124769,41 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124668
124769
|
if (handle === void 0) throw new Error("records are not available");
|
|
124669
124770
|
return {
|
|
124670
124771
|
readRawBrokers: async () => (await this.ctx.settings?.readAddonStore())?.["brokers"],
|
|
124772
|
+
readLegacyEmbedded: async () => {
|
|
124773
|
+
const raw = await this.ctx.settings?.readAddonStore() ?? {};
|
|
124774
|
+
const secrets = this.ctx.secrets;
|
|
124775
|
+
const { value } = secrets === void 0 ? { value: raw } : await resolveSecretMarkers(raw, secrets, {
|
|
124776
|
+
unresolved: "keep-marker",
|
|
124777
|
+
ownedBy: secrets.owner
|
|
124778
|
+
});
|
|
124779
|
+
const text = (key) => {
|
|
124780
|
+
const v = value[key];
|
|
124781
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
124782
|
+
};
|
|
124783
|
+
const port = value["embeddedPort"];
|
|
124784
|
+
const username = text("embeddedUsername");
|
|
124785
|
+
const password = this.storedPassword(text("embeddedPassword"));
|
|
124786
|
+
const enabled = value["embeddedEnabled"] === true;
|
|
124787
|
+
if (!(enabled || text("embeddedId") !== void 0 || username !== void 0 || password !== void 0)) return null;
|
|
124788
|
+
return {
|
|
124789
|
+
enabled,
|
|
124790
|
+
id: text("embeddedId") ?? "embedded",
|
|
124791
|
+
port: typeof port === "number" && Number.isInteger(port) ? port : 1883,
|
|
124792
|
+
allowAnonymous: value["embeddedAllowAnonymous"] === true,
|
|
124793
|
+
...username !== void 0 ? { username } : {},
|
|
124794
|
+
...password !== void 0 ? { password } : {}
|
|
124795
|
+
};
|
|
124796
|
+
},
|
|
124797
|
+
readRawEmbeddedKeys: async () => {
|
|
124798
|
+
const raw = await this.ctx.settings?.readAddonStore() ?? {};
|
|
124799
|
+
return Object.fromEntries(LEGACY_EMBEDDED_KEYS.filter((key) => key in raw).map((key) => [key, raw[key]]));
|
|
124800
|
+
},
|
|
124801
|
+
putOnboardRecord: async (id, body) => {
|
|
124802
|
+
await handle.put({
|
|
124803
|
+
id,
|
|
124804
|
+
data: mqttBrokersEntryKind.schema.parse(body)
|
|
124805
|
+
}, { ifRev: 0 });
|
|
124806
|
+
},
|
|
124671
124807
|
readRecords: async () => {
|
|
124672
124808
|
const page = await handle.query({ limit: 1e3 });
|
|
124673
124809
|
return new Map(page.records.map((r) => [r.id, { hasPassword: (r.data.password ?? "").length > 0 }]));
|
|
@@ -124684,184 +124820,105 @@ var MqttBrokerAddon = class extends BaseAddon {
|
|
|
124684
124820
|
return ["mqtt-broker", "broker"];
|
|
124685
124821
|
}
|
|
124686
124822
|
/**
|
|
124687
|
-
* The
|
|
124823
|
+
* The credentials and the ACL of the onboard brokers. The brokers are records, listed
|
|
124688
124824
|
* on the Brokers page and edited as integration instances (`brokerForm`).
|
|
124689
124825
|
*/
|
|
124690
124826
|
async clusterSettingsSchema(cap) {
|
|
124691
124827
|
if (cap !== "mqtt-broker" && cap !== "broker") return null;
|
|
124692
|
-
return this.schema({ sections: [
|
|
124693
|
-
|
|
124694
|
-
|
|
124695
|
-
|
|
124696
|
-
|
|
124697
|
-
|
|
124698
|
-
|
|
124699
|
-
|
|
124700
|
-
|
|
124701
|
-
|
|
124702
|
-
|
|
124703
|
-
|
|
124704
|
-
|
|
124705
|
-
|
|
124706
|
-
|
|
124707
|
-
|
|
124708
|
-
|
|
124709
|
-
|
|
124710
|
-
|
|
124711
|
-
|
|
124712
|
-
|
|
124713
|
-
|
|
124714
|
-
|
|
124715
|
-
|
|
124716
|
-
|
|
124717
|
-
|
|
124718
|
-
|
|
124719
|
-
|
|
124720
|
-
|
|
124721
|
-
|
|
124722
|
-
|
|
124723
|
-
|
|
124724
|
-
|
|
124725
|
-
|
|
124726
|
-
|
|
124727
|
-
|
|
124728
|
-
|
|
124729
|
-
|
|
124730
|
-
|
|
124731
|
-
|
|
124732
|
-
|
|
124733
|
-
|
|
124734
|
-
|
|
124735
|
-
|
|
124736
|
-
|
|
124737
|
-
|
|
124738
|
-
|
|
124739
|
-
|
|
124740
|
-
|
|
124741
|
-
|
|
124742
|
-
|
|
124743
|
-
|
|
124744
|
-
|
|
124745
|
-
|
|
124746
|
-
|
|
124747
|
-
default: "",
|
|
124748
|
-
showWhen: {
|
|
124749
|
-
field: "embeddedEnabled",
|
|
124750
|
-
equals: true
|
|
124751
|
-
}
|
|
124752
|
-
}),
|
|
124753
|
-
this.field({
|
|
124754
|
-
type: "password",
|
|
124755
|
-
key: "embeddedPassword",
|
|
124756
|
-
label: "Shared password",
|
|
124757
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.label",
|
|
124758
|
-
description: "Kept in the keychain. Paired with the shared username above.",
|
|
124759
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.description",
|
|
124760
|
-
showToggle: true,
|
|
124761
|
-
default: "",
|
|
124762
|
-
showWhen: {
|
|
124763
|
-
field: "embeddedEnabled",
|
|
124764
|
-
equals: true
|
|
124765
|
-
}
|
|
124766
|
-
})
|
|
124767
|
-
]
|
|
124768
|
-
},
|
|
124769
|
-
{
|
|
124770
|
-
id: "mqtt-broker-embedded-clients",
|
|
124771
|
-
title: "Embedded broker — credentials",
|
|
124772
|
-
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
|
|
124773
|
-
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.",
|
|
124774
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
|
|
124775
|
-
columns: 1,
|
|
124776
|
-
fields: [{
|
|
124777
|
-
type: "editable-array",
|
|
124778
|
-
key: "embeddedClients",
|
|
124779
|
-
label: "Clients",
|
|
124780
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
|
|
124781
|
-
emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
|
|
124782
|
-
addLabel: "Add client",
|
|
124783
|
-
rowTitleTemplate: "{username}",
|
|
124784
|
-
defaultItem: {
|
|
124785
|
-
username: "",
|
|
124786
|
-
passwordHash: ""
|
|
124787
|
-
},
|
|
124788
|
-
itemFields: [{
|
|
124828
|
+
return this.schema({ sections: [{
|
|
124829
|
+
id: "mqtt-broker-embedded-clients",
|
|
124830
|
+
title: "Embedded broker — credentials",
|
|
124831
|
+
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
|
|
124832
|
+
description: "Per-client username + bcrypt password hash for the embedded aedes broker. Generate the hash with `npx bcrypt-cli` (or any bcrypt tool, cost factor 10) and paste it here.",
|
|
124833
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
|
|
124834
|
+
columns: 1,
|
|
124835
|
+
fields: [{
|
|
124836
|
+
type: "editable-array",
|
|
124837
|
+
key: "embeddedClients",
|
|
124838
|
+
label: "Clients",
|
|
124839
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
|
|
124840
|
+
emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
|
|
124841
|
+
addLabel: "Add client",
|
|
124842
|
+
rowTitleTemplate: "{username}",
|
|
124843
|
+
defaultItem: {
|
|
124844
|
+
username: "",
|
|
124845
|
+
passwordHash: ""
|
|
124846
|
+
},
|
|
124847
|
+
itemFields: [{
|
|
124848
|
+
type: "text",
|
|
124849
|
+
key: "username",
|
|
124850
|
+
label: "Username",
|
|
124851
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.username.label"
|
|
124852
|
+
}, {
|
|
124853
|
+
type: "text",
|
|
124854
|
+
key: "passwordHash",
|
|
124855
|
+
label: "bcrypt hash",
|
|
124856
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.label",
|
|
124857
|
+
description: "bcrypt hash of the password (cost factor 10).",
|
|
124858
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.description"
|
|
124859
|
+
}]
|
|
124860
|
+
}]
|
|
124861
|
+
}, {
|
|
124862
|
+
id: "mqtt-broker-embedded-acl",
|
|
124863
|
+
title: "Embedded broker — topic ACL",
|
|
124864
|
+
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
|
|
124865
|
+
description: "Per-user pub/sub allow-list. With zero rules the broker is fully open to authenticated clients. With at least one rule, every user needs at least one rule that matches the topic.",
|
|
124866
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
|
|
124867
|
+
columns: 1,
|
|
124868
|
+
fields: [{
|
|
124869
|
+
type: "editable-array",
|
|
124870
|
+
key: "embeddedAcl",
|
|
124871
|
+
label: "Rules",
|
|
124872
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
|
|
124873
|
+
emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
|
|
124874
|
+
addLabel: "Add rule",
|
|
124875
|
+
rowTitleTemplate: "{username} → {topicPattern} ({access})",
|
|
124876
|
+
defaultItem: {
|
|
124877
|
+
username: "",
|
|
124878
|
+
topicPattern: "#",
|
|
124879
|
+
access: "both"
|
|
124880
|
+
},
|
|
124881
|
+
itemFields: [
|
|
124882
|
+
{
|
|
124789
124883
|
type: "text",
|
|
124790
124884
|
key: "username",
|
|
124791
124885
|
label: "Username",
|
|
124792
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124793
|
-
},
|
|
124886
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.username.label"
|
|
124887
|
+
},
|
|
124888
|
+
{
|
|
124794
124889
|
type: "text",
|
|
124795
|
-
key: "
|
|
124796
|
-
label: "
|
|
124797
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124798
|
-
description: "
|
|
124799
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-
|
|
124800
|
-
}]
|
|
124801
|
-
}]
|
|
124802
|
-
},
|
|
124803
|
-
{
|
|
124804
|
-
id: "mqtt-broker-embedded-acl",
|
|
124805
|
-
title: "Embedded broker — topic ACL",
|
|
124806
|
-
titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
|
|
124807
|
-
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.",
|
|
124808
|
-
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
|
|
124809
|
-
columns: 1,
|
|
124810
|
-
fields: [{
|
|
124811
|
-
type: "editable-array",
|
|
124812
|
-
key: "embeddedAcl",
|
|
124813
|
-
label: "Rules",
|
|
124814
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
|
|
124815
|
-
emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
|
|
124816
|
-
addLabel: "Add rule",
|
|
124817
|
-
rowTitleTemplate: "{username} → {topicPattern} ({access})",
|
|
124818
|
-
defaultItem: {
|
|
124819
|
-
username: "",
|
|
124820
|
-
topicPattern: "#",
|
|
124821
|
-
access: "both"
|
|
124890
|
+
key: "topicPattern",
|
|
124891
|
+
label: "Topic pattern",
|
|
124892
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.label",
|
|
124893
|
+
description: "MQTT wildcards: `+` = single level, `#` = multi-level.",
|
|
124894
|
+
descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.description"
|
|
124822
124895
|
},
|
|
124823
|
-
|
|
124824
|
-
|
|
124825
|
-
|
|
124826
|
-
|
|
124827
|
-
|
|
124828
|
-
|
|
124829
|
-
|
|
124830
|
-
|
|
124831
|
-
|
|
124832
|
-
|
|
124833
|
-
|
|
124834
|
-
|
|
124835
|
-
|
|
124836
|
-
|
|
124837
|
-
|
|
124838
|
-
|
|
124839
|
-
|
|
124840
|
-
|
|
124841
|
-
|
|
124842
|
-
|
|
124843
|
-
|
|
124844
|
-
|
|
124845
|
-
|
|
124846
|
-
|
|
124847
|
-
|
|
124848
|
-
|
|
124849
|
-
{
|
|
124850
|
-
value: "pub",
|
|
124851
|
-
label: "Publish only",
|
|
124852
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
|
|
124853
|
-
},
|
|
124854
|
-
{
|
|
124855
|
-
value: "both",
|
|
124856
|
-
label: "Subscribe + publish",
|
|
124857
|
-
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
|
|
124858
|
-
}
|
|
124859
|
-
]
|
|
124860
|
-
}
|
|
124861
|
-
]
|
|
124862
|
-
}]
|
|
124863
|
-
}
|
|
124864
|
-
] });
|
|
124896
|
+
{
|
|
124897
|
+
type: "select",
|
|
124898
|
+
key: "access",
|
|
124899
|
+
label: "Access",
|
|
124900
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.label",
|
|
124901
|
+
options: [
|
|
124902
|
+
{
|
|
124903
|
+
value: "sub",
|
|
124904
|
+
label: "Subscribe only",
|
|
124905
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_sub.label"
|
|
124906
|
+
},
|
|
124907
|
+
{
|
|
124908
|
+
value: "pub",
|
|
124909
|
+
label: "Publish only",
|
|
124910
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
|
|
124911
|
+
},
|
|
124912
|
+
{
|
|
124913
|
+
value: "both",
|
|
124914
|
+
label: "Subscribe + publish",
|
|
124915
|
+
labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
|
|
124916
|
+
}
|
|
124917
|
+
]
|
|
124918
|
+
}
|
|
124919
|
+
]
|
|
124920
|
+
}]
|
|
124921
|
+
}] });
|
|
124865
124922
|
}
|
|
124866
124923
|
};
|
|
124867
124924
|
/**
|
|
@@ -124891,13 +124948,6 @@ function bcryptCompareSafe(plain, hash) {
|
|
|
124891
124948
|
}
|
|
124892
124949
|
}
|
|
124893
124950
|
/**
|
|
124894
|
-
* Decide whether a config update to the embedded broker requires a full
|
|
124895
|
-
* restart (port change) or can be hot-reloaded (credential / ACL change).
|
|
124896
|
-
*/
|
|
124897
|
-
function requiresEmbeddedRespawn(desired, running) {
|
|
124898
|
-
return desired.id !== running.id || desired.port !== running.port || desired.allowAnonymous !== running.allowAnonymous || desired.username !== running.username || desired.password !== running.password;
|
|
124899
|
-
}
|
|
124900
|
-
/**
|
|
124901
124951
|
* Read the `username` field off an aedes Client without leaking a cast.
|
|
124902
124952
|
* aedes attaches the username at runtime but its `.d.ts` doesn't carry
|
|
124903
124953
|
* it — we narrow with a small structural check.
|
|
@@ -124950,15 +125000,5 @@ async function findFreePort() {
|
|
|
124950
125000
|
});
|
|
124951
125001
|
});
|
|
124952
125002
|
}
|
|
124953
|
-
function mapMqttStatus(s) {
|
|
124954
|
-
switch (s) {
|
|
124955
|
-
case "connected": return "connected";
|
|
124956
|
-
case "disconnected": return "disconnected";
|
|
124957
|
-
case "auth-failed": return "auth-failed";
|
|
124958
|
-
case "unreachable": return "unreachable";
|
|
124959
|
-
case "tls-error": return "error";
|
|
124960
|
-
default: return "error";
|
|
124961
|
-
}
|
|
124962
|
-
}
|
|
124963
125003
|
//#endregion
|
|
124964
125004
|
export { MQTT_EMBEDDED_BCRYPT_ROUNDS, MqttBrokerAddon, MqttBrokerAddon as default };
|