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