@camstack/system 1.2.66 → 1.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/blocks-integration.d.ts +24 -0
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +14 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +99 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +99 -2
- package/dist/builtins/device-manager/device-manager.addon.js +3 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +3 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-DqPyKwVB.mjs → dist-CbS6uoSb.mjs} +2465 -2075
- package/dist/{dist-CIkGOAcz.js → dist-NRYjb8qK.js} +1891 -1489
- package/dist/index.js +28 -4
- package/dist/index.mjs +28 -4
- package/dist/kernel/cap-router-builder.d.ts +19 -0
- package/dist/kernel/capability-registry.d.ts +14 -1
- package/package.json +1 -1
|
@@ -4050,1526 +4050,2326 @@ var ConvertResultSchema = z.object({
|
|
|
4050
4050
|
artifacts: z.array(ConvertArtifactSchema).readonly()
|
|
4051
4051
|
});
|
|
4052
4052
|
/**
|
|
4053
|
-
*
|
|
4054
|
-
*
|
|
4055
|
-
*
|
|
4056
|
-
* The provider iterates every `addon-pages-source` (collection) provider
|
|
4057
|
-
* and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
|
|
4058
|
-
* pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
|
|
4059
|
-
* filesystem `mtime` cache-buster lets the browser pick up addon
|
|
4060
|
-
* rebuilds without manual reload.
|
|
4061
|
-
*
|
|
4062
|
-
* The hub-local builtin `addon-pages-aggregator` (see
|
|
4063
|
-
* `@camstack/system/builtins/addon-pages-aggregator`) registers the
|
|
4064
|
-
* provider. Splitting the public aggregator from the raw collection
|
|
4065
|
-
* keeps both ends in codegen — there's no hand-written
|
|
4066
|
-
* `addon-pages.router.ts` wrapper anymore.
|
|
4067
|
-
*/
|
|
4068
|
-
var AddonPageDeclarationSchema$1 = z.object({
|
|
4069
|
-
id: z.string(),
|
|
4070
|
-
label: z.string(),
|
|
4071
|
-
icon: z.string(),
|
|
4072
|
-
path: z.string(),
|
|
4073
|
-
remoteName: z.string(),
|
|
4074
|
-
bundle: z.string(),
|
|
4075
|
-
section: z.string().optional(),
|
|
4076
|
-
sectionLabel: z.string().optional()
|
|
4077
|
-
});
|
|
4078
|
-
var AddonPageInfoSchema = z.object({
|
|
4079
|
-
addonId: z.string(),
|
|
4080
|
-
page: AddonPageDeclarationSchema$1,
|
|
4081
|
-
bundleUrl: z.string()
|
|
4082
|
-
});
|
|
4083
|
-
var addonPagesCapability = {
|
|
4084
|
-
name: "addon-pages",
|
|
4085
|
-
scope: "system",
|
|
4086
|
-
mode: "singleton",
|
|
4087
|
-
methods: { listPages: method(z.void(), z.array(AddonPageInfoSchema).readonly()) }
|
|
4088
|
-
};
|
|
4089
|
-
/**
|
|
4090
|
-
* `addon-pages-source` — collection cap exposing per-provider raw page
|
|
4091
|
-
* declarations. Every addon that contributes a UI page registers a
|
|
4092
|
-
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
4093
|
-
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
4094
|
-
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
4095
|
-
* that admin-ui consumes.
|
|
4096
|
-
*
|
|
4097
|
-
* The split exists because the public listing has a different output
|
|
4098
|
-
* shape than the per-provider raw declarations, and we want both ends
|
|
4099
|
-
* to flow through codegen instead of relying on a hand-written wrapper.
|
|
4053
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
4054
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
4055
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
4100
4056
|
*/
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
* Conventionally `addon_<id>_page` (snake_case; MF names cannot
|
|
4111
|
-
* contain hyphens).
|
|
4112
|
-
*/
|
|
4113
|
-
remoteName: z.string(),
|
|
4114
|
-
/**
|
|
4115
|
-
* Bundle filename inside the addon's `dist/` dir served at
|
|
4116
|
-
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
4117
|
-
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
4118
|
-
* the static-file route can compute an mtime-based cache-buster URL
|
|
4119
|
-
* without a separate filesystem stat.
|
|
4120
|
-
*/
|
|
4121
|
-
bundle: z.string(),
|
|
4122
|
-
/**
|
|
4123
|
-
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
4124
|
-
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
4125
|
-
* Any OTHER string creates (or joins) a custom section rendered after
|
|
4126
|
-
* the built-in groups; its label comes from `sectionLabel` (first
|
|
4127
|
-
* declaration wins), falling back to the id. Absent → the legacy
|
|
4128
|
-
* "Addon Pages" group.
|
|
4129
|
-
*/
|
|
4130
|
-
section: z.string().optional(),
|
|
4131
|
-
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
4132
|
-
sectionLabel: z.string().optional()
|
|
4133
|
-
});
|
|
4134
|
-
var addonPagesSourceCapability = {
|
|
4135
|
-
name: "addon-pages-source",
|
|
4136
|
-
scope: "system",
|
|
4137
|
-
mode: "collection",
|
|
4138
|
-
internal: true,
|
|
4139
|
-
methods: { listPages: method(z.void(), z.array(AddonPageDeclarationSchema).readonly()) }
|
|
4057
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
4058
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
4059
|
+
var ExpressionParseError = class extends Error {
|
|
4060
|
+
position;
|
|
4061
|
+
constructor(message, position) {
|
|
4062
|
+
super(message);
|
|
4063
|
+
this.name = "ExpressionParseError";
|
|
4064
|
+
this.position = position;
|
|
4065
|
+
}
|
|
4140
4066
|
};
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
"
|
|
4147
|
-
|
|
4148
|
-
]),
|
|
4149
|
-
path: z.string(),
|
|
4150
|
-
access: z.enum([
|
|
4151
|
-
"public",
|
|
4152
|
-
"authenticated",
|
|
4153
|
-
"admin"
|
|
4154
|
-
]).optional(),
|
|
4155
|
-
description: z.string().optional()
|
|
4156
|
-
});
|
|
4157
|
-
/**
|
|
4158
|
-
* Cross-process route invocation envelope. The hub captures the
|
|
4159
|
-
* request as plain data, ships it to the worker via Moleculer, and
|
|
4160
|
-
* the worker runs the local handler against a capturing reply. The
|
|
4161
|
-
* envelope returned describes what the handler intended (status,
|
|
4162
|
-
* headers, body, or a redirect) so the hub can translate it back to
|
|
4163
|
-
* the Fastify reply that's actually wired to the socket.
|
|
4164
|
-
*/
|
|
4165
|
-
var InvokeRequestSchema = z.object({
|
|
4166
|
-
method: z.string(),
|
|
4167
|
-
path: z.string(),
|
|
4168
|
-
params: z.record(z.string(), z.string()),
|
|
4169
|
-
query: z.record(z.string(), z.string()),
|
|
4170
|
-
body: z.unknown(),
|
|
4171
|
-
headers: z.record(z.string(), z.string()),
|
|
4172
|
-
user: z.object({
|
|
4173
|
-
id: z.string(),
|
|
4174
|
-
username: z.string(),
|
|
4175
|
-
isAdmin: z.boolean()
|
|
4176
|
-
}).optional(),
|
|
4177
|
-
scopedToken: z.unknown().optional()
|
|
4178
|
-
});
|
|
4179
|
-
var InvokeReplyEnvelopeSchema = z.object({
|
|
4180
|
-
status: z.number().int(),
|
|
4181
|
-
headers: z.record(z.string(), z.string()),
|
|
4182
|
-
/** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
|
|
4183
|
-
* sending `body`. Status defaults to 302 when this is set unless
|
|
4184
|
-
* the handler called `reply.code(...)` explicitly. */
|
|
4185
|
-
redirectUrl: z.string().nullable(),
|
|
4186
|
-
/** JSON-serializable body. `undefined` is treated as "no body". */
|
|
4187
|
-
body: z.unknown().optional(),
|
|
4188
|
-
/** Set when the handler called `reply.type(mime)`. */
|
|
4189
|
-
contentType: z.string().optional()
|
|
4190
|
-
});
|
|
4191
|
-
var addonRoutesCapability = {
|
|
4192
|
-
name: "addon-routes",
|
|
4193
|
-
scope: "system",
|
|
4194
|
-
mode: "collection",
|
|
4195
|
-
internal: true,
|
|
4196
|
-
methods: {
|
|
4197
|
-
getRoutes: method(z.void(), z.array(AddonHttpRouteSchema)),
|
|
4198
|
-
/**
|
|
4199
|
-
* Cross-process dispatch entry point. Forked addons implement this
|
|
4200
|
-
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
4201
|
-
* can route through Moleculer when the handler lives in a worker.
|
|
4202
|
-
*
|
|
4203
|
-
* Local addons can implement it for free with the same helper;
|
|
4204
|
-
* the hub bypasses the wire on co-located addons.
|
|
4205
|
-
*/
|
|
4206
|
-
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
4207
|
-
},
|
|
4208
|
-
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
4209
|
-
mount: { kind: "skip" }
|
|
4067
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
4068
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
4069
|
+
var ExpressionEvalError = class extends Error {
|
|
4070
|
+
constructor(message) {
|
|
4071
|
+
super(message);
|
|
4072
|
+
this.name = "ExpressionEvalError";
|
|
4073
|
+
}
|
|
4210
4074
|
};
|
|
4211
|
-
var ConfigTabDeclarationSchema = z.object({
|
|
4212
|
-
id: z.string(),
|
|
4213
|
-
label: z.string(),
|
|
4214
|
-
icon: z.string(),
|
|
4215
|
-
order: z.number().optional()
|
|
4216
|
-
});
|
|
4217
|
-
var ConfigSectionWithValuesSchema = z.object({
|
|
4218
|
-
id: z.string(),
|
|
4219
|
-
title: z.string(),
|
|
4220
|
-
description: z.string().optional(),
|
|
4221
|
-
style: z.enum(["card", "accordion"]).optional(),
|
|
4222
|
-
defaultCollapsed: z.boolean().optional(),
|
|
4223
|
-
columns: z.union([
|
|
4224
|
-
z.literal(1),
|
|
4225
|
-
z.literal(2),
|
|
4226
|
-
z.literal(3),
|
|
4227
|
-
z.literal(4)
|
|
4228
|
-
]).optional(),
|
|
4229
|
-
tab: z.string().optional(),
|
|
4230
|
-
location: z.enum(["settings", "top-tab"]).optional(),
|
|
4231
|
-
order: z.number().optional(),
|
|
4232
|
-
fields: z.array(z.any())
|
|
4233
|
-
});
|
|
4234
|
-
var SettingsSchemaWithValuesSchema = z.object({
|
|
4235
|
-
tabs: z.array(ConfigTabDeclarationSchema).optional(),
|
|
4236
|
-
sections: z.array(ConfigSectionWithValuesSchema)
|
|
4237
|
-
});
|
|
4238
|
-
/** Patch object — keys are field names, values are the new field values. */
|
|
4239
|
-
var SettingsPatchSchema = z.record(z.string(), z.unknown());
|
|
4240
|
-
/** Standard success response for update operations. */
|
|
4241
|
-
var SettingsUpdateResultSchema = z.object({ success: z.literal(true) });
|
|
4242
4075
|
/**
|
|
4243
|
-
*
|
|
4244
|
-
*
|
|
4245
|
-
*
|
|
4246
|
-
*
|
|
4247
|
-
* addons the call is direct; for remote agents it proxies via the
|
|
4248
|
-
* per-addon Moleculer service.
|
|
4249
|
-
*
|
|
4250
|
-
* Replaces the `$addonHost` Moleculer service. Transport transparency
|
|
4251
|
-
* is handled by the provider implementation — callers (admin UI, other
|
|
4252
|
-
* addons via `ctx.api`) never know which node hosts the target addon.
|
|
4076
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
4077
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
4078
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
4079
|
+
* own-property check against it.
|
|
4253
4080
|
*
|
|
4254
|
-
*
|
|
4255
|
-
*
|
|
4256
|
-
*
|
|
4257
|
-
*
|
|
4081
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
4082
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
4083
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
4084
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
4085
|
+
* callable — they are simply "unknown function" at parse time.
|
|
4258
4086
|
*
|
|
4259
|
-
*
|
|
4260
|
-
*
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
* `addon-settings` is a **hub-centric** cap: the hub hosts the single
|
|
4264
|
-
* provider, and `nodeId` in the input is data for the hub provider's
|
|
4265
|
-
* internal dispatcher, not a routing hint for the cap-router. See
|
|
4266
|
-
* `CapabilityDefinition.nodeIdMode` for the contract.
|
|
4087
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
4088
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
4089
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
4090
|
+
* closed rather than emitting a garbage value.
|
|
4267
4091
|
*/
|
|
4268
|
-
|
|
4269
|
-
name:
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4092
|
+
function asFiniteNumber(value, name, index) {
|
|
4093
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
4094
|
+
return value;
|
|
4095
|
+
}
|
|
4096
|
+
function asString$1(value, name, index) {
|
|
4097
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
4098
|
+
return value;
|
|
4099
|
+
}
|
|
4100
|
+
function finiteResult(value, name) {
|
|
4101
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
4102
|
+
return value;
|
|
4103
|
+
}
|
|
4104
|
+
function allFiniteNumbers(args, name) {
|
|
4105
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
4106
|
+
}
|
|
4107
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
4108
|
+
var table = {
|
|
4109
|
+
min: {
|
|
4110
|
+
minArgs: 1,
|
|
4111
|
+
maxArgs: INF,
|
|
4112
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
4113
|
+
},
|
|
4114
|
+
max: {
|
|
4115
|
+
minArgs: 1,
|
|
4116
|
+
maxArgs: INF,
|
|
4117
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
4118
|
+
},
|
|
4119
|
+
abs: {
|
|
4120
|
+
minArgs: 1,
|
|
4121
|
+
maxArgs: 1,
|
|
4122
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
4123
|
+
},
|
|
4124
|
+
floor: {
|
|
4125
|
+
minArgs: 1,
|
|
4126
|
+
maxArgs: 1,
|
|
4127
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
4128
|
+
},
|
|
4129
|
+
ceil: {
|
|
4130
|
+
minArgs: 1,
|
|
4131
|
+
maxArgs: 1,
|
|
4132
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
4133
|
+
},
|
|
4134
|
+
sqrt: {
|
|
4135
|
+
minArgs: 1,
|
|
4136
|
+
maxArgs: 1,
|
|
4137
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
4138
|
+
},
|
|
4139
|
+
round: {
|
|
4140
|
+
minArgs: 1,
|
|
4141
|
+
maxArgs: 2,
|
|
4142
|
+
apply: (args) => {
|
|
4143
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
4144
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
4145
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
4146
|
+
const factor = 10 ** digits;
|
|
4147
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
4148
|
+
}
|
|
4149
|
+
},
|
|
4150
|
+
pow: {
|
|
4151
|
+
minArgs: 2,
|
|
4152
|
+
maxArgs: 2,
|
|
4153
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
4154
|
+
},
|
|
4155
|
+
clamp: {
|
|
4156
|
+
minArgs: 3,
|
|
4157
|
+
maxArgs: 3,
|
|
4158
|
+
apply: (args) => {
|
|
4159
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
4160
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
4161
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
4162
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
4163
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
4164
|
+
}
|
|
4165
|
+
},
|
|
4166
|
+
avg: {
|
|
4167
|
+
minArgs: 1,
|
|
4168
|
+
maxArgs: INF,
|
|
4169
|
+
apply: (args) => {
|
|
4170
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
4171
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
4172
|
+
}
|
|
4173
|
+
},
|
|
4174
|
+
sum: {
|
|
4175
|
+
minArgs: 1,
|
|
4176
|
+
maxArgs: INF,
|
|
4177
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
4178
|
+
},
|
|
4179
|
+
coalesce: {
|
|
4180
|
+
minArgs: 1,
|
|
4181
|
+
maxArgs: INF,
|
|
4182
|
+
apply: (args) => {
|
|
4183
|
+
for (const a of args) if (a !== null) return a;
|
|
4184
|
+
return null;
|
|
4185
|
+
}
|
|
4186
|
+
},
|
|
4187
|
+
age: {
|
|
4188
|
+
minArgs: 2,
|
|
4189
|
+
maxArgs: 2,
|
|
4190
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
4191
|
+
},
|
|
4192
|
+
convert: {
|
|
4193
|
+
minArgs: 3,
|
|
4194
|
+
maxArgs: 3,
|
|
4195
|
+
apply: (args, hooks) => {
|
|
4196
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
4197
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
4198
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
4199
|
+
if (hooks.convert) {
|
|
4200
|
+
const out = hooks.convert(x, from, to);
|
|
4201
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
4202
|
+
return finiteResult(out, "convert");
|
|
4203
|
+
}
|
|
4204
|
+
if (from === to) return x;
|
|
4205
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
4206
|
+
}
|
|
4302
4207
|
}
|
|
4303
4208
|
};
|
|
4209
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
4210
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
4211
|
+
* callees at parse time (immediate author feedback). */
|
|
4212
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
4304
4213
|
/**
|
|
4305
|
-
*
|
|
4306
|
-
* declarations. Mirrors the addon-pages split: every addon shipping
|
|
4307
|
-
* widgets registers a provider on this collection cap; the hub-local
|
|
4308
|
-
* aggregator (`addon-widgets`, see `addon-widgets.cap.ts`) walks the
|
|
4309
|
-
* collection, stamps versioned `bundleUrl`s onto each declaration, and
|
|
4310
|
-
* exposes the public listing surface that admin-ui consumes.
|
|
4311
|
-
*
|
|
4312
|
-
* The split exists because the public listing has a different output
|
|
4313
|
-
* shape (flat enriched metadata with `addonId` + `bundleUrl`) than the
|
|
4314
|
-
* per-provider raw declarations. Both ends flow through codegen.
|
|
4214
|
+
* Resource-bound constants for the safe expression engine.
|
|
4315
4215
|
*
|
|
4316
|
-
*
|
|
4317
|
-
*
|
|
4318
|
-
*
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
* descriptor; the `UiContribution` core (`tab` / `label` / `order` /
|
|
4322
|
-
* `kind` / `remote`) carries identity + placement + the MF remote.
|
|
4216
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
4217
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
4218
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
4219
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
4220
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
4323
4221
|
*/
|
|
4324
|
-
/**
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
"
|
|
4333
|
-
"
|
|
4334
|
-
"
|
|
4335
|
-
"
|
|
4222
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
4223
|
+
* rejected without allocation. */
|
|
4224
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
4225
|
+
/** A legal binding / identifier name. */
|
|
4226
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4227
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
4228
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
4229
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
4230
|
+
"now",
|
|
4231
|
+
"true",
|
|
4232
|
+
"false",
|
|
4233
|
+
"null"
|
|
4336
4234
|
]);
|
|
4337
4235
|
/**
|
|
4338
|
-
*
|
|
4339
|
-
*
|
|
4340
|
-
*
|
|
4341
|
-
*
|
|
4342
|
-
* `
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
remoteName: z.string(),
|
|
4346
|
-
exposedModule: z.string(),
|
|
4347
|
-
componentKey: z.string().optional()
|
|
4348
|
-
});
|
|
4349
|
-
/**
|
|
4350
|
-
* One widget declaration — a `UiContribution` (`kind:'remote'`) plus
|
|
4351
|
-
* widget-only metadata. The `UiContribution` core fields:
|
|
4352
|
-
*
|
|
4353
|
-
* - `tab` — where the widget hosts. A widget that runs on the
|
|
4354
|
-
* dashboard declares `tab:'dashboard'`; a device-tab
|
|
4355
|
-
* widget declares the target device-detail tab id.
|
|
4356
|
-
* - `subTab` — optional sub-tab within `tab`.
|
|
4357
|
-
* - `label` — operator-facing label.
|
|
4358
|
-
* - `order` — ordering within `(tab, subTab)`.
|
|
4359
|
-
* - `kind` — always `'remote'` for widgets.
|
|
4360
|
-
* - `remote` — the MF remote `{ remoteName, exposedModule, componentKey }`.
|
|
4361
|
-
*
|
|
4362
|
-
* Widget-only fields retained alongside the contribution core:
|
|
4363
|
-
*
|
|
4364
|
-
* - `stableId` — stable identity within the addon (the MF
|
|
4365
|
-
* `componentKey`; kept top-level so consumers have
|
|
4366
|
-
* a stable key without reaching into `remote`).
|
|
4367
|
-
* - `description` / `icon` — picker metadata.
|
|
4368
|
-
* - `bundle` — entry filename inside the addon `dist/` dir; the
|
|
4369
|
-
* aggregator stamps a versioned `bundleUrl` from it.
|
|
4370
|
-
* - `hosts` — every host the widget supports (a widget can run
|
|
4371
|
-
* both on the dashboard and a device tab). `tab`
|
|
4372
|
-
* is the PRIMARY host; `hosts` is the full set the
|
|
4373
|
-
* picker filters on.
|
|
4374
|
-
* - `requires` — host-context requirements validated at mount.
|
|
4375
|
-
* - `defaultSize` / `allowedSizes` / `defaultColumns` / `defaultRows`
|
|
4376
|
-
* — dashboard placement hints.
|
|
4236
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
4237
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
4238
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
4239
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
4240
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
4241
|
+
* is a parse error with a source position, so member access / assignment /
|
|
4242
|
+
* template literals are lexically impossible.
|
|
4377
4243
|
*/
|
|
4378
|
-
var
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4244
|
+
var KEYWORDS = new Set([
|
|
4245
|
+
"true",
|
|
4246
|
+
"false",
|
|
4247
|
+
"null"
|
|
4248
|
+
]);
|
|
4249
|
+
function isDigit(ch) {
|
|
4250
|
+
return ch >= "0" && ch <= "9";
|
|
4251
|
+
}
|
|
4252
|
+
function isIdentStart(ch) {
|
|
4253
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
4254
|
+
}
|
|
4255
|
+
function isIdentPart(ch) {
|
|
4256
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
4257
|
+
}
|
|
4258
|
+
function isWhitespace(ch) {
|
|
4259
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
4260
|
+
}
|
|
4261
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
4262
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
4263
|
+
* string. */
|
|
4264
|
+
function tokenize(source) {
|
|
4265
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
4266
|
+
const tokens = [];
|
|
4267
|
+
let i = 0;
|
|
4268
|
+
const n = source.length;
|
|
4269
|
+
while (i < n) {
|
|
4270
|
+
const ch = source[i];
|
|
4271
|
+
if (isWhitespace(ch)) {
|
|
4272
|
+
i += 1;
|
|
4273
|
+
continue;
|
|
4274
|
+
}
|
|
4275
|
+
if (isDigit(ch)) {
|
|
4276
|
+
const start = i;
|
|
4277
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
4278
|
+
if (i < n && source[i] === ".") {
|
|
4279
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
4280
|
+
i += 1;
|
|
4281
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
4282
|
+
}
|
|
4283
|
+
const text = source.slice(start, i);
|
|
4284
|
+
const value = Number(text);
|
|
4285
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
4286
|
+
tokens.push({
|
|
4287
|
+
type: "number",
|
|
4288
|
+
value,
|
|
4289
|
+
pos: start
|
|
4290
|
+
});
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
if (ch === "'" || ch === "\"") {
|
|
4294
|
+
const quote = ch;
|
|
4295
|
+
const start = i;
|
|
4296
|
+
i += 1;
|
|
4297
|
+
let out = "";
|
|
4298
|
+
let closed = false;
|
|
4299
|
+
while (i < n) {
|
|
4300
|
+
const c = source[i];
|
|
4301
|
+
if (c === "\\") {
|
|
4302
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
4303
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
4304
|
+
out += next;
|
|
4305
|
+
i += 2;
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
4309
|
+
}
|
|
4310
|
+
if (c === quote) {
|
|
4311
|
+
closed = true;
|
|
4312
|
+
i += 1;
|
|
4313
|
+
break;
|
|
4314
|
+
}
|
|
4315
|
+
out += c;
|
|
4316
|
+
i += 1;
|
|
4317
|
+
}
|
|
4318
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
4319
|
+
tokens.push({
|
|
4320
|
+
type: "string",
|
|
4321
|
+
value: out,
|
|
4322
|
+
pos: start
|
|
4323
|
+
});
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (isIdentStart(ch)) {
|
|
4327
|
+
const start = i;
|
|
4328
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
4329
|
+
const text = source.slice(start, i);
|
|
4330
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
4331
|
+
type: "keyword",
|
|
4332
|
+
keyword: keywordOf(text),
|
|
4333
|
+
pos: start
|
|
4334
|
+
});
|
|
4335
|
+
else tokens.push({
|
|
4336
|
+
type: "identifier",
|
|
4337
|
+
name: text,
|
|
4338
|
+
pos: start
|
|
4339
|
+
});
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
4343
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
4344
|
+
tokens.push({
|
|
4345
|
+
type: "punct",
|
|
4346
|
+
punct: two,
|
|
4347
|
+
pos: i
|
|
4348
|
+
});
|
|
4349
|
+
i += 2;
|
|
4350
|
+
continue;
|
|
4351
|
+
}
|
|
4352
|
+
if (isSinglePunct(ch)) {
|
|
4353
|
+
tokens.push({
|
|
4354
|
+
type: "punct",
|
|
4355
|
+
punct: ch,
|
|
4356
|
+
pos: i
|
|
4357
|
+
});
|
|
4358
|
+
i += 1;
|
|
4359
|
+
continue;
|
|
4360
|
+
}
|
|
4361
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
4362
|
+
}
|
|
4363
|
+
tokens.push({
|
|
4364
|
+
type: "eof",
|
|
4365
|
+
pos: n
|
|
4366
|
+
});
|
|
4367
|
+
return tokens;
|
|
4368
|
+
}
|
|
4369
|
+
function keywordOf(text) {
|
|
4370
|
+
if (text === "true") return "true";
|
|
4371
|
+
if (text === "false") return "false";
|
|
4372
|
+
return "null";
|
|
4373
|
+
}
|
|
4374
|
+
function isSinglePunct(ch) {
|
|
4375
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
4376
|
+
}
|
|
4438
4377
|
/**
|
|
4439
|
-
*
|
|
4440
|
-
* surface that admin-ui consumes through `useAddonWidgetsListWidgets()`.
|
|
4378
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
4441
4379
|
*
|
|
4442
|
-
*
|
|
4443
|
-
*
|
|
4444
|
-
* `
|
|
4445
|
-
*
|
|
4446
|
-
*
|
|
4447
|
-
*
|
|
4380
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
4381
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
4382
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
4383
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
4384
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
4385
|
+
* that references a since-removed builtin degrades at read.
|
|
4448
4386
|
*
|
|
4449
|
-
*
|
|
4450
|
-
*
|
|
4451
|
-
* provider. Splitting the public aggregator from the raw collection
|
|
4452
|
-
* keeps both ends in codegen — there's no hand-written wrapper.
|
|
4387
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
4388
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
4453
4389
|
*/
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4390
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
4391
|
+
var BINARY_PRECEDENCE = {
|
|
4392
|
+
"||": 1,
|
|
4393
|
+
"&&": 2,
|
|
4394
|
+
"==": 3,
|
|
4395
|
+
"!=": 3,
|
|
4396
|
+
"<": 4,
|
|
4397
|
+
"<=": 4,
|
|
4398
|
+
">": 4,
|
|
4399
|
+
">=": 4,
|
|
4400
|
+
"+": 5,
|
|
4401
|
+
"-": 5,
|
|
4402
|
+
"*": 6,
|
|
4403
|
+
"/": 6,
|
|
4404
|
+
"%": 6
|
|
4463
4405
|
};
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
*
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
}
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4406
|
+
function isLogicalOp(op) {
|
|
4407
|
+
return op === "&&" || op === "||";
|
|
4408
|
+
}
|
|
4409
|
+
function isBinaryOp(op) {
|
|
4410
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
4411
|
+
}
|
|
4412
|
+
var Parser = class {
|
|
4413
|
+
tokens;
|
|
4414
|
+
pos = 0;
|
|
4415
|
+
nodeCount = 0;
|
|
4416
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
4417
|
+
callees = /* @__PURE__ */ new Set();
|
|
4418
|
+
constructor(tokens) {
|
|
4419
|
+
this.tokens = tokens;
|
|
4420
|
+
}
|
|
4421
|
+
parse() {
|
|
4422
|
+
const ast = this.parseTernary();
|
|
4423
|
+
const tok = this.peek();
|
|
4424
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
4425
|
+
return {
|
|
4426
|
+
ast,
|
|
4427
|
+
identifiers: this.identifiers,
|
|
4428
|
+
callees: this.callees,
|
|
4429
|
+
nodeCount: this.nodeCount
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
peek() {
|
|
4433
|
+
return this.tokens[this.pos];
|
|
4434
|
+
}
|
|
4435
|
+
next() {
|
|
4436
|
+
return this.tokens[this.pos++];
|
|
4437
|
+
}
|
|
4438
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
4439
|
+
expectPunct(punct) {
|
|
4440
|
+
const tok = this.peek();
|
|
4441
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
4442
|
+
this.pos += 1;
|
|
4443
|
+
}
|
|
4444
|
+
matchPunct(punct) {
|
|
4445
|
+
const tok = this.peek();
|
|
4446
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
4447
|
+
this.pos += 1;
|
|
4448
|
+
return true;
|
|
4449
|
+
}
|
|
4450
|
+
return false;
|
|
4451
|
+
}
|
|
4452
|
+
countNode() {
|
|
4453
|
+
this.nodeCount += 1;
|
|
4454
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
4455
|
+
}
|
|
4456
|
+
parseTernary() {
|
|
4457
|
+
const test = this.parseBinary(1);
|
|
4458
|
+
if (this.matchPunct("?")) {
|
|
4459
|
+
const consequent = this.parseTernary();
|
|
4460
|
+
this.expectPunct(":");
|
|
4461
|
+
const alternate = this.parseTernary();
|
|
4462
|
+
this.countNode();
|
|
4463
|
+
return {
|
|
4464
|
+
kind: "conditional",
|
|
4465
|
+
test,
|
|
4466
|
+
consequent,
|
|
4467
|
+
alternate
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
4470
|
+
return test;
|
|
4471
|
+
}
|
|
4472
|
+
parseBinary(minPrec) {
|
|
4473
|
+
let left = this.parseUnary();
|
|
4474
|
+
for (;;) {
|
|
4475
|
+
const tok = this.peek();
|
|
4476
|
+
if (tok.type !== "punct") break;
|
|
4477
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
4478
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
4479
|
+
const op = tok.punct;
|
|
4480
|
+
this.pos += 1;
|
|
4481
|
+
const right = this.parseBinary(prec + 1);
|
|
4482
|
+
this.countNode();
|
|
4483
|
+
if (isLogicalOp(op)) left = {
|
|
4484
|
+
kind: "logical",
|
|
4485
|
+
op,
|
|
4486
|
+
left,
|
|
4487
|
+
right
|
|
4488
|
+
};
|
|
4489
|
+
else if (isBinaryOp(op)) left = {
|
|
4490
|
+
kind: "binary",
|
|
4491
|
+
op,
|
|
4492
|
+
left,
|
|
4493
|
+
right
|
|
4494
|
+
};
|
|
4495
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
4496
|
+
}
|
|
4497
|
+
return left;
|
|
4498
|
+
}
|
|
4499
|
+
parseUnary() {
|
|
4500
|
+
const tok = this.peek();
|
|
4501
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
4502
|
+
const op = tok.punct;
|
|
4503
|
+
this.pos += 1;
|
|
4504
|
+
const operand = this.parseUnary();
|
|
4505
|
+
this.countNode();
|
|
4506
|
+
return {
|
|
4507
|
+
kind: "unary",
|
|
4508
|
+
op,
|
|
4509
|
+
operand
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
return this.parsePrimary();
|
|
4513
|
+
}
|
|
4514
|
+
parsePrimary() {
|
|
4515
|
+
const tok = this.next();
|
|
4516
|
+
switch (tok.type) {
|
|
4517
|
+
case "number":
|
|
4518
|
+
this.countNode();
|
|
4519
|
+
return {
|
|
4520
|
+
kind: "literal",
|
|
4521
|
+
value: tok.value
|
|
4522
|
+
};
|
|
4523
|
+
case "string":
|
|
4524
|
+
this.countNode();
|
|
4525
|
+
return {
|
|
4526
|
+
kind: "literal",
|
|
4527
|
+
value: tok.value
|
|
4528
|
+
};
|
|
4529
|
+
case "keyword":
|
|
4530
|
+
this.countNode();
|
|
4531
|
+
return {
|
|
4532
|
+
kind: "literal",
|
|
4533
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
4534
|
+
};
|
|
4535
|
+
case "identifier": {
|
|
4536
|
+
const nextTok = this.peek();
|
|
4537
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
4538
|
+
this.identifiers.add(tok.name);
|
|
4539
|
+
this.countNode();
|
|
4540
|
+
return {
|
|
4541
|
+
kind: "identifier",
|
|
4542
|
+
name: tok.name
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
case "punct":
|
|
4546
|
+
if (tok.punct === "(") {
|
|
4547
|
+
const inner = this.parseTernary();
|
|
4548
|
+
this.expectPunct(")");
|
|
4549
|
+
return inner;
|
|
4550
|
+
}
|
|
4551
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
4552
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
parseCall(callee, pos) {
|
|
4556
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
4557
|
+
this.expectPunct("(");
|
|
4558
|
+
const args = [];
|
|
4559
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
4560
|
+
args.push(this.parseTernary());
|
|
4561
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
4562
|
+
if (this.matchPunct(",")) continue;
|
|
4563
|
+
this.expectPunct(")");
|
|
4564
|
+
break;
|
|
4565
|
+
}
|
|
4566
|
+
this.callees.add(callee);
|
|
4567
|
+
this.countNode();
|
|
4568
|
+
return {
|
|
4569
|
+
kind: "call",
|
|
4570
|
+
callee,
|
|
4571
|
+
args
|
|
4572
|
+
};
|
|
4520
4573
|
}
|
|
4521
4574
|
};
|
|
4575
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
4576
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
4577
|
+
function parseExpression(source) {
|
|
4578
|
+
return new Parser(tokenize(source)).parse();
|
|
4579
|
+
}
|
|
4522
4580
|
/**
|
|
4523
|
-
*
|
|
4524
|
-
*
|
|
4525
|
-
*
|
|
4526
|
-
*
|
|
4527
|
-
* (`processChunk`, engine lifecycle) while `audio-analysis` exposes the
|
|
4528
|
-
* per-device surface — settings resolution, device-settings contribution,
|
|
4529
|
-
* audio-level events — and is the binding row every camera sees in the
|
|
4530
|
-
* device-manager bindings UI.
|
|
4581
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
4582
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
4583
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
4584
|
+
* one per read on a hot resolve path.
|
|
4531
4585
|
*
|
|
4532
|
-
* The
|
|
4533
|
-
*
|
|
4534
|
-
*
|
|
4535
|
-
* `device-manager.setWrapperActive` when they don't want a given camera to be
|
|
4536
|
-
* analysed (e.g. muted / motion-only cams).
|
|
4537
|
-
*/
|
|
4538
|
-
var audioAnalysisCapability = {
|
|
4539
|
-
name: "audio-analysis",
|
|
4540
|
-
scope: "device",
|
|
4541
|
-
mode: "singleton",
|
|
4542
|
-
kind: "wrapper",
|
|
4543
|
-
defaultActive: true,
|
|
4544
|
-
deviceTypes: [DeviceType.Camera],
|
|
4545
|
-
exposesDeviceSettings: true,
|
|
4546
|
-
methods: {
|
|
4547
|
-
/**
|
|
4548
|
-
* Resolve per-device audio analysis settings (minConfidence,
|
|
4549
|
-
* allowedClasses) from the addon settings store for a given camera.
|
|
4550
|
-
* Orchestrator callers use this before handing chunks to
|
|
4551
|
-
* `audio-analyzer.processChunk`.
|
|
4552
|
-
*/
|
|
4553
|
-
resolveDeviceSettings: method(z.object({ deviceId: z.number() }), z.custom()) },
|
|
4554
|
-
events: { onAudioLevel: event(z.object({
|
|
4555
|
-
deviceId: z.number(),
|
|
4556
|
-
rms: z.number(),
|
|
4557
|
-
dbfs: z.number()
|
|
4558
|
-
})) }
|
|
4559
|
-
};
|
|
4560
|
-
/** Shared Zod schemas used across detection capabilities. */
|
|
4561
|
-
/**
|
|
4562
|
-
* Canonical frame-format enum mirrored on `FrameFormat` in
|
|
4563
|
-
* `packages/types/src/types/io.ts`. Kept inline (vs imported) so the
|
|
4564
|
-
* Zod runtime schema and TypeScript type stay in sync at the call site
|
|
4565
|
-
* — adding a new format requires changing both this enum and the
|
|
4566
|
-
* `FrameFormat` type alias together.
|
|
4586
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
4587
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
4588
|
+
* callers is safe and maximises hit rate.
|
|
4567
4589
|
*/
|
|
4568
|
-
var
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
}
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
}
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4590
|
+
var cache = /* @__PURE__ */ new Map();
|
|
4591
|
+
function getCached(source) {
|
|
4592
|
+
const hit = cache.get(source);
|
|
4593
|
+
if (hit !== void 0) {
|
|
4594
|
+
cache.delete(source);
|
|
4595
|
+
cache.set(source, hit);
|
|
4596
|
+
return hit;
|
|
4597
|
+
}
|
|
4598
|
+
let result;
|
|
4599
|
+
try {
|
|
4600
|
+
result = {
|
|
4601
|
+
ok: true,
|
|
4602
|
+
parsed: parseExpression(source)
|
|
4603
|
+
};
|
|
4604
|
+
} catch (err) {
|
|
4605
|
+
result = {
|
|
4606
|
+
ok: false,
|
|
4607
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
cache.set(source, result);
|
|
4611
|
+
if (cache.size > 256) {
|
|
4612
|
+
const oldest = cache.keys().next().value;
|
|
4613
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
4614
|
+
}
|
|
4615
|
+
return result;
|
|
4616
|
+
}
|
|
4617
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
4618
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
4619
|
+
function compileExpressionSafe(source) {
|
|
4620
|
+
return getCached(source);
|
|
4621
|
+
}
|
|
4622
|
+
Object.freeze({});
|
|
4594
4623
|
/**
|
|
4595
|
-
*
|
|
4596
|
-
*
|
|
4597
|
-
*
|
|
4598
|
-
*
|
|
4599
|
-
*
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4624
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
4625
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
4626
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
4627
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
4628
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
4629
|
+
*/
|
|
4630
|
+
function validateExpressionSource(src) {
|
|
4631
|
+
const names = Object.keys(src.bindings);
|
|
4632
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
4633
|
+
for (const name of names) {
|
|
4634
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
4635
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
4636
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
4637
|
+
}
|
|
4638
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
4639
|
+
if (!compiled.ok) return compiled.error;
|
|
4640
|
+
const bound = new Set(names);
|
|
4641
|
+
for (const id of compiled.parsed.identifiers) {
|
|
4642
|
+
if (id === "now") continue;
|
|
4643
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
4644
|
+
}
|
|
4645
|
+
return null;
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* What an expression's named bindings READ from.
|
|
4603
4649
|
*
|
|
4604
|
-
*
|
|
4605
|
-
*
|
|
4650
|
+
* Salvaged verbatim from the deleted device-link mechanism. Wiring's source
|
|
4651
|
+
* kinds were the one part of it worth keeping: addressing a device field by
|
|
4652
|
+
* re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
|
|
4653
|
+
* read are the vocabulary any cross-device derivation needs, and they were
|
|
4654
|
+
* already correct. What wiring got wrong was the DESTINATION — a field on
|
|
4655
|
+
* somebody else's device, with no identity — not the source.
|
|
4656
|
+
*
|
|
4657
|
+
* These shapes are therefore kept, re-homed next to the engine that consumes
|
|
4658
|
+
* them, and are the binding type of a composition recipe (the source picker
|
|
4659
|
+
* stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
|
|
4660
|
+
* binding is a read, never another expression.
|
|
4661
|
+
*
|
|
4662
|
+
* Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
|
|
4663
|
+
* a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
|
|
4606
4664
|
*/
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4665
|
+
/** Read a sibling accessory's status field, addressed by the sibling's key.
|
|
4666
|
+
* `kind` is optional for wire compatibility — absent means `'field'`. */
|
|
4667
|
+
var ExpressionFieldBindingSchema = z.object({
|
|
4668
|
+
kind: z.literal("field").optional(),
|
|
4669
|
+
sourceKey: z.string(),
|
|
4670
|
+
cap: z.string(),
|
|
4671
|
+
fieldPath: z.string()
|
|
4614
4672
|
});
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4673
|
+
/** A constant. No device is read. */
|
|
4674
|
+
var ExpressionLiteralBindingSchema = z.object({
|
|
4675
|
+
kind: z.literal("literal"),
|
|
4676
|
+
value: z.union([
|
|
4677
|
+
z.string(),
|
|
4678
|
+
z.number(),
|
|
4679
|
+
z.boolean(),
|
|
4680
|
+
z.null()
|
|
4681
|
+
])
|
|
4618
4682
|
});
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
className: z.string(),
|
|
4627
|
-
/**
|
|
4628
|
-
* Raw backend-native label the classifier actually emitted (e.g.
|
|
4629
|
-
* `Dog bark` for YAMNet, `dog_bark` for Apple SoundAnalysis). For
|
|
4630
|
-
* macro-aggregated entries this is the top raw contributor. Mirrors
|
|
4631
|
-
* `originalClass` on `SpatialDetection`.
|
|
4632
|
-
*/
|
|
4633
|
-
originalClass: z.string().optional(),
|
|
4634
|
-
score: z.number()
|
|
4683
|
+
/** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
|
|
4684
|
+
* never by numeric id, which a re-adoption reissues. */
|
|
4685
|
+
var ExpressionGlobalBindingSchema = z.object({
|
|
4686
|
+
kind: z.literal("global"),
|
|
4687
|
+
sourceStableId: z.string(),
|
|
4688
|
+
cap: z.string(),
|
|
4689
|
+
fieldPath: z.string()
|
|
4635
4690
|
});
|
|
4636
|
-
var
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4691
|
+
var ExpressionBindingSourceSchema = z.union([
|
|
4692
|
+
ExpressionFieldBindingSchema,
|
|
4693
|
+
ExpressionLiteralBindingSchema,
|
|
4694
|
+
ExpressionGlobalBindingSchema
|
|
4695
|
+
]);
|
|
4696
|
+
z.object({
|
|
4697
|
+
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
4698
|
+
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
4699
|
+
}).superRefine((src, ctx) => {
|
|
4700
|
+
const err = validateExpressionSource(src);
|
|
4701
|
+
if (err !== null) ctx.addIssue({
|
|
4702
|
+
code: "custom",
|
|
4703
|
+
message: err,
|
|
4704
|
+
path: ["expr"]
|
|
4705
|
+
});
|
|
4643
4706
|
});
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4707
|
+
/** How a leaf compares a device field to a value. Derived from the field's
|
|
4708
|
+
* `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
4709
|
+
var AutomationConditionOperatorSchema = z.enum([
|
|
4710
|
+
"eq",
|
|
4711
|
+
"ne",
|
|
4712
|
+
"gt",
|
|
4713
|
+
"gte",
|
|
4714
|
+
"lt",
|
|
4715
|
+
"lte",
|
|
4716
|
+
"contains",
|
|
4717
|
+
"in"
|
|
4718
|
+
]);
|
|
4719
|
+
var AutomationConditionLeafSchema = z.object({
|
|
4720
|
+
kind: z.literal("condition"),
|
|
4721
|
+
deviceId: z.number().int().nonnegative(),
|
|
4722
|
+
cap: z.string().min(1),
|
|
4723
|
+
fieldPath: z.string().min(1),
|
|
4724
|
+
operator: AutomationConditionOperatorSchema,
|
|
4725
|
+
value: z.union([
|
|
4726
|
+
z.string(),
|
|
4727
|
+
z.number(),
|
|
4728
|
+
z.boolean(),
|
|
4729
|
+
z.array(z.union([z.string(), z.number()]))
|
|
4730
|
+
])
|
|
4647
4731
|
});
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4732
|
+
/**
|
|
4733
|
+
* The expression leaf, declared as a plain object rather than an intersection
|
|
4734
|
+
* with {@link ExpressionSourceSchema}: a discriminated union has to be able to
|
|
4735
|
+
* read `kind` off each option, and an intersection hides it. The author-time
|
|
4736
|
+
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
4737
|
+
* cannot drift — an expression that one accepts, the other accepts.
|
|
4738
|
+
*/
|
|
4739
|
+
var AutomationConditionExpressionSchema = z.object({
|
|
4740
|
+
kind: z.literal("expression"),
|
|
4741
|
+
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
4742
|
+
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
4743
|
+
}).superRefine((src, ctx) => {
|
|
4744
|
+
const err = validateExpressionSource(src);
|
|
4745
|
+
if (err !== null) ctx.addIssue({
|
|
4746
|
+
code: "custom",
|
|
4747
|
+
message: err,
|
|
4748
|
+
path: ["expr"]
|
|
4749
|
+
});
|
|
4652
4750
|
});
|
|
4653
|
-
var
|
|
4654
|
-
|
|
4751
|
+
var AutomationConditionSchema = z.lazy(() => z.discriminatedUnion("kind", [
|
|
4752
|
+
z.object({
|
|
4753
|
+
kind: z.literal("all"),
|
|
4754
|
+
children: z.array(AutomationConditionSchema)
|
|
4755
|
+
}),
|
|
4756
|
+
z.object({
|
|
4757
|
+
kind: z.literal("any"),
|
|
4758
|
+
children: z.array(AutomationConditionSchema)
|
|
4759
|
+
}),
|
|
4760
|
+
z.object({
|
|
4761
|
+
kind: z.literal("not"),
|
|
4762
|
+
child: AutomationConditionSchema
|
|
4763
|
+
}),
|
|
4764
|
+
AutomationConditionLeafSchema,
|
|
4765
|
+
AutomationConditionExpressionSchema
|
|
4766
|
+
]));
|
|
4767
|
+
/**
|
|
4768
|
+
* What starts a run.
|
|
4769
|
+
*
|
|
4770
|
+
* D8 compliance, and it is the reason `device-state` is not merely an event
|
|
4771
|
+
* subscription: the trigger evaluates against the **state mirror**, which is
|
|
4772
|
+
* reconciled, and an event only WAKES the evaluation. A dropped event therefore
|
|
4773
|
+
* DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
|
|
4774
|
+
* already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
|
|
4775
|
+
* not.
|
|
4776
|
+
*/
|
|
4777
|
+
var AutomationTriggerSchema = z.discriminatedUnion("kind", [
|
|
4778
|
+
z.object({
|
|
4779
|
+
kind: z.literal("device-state"),
|
|
4780
|
+
deviceId: z.number().int().nonnegative(),
|
|
4781
|
+
cap: z.string().min(1),
|
|
4782
|
+
fieldPath: z.string().min(1),
|
|
4783
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
4784
|
+
becomes: z.union([
|
|
4785
|
+
z.string(),
|
|
4786
|
+
z.number(),
|
|
4787
|
+
z.boolean()
|
|
4788
|
+
]).optional(),
|
|
4789
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
4790
|
+
edge: z.boolean().optional(),
|
|
4791
|
+
/** The condition must hold this long before the run starts. */
|
|
4792
|
+
forMs: z.number().int().min(0).max(864e5).optional(),
|
|
4793
|
+
/** Collapse a burst into one run. */
|
|
4794
|
+
debounceMs: z.number().int().min(0).max(6e5).optional()
|
|
4795
|
+
}),
|
|
4796
|
+
z.object({
|
|
4797
|
+
kind: z.literal("device-event"),
|
|
4798
|
+
/** An `EventCategory` value. */
|
|
4799
|
+
category: z.string().min(1),
|
|
4800
|
+
deviceId: z.number().int().nonnegative().optional()
|
|
4801
|
+
}),
|
|
4802
|
+
z.object({
|
|
4803
|
+
kind: z.literal("schedule"),
|
|
4804
|
+
cron: z.string().min(1).max(120)
|
|
4805
|
+
}),
|
|
4806
|
+
z.object({ kind: z.literal("manual") })
|
|
4807
|
+
]);
|
|
4808
|
+
/**
|
|
4809
|
+
* One action step.
|
|
4810
|
+
*
|
|
4811
|
+
* `wait` and `cap` are `NcRuleActionSchema`'s two members, kept structurally
|
|
4812
|
+
* identical so `NcRuleActionRunner` runs them unchanged — its device-scope
|
|
4813
|
+
* check, stop-at-first-failure and per-sequence throttle are the whole reason
|
|
4814
|
+
* to reuse it, and none of them are re-implemented here.
|
|
4815
|
+
*
|
|
4816
|
+
* **The one divergence, and it is forced.** `NcRuleActionSchema.cap.deviceId` is
|
|
4817
|
+
* a literal `z.number().int()`, and the NC runner's own `RunSequencesInput`
|
|
4818
|
+
* documents its subject device as *"for the log tag, never for routing"*. So an
|
|
4819
|
+
* NC action can never target the device that triggered it — which is fine for
|
|
4820
|
+
* the NC (its rules already scope to a device) and fatal for an automation
|
|
4821
|
+
* ("sound the siren of the camera that saw the person"). `deviceId` therefore
|
|
4822
|
+
* also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
|
|
4823
|
+
* is called. The runner still receives a number and is untouched; the
|
|
4824
|
+
* resolution is the recipe's job, not the runner's.
|
|
4825
|
+
*/
|
|
4826
|
+
var AutomationActionSchema = z.discriminatedUnion("kind", [
|
|
4827
|
+
z.object({
|
|
4828
|
+
kind: z.literal("wait"),
|
|
4829
|
+
seconds: z.number().min(0).max(300)
|
|
4830
|
+
}),
|
|
4831
|
+
z.object({
|
|
4832
|
+
kind: z.literal("cap"),
|
|
4833
|
+
deviceId: z.union([z.number().int(), z.object({ $var: z.string().min(1) })]),
|
|
4834
|
+
cap: z.string().min(1),
|
|
4835
|
+
method: z.string().min(1),
|
|
4836
|
+
/** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
|
|
4837
|
+
* evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
|
|
4838
|
+
args: z.record(z.string(), z.unknown()).optional()
|
|
4839
|
+
}),
|
|
4840
|
+
z.object({
|
|
4841
|
+
kind: z.literal("code"),
|
|
4842
|
+
/** Compiled into the automation's OWN block by esbuild — not a third
|
|
4843
|
+
* runtime, not a `vm`, and not dynamically evaluated. */
|
|
4844
|
+
code: z.string().min(1).max(2e4)
|
|
4845
|
+
})
|
|
4846
|
+
]);
|
|
4847
|
+
z.object({
|
|
4848
|
+
triggers: z.array(AutomationTriggerSchema),
|
|
4849
|
+
conditions: AutomationConditionSchema.optional(),
|
|
4850
|
+
actions: z.array(AutomationActionSchema)
|
|
4851
|
+
});
|
|
4852
|
+
/**
|
|
4853
|
+
* `addon-pages` — system-scoped singleton aggregator cap. Public-facing
|
|
4854
|
+
* surface that admin-ui consumes through `useAddonPagesListPages()`.
|
|
4855
|
+
*
|
|
4856
|
+
* The provider iterates every `addon-pages-source` (collection) provider
|
|
4857
|
+
* and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
|
|
4858
|
+
* pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
|
|
4859
|
+
* filesystem `mtime` cache-buster lets the browser pick up addon
|
|
4860
|
+
* rebuilds without manual reload.
|
|
4861
|
+
*
|
|
4862
|
+
* The hub-local builtin `addon-pages-aggregator` (see
|
|
4863
|
+
* `@camstack/system/builtins/addon-pages-aggregator`) registers the
|
|
4864
|
+
* provider. Splitting the public aggregator from the raw collection
|
|
4865
|
+
* keeps both ends in codegen — there's no hand-written
|
|
4866
|
+
* `addon-pages.router.ts` wrapper anymore.
|
|
4867
|
+
*/
|
|
4868
|
+
var AddonPageDeclarationSchema$1 = z.object({
|
|
4869
|
+
id: z.string(),
|
|
4870
|
+
label: z.string(),
|
|
4871
|
+
icon: z.string(),
|
|
4872
|
+
path: z.string(),
|
|
4873
|
+
remoteName: z.string(),
|
|
4874
|
+
bundle: z.string(),
|
|
4875
|
+
section: z.string().optional(),
|
|
4876
|
+
sectionLabel: z.string().optional()
|
|
4877
|
+
});
|
|
4878
|
+
var AddonPageInfoSchema = z.object({
|
|
4879
|
+
addonId: z.string(),
|
|
4880
|
+
page: AddonPageDeclarationSchema$1,
|
|
4881
|
+
bundleUrl: z.string()
|
|
4882
|
+
});
|
|
4883
|
+
var addonPagesCapability = {
|
|
4884
|
+
name: "addon-pages",
|
|
4655
4885
|
scope: "system",
|
|
4656
4886
|
mode: "singleton",
|
|
4657
|
-
methods: {
|
|
4658
|
-
analyseChunk: method(z.object({
|
|
4659
|
-
chunk: AudioChunkInputSchema,
|
|
4660
|
-
settings: AudioAnalysisSettingsSchema
|
|
4661
|
-
}), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }),
|
|
4662
|
-
classify: method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }),
|
|
4663
|
-
isReady: method(z.void(), z.boolean()),
|
|
4664
|
-
dispose: method(z.void(), z.void(), { kind: "mutation" }),
|
|
4665
|
-
/**
|
|
4666
|
-
* Re-run the host platform-probe for the audio backend and persist
|
|
4667
|
-
* the detected value into `probedBestAudioBackend`. Replaces the
|
|
4668
|
-
* orchestrator's legacy `AgentPipelineSettings.audio.engine` —
|
|
4669
|
-
* audio-analyzer now owns the choice. Operator `audioBackend` is
|
|
4670
|
-
* not touched; only the probed-best hint.
|
|
4671
|
-
*/
|
|
4672
|
-
reprobeAudioEngine: method(z.void(), z.object({ backend: z.string() }), {
|
|
4673
|
-
kind: "mutation",
|
|
4674
|
-
auth: "admin"
|
|
4675
|
-
})
|
|
4676
|
-
}
|
|
4887
|
+
methods: { listPages: method(z.void(), z.array(AddonPageInfoSchema).readonly()) }
|
|
4677
4888
|
};
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4889
|
+
/**
|
|
4890
|
+
* `addon-pages-source` — collection cap exposing per-provider raw page
|
|
4891
|
+
* declarations. Every addon that contributes a UI page registers a
|
|
4892
|
+
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
4893
|
+
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
4894
|
+
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
4895
|
+
* that admin-ui consumes.
|
|
4896
|
+
*
|
|
4897
|
+
* The split exists because the public listing has a different output
|
|
4898
|
+
* shape than the per-provider raw declarations, and we want both ends
|
|
4899
|
+
* to flow through codegen instead of relying on a hand-written wrapper.
|
|
4900
|
+
*/
|
|
4901
|
+
var AddonPageDeclarationSchema = z.object({
|
|
4902
|
+
id: z.string(),
|
|
4903
|
+
label: z.string(),
|
|
4904
|
+
icon: z.string(),
|
|
4905
|
+
path: z.string(),
|
|
4906
|
+
/**
|
|
4907
|
+
* Module Federation remote name — must match the `name` field on the
|
|
4908
|
+
* page addon's `federation()` plugin config. Used by admin-ui's
|
|
4909
|
+
* `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
|
|
4910
|
+
* Conventionally `addon_<id>_page` (snake_case; MF names cannot
|
|
4911
|
+
* contain hyphens).
|
|
4912
|
+
*/
|
|
4913
|
+
remoteName: z.string(),
|
|
4914
|
+
/**
|
|
4915
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
4916
|
+
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
4917
|
+
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
4918
|
+
* the static-file route can compute an mtime-based cache-buster URL
|
|
4919
|
+
* without a separate filesystem stat.
|
|
4920
|
+
*/
|
|
4921
|
+
bundle: z.string(),
|
|
4922
|
+
/**
|
|
4923
|
+
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
4924
|
+
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
4925
|
+
* Any OTHER string creates (or joins) a custom section rendered after
|
|
4926
|
+
* the built-in groups; its label comes from `sectionLabel` (first
|
|
4927
|
+
* declaration wins), falling back to the id. Absent → the legacy
|
|
4928
|
+
* "Addon Pages" group.
|
|
4929
|
+
*/
|
|
4930
|
+
section: z.string().optional(),
|
|
4931
|
+
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
4932
|
+
sectionLabel: z.string().optional()
|
|
4684
4933
|
});
|
|
4685
|
-
var
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4934
|
+
var addonPagesSourceCapability = {
|
|
4935
|
+
name: "addon-pages-source",
|
|
4936
|
+
scope: "system",
|
|
4937
|
+
mode: "collection",
|
|
4938
|
+
internal: true,
|
|
4939
|
+
methods: { listPages: method(z.void(), z.array(AddonPageDeclarationSchema).readonly()) }
|
|
4940
|
+
};
|
|
4941
|
+
var AddonHttpRouteSchema = z.object({
|
|
4942
|
+
method: z.enum([
|
|
4943
|
+
"GET",
|
|
4944
|
+
"POST",
|
|
4945
|
+
"PUT",
|
|
4946
|
+
"DELETE",
|
|
4947
|
+
"PATCH"
|
|
4948
|
+
]),
|
|
4949
|
+
path: z.string(),
|
|
4950
|
+
access: z.enum([
|
|
4951
|
+
"public",
|
|
4952
|
+
"authenticated",
|
|
4953
|
+
"admin"
|
|
4954
|
+
]).optional(),
|
|
4955
|
+
description: z.string().optional()
|
|
4695
4956
|
});
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4957
|
+
/**
|
|
4958
|
+
* Cross-process route invocation envelope. The hub captures the
|
|
4959
|
+
* request as plain data, ships it to the worker via Moleculer, and
|
|
4960
|
+
* the worker runs the local handler against a capturing reply. The
|
|
4961
|
+
* envelope returned describes what the handler intended (status,
|
|
4962
|
+
* headers, body, or a redirect) so the hub can translate it back to
|
|
4963
|
+
* the Fastify reply that's actually wired to the socket.
|
|
4964
|
+
*/
|
|
4965
|
+
var InvokeRequestSchema = z.object({
|
|
4966
|
+
method: z.string(),
|
|
4967
|
+
path: z.string(),
|
|
4968
|
+
params: z.record(z.string(), z.string()),
|
|
4969
|
+
query: z.record(z.string(), z.string()),
|
|
4970
|
+
body: z.unknown(),
|
|
4971
|
+
headers: z.record(z.string(), z.string()),
|
|
4972
|
+
user: z.object({
|
|
4973
|
+
id: z.string(),
|
|
4974
|
+
username: z.string(),
|
|
4975
|
+
isAdmin: z.boolean()
|
|
4976
|
+
}).optional(),
|
|
4977
|
+
scopedToken: z.unknown().optional()
|
|
4706
4978
|
});
|
|
4707
|
-
var
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4979
|
+
var InvokeReplyEnvelopeSchema = z.object({
|
|
4980
|
+
status: z.number().int(),
|
|
4981
|
+
headers: z.record(z.string(), z.string()),
|
|
4982
|
+
/** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
|
|
4983
|
+
* sending `body`. Status defaults to 302 when this is set unless
|
|
4984
|
+
* the handler called `reply.code(...)` explicitly. */
|
|
4985
|
+
redirectUrl: z.string().nullable(),
|
|
4986
|
+
/** JSON-serializable body. `undefined` is treated as "no body". */
|
|
4987
|
+
body: z.unknown().optional(),
|
|
4988
|
+
/** Set when the handler called `reply.type(mime)`. */
|
|
4989
|
+
contentType: z.string().optional()
|
|
4713
4990
|
});
|
|
4714
|
-
var
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4991
|
+
var addonRoutesCapability = {
|
|
4992
|
+
name: "addon-routes",
|
|
4993
|
+
scope: "system",
|
|
4994
|
+
mode: "collection",
|
|
4995
|
+
internal: true,
|
|
4996
|
+
methods: {
|
|
4997
|
+
getRoutes: method(z.void(), z.array(AddonHttpRouteSchema)),
|
|
4998
|
+
/**
|
|
4999
|
+
* Cross-process dispatch entry point. Forked addons implement this
|
|
5000
|
+
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
5001
|
+
* can route through Moleculer when the handler lives in a worker.
|
|
5002
|
+
*
|
|
5003
|
+
* Local addons can implement it for free with the same helper;
|
|
5004
|
+
* the hub bypasses the wire on co-located addons.
|
|
5005
|
+
*/
|
|
5006
|
+
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
5007
|
+
},
|
|
5008
|
+
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
5009
|
+
mount: { kind: "skip" }
|
|
5010
|
+
};
|
|
5011
|
+
var ConfigTabDeclarationSchema = z.object({
|
|
5012
|
+
id: z.string(),
|
|
5013
|
+
label: z.string(),
|
|
5014
|
+
icon: z.string(),
|
|
5015
|
+
order: z.number().optional()
|
|
4719
5016
|
});
|
|
4720
|
-
var
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
5017
|
+
var ConfigSectionWithValuesSchema = z.object({
|
|
5018
|
+
id: z.string(),
|
|
5019
|
+
title: z.string(),
|
|
5020
|
+
description: z.string().optional(),
|
|
5021
|
+
style: z.enum(["card", "accordion"]).optional(),
|
|
5022
|
+
defaultCollapsed: z.boolean().optional(),
|
|
5023
|
+
columns: z.union([
|
|
5024
|
+
z.literal(1),
|
|
5025
|
+
z.literal(2),
|
|
5026
|
+
z.literal(3),
|
|
5027
|
+
z.literal(4)
|
|
5028
|
+
]).optional(),
|
|
5029
|
+
tab: z.string().optional(),
|
|
5030
|
+
location: z.enum(["settings", "top-tab"]).optional(),
|
|
5031
|
+
order: z.number().optional(),
|
|
5032
|
+
fields: z.array(z.any())
|
|
5033
|
+
});
|
|
5034
|
+
var SettingsSchemaWithValuesSchema = z.object({
|
|
5035
|
+
tabs: z.array(ConfigTabDeclarationSchema).optional(),
|
|
5036
|
+
sections: z.array(ConfigSectionWithValuesSchema)
|
|
4734
5037
|
});
|
|
5038
|
+
/** Patch object — keys are field names, values are the new field values. */
|
|
5039
|
+
var SettingsPatchSchema = z.record(z.string(), z.unknown());
|
|
5040
|
+
/** Standard success response for update operations. */
|
|
5041
|
+
var SettingsUpdateResultSchema = z.object({ success: z.literal(true) });
|
|
4735
5042
|
/**
|
|
4736
|
-
*
|
|
5043
|
+
* addon-settings — singleton gateway for three-level addon settings.
|
|
4737
5044
|
*
|
|
4738
|
-
*
|
|
4739
|
-
*
|
|
4740
|
-
*
|
|
4741
|
-
*
|
|
5045
|
+
* Works like `device-manager`: a single hub-side provider that resolves
|
|
5046
|
+
* `addonId` to the target addon and delegates the call. For hub-local
|
|
5047
|
+
* addons the call is direct; for remote agents it proxies via the
|
|
5048
|
+
* per-addon Moleculer service.
|
|
4742
5049
|
*
|
|
4743
|
-
*
|
|
4744
|
-
*
|
|
4745
|
-
*
|
|
5050
|
+
* Replaces the `$addonHost` Moleculer service. Transport transparency
|
|
5051
|
+
* is handled by the provider implementation — callers (admin UI, other
|
|
5052
|
+
* addons via `ctx.api`) never know which node hosts the target addon.
|
|
5053
|
+
*
|
|
5054
|
+
* Three levels:
|
|
5055
|
+
* - **addon**: addon-scoped settings (installation config, API keys, …)
|
|
5056
|
+
* - **global**: settings applied to all devices by default
|
|
5057
|
+
* - **device**: per-device overrides
|
|
5058
|
+
*
|
|
5059
|
+
* Optional `nodeId` allows explicit node targeting. When absent the
|
|
5060
|
+
* provider resolves the addon's host node automatically.
|
|
4746
5061
|
*/
|
|
4747
|
-
|
|
4748
|
-
|
|
5062
|
+
/**
|
|
5063
|
+
* `addon-settings` is a **hub-centric** cap: the hub hosts the single
|
|
5064
|
+
* provider, and `nodeId` in the input is data for the hub provider's
|
|
5065
|
+
* internal dispatcher, not a routing hint for the cap-router. See
|
|
5066
|
+
* `CapabilityDefinition.nodeIdMode` for the contract.
|
|
5067
|
+
*/
|
|
5068
|
+
var addonSettingsCapability = {
|
|
5069
|
+
name: "addon-settings",
|
|
4749
5070
|
scope: "system",
|
|
4750
5071
|
mode: "singleton",
|
|
4751
|
-
|
|
5072
|
+
nodeIdMode: "data",
|
|
4752
5073
|
methods: {
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
/** Cheap predicate — does the runtime support `(codec, kind)`? */
|
|
4756
|
-
canHandle: method(z.object({
|
|
4757
|
-
codec: z.string(),
|
|
4758
|
-
kind: z.enum(["decode", "encode"])
|
|
4759
|
-
}), z.boolean()),
|
|
4760
|
-
createDecodeSession: method(AudioDecodeSessionConfigSchema, z.object({
|
|
4761
|
-
sessionId: z.string(),
|
|
4762
|
-
nodeId: z.string()
|
|
4763
|
-
}), { kind: "mutation" }),
|
|
4764
|
-
createEncodeSession: method(AudioEncodeSessionConfigSchema, z.object({
|
|
4765
|
-
sessionId: z.string(),
|
|
4766
|
-
nodeId: z.string()
|
|
4767
|
-
}), { kind: "mutation" }),
|
|
4768
|
-
closeSession: method(z.object({
|
|
4769
|
-
sessionId: z.string(),
|
|
4770
|
-
nodeId: z.string().optional()
|
|
4771
|
-
}), z.void(), { kind: "mutation" }),
|
|
4772
|
-
/** Push one encoded audio frame into a decode session. */
|
|
4773
|
-
pushEncodedFrame: method(z.object({
|
|
4774
|
-
sessionId: z.string(),
|
|
4775
|
-
nodeId: z.string().optional(),
|
|
4776
|
-
data: z.instanceof(Uint8Array),
|
|
4777
|
-
/** Source PTS in milliseconds. Synthesised when omitted. */
|
|
4778
|
-
pts: z.number().optional()
|
|
4779
|
-
}), z.void(), { kind: "mutation" }),
|
|
4780
|
-
/** Pull up to `maxCount` PCM chunks from a decode session. */
|
|
4781
|
-
pullPcm: method(z.object({
|
|
4782
|
-
sessionId: z.string(),
|
|
4783
|
-
nodeId: z.string().optional(),
|
|
4784
|
-
maxCount: z.number().int().positive().default(8)
|
|
4785
|
-
}), z.array(AudioPcmChunkSchema)),
|
|
4786
|
-
/** Push one PCM chunk into an encode session. */
|
|
4787
|
-
pushPcm: method(z.object({
|
|
4788
|
-
sessionId: z.string(),
|
|
5074
|
+
getGlobalSettings: method(z.object({
|
|
5075
|
+
addonId: z.string(),
|
|
4789
5076
|
nodeId: z.string().optional(),
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
pullEncoded: method(z.object({
|
|
4796
|
-
sessionId: z.string(),
|
|
5077
|
+
overlay: z.record(z.string(), z.unknown()).optional(),
|
|
5078
|
+
cap: z.string().optional()
|
|
5079
|
+
}), SettingsSchemaWithValuesSchema.nullable()),
|
|
5080
|
+
updateGlobalSettings: method(z.object({
|
|
5081
|
+
addonId: z.string(),
|
|
4797
5082
|
nodeId: z.string().optional(),
|
|
4798
|
-
|
|
4799
|
-
}),
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
5083
|
+
patch: SettingsPatchSchema
|
|
5084
|
+
}), SettingsUpdateResultSchema, {
|
|
5085
|
+
kind: "mutation",
|
|
5086
|
+
auth: "admin"
|
|
5087
|
+
}),
|
|
5088
|
+
getDeviceSettings: method(z.object({
|
|
5089
|
+
addonId: z.string(),
|
|
5090
|
+
deviceId: z.number(),
|
|
4803
5091
|
nodeId: z.string().optional()
|
|
4804
|
-
}),
|
|
4805
|
-
|
|
5092
|
+
}), SettingsSchemaWithValuesSchema.nullable()),
|
|
5093
|
+
updateDeviceSettings: method(z.object({
|
|
5094
|
+
addonId: z.string(),
|
|
5095
|
+
deviceId: z.number(),
|
|
5096
|
+
nodeId: z.string().optional(),
|
|
5097
|
+
patch: SettingsPatchSchema
|
|
5098
|
+
}), SettingsUpdateResultSchema, {
|
|
5099
|
+
kind: "mutation",
|
|
5100
|
+
auth: "admin"
|
|
5101
|
+
})
|
|
4806
5102
|
}
|
|
4807
5103
|
};
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
5104
|
+
/**
|
|
5105
|
+
* `addon-widgets-source` — collection cap exposing per-addon raw widget
|
|
5106
|
+
* declarations. Mirrors the addon-pages split: every addon shipping
|
|
5107
|
+
* widgets registers a provider on this collection cap; the hub-local
|
|
5108
|
+
* aggregator (`addon-widgets`, see `addon-widgets.cap.ts`) walks the
|
|
5109
|
+
* collection, stamps versioned `bundleUrl`s onto each declaration, and
|
|
5110
|
+
* exposes the public listing surface that admin-ui consumes.
|
|
5111
|
+
*
|
|
5112
|
+
* The split exists because the public listing has a different output
|
|
5113
|
+
* shape (flat enriched metadata with `addonId` + `bundleUrl`) than the
|
|
5114
|
+
* per-provider raw declarations. Both ends flow through codegen.
|
|
5115
|
+
*
|
|
5116
|
+
* Unified UI-contribution model (Task 10): a widget descriptor IS a
|
|
5117
|
+
* `UiContribution` with `kind:'remote'`. The host renders it through the
|
|
5118
|
+
* same `ContributionRenderer` / Module-Federation path as every other
|
|
5119
|
+
* contributed UI surface — no bespoke widget-rendering path. The widget-
|
|
5120
|
+
* only metadata (sizing hints, `requires`) lives as extra fields on the
|
|
5121
|
+
* descriptor; the `UiContribution` core (`tab` / `label` / `order` /
|
|
5122
|
+
* `kind` / `remote`) carries identity + placement + the MF remote.
|
|
5123
|
+
*/
|
|
5124
|
+
/** Where the widget makes sense to render — maps to a contribution `tab`. */
|
|
5125
|
+
var WidgetHostEnum = z.enum([
|
|
5126
|
+
"device-tab",
|
|
5127
|
+
"dashboard",
|
|
5128
|
+
"integration-detail"
|
|
5129
|
+
]);
|
|
5130
|
+
var WidgetSizeEnum = z.enum([
|
|
5131
|
+
"xs",
|
|
5132
|
+
"sm",
|
|
5133
|
+
"md",
|
|
5134
|
+
"lg",
|
|
5135
|
+
"xl"
|
|
5136
|
+
]);
|
|
5137
|
+
/**
|
|
5138
|
+
* MF remote descriptor — mirrors `UiContributionRemote` from
|
|
5139
|
+
* `capability-definition.ts`. Widget remotes expose a single
|
|
5140
|
+
* `'./widgets'` module whose default export is a
|
|
5141
|
+
* `Record<componentKey, Component>` map; `componentKey` (the widget
|
|
5142
|
+
* `stableId`) picks the entry the host mounts.
|
|
5143
|
+
*/
|
|
5144
|
+
var WidgetRemoteSchema = z.object({
|
|
5145
|
+
remoteName: z.string(),
|
|
5146
|
+
exposedModule: z.string(),
|
|
5147
|
+
componentKey: z.string().optional()
|
|
5148
|
+
});
|
|
5149
|
+
/**
|
|
5150
|
+
* One widget declaration — a `UiContribution` (`kind:'remote'`) plus
|
|
5151
|
+
* widget-only metadata. The `UiContribution` core fields:
|
|
5152
|
+
*
|
|
5153
|
+
* - `tab` — where the widget hosts. A widget that runs on the
|
|
5154
|
+
* dashboard declares `tab:'dashboard'`; a device-tab
|
|
5155
|
+
* widget declares the target device-detail tab id.
|
|
5156
|
+
* - `subTab` — optional sub-tab within `tab`.
|
|
5157
|
+
* - `label` — operator-facing label.
|
|
5158
|
+
* - `order` — ordering within `(tab, subTab)`.
|
|
5159
|
+
* - `kind` — always `'remote'` for widgets.
|
|
5160
|
+
* - `remote` — the MF remote `{ remoteName, exposedModule, componentKey }`.
|
|
5161
|
+
*
|
|
5162
|
+
* Widget-only fields retained alongside the contribution core:
|
|
5163
|
+
*
|
|
5164
|
+
* - `stableId` — stable identity within the addon (the MF
|
|
5165
|
+
* `componentKey`; kept top-level so consumers have
|
|
5166
|
+
* a stable key without reaching into `remote`).
|
|
5167
|
+
* - `description` / `icon` — picker metadata.
|
|
5168
|
+
* - `bundle` — entry filename inside the addon `dist/` dir; the
|
|
5169
|
+
* aggregator stamps a versioned `bundleUrl` from it.
|
|
5170
|
+
* - `hosts` — every host the widget supports (a widget can run
|
|
5171
|
+
* both on the dashboard and a device tab). `tab`
|
|
5172
|
+
* is the PRIMARY host; `hosts` is the full set the
|
|
5173
|
+
* picker filters on.
|
|
5174
|
+
* - `requires` — host-context requirements validated at mount.
|
|
5175
|
+
* - `defaultSize` / `allowedSizes` / `defaultColumns` / `defaultRows`
|
|
5176
|
+
* — dashboard placement hints.
|
|
5177
|
+
*/
|
|
5178
|
+
var WidgetMetadataSchema = z.object({
|
|
5179
|
+
/** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
|
|
5180
|
+
tab: z.string(),
|
|
5181
|
+
/** Optional sub-tab within `tab`. */
|
|
5182
|
+
subTab: z.string().optional(),
|
|
5183
|
+
/** Operator-facing label. */
|
|
5184
|
+
label: z.string(),
|
|
5185
|
+
/** Ordering within `(tab, subTab)`, ascending. */
|
|
5186
|
+
order: z.number().optional(),
|
|
5187
|
+
/** Always `'remote'` — a widget is a Module Federation remote. */
|
|
5188
|
+
kind: z.literal("remote"),
|
|
5189
|
+
/** MF remote descriptor. */
|
|
5190
|
+
remote: WidgetRemoteSchema,
|
|
5191
|
+
/** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
|
|
5192
|
+
stableId: z.string(),
|
|
5193
|
+
description: z.string().optional(),
|
|
5194
|
+
icon: z.string().optional(),
|
|
4813
5195
|
/**
|
|
4814
|
-
*
|
|
4815
|
-
*
|
|
4816
|
-
*
|
|
4817
|
-
*
|
|
4818
|
-
*
|
|
5196
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
5197
|
+
* `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
|
|
5198
|
+
* this is always `'remoteEntry.js'` — the value is kept on the
|
|
5199
|
+
* metadata so the static-file route can compute an mtime-based
|
|
5200
|
+
* cache-buster URL without a separate filesystem stat.
|
|
4819
5201
|
*/
|
|
4820
|
-
|
|
5202
|
+
bundle: z.string(),
|
|
5203
|
+
/** Every host the widget supports. The picker filters on this set. */
|
|
5204
|
+
hosts: z.array(WidgetHostEnum).readonly(),
|
|
5205
|
+
/** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
|
|
5206
|
+
requires: z.object({
|
|
5207
|
+
deviceContext: z.boolean().default(false),
|
|
5208
|
+
integrationContext: z.boolean().default(false)
|
|
5209
|
+
}),
|
|
5210
|
+
/**
|
|
5211
|
+
* Loadable BEFORE authentication. The normal widget registry listing
|
|
5212
|
+
* (`addon-widgets.listWidgets`) is auth-gated, so a pre-auth surface
|
|
5213
|
+
* (the login page) cannot discover a widget through it. A widget that
|
|
5214
|
+
* declares `preAuth: true` marks itself as safe to mount on a pre-auth
|
|
5215
|
+
* screen — it is surfaced through the PUBLIC `auth.listLoginMethods`
|
|
5216
|
+
* login-method contribution channel (see `login-method.cap.ts`) rather
|
|
5217
|
+
* than the authenticated registry, and its bundle is served by the
|
|
5218
|
+
* public `/api/addon-widgets/:addonId/*` static route. Defaults false.
|
|
5219
|
+
*/
|
|
5220
|
+
preAuth: z.boolean().optional().default(false),
|
|
5221
|
+
/** Dashboard placement HINTS (operator can override per instance). */
|
|
5222
|
+
defaultSize: WidgetSizeEnum.default("md"),
|
|
5223
|
+
allowedSizes: z.array(WidgetSizeEnum).readonly().default([
|
|
5224
|
+
"sm",
|
|
5225
|
+
"md",
|
|
5226
|
+
"lg"
|
|
5227
|
+
]),
|
|
5228
|
+
defaultColumns: z.number().int().min(1).max(12).default(6),
|
|
5229
|
+
defaultRows: z.number().int().min(1).max(12).default(1)
|
|
4821
5230
|
});
|
|
4822
|
-
var
|
|
4823
|
-
name: "
|
|
5231
|
+
var addonWidgetsSourceCapability = {
|
|
5232
|
+
name: "addon-widgets-source",
|
|
4824
5233
|
scope: "system",
|
|
4825
5234
|
mode: "collection",
|
|
4826
5235
|
internal: true,
|
|
4827
|
-
methods: {
|
|
4828
|
-
validateCredentials: method(z.object({
|
|
4829
|
-
username: z.string(),
|
|
4830
|
-
password: z.string()
|
|
4831
|
-
}), AuthResultSchema.nullable(), { kind: "mutation" }),
|
|
4832
|
-
getLoginUrl: method(z.object({ state: z.string() }), z.string()),
|
|
4833
|
-
handleCallback: method(z.record(z.string(), z.string()), AuthResultSchema, { kind: "mutation" }),
|
|
4834
|
-
validateToken: method(z.object({ token: z.string() }), AuthResultSchema.nullable())
|
|
4835
|
-
},
|
|
4836
|
-
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
4837
|
-
mount: { kind: "skip" }
|
|
5236
|
+
methods: { listWidgets: method(z.void(), z.array(WidgetMetadataSchema).readonly()) }
|
|
4838
5237
|
};
|
|
4839
5238
|
/**
|
|
4840
|
-
*
|
|
4841
|
-
*
|
|
4842
|
-
* (admin UI, restore flow) see one canonical key.
|
|
5239
|
+
* `addon-widgets` — system-scoped singleton aggregator cap. Public-facing
|
|
5240
|
+
* surface that admin-ui consumes through `useAddonWidgetsListWidgets()`.
|
|
4843
5241
|
*
|
|
4844
|
-
*
|
|
4845
|
-
*
|
|
4846
|
-
*
|
|
4847
|
-
*
|
|
4848
|
-
* the
|
|
5242
|
+
* The provider iterates every `addon-widgets-source` (collection)
|
|
5243
|
+
* provider and emits `EnrichedWidgetMetadata[]` enriched with versioned
|
|
5244
|
+
* `bundleUrl` strings pointing at
|
|
5245
|
+
* `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem
|
|
5246
|
+
* `mtime` cache-buster lets the browser pick up addon rebuilds without
|
|
5247
|
+
* manual reload — same scheme used by `addon-pages`.
|
|
4849
5248
|
*
|
|
4850
|
-
*
|
|
4851
|
-
*
|
|
4852
|
-
*
|
|
5249
|
+
* The hub-local builtin `addon-widgets-aggregator` (see
|
|
5250
|
+
* `@camstack/system/builtins/addon-widgets-aggregator`) registers the
|
|
5251
|
+
* provider. Splitting the public aggregator from the raw collection
|
|
5252
|
+
* keeps both ends in codegen — there's no hand-written wrapper.
|
|
4853
5253
|
*/
|
|
4854
|
-
var
|
|
4855
|
-
/**
|
|
4856
|
-
* Sub-id within this addon. Convention `default` for single-
|
|
4857
|
-
* destination addons; addons that host many (S3 with N buckets)
|
|
4858
|
-
* pick stable per-bucket strings. The orchestrator stitches
|
|
4859
|
-
* `<manifestId>:<subId>` for the wire format.
|
|
4860
|
-
*/
|
|
4861
|
-
subId: z.string(),
|
|
4862
|
-
displayName: z.string(),
|
|
4863
|
-
description: z.string().optional(),
|
|
4864
|
-
kind: z.string(),
|
|
4865
|
-
triggerSupported: z.boolean(),
|
|
4866
|
-
restoreSupported: z.boolean()
|
|
4867
|
-
}).extend({
|
|
4868
|
-
/** `<addonId>:<subId>` — globally-unique dispatch key. */
|
|
4869
|
-
id: z.string(),
|
|
4870
|
-
/**
|
|
4871
|
-
* Manifest id of the owning addon. The Settings button on the
|
|
4872
|
-
* destination card opens that addon's `globalSettingsSchema` panel.
|
|
4873
|
-
*/
|
|
5254
|
+
var EnrichedWidgetMetadataSchema = WidgetMetadataSchema.extend({
|
|
4874
5255
|
addonId: z.string(),
|
|
4875
|
-
|
|
4876
|
-
enabled: z.boolean(),
|
|
4877
|
-
/** Operator-defined retention count (archives kept per destination). */
|
|
4878
|
-
retentionCount: z.number(),
|
|
4879
|
-
/** Operator-defined display label (overrides storage location displayName). */
|
|
4880
|
-
label: z.string().optional(),
|
|
4881
|
-
/** Newest-archive timestamp from `manifests.json`, or undefined. */
|
|
4882
|
-
lastSuccessAt: z.number().optional(),
|
|
4883
|
-
/** Newest-archive size from `manifests.json`, or undefined. */
|
|
4884
|
-
lastSuccessSizeBytes: z.number().optional(),
|
|
4885
|
-
/** Per-destination cron expression. Empty = manual-only (no schedule). */
|
|
4886
|
-
cron: z.string().optional(),
|
|
4887
|
-
/** ms-epoch of next computed firing for this destination's cron, if any. */
|
|
4888
|
-
nextRunAt: z.number().optional(),
|
|
4889
|
-
/** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
|
|
4890
|
-
lastRunAt: z.number().optional()
|
|
5256
|
+
bundleUrl: z.string()
|
|
4891
5257
|
});
|
|
5258
|
+
var addonWidgetsCapability = {
|
|
5259
|
+
name: "addon-widgets",
|
|
5260
|
+
scope: "system",
|
|
5261
|
+
mode: "singleton",
|
|
5262
|
+
methods: { listWidgets: method(z.void(), z.array(EnrichedWidgetMetadataSchema).readonly()) }
|
|
5263
|
+
};
|
|
4892
5264
|
/**
|
|
4893
|
-
*
|
|
4894
|
-
*
|
|
4895
|
-
*
|
|
5265
|
+
* Alerts capability — collection-based internal alert system.
|
|
5266
|
+
*
|
|
5267
|
+
* Multiple providers can register. Each provider filters by EventBus category
|
|
5268
|
+
* and creates/updates alerts. The built-in Alert Center addon persists alerts
|
|
5269
|
+
* in the DB and serves them to the admin UI.
|
|
4896
5270
|
*/
|
|
4897
|
-
var
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5271
|
+
var AlertSeveritySchema = z.enum([
|
|
5272
|
+
"info",
|
|
5273
|
+
"success",
|
|
5274
|
+
"warning",
|
|
5275
|
+
"error"
|
|
5276
|
+
]);
|
|
5277
|
+
var AlertStatusSchema = z.enum([
|
|
5278
|
+
"active",
|
|
5279
|
+
"in-progress",
|
|
5280
|
+
"completed",
|
|
5281
|
+
"failed",
|
|
5282
|
+
"dismissed"
|
|
5283
|
+
]);
|
|
5284
|
+
var AlertSourceSchema = z.object({
|
|
5285
|
+
type: z.string(),
|
|
5286
|
+
id: z.string()
|
|
4905
5287
|
});
|
|
4906
|
-
var
|
|
5288
|
+
var AlertSchema = z.object({
|
|
4907
5289
|
id: z.string(),
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
var ArchiveEntrySchema = z.object({
|
|
4916
|
-
path: z.string(),
|
|
4917
|
-
kind: z.enum([
|
|
4918
|
-
"file",
|
|
4919
|
-
"dir",
|
|
4920
|
-
"symlink"
|
|
4921
|
-
]),
|
|
4922
|
-
sizeBytes: z.number(),
|
|
4923
|
-
mtime: z.number()
|
|
4924
|
-
});
|
|
4925
|
-
var ArchiveManifestSchema = z.object({
|
|
4926
|
-
archiveVersion: z.literal(1),
|
|
5290
|
+
category: z.string(),
|
|
5291
|
+
severity: AlertSeveritySchema,
|
|
5292
|
+
title: z.string(),
|
|
5293
|
+
message: z.string(),
|
|
5294
|
+
status: AlertStatusSchema,
|
|
5295
|
+
progress: z.number().optional(),
|
|
5296
|
+
read: z.boolean(),
|
|
4927
5297
|
createdAt: z.number(),
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
totalBytes: z.number(),
|
|
4932
|
-
totalFiles: z.number()
|
|
4933
|
-
});
|
|
4934
|
-
var LocationStatSchema = z.object({
|
|
4935
|
-
name: z.string(),
|
|
4936
|
-
sizeBytes: z.number(),
|
|
4937
|
-
fileCount: z.number(),
|
|
4938
|
-
present: z.boolean()
|
|
5298
|
+
updatedAt: z.number(),
|
|
5299
|
+
source: AlertSourceSchema.optional(),
|
|
5300
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
4939
5301
|
});
|
|
5302
|
+
var alertsCapability = {
|
|
5303
|
+
name: "alerts",
|
|
5304
|
+
scope: "system",
|
|
5305
|
+
mode: "singleton",
|
|
5306
|
+
methods: {
|
|
5307
|
+
emit: method(AlertSchema, z.void(), { kind: "mutation" }),
|
|
5308
|
+
update: method(z.object({
|
|
5309
|
+
alertId: z.string(),
|
|
5310
|
+
patch: AlertSchema.partial()
|
|
5311
|
+
}), z.void(), { kind: "mutation" }),
|
|
5312
|
+
list: method(z.object({
|
|
5313
|
+
unreadOnly: z.boolean().optional(),
|
|
5314
|
+
limit: z.number().optional()
|
|
5315
|
+
}).optional(), z.array(AlertSchema).readonly()),
|
|
5316
|
+
getUnreadCount: method(z.void(), z.number()),
|
|
5317
|
+
markRead: method(z.object({ alertId: z.string() }), z.void(), { kind: "mutation" }),
|
|
5318
|
+
markAllRead: method(z.void(), z.void(), { kind: "mutation" }),
|
|
5319
|
+
dismiss: method(z.object({ alertId: z.string() }), z.void(), { kind: "mutation" })
|
|
5320
|
+
}
|
|
5321
|
+
};
|
|
4940
5322
|
/**
|
|
4941
|
-
*
|
|
4942
|
-
* SET of destination locations. Supersedes the per-location cron on
|
|
4943
|
-
* `BackupDestinationPolicy`: an operator creates a schedule, picks the
|
|
4944
|
-
* `backups` locations it should write to, and the orchestrator fans a
|
|
4945
|
-
* single archive out to all of them when the cron fires.
|
|
5323
|
+
* audio-analysis — device-scoped facade over the system `audio-analyzer`.
|
|
4946
5324
|
*
|
|
4947
|
-
*
|
|
4948
|
-
*
|
|
4949
|
-
*
|
|
5325
|
+
* Pairs with `audio-analyzer` (system, singleton) the way `camera-streams`
|
|
5326
|
+
* pairs with `stream-broker`: the system cap owns the compute path
|
|
5327
|
+
* (`processChunk`, engine lifecycle) while `audio-analysis` exposes the
|
|
5328
|
+
* per-device surface — settings resolution, device-settings contribution,
|
|
5329
|
+
* audio-level events — and is the binding row every camera sees in the
|
|
5330
|
+
* device-manager bindings UI.
|
|
4950
5331
|
*
|
|
4951
|
-
*
|
|
4952
|
-
*
|
|
4953
|
-
*
|
|
5332
|
+
* The cap definition itself declares `kind: 'wrapper'` and `defaultActive: true`,
|
|
5333
|
+
* so every camera picks up audio analysis automatically without any runtime flag
|
|
5334
|
+
* in the addon's registration. Operators can disable per-device via
|
|
5335
|
+
* `device-manager.setWrapperActive` when they don't want a given camera to be
|
|
5336
|
+
* analysed (e.g. muted / motion-only cams).
|
|
4954
5337
|
*/
|
|
4955
|
-
var
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
/**
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
5338
|
+
var audioAnalysisCapability = {
|
|
5339
|
+
name: "audio-analysis",
|
|
5340
|
+
scope: "device",
|
|
5341
|
+
mode: "singleton",
|
|
5342
|
+
kind: "wrapper",
|
|
5343
|
+
defaultActive: true,
|
|
5344
|
+
deviceTypes: [DeviceType.Camera],
|
|
5345
|
+
exposesDeviceSettings: true,
|
|
5346
|
+
methods: {
|
|
5347
|
+
/**
|
|
5348
|
+
* Resolve per-device audio analysis settings (minConfidence,
|
|
5349
|
+
* allowedClasses) from the addon settings store for a given camera.
|
|
5350
|
+
* Orchestrator callers use this before handing chunks to
|
|
5351
|
+
* `audio-analyzer.processChunk`.
|
|
5352
|
+
*/
|
|
5353
|
+
resolveDeviceSettings: method(z.object({ deviceId: z.number() }), z.custom()) },
|
|
5354
|
+
events: { onAudioLevel: event(z.object({
|
|
5355
|
+
deviceId: z.number(),
|
|
5356
|
+
rms: z.number(),
|
|
5357
|
+
dbfs: z.number()
|
|
5358
|
+
})) }
|
|
5359
|
+
};
|
|
5360
|
+
/** Shared Zod schemas used across detection capabilities. */
|
|
5361
|
+
/**
|
|
5362
|
+
* Canonical frame-format enum mirrored on `FrameFormat` in
|
|
5363
|
+
* `packages/types/src/types/io.ts`. Kept inline (vs imported) so the
|
|
5364
|
+
* Zod runtime schema and TypeScript type stay in sync at the call site
|
|
5365
|
+
* — adding a new format requires changing both this enum and the
|
|
5366
|
+
* `FrameFormat` type alias together.
|
|
5367
|
+
*/
|
|
5368
|
+
var FrameFormatSchema = z.enum([
|
|
5369
|
+
"jpeg",
|
|
5370
|
+
"rgb",
|
|
5371
|
+
"bgr",
|
|
5372
|
+
"yuv420",
|
|
5373
|
+
"gray"
|
|
5374
|
+
]);
|
|
5375
|
+
var FrameInputSchema = z.object({
|
|
5376
|
+
data: z.custom(),
|
|
5377
|
+
format: FrameFormatSchema,
|
|
5378
|
+
width: z.number(),
|
|
5379
|
+
height: z.number(),
|
|
5380
|
+
timestamp: z.number()
|
|
5381
|
+
});
|
|
5382
|
+
var BoundingBoxSchema = z.object({
|
|
5383
|
+
x: z.number(),
|
|
5384
|
+
y: z.number(),
|
|
5385
|
+
w: z.number(),
|
|
5386
|
+
h: z.number()
|
|
5387
|
+
});
|
|
5388
|
+
z.object({
|
|
5389
|
+
class: z.string(),
|
|
5390
|
+
originalClass: z.string(),
|
|
5391
|
+
score: z.number(),
|
|
5392
|
+
bbox: BoundingBoxSchema
|
|
4974
5393
|
});
|
|
4975
5394
|
/**
|
|
4976
|
-
*
|
|
5395
|
+
* `data` carries the raw f32le bytes of the PCM samples (4 bytes per
|
|
5396
|
+
* sample, little-endian IEEE 754). `Uint8Array` is the wire-safe choice
|
|
5397
|
+
* because `@msgpack/msgpack` serialises it as a MsgPack `bin` type that
|
|
5398
|
+
* round-trips losslessly over the UDS transport. `Float32Array` is NOT
|
|
5399
|
+
* preserved — the encoder serialises it as `bin` (its raw bytes) but
|
|
5400
|
+
* the decoder returns `Uint8Array`, so treating a `Float32Array` as the
|
|
5401
|
+
* wire type causes receivers to read bytes as sample values, producing
|
|
5402
|
+
* wildly wrong RMS/dBFS results (~+43 dBFS instead of ≤0).
|
|
4977
5403
|
*
|
|
4978
|
-
*
|
|
4979
|
-
*
|
|
5404
|
+
* Callers that need float arithmetic reconstruct the view with:
|
|
5405
|
+
* `new Float32Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength / 4)`
|
|
4980
5406
|
*/
|
|
4981
|
-
var
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
* from the archive. Default = every location the archive
|
|
5040
|
-
* carries (full restore). The boot-time apply hook reads
|
|
5041
|
-
* this list and skips entries whose path doesn't start with
|
|
5042
|
-
* any of them.
|
|
5043
|
-
*/
|
|
5044
|
-
locations: z.array(z.string()).optional()
|
|
5045
|
-
}), z.void(), {
|
|
5046
|
-
kind: "mutation",
|
|
5047
|
-
auth: "admin"
|
|
5048
|
-
}),
|
|
5049
|
-
delete: method(z.object({
|
|
5050
|
-
destinationId: z.string(),
|
|
5051
|
-
backupId: z.string()
|
|
5052
|
-
}), z.void(), {
|
|
5053
|
-
kind: "mutation",
|
|
5054
|
-
auth: "admin"
|
|
5055
|
-
}),
|
|
5056
|
-
/**
|
|
5057
|
-
* List archives at a single destination. Reads the per-location
|
|
5058
|
-
* `manifests.json` and returns one entry per archive (newest
|
|
5059
|
-
* first). Powers the destinations-table drill-in in admin UI.
|
|
5060
|
-
*/
|
|
5061
|
-
listArchives: method(z.object({ destinationId: z.string() }), z.array(BackupArchiveEntrySchema).readonly(), { auth: "admin" }),
|
|
5062
|
-
/**
|
|
5063
|
-
* Upsert a per-destination policy row. The `locationId` MUST be
|
|
5064
|
-
* the id of an existing `backups`-typed `StorageLocation`. Used
|
|
5065
|
-
* by the admin-UI destinations table (enable toggle + retention
|
|
5066
|
-
* input + optional label).
|
|
5067
|
-
*/
|
|
5068
|
-
upsertDestinationPolicy: method(z.object({
|
|
5069
|
-
locationId: z.string(),
|
|
5070
|
-
enabled: z.boolean(),
|
|
5071
|
-
retentionCount: z.number().int().min(1).max(1e3),
|
|
5072
|
-
label: z.string().optional(),
|
|
5073
|
-
/**
|
|
5074
|
-
* Per-destination cron expression. Empty string clears the
|
|
5075
|
-
* schedule (manual-only). Validated server-side via croner;
|
|
5076
|
-
* malformed expressions reject the upsert with an actionable
|
|
5077
|
-
* message.
|
|
5078
|
-
*/
|
|
5079
|
-
cron: z.string().optional()
|
|
5080
|
-
}), z.void(), {
|
|
5081
|
-
kind: "mutation",
|
|
5082
|
-
auth: "admin"
|
|
5083
|
-
}),
|
|
5084
|
-
/**
|
|
5085
|
-
* Validate a cron expression and peek the next N firing times.
|
|
5086
|
-
* Used by the admin-UI CronEditor to render a live "next-run"
|
|
5087
|
-
* preview as the operator edits the pattern.
|
|
5088
|
-
*/
|
|
5089
|
-
previewSchedule: method(z.object({
|
|
5090
|
-
cron: z.string(),
|
|
5091
|
-
count: z.number().int().min(1).max(20).optional()
|
|
5092
|
-
}), z.object({
|
|
5093
|
-
ok: z.boolean(),
|
|
5094
|
-
error: z.string().optional(),
|
|
5095
|
-
nextRuns: z.array(z.number()).readonly()
|
|
5096
|
-
})),
|
|
5097
|
-
/**
|
|
5098
|
-
* List every backup schedule (the N:M entries), each with its
|
|
5099
|
-
* computed `nextRunAt`. Powers the Schedules section of the
|
|
5100
|
-
* admin-UI Backups page.
|
|
5101
|
-
*/
|
|
5102
|
-
listSchedules: method(z.void(), z.array(BackupScheduleSchema).readonly(), { auth: "admin" }),
|
|
5407
|
+
var AudioChunkInputSchema = z.object({
|
|
5408
|
+
data: z.instanceof(Uint8Array),
|
|
5409
|
+
sampleRate: z.number(),
|
|
5410
|
+
channels: z.number(),
|
|
5411
|
+
timestamp: z.number(),
|
|
5412
|
+
/** Originating device id — used by the classifier for per-camera concurrency tracking. */
|
|
5413
|
+
deviceId: z.number().optional()
|
|
5414
|
+
});
|
|
5415
|
+
var AudioLevelSchema = z.object({
|
|
5416
|
+
rms: z.number(),
|
|
5417
|
+
dbfs: z.number()
|
|
5418
|
+
});
|
|
5419
|
+
var AudioClassificationLabelSchema = z.object({
|
|
5420
|
+
/**
|
|
5421
|
+
* Primary display class. Depending on how the label was produced
|
|
5422
|
+
* this is either the macro category (e.g. `dog`) or the raw
|
|
5423
|
+
* backend label (e.g. `Dog bark`). Mirrors `class` on
|
|
5424
|
+
* `SpatialDetection`.
|
|
5425
|
+
*/
|
|
5426
|
+
className: z.string(),
|
|
5427
|
+
/**
|
|
5428
|
+
* Raw backend-native label the classifier actually emitted (e.g.
|
|
5429
|
+
* `Dog bark` for YAMNet, `dog_bark` for Apple SoundAnalysis). For
|
|
5430
|
+
* macro-aggregated entries this is the top raw contributor. Mirrors
|
|
5431
|
+
* `originalClass` on `SpatialDetection`.
|
|
5432
|
+
*/
|
|
5433
|
+
originalClass: z.string().optional(),
|
|
5434
|
+
score: z.number()
|
|
5435
|
+
});
|
|
5436
|
+
var AudioAnalysisResultSchema = z.object({
|
|
5437
|
+
level: AudioLevelSchema,
|
|
5438
|
+
classification: z.object({
|
|
5439
|
+
labels: z.array(AudioClassificationLabelSchema).readonly(),
|
|
5440
|
+
inferenceMs: z.number()
|
|
5441
|
+
}).optional(),
|
|
5442
|
+
timestamp: z.number()
|
|
5443
|
+
});
|
|
5444
|
+
var AudioAnalysisSettingsSchema = z.object({
|
|
5445
|
+
minConfidence: z.number().min(0).max(1).default(.3),
|
|
5446
|
+
allowedClasses: z.array(z.string()).default([])
|
|
5447
|
+
});
|
|
5448
|
+
var AudioClassificationResultSchema = z.object({
|
|
5449
|
+
labels: z.array(AudioClassificationLabelSchema).readonly(),
|
|
5450
|
+
rawLabels: z.array(AudioClassificationLabelSchema).readonly().optional(),
|
|
5451
|
+
inferenceMs: z.number()
|
|
5452
|
+
});
|
|
5453
|
+
var audioAnalyzerCapability = {
|
|
5454
|
+
name: "audio-analyzer",
|
|
5455
|
+
scope: "system",
|
|
5456
|
+
mode: "singleton",
|
|
5457
|
+
methods: {
|
|
5458
|
+
analyseChunk: method(z.object({
|
|
5459
|
+
chunk: AudioChunkInputSchema,
|
|
5460
|
+
settings: AudioAnalysisSettingsSchema
|
|
5461
|
+
}), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }),
|
|
5462
|
+
classify: method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }),
|
|
5463
|
+
isReady: method(z.void(), z.boolean()),
|
|
5464
|
+
dispose: method(z.void(), z.void(), { kind: "mutation" }),
|
|
5103
5465
|
/**
|
|
5104
|
-
*
|
|
5105
|
-
*
|
|
5106
|
-
*
|
|
5107
|
-
*
|
|
5466
|
+
* Re-run the host platform-probe for the audio backend and persist
|
|
5467
|
+
* the detected value into `probedBestAudioBackend`. Replaces the
|
|
5468
|
+
* orchestrator's legacy `AgentPipelineSettings.audio.engine` —
|
|
5469
|
+
* audio-analyzer now owns the choice. Operator `audioBackend` is
|
|
5470
|
+
* not touched; only the probed-best hint.
|
|
5108
5471
|
*/
|
|
5109
|
-
|
|
5110
|
-
id: z.string().optional(),
|
|
5111
|
-
label: z.string(),
|
|
5112
|
-
cron: z.string(),
|
|
5113
|
-
enabled: z.boolean(),
|
|
5114
|
-
locationIds: z.array(z.string()).readonly(),
|
|
5115
|
-
retentionCount: z.number().int().min(1).max(1e3),
|
|
5116
|
-
dataSources: z.array(z.string()).readonly().optional()
|
|
5117
|
-
}), BackupScheduleSchema, {
|
|
5118
|
-
kind: "mutation",
|
|
5119
|
-
auth: "admin"
|
|
5120
|
-
}),
|
|
5121
|
-
/** Delete a schedule by id. Does not touch the target locations. */
|
|
5122
|
-
deleteSchedule: method(z.object({ id: z.string() }), z.void(), {
|
|
5472
|
+
reprobeAudioEngine: method(z.void(), z.object({ backend: z.string() }), {
|
|
5123
5473
|
kind: "mutation",
|
|
5124
5474
|
auth: "admin"
|
|
5125
5475
|
})
|
|
5126
5476
|
}
|
|
5127
5477
|
};
|
|
5478
|
+
var PcmSampleFormatSchema = z.enum(["f32le", "s16le"]);
|
|
5479
|
+
var AudioCodecInfoSchema = z.object({
|
|
5480
|
+
codec: z.string(),
|
|
5481
|
+
canDecode: z.boolean(),
|
|
5482
|
+
canEncode: z.boolean(),
|
|
5483
|
+
label: z.string().optional()
|
|
5484
|
+
});
|
|
5485
|
+
var AudioDecodeSessionConfigSchema = z.object({
|
|
5486
|
+
codec: z.string(),
|
|
5487
|
+
sourceSampleRate: z.number().int().positive(),
|
|
5488
|
+
sourceChannels: z.number().int().positive(),
|
|
5489
|
+
extraData: z.instanceof(Uint8Array).optional(),
|
|
5490
|
+
targetSampleRate: z.number().int().positive(),
|
|
5491
|
+
targetChannels: z.number().int().positive(),
|
|
5492
|
+
targetFormat: PcmSampleFormatSchema.optional(),
|
|
5493
|
+
idleMs: z.number().int().positive().optional(),
|
|
5494
|
+
tag: z.string().optional()
|
|
5495
|
+
});
|
|
5496
|
+
var AudioEncodeSessionConfigSchema = z.object({
|
|
5497
|
+
codec: z.string(),
|
|
5498
|
+
sourceSampleRate: z.number().int().positive(),
|
|
5499
|
+
sourceChannels: z.number().int().positive(),
|
|
5500
|
+
sourceFormat: PcmSampleFormatSchema.optional(),
|
|
5501
|
+
targetSampleRate: z.number().int().positive(),
|
|
5502
|
+
targetChannels: z.number().int().positive(),
|
|
5503
|
+
bitrateKbps: z.number().int().positive().optional(),
|
|
5504
|
+
idleMs: z.number().int().positive().optional(),
|
|
5505
|
+
tag: z.string().optional()
|
|
5506
|
+
});
|
|
5507
|
+
var AudioPcmChunkSchema = z.object({
|
|
5508
|
+
data: z.instanceof(Uint8Array),
|
|
5509
|
+
sampleRate: z.number().int().positive(),
|
|
5510
|
+
channels: z.number().int().positive(),
|
|
5511
|
+
format: PcmSampleFormatSchema,
|
|
5512
|
+
pts: z.number()
|
|
5513
|
+
});
|
|
5514
|
+
var AudioEncodedChunkSchema = z.object({
|
|
5515
|
+
data: z.instanceof(Uint8Array),
|
|
5516
|
+
codec: z.string(),
|
|
5517
|
+
pts: z.number(),
|
|
5518
|
+
frameComplete: z.boolean()
|
|
5519
|
+
});
|
|
5520
|
+
var SessionInventoryEntrySchema = z.object({
|
|
5521
|
+
sessionId: z.string(),
|
|
5522
|
+
kind: z.enum(["decode", "encode"]),
|
|
5523
|
+
codec: z.string(),
|
|
5524
|
+
sourceSampleRate: z.number(),
|
|
5525
|
+
sourceChannels: z.number(),
|
|
5526
|
+
targetSampleRate: z.number(),
|
|
5527
|
+
targetChannels: z.number(),
|
|
5528
|
+
format: PcmSampleFormatSchema,
|
|
5529
|
+
tag: z.string().optional(),
|
|
5530
|
+
createdAtMs: z.number(),
|
|
5531
|
+
lastActivityMs: z.number(),
|
|
5532
|
+
framesIn: z.number(),
|
|
5533
|
+
framesOut: z.number()
|
|
5534
|
+
});
|
|
5128
5535
|
/**
|
|
5129
|
-
*
|
|
5536
|
+
* audio-codec — bidirectional PCM ↔ encoded audio I/O box.
|
|
5130
5537
|
*
|
|
5131
|
-
*
|
|
5132
|
-
*
|
|
5133
|
-
*
|
|
5134
|
-
*
|
|
5135
|
-
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
5136
|
-
* the same surface.
|
|
5538
|
+
* Independent per-consumer sessions. The provider runs decode + resample
|
|
5539
|
+
* (or resample + encode) inside the session so a 16kHz mono ASA
|
|
5540
|
+
* subscriber and a 48kHz stereo WebRTC subscriber on the same source
|
|
5541
|
+
* stream don't share resamplers.
|
|
5137
5542
|
*
|
|
5138
|
-
*
|
|
5139
|
-
*
|
|
5140
|
-
*
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5543
|
+
* Singleton on each node. Decoder and encoder live in the same provider
|
|
5544
|
+
* because they share the underlying libav contexts (node-av today,
|
|
5545
|
+
* pluggable later) — operators always install one or the other together.
|
|
5546
|
+
*/
|
|
5547
|
+
var audioCodecCapability = {
|
|
5548
|
+
name: "audio-codec",
|
|
5549
|
+
scope: "system",
|
|
5550
|
+
mode: "singleton",
|
|
5551
|
+
preferredProvider: "decoder-nodeav",
|
|
5552
|
+
methods: {
|
|
5553
|
+
/** Probe the local runtime and return the supported codec matrix. */
|
|
5554
|
+
listSupportedCodecs: method(z.void(), z.array(AudioCodecInfoSchema).readonly()),
|
|
5555
|
+
/** Cheap predicate — does the runtime support `(codec, kind)`? */
|
|
5556
|
+
canHandle: method(z.object({
|
|
5557
|
+
codec: z.string(),
|
|
5558
|
+
kind: z.enum(["decode", "encode"])
|
|
5559
|
+
}), z.boolean()),
|
|
5560
|
+
createDecodeSession: method(AudioDecodeSessionConfigSchema, z.object({
|
|
5561
|
+
sessionId: z.string(),
|
|
5562
|
+
nodeId: z.string()
|
|
5563
|
+
}), { kind: "mutation" }),
|
|
5564
|
+
createEncodeSession: method(AudioEncodeSessionConfigSchema, z.object({
|
|
5565
|
+
sessionId: z.string(),
|
|
5566
|
+
nodeId: z.string()
|
|
5567
|
+
}), { kind: "mutation" }),
|
|
5568
|
+
closeSession: method(z.object({
|
|
5569
|
+
sessionId: z.string(),
|
|
5570
|
+
nodeId: z.string().optional()
|
|
5571
|
+
}), z.void(), { kind: "mutation" }),
|
|
5572
|
+
/** Push one encoded audio frame into a decode session. */
|
|
5573
|
+
pushEncodedFrame: method(z.object({
|
|
5574
|
+
sessionId: z.string(),
|
|
5575
|
+
nodeId: z.string().optional(),
|
|
5576
|
+
data: z.instanceof(Uint8Array),
|
|
5577
|
+
/** Source PTS in milliseconds. Synthesised when omitted. */
|
|
5578
|
+
pts: z.number().optional()
|
|
5579
|
+
}), z.void(), { kind: "mutation" }),
|
|
5580
|
+
/** Pull up to `maxCount` PCM chunks from a decode session. */
|
|
5581
|
+
pullPcm: method(z.object({
|
|
5582
|
+
sessionId: z.string(),
|
|
5583
|
+
nodeId: z.string().optional(),
|
|
5584
|
+
maxCount: z.number().int().positive().default(8)
|
|
5585
|
+
}), z.array(AudioPcmChunkSchema)),
|
|
5586
|
+
/** Push one PCM chunk into an encode session. */
|
|
5587
|
+
pushPcm: method(z.object({
|
|
5588
|
+
sessionId: z.string(),
|
|
5589
|
+
nodeId: z.string().optional(),
|
|
5590
|
+
data: z.instanceof(Uint8Array),
|
|
5591
|
+
/** Source PTS in milliseconds. */
|
|
5592
|
+
pts: z.number().optional()
|
|
5593
|
+
}), z.void(), { kind: "mutation" }),
|
|
5594
|
+
/** Pull up to `maxCount` encoded chunks from an encode session. */
|
|
5595
|
+
pullEncoded: method(z.object({
|
|
5596
|
+
sessionId: z.string(),
|
|
5597
|
+
nodeId: z.string().optional(),
|
|
5598
|
+
maxCount: z.number().int().positive().default(8)
|
|
5599
|
+
}), z.array(AudioEncodedChunkSchema)),
|
|
5600
|
+
/** Flush any pending encoded output (call before close on graceful tear). */
|
|
5601
|
+
flushEncode: method(z.object({
|
|
5602
|
+
sessionId: z.string(),
|
|
5603
|
+
nodeId: z.string().optional()
|
|
5604
|
+
}), z.array(AudioEncodedChunkSchema), { kind: "mutation" }),
|
|
5605
|
+
listActiveSessions: method(z.void(), z.array(SessionInventoryEntrySchema).readonly())
|
|
5606
|
+
}
|
|
5607
|
+
};
|
|
5608
|
+
var AuthResultSchema = z.object({
|
|
5609
|
+
userId: z.string(),
|
|
5610
|
+
username: z.string(),
|
|
5611
|
+
email: z.string().optional(),
|
|
5612
|
+
displayName: z.string().optional(),
|
|
5613
|
+
/**
|
|
5614
|
+
* Whether the authenticating user is an admin. The auth-provider
|
|
5615
|
+
* surface returns this so the server's login flow can mint a JWT
|
|
5616
|
+
* with the correct bypass flag. Non-admin users authenticated via
|
|
5617
|
+
* an external IdP still need their scopes assigned by an admin via
|
|
5618
|
+
* `setUserScopes` — the SSO flow doesn't carry permissions.
|
|
5619
|
+
*/
|
|
5620
|
+
isAdmin: z.boolean().default(false)
|
|
5621
|
+
});
|
|
5622
|
+
var authProviderCapability = {
|
|
5623
|
+
name: "auth-provider",
|
|
5624
|
+
scope: "system",
|
|
5625
|
+
mode: "collection",
|
|
5626
|
+
internal: true,
|
|
5627
|
+
methods: {
|
|
5628
|
+
validateCredentials: method(z.object({
|
|
5629
|
+
username: z.string(),
|
|
5630
|
+
password: z.string()
|
|
5631
|
+
}), AuthResultSchema.nullable(), { kind: "mutation" }),
|
|
5632
|
+
getLoginUrl: method(z.object({ state: z.string() }), z.string()),
|
|
5633
|
+
handleCallback: method(z.record(z.string(), z.string()), AuthResultSchema, { kind: "mutation" }),
|
|
5634
|
+
validateToken: method(z.object({ token: z.string() }), AuthResultSchema.nullable())
|
|
5635
|
+
},
|
|
5636
|
+
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
5637
|
+
mount: { kind: "skip" }
|
|
5638
|
+
};
|
|
5639
|
+
/**
|
|
5640
|
+
* Orchestrator-side destination metadata. The orchestrator computes
|
|
5641
|
+
* `id = <addonId>:<subId>` from its provider lookup so consumers
|
|
5642
|
+
* (admin UI, restore flow) see one canonical key.
|
|
5156
5643
|
*
|
|
5157
|
-
*
|
|
5158
|
-
*
|
|
5159
|
-
*
|
|
5160
|
-
*
|
|
5161
|
-
*
|
|
5162
|
-
* event handler. Subscriptions persist across reconnects — the
|
|
5163
|
-
* provider re-subscribes upstream after a transport drop.
|
|
5644
|
+
* Phase 4 (admin UI redesign) adds the per-destination policy fields
|
|
5645
|
+
* (`enabled`, `retentionCount`, `label`) so the destinations table can
|
|
5646
|
+
* render the joined view without a follow-up round-trip. The backend
|
|
5647
|
+
* already joins these in `listDestinations` — we now surface them on
|
|
5648
|
+
* the wire.
|
|
5164
5649
|
*
|
|
5165
|
-
*
|
|
5166
|
-
*
|
|
5167
|
-
*
|
|
5168
|
-
* drop is recoverable via `getState({key})`).
|
|
5169
|
-
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
5170
|
-
* - Cross-process: events already route through `$event-bus`; no
|
|
5171
|
-
* per-broker wire-shaping needed.
|
|
5650
|
+
* `lastSuccessAt` / `lastSuccessSizeBytes` are computed from each
|
|
5651
|
+
* location's `manifests.json` (newest archive). Optional — locations
|
|
5652
|
+
* with no archives yet leave them undefined.
|
|
5172
5653
|
*/
|
|
5173
|
-
var
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5654
|
+
var BackupDestinationInfoSchema = z.object({
|
|
5655
|
+
/**
|
|
5656
|
+
* Sub-id within this addon. Convention `default` for single-
|
|
5657
|
+
* destination addons; addons that host many (S3 with N buckets)
|
|
5658
|
+
* pick stable per-bucket strings. The orchestrator stitches
|
|
5659
|
+
* `<manifestId>:<subId>` for the wire format.
|
|
5660
|
+
*/
|
|
5661
|
+
subId: z.string(),
|
|
5662
|
+
displayName: z.string(),
|
|
5663
|
+
description: z.string().optional(),
|
|
5664
|
+
kind: z.string(),
|
|
5665
|
+
triggerSupported: z.boolean(),
|
|
5666
|
+
restoreSupported: z.boolean()
|
|
5667
|
+
}).extend({
|
|
5668
|
+
/** `<addonId>:<subId>` — globally-unique dispatch key. */
|
|
5183
5669
|
id: z.string(),
|
|
5184
|
-
/**
|
|
5185
|
-
*
|
|
5186
|
-
*
|
|
5187
|
-
|
|
5188
|
-
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
5189
|
-
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
5190
|
-
* device belongs to one integration. The admin UI threads this id
|
|
5191
|
-
* back as the `{ addonId }` system-collection selector on every
|
|
5192
|
-
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
5193
|
-
* `testConnection`), so the call routes to the OWNING provider
|
|
5194
|
-
* instead of defaulting to the first-registered one. */
|
|
5670
|
+
/**
|
|
5671
|
+
* Manifest id of the owning addon. The Settings button on the
|
|
5672
|
+
* destination card opens that addon's `globalSettingsSchema` panel.
|
|
5673
|
+
*/
|
|
5195
5674
|
addonId: z.string(),
|
|
5196
|
-
/**
|
|
5197
|
-
|
|
5198
|
-
/**
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
/**
|
|
5205
|
-
|
|
5206
|
-
/**
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5675
|
+
/** Operator-toggled enable flag from the destination-policy table. */
|
|
5676
|
+
enabled: z.boolean(),
|
|
5677
|
+
/** Operator-defined retention count (archives kept per destination). */
|
|
5678
|
+
retentionCount: z.number(),
|
|
5679
|
+
/** Operator-defined display label (overrides storage location displayName). */
|
|
5680
|
+
label: z.string().optional(),
|
|
5681
|
+
/** Newest-archive timestamp from `manifests.json`, or undefined. */
|
|
5682
|
+
lastSuccessAt: z.number().optional(),
|
|
5683
|
+
/** Newest-archive size from `manifests.json`, or undefined. */
|
|
5684
|
+
lastSuccessSizeBytes: z.number().optional(),
|
|
5685
|
+
/** Per-destination cron expression. Empty = manual-only (no schedule). */
|
|
5686
|
+
cron: z.string().optional(),
|
|
5687
|
+
/** ms-epoch of next computed firing for this destination's cron, if any. */
|
|
5688
|
+
nextRunAt: z.number().optional(),
|
|
5689
|
+
/** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
|
|
5690
|
+
lastRunAt: z.number().optional()
|
|
5212
5691
|
});
|
|
5213
5692
|
/**
|
|
5214
|
-
*
|
|
5215
|
-
*
|
|
5216
|
-
*
|
|
5217
|
-
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
5218
|
-
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
5219
|
-
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
5220
|
-
* offers a kind whose creation form can't be rendered).
|
|
5693
|
+
* Per-archive entry returned by `backup.listArchives({ destinationId })`.
|
|
5694
|
+
* Same shape the destination drill-in renders. Sourced from the per-
|
|
5695
|
+
* location `manifests.json` (no per-archive tarball read required).
|
|
5221
5696
|
*/
|
|
5222
|
-
var
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5697
|
+
var BackupArchiveEntrySchema = z.object({
|
|
5698
|
+
id: z.string(),
|
|
5699
|
+
filename: z.string(),
|
|
5700
|
+
createdAt: z.number(),
|
|
5701
|
+
sizeBytes: z.number(),
|
|
5702
|
+
label: z.string().optional(),
|
|
5703
|
+
/** Top-level locations included in the archive (db, addons, tls, …). */
|
|
5704
|
+
locations: z.array(z.string()).readonly()
|
|
5230
5705
|
});
|
|
5231
|
-
var
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
/**
|
|
5240
|
-
* ADOPT an existing id instead of minting a new one.
|
|
5241
|
-
*
|
|
5242
|
-
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
5243
|
-
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
5244
|
-
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
5245
|
-
* bound to a broker that no longer exists. Re-entering the password under the
|
|
5246
|
-
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
5247
|
-
* password.
|
|
5248
|
-
*
|
|
5249
|
-
* A provider MUST refuse an id that is already in use — adopting a live
|
|
5250
|
-
* broker's id would silently take it over.
|
|
5251
|
-
*/
|
|
5252
|
-
id: z.string().min(1).optional(),
|
|
5253
|
-
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
5254
|
-
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
5255
|
-
* branch on receipt — invalid shape rejects the add. */
|
|
5256
|
-
settings: z.record(z.string(), z.unknown())
|
|
5257
|
-
});
|
|
5258
|
-
var AddResultSchema = z.object({ id: z.string() });
|
|
5259
|
-
var RemoveInputSchema = z.object({ id: z.string() });
|
|
5260
|
-
var TestConnectionResultSchema = z.discriminatedUnion("ok", [z.object({
|
|
5261
|
-
ok: z.literal(true),
|
|
5262
|
-
latencyMs: z.number().nonnegative()
|
|
5263
|
-
}), z.object({
|
|
5264
|
-
ok: z.literal(false),
|
|
5265
|
-
error: z.string()
|
|
5266
|
-
})]);
|
|
5267
|
-
var SettingsRecordSchema$1 = z.record(z.string(), z.unknown());
|
|
5268
|
-
var SettingsSchemaInputSchema = z.object({ kind: z.string() });
|
|
5269
|
-
var TestSettingsInputSchema = z.object({
|
|
5270
|
-
kind: z.string(),
|
|
5271
|
-
settings: SettingsRecordSchema$1
|
|
5706
|
+
var BackupEntrySchema = z.object({
|
|
5707
|
+
id: z.string(),
|
|
5708
|
+
/** Addon id of the destination that owns this backup (e.g. `local-backup`, `s3-backup`). */
|
|
5709
|
+
destinationId: z.string().optional(),
|
|
5710
|
+
label: z.string().optional(),
|
|
5711
|
+
createdAt: z.number(),
|
|
5712
|
+
sizeBytes: z.number(),
|
|
5713
|
+
locations: z.array(z.string()).optional()
|
|
5272
5714
|
});
|
|
5273
|
-
var
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
brokerId: z.string(),
|
|
5283
|
-
/** Kind-specific routing target.
|
|
5284
|
-
* MQTT: `{ topic: string, qos?: 0|1|2, retain?: boolean }`.
|
|
5285
|
-
* HA: `{ domain: string, service: string, entityId?: string, area?: string }`. */
|
|
5286
|
-
target: z.record(z.string(), z.unknown()),
|
|
5287
|
-
/** Kind-specific payload.
|
|
5288
|
-
* MQTT: raw string / number / object (provider serialises).
|
|
5289
|
-
* HA: service-call `data` object (transition, brightness, …). */
|
|
5290
|
-
payload: z.unknown().optional()
|
|
5715
|
+
var ArchiveEntrySchema = z.object({
|
|
5716
|
+
path: z.string(),
|
|
5717
|
+
kind: z.enum([
|
|
5718
|
+
"file",
|
|
5719
|
+
"dir",
|
|
5720
|
+
"symlink"
|
|
5721
|
+
]),
|
|
5722
|
+
sizeBytes: z.number(),
|
|
5723
|
+
mtime: z.number()
|
|
5291
5724
|
});
|
|
5292
|
-
var
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5725
|
+
var ArchiveManifestSchema = z.object({
|
|
5726
|
+
archiveVersion: z.literal(1),
|
|
5727
|
+
createdAt: z.number(),
|
|
5728
|
+
dataDir: z.string(),
|
|
5729
|
+
locations: z.array(z.string()),
|
|
5730
|
+
entries: z.array(ArchiveEntrySchema),
|
|
5731
|
+
totalBytes: z.number(),
|
|
5732
|
+
totalFiles: z.number()
|
|
5298
5733
|
});
|
|
5299
|
-
var
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
brokerId: z.string(),
|
|
5305
|
-
subscriptionId: z.string()
|
|
5734
|
+
var LocationStatSchema = z.object({
|
|
5735
|
+
name: z.string(),
|
|
5736
|
+
sizeBytes: z.number(),
|
|
5737
|
+
fileCount: z.number(),
|
|
5738
|
+
present: z.boolean()
|
|
5306
5739
|
});
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5740
|
+
/**
|
|
5741
|
+
* A backup schedule — the N:M "entry" that binds one cron cadence to a
|
|
5742
|
+
* SET of destination locations. Supersedes the per-location cron on
|
|
5743
|
+
* `BackupDestinationPolicy`: an operator creates a schedule, picks the
|
|
5744
|
+
* `backups` locations it should write to, and the orchestrator fans a
|
|
5745
|
+
* single archive out to all of them when the cron fires.
|
|
5746
|
+
*
|
|
5747
|
+
* `retentionCount` is per-schedule (D-decision 2026-07-28): every
|
|
5748
|
+
* location targeted by this schedule keeps this many archives from
|
|
5749
|
+
* this schedule's runs.
|
|
5750
|
+
*
|
|
5751
|
+
* `dataSources` optionally narrows which top-level state locations
|
|
5752
|
+
* (db, addons, tls, …) are archived; omitted = the orchestrator's
|
|
5753
|
+
* default full set.
|
|
5754
|
+
*/
|
|
5755
|
+
var BackupScheduleSchema = z.object({
|
|
5756
|
+
/** Stable id. Generated by the orchestrator on first upsert if absent. */
|
|
5757
|
+
id: z.string(),
|
|
5758
|
+
/** Operator-facing display name. */
|
|
5759
|
+
label: z.string(),
|
|
5760
|
+
/** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
|
|
5761
|
+
cron: z.string(),
|
|
5762
|
+
/** Master on/off toggle for the whole schedule. */
|
|
5763
|
+
enabled: z.boolean(),
|
|
5764
|
+
/** `backups`-location ids this schedule writes to (fan-out set). */
|
|
5765
|
+
locationIds: z.array(z.string()).readonly(),
|
|
5766
|
+
/** Archives kept per targeted location for this schedule. */
|
|
5767
|
+
retentionCount: z.number().int().min(1).max(1e3),
|
|
5768
|
+
/** Optional subset of source locations to include; omitted = all. */
|
|
5769
|
+
dataSources: z.array(z.string()).readonly().optional(),
|
|
5770
|
+
/** ms-epoch of last successful run. */
|
|
5771
|
+
lastRunAt: z.number().optional(),
|
|
5772
|
+
/** ms-epoch of next computed firing (read-only, filled on list). */
|
|
5773
|
+
nextRunAt: z.number().optional()
|
|
5313
5774
|
});
|
|
5314
|
-
|
|
5315
|
-
|
|
5775
|
+
/**
|
|
5776
|
+
* backup — singleton capability for backup management.
|
|
5777
|
+
*
|
|
5778
|
+
* Implemented by `local-backup` addon. Future providers (S3, rsync)
|
|
5779
|
+
* can replace it by registering the same capability.
|
|
5780
|
+
*/
|
|
5781
|
+
var backupCapability = {
|
|
5782
|
+
name: "backup",
|
|
5316
5783
|
scope: "system",
|
|
5317
|
-
mode: "
|
|
5318
|
-
providerKind: "broker",
|
|
5319
|
-
status: {
|
|
5320
|
-
schema: RegistryStatusSchema,
|
|
5321
|
-
kind: "poll"
|
|
5322
|
-
},
|
|
5784
|
+
mode: "singleton",
|
|
5323
5785
|
methods: {
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
*
|
|
5328
|
-
*
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5786
|
+
/**
|
|
5787
|
+
* Flat aggregate of every destination across every registered
|
|
5788
|
+
* `backup-destination` provider. This is the orchestrator-side
|
|
5789
|
+
* surface; it expands per-addon `listDestinations()` results into
|
|
5790
|
+
* one list the UI can render directly.
|
|
5791
|
+
*/
|
|
5792
|
+
listDestinations: method(z.void(), z.array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }),
|
|
5793
|
+
/**
|
|
5794
|
+
* Trigger a backup. Without `destinations` the orchestrator fans
|
|
5795
|
+
* out to every destination flagged as enabled in the routing
|
|
5796
|
+
* config; with it, only the listed addons receive the archive.
|
|
5797
|
+
*/
|
|
5798
|
+
trigger: method(z.object({
|
|
5799
|
+
/** Subset of registered `backup-destination` addon ids to write to. */
|
|
5800
|
+
destinations: z.array(z.string()).optional(),
|
|
5801
|
+
locations: z.array(z.string()).optional(),
|
|
5802
|
+
label: z.string().optional(),
|
|
5803
|
+
/**
|
|
5804
|
+
* Per-run retention override applied to every targeted
|
|
5805
|
+
* destination. Used by schedule-driven runs (per-entry
|
|
5806
|
+
* retention). Omitted = each destination's own policy
|
|
5807
|
+
* retention (manual runs).
|
|
5808
|
+
*/
|
|
5809
|
+
retentionCount: z.number().int().min(1).max(1e3).optional()
|
|
5810
|
+
}).optional(), z.array(BackupEntrySchema).readonly(), {
|
|
5336
5811
|
kind: "mutation",
|
|
5337
5812
|
auth: "admin"
|
|
5338
5813
|
}),
|
|
5339
|
-
|
|
5814
|
+
/** Union of every destination's archives, each tagged with `destinationId`. */
|
|
5815
|
+
list: method(z.void(), z.array(BackupEntrySchema).readonly(), { auth: "admin" }),
|
|
5816
|
+
/**
|
|
5817
|
+
* Pre-backup snapshot of the well-known locations on disk — sizes
|
|
5818
|
+
* + file counts. Powers the opt-in checklist that lets the
|
|
5819
|
+
* operator pick which subsections of state get archived.
|
|
5820
|
+
*/
|
|
5821
|
+
listLocations: method(z.void(), z.array(LocationStatSchema).readonly(), { auth: "admin" }),
|
|
5822
|
+
/**
|
|
5823
|
+
* Read the embedded `.camstack-backup-manifest.json` from a
|
|
5824
|
+
* previously-created archive. The manifest carries the full
|
|
5825
|
+
* file/dir listing with sizes + mtimes — the readdir snapshot
|
|
5826
|
+
* the UI shows in the "Contents" panel. Returns `null` when the
|
|
5827
|
+
* archive predates manifests (created before this feature
|
|
5828
|
+
* shipped).
|
|
5829
|
+
*/
|
|
5830
|
+
getEntries: method(z.object({
|
|
5831
|
+
destinationId: z.string(),
|
|
5832
|
+
backupId: z.string()
|
|
5833
|
+
}), ArchiveManifestSchema.nullable(), { auth: "admin" }),
|
|
5834
|
+
restore: method(z.object({
|
|
5835
|
+
destinationId: z.string(),
|
|
5836
|
+
backupId: z.string(),
|
|
5837
|
+
/**
|
|
5838
|
+
* Optional whitelist — only restore these top-level locations
|
|
5839
|
+
* from the archive. Default = every location the archive
|
|
5840
|
+
* carries (full restore). The boot-time apply hook reads
|
|
5841
|
+
* this list and skips entries whose path doesn't start with
|
|
5842
|
+
* any of them.
|
|
5843
|
+
*/
|
|
5844
|
+
locations: z.array(z.string()).optional()
|
|
5845
|
+
}), z.void(), {
|
|
5340
5846
|
kind: "mutation",
|
|
5341
5847
|
auth: "admin"
|
|
5342
5848
|
}),
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
* fallback may route a foreign id to the first provider). */
|
|
5347
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
5348
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
5349
|
-
* provider validates the shape and applies the change (reconnects
|
|
5350
|
-
* if credentials changed). */
|
|
5351
|
-
setSettings: method(z.object({
|
|
5352
|
-
id: z.string(),
|
|
5353
|
-
settings: SettingsRecordSchema$1
|
|
5849
|
+
delete: method(z.object({
|
|
5850
|
+
destinationId: z.string(),
|
|
5851
|
+
backupId: z.string()
|
|
5354
5852
|
}), z.void(), {
|
|
5355
5853
|
kind: "mutation",
|
|
5356
5854
|
auth: "admin"
|
|
5357
5855
|
}),
|
|
5358
|
-
/**
|
|
5359
|
-
*
|
|
5360
|
-
*
|
|
5361
|
-
*
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5856
|
+
/**
|
|
5857
|
+
* List archives at a single destination. Reads the per-location
|
|
5858
|
+
* `manifests.json` and returns one entry per archive (newest
|
|
5859
|
+
* first). Powers the destinations-table drill-in in admin UI.
|
|
5860
|
+
*/
|
|
5861
|
+
listArchives: method(z.object({ destinationId: z.string() }), z.array(BackupArchiveEntrySchema).readonly(), { auth: "admin" }),
|
|
5862
|
+
/**
|
|
5863
|
+
* Upsert a per-destination policy row. The `locationId` MUST be
|
|
5864
|
+
* the id of an existing `backups`-typed `StorageLocation`. Used
|
|
5865
|
+
* by the admin-UI destinations table (enable toggle + retention
|
|
5866
|
+
* input + optional label).
|
|
5867
|
+
*/
|
|
5868
|
+
upsertDestinationPolicy: method(z.object({
|
|
5869
|
+
locationId: z.string(),
|
|
5870
|
+
enabled: z.boolean(),
|
|
5871
|
+
retentionCount: z.number().int().min(1).max(1e3),
|
|
5872
|
+
label: z.string().optional(),
|
|
5873
|
+
/**
|
|
5874
|
+
* Per-destination cron expression. Empty string clears the
|
|
5875
|
+
* schedule (manual-only). Validated server-side via croner;
|
|
5876
|
+
* malformed expressions reject the upsert with an actionable
|
|
5877
|
+
* message.
|
|
5878
|
+
*/
|
|
5879
|
+
cron: z.string().optional()
|
|
5880
|
+
}), z.void(), {
|
|
5366
5881
|
kind: "mutation",
|
|
5367
5882
|
auth: "admin"
|
|
5368
5883
|
}),
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5884
|
+
/**
|
|
5885
|
+
* Validate a cron expression and peek the next N firing times.
|
|
5886
|
+
* Used by the admin-UI CronEditor to render a live "next-run"
|
|
5887
|
+
* preview as the operator edits the pattern.
|
|
5888
|
+
*/
|
|
5889
|
+
previewSchedule: method(z.object({
|
|
5890
|
+
cron: z.string(),
|
|
5891
|
+
count: z.number().int().min(1).max(20).optional()
|
|
5892
|
+
}), z.object({
|
|
5893
|
+
ok: z.boolean(),
|
|
5894
|
+
error: z.string().optional(),
|
|
5895
|
+
nextRuns: z.array(z.number()).readonly()
|
|
5896
|
+
})),
|
|
5897
|
+
/**
|
|
5898
|
+
* List every backup schedule (the N:M entries), each with its
|
|
5899
|
+
* computed `nextRunAt`. Powers the Schedules section of the
|
|
5900
|
+
* admin-UI Backups page.
|
|
5901
|
+
*/
|
|
5902
|
+
listSchedules: method(z.void(), z.array(BackupScheduleSchema).readonly(), { auth: "admin" }),
|
|
5903
|
+
/**
|
|
5904
|
+
* Create or replace a schedule. An empty / absent `id` mints a new
|
|
5905
|
+
* one; a present `id` replaces in place. `cron` is validated
|
|
5906
|
+
* server-side; `locationIds` must reference existing `backups`
|
|
5907
|
+
* locations (unknown ids are dropped with a warning).
|
|
5908
|
+
*/
|
|
5909
|
+
upsertSchedule: method(z.object({
|
|
5910
|
+
id: z.string().optional(),
|
|
5911
|
+
label: z.string(),
|
|
5912
|
+
cron: z.string(),
|
|
5913
|
+
enabled: z.boolean(),
|
|
5914
|
+
locationIds: z.array(z.string()).readonly(),
|
|
5915
|
+
retentionCount: z.number().int().min(1).max(1e3),
|
|
5916
|
+
dataSources: z.array(z.string()).readonly().optional()
|
|
5917
|
+
}), BackupScheduleSchema, {
|
|
5374
5918
|
kind: "mutation",
|
|
5375
5919
|
auth: "admin"
|
|
5376
5920
|
}),
|
|
5377
|
-
|
|
5921
|
+
/** Delete a schedule by id. Does not touch the target locations. */
|
|
5922
|
+
deleteSchedule: method(z.object({ id: z.string() }), z.void(), {
|
|
5378
5923
|
kind: "mutation",
|
|
5379
5924
|
auth: "admin"
|
|
5380
|
-
})
|
|
5381
|
-
/** Read the broker's cached state for a key. Returns `null` when
|
|
5382
|
-
* unknown to the broker (never published / unknown entity). */
|
|
5383
|
-
getState: method(GetStateInputSchema, z.unknown().nullable()),
|
|
5384
|
-
/** Status method — explicit registration with a `z.void()` input so
|
|
5385
|
-
* the codegen-generated tRPC router types its input as
|
|
5386
|
-
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
5387
|
-
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
5388
|
-
getStatus: method(z.void(), RegistryStatusSchema)
|
|
5925
|
+
})
|
|
5389
5926
|
}
|
|
5390
5927
|
};
|
|
5391
5928
|
/**
|
|
5392
|
-
*
|
|
5393
|
-
* pipeline-orchestrator's PER-DEVICE settings contribution into the
|
|
5394
|
-
* binding-driven device-detail aggregate (D12).
|
|
5929
|
+
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
5395
5930
|
*
|
|
5396
|
-
*
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
5399
|
-
*
|
|
5400
|
-
*
|
|
5401
|
-
*
|
|
5402
|
-
* `getBindings(deviceId)`, so the D12 binding-driven aggregate
|
|
5403
|
-
* (`device-manager.getDeviceAggregate`) silently dropped the entire
|
|
5404
|
-
* contribution — the zones tab and the pipeline/motion settings vanished
|
|
5405
|
-
* from device-detail.
|
|
5931
|
+
* The cap models any kind-tagged message broker the user wants to
|
|
5932
|
+
* register and that other addons might consume. The first two kinds
|
|
5933
|
+
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
5934
|
+
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
5935
|
+
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
5936
|
+
* the same surface.
|
|
5406
5937
|
*
|
|
5407
|
-
*
|
|
5408
|
-
*
|
|
5409
|
-
*
|
|
5410
|
-
*
|
|
5938
|
+
* Why one cap, not one cap per kind:
|
|
5939
|
+
* - The integrations page wants a single table of "every broker"
|
|
5940
|
+
* across kinds; a unified cap drives it without join logic.
|
|
5941
|
+
* - Generic admin operations (add / remove / test / status) are
|
|
5942
|
+
* identical across kinds — duplicating them per cap was busywork.
|
|
5943
|
+
* - The `network-access` cap precedent: same interface, many
|
|
5944
|
+
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
5411
5945
|
*
|
|
5412
|
-
*
|
|
5413
|
-
*
|
|
5414
|
-
*
|
|
5415
|
-
*
|
|
5416
|
-
*
|
|
5417
|
-
*
|
|
5418
|
-
*
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
scope: "device",
|
|
5423
|
-
mode: "singleton",
|
|
5424
|
-
kind: "wrapper",
|
|
5425
|
-
defaultActive: true,
|
|
5426
|
-
deviceTypes: [DeviceType.Camera],
|
|
5427
|
-
exposesDeviceSettings: true,
|
|
5428
|
-
methods: {}
|
|
5429
|
-
};
|
|
5430
|
-
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
5431
|
-
var StreamFormatSchema = z.enum([
|
|
5432
|
-
"webrtc",
|
|
5433
|
-
"hls",
|
|
5434
|
-
"mjpeg",
|
|
5435
|
-
"rtsp"
|
|
5436
|
-
]);
|
|
5437
|
-
/** A container `produceEventMedia` can emit. */
|
|
5438
|
-
var EventMediaKindSchema = z.enum(["mp4", "gif"]);
|
|
5439
|
-
/**
|
|
5440
|
-
* One produced artifact, referenced by HANDLE.
|
|
5946
|
+
* What stays kind-specific:
|
|
5947
|
+
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
5948
|
+
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
5949
|
+
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
5950
|
+
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
5951
|
+
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
5952
|
+
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
5953
|
+
* without rebreaking the cap signature.
|
|
5954
|
+
* - `add` `settings` payload (different fields per kind, validated by
|
|
5955
|
+
* the kind-specific provider).
|
|
5441
5956
|
*
|
|
5442
|
-
*
|
|
5443
|
-
*
|
|
5444
|
-
*
|
|
5445
|
-
*
|
|
5446
|
-
*
|
|
5957
|
+
* Bidirectionality:
|
|
5958
|
+
* - `publish` is RPC — synchronous result (provider-defined).
|
|
5959
|
+
* - `subscribe` returns a subscription handle; the actual message
|
|
5960
|
+
* stream flows over the typed event-bus as `broker.message` events
|
|
5961
|
+
* keyed by `(brokerId, subscriptionId)`. Consumers filter in their
|
|
5962
|
+
* event handler. Subscriptions persist across reconnects — the
|
|
5963
|
+
* provider re-subscribes upstream after a transport drop.
|
|
5964
|
+
*
|
|
5965
|
+
* Why event-bus push (not a subscription RPC stream):
|
|
5966
|
+
* - Matches the rest of CamStack's D8 contract — events for telemetry,
|
|
5967
|
+
* RPC for loss-is-a-bug. Broker messages are telemetry (a single
|
|
5968
|
+
* drop is recoverable via `getState({key})`).
|
|
5969
|
+
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
5970
|
+
* - Cross-process: events already route through `$event-bus`; no
|
|
5971
|
+
* per-broker wire-shaping needed.
|
|
5447
5972
|
*/
|
|
5448
|
-
var
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5973
|
+
var BrokerStatusEnum = z.enum([
|
|
5974
|
+
"connected",
|
|
5975
|
+
"disconnected",
|
|
5976
|
+
"connecting",
|
|
5977
|
+
"auth-failed",
|
|
5978
|
+
"unreachable",
|
|
5979
|
+
"error"
|
|
5980
|
+
]);
|
|
5981
|
+
var BrokerInfoSchema$1 = z.object({
|
|
5982
|
+
/** Stable broker id. Persisted; survives addon restarts. */
|
|
5983
|
+
id: z.string(),
|
|
5984
|
+
/** Addon id of the provider that OWNS this broker.
|
|
5454
5985
|
*
|
|
5455
|
-
* `
|
|
5456
|
-
*
|
|
5457
|
-
*
|
|
5458
|
-
*
|
|
5459
|
-
*
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5986
|
+
* The `broker` cap is a system-scoped collection: several addons
|
|
5987
|
+
* register a `broker` provider, each owning a DISJOINT set of
|
|
5988
|
+
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
5989
|
+
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
5990
|
+
* device belongs to one integration. The admin UI threads this id
|
|
5991
|
+
* back as the `{ addonId }` system-collection selector on every
|
|
5992
|
+
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
5993
|
+
* `testConnection`), so the call routes to the OWNING provider
|
|
5994
|
+
* instead of defaulting to the first-registered one. */
|
|
5995
|
+
addonId: z.string(),
|
|
5996
|
+
/** Human-readable name (operator-chosen at add-time). */
|
|
5997
|
+
name: z.string(),
|
|
5998
|
+
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
5999
|
+
kind: z.string(),
|
|
6000
|
+
status: BrokerStatusEnum,
|
|
6001
|
+
/** Free-form provider-specific info (HA version, MQTT broker
|
|
6002
|
+
* flavour, latency, mTLS active, …). */
|
|
6003
|
+
info: z.record(z.string(), z.unknown()),
|
|
6004
|
+
/** Ms epoch of the last connection probe / status update. */
|
|
6005
|
+
lastCheckedAt: z.number().nullable(),
|
|
6006
|
+
/** Last error message, when `status` indicates failure. */
|
|
6007
|
+
error: z.string().nullable()
|
|
5466
6008
|
});
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
*
|
|
5471
|
-
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
5472
|
-
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
5473
|
-
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
5474
|
-
* event is a production with no pre-roll, and that is exactly the defect this
|
|
5475
|
-
* method exists to make visible rather than plausible.
|
|
5476
|
-
*/
|
|
5477
|
-
var EventMediaCoverageSchema = z.object({
|
|
5478
|
-
fromTs: z.number(),
|
|
5479
|
-
toTs: z.number(),
|
|
5480
|
-
/** Encoded packets in the muxed window. */
|
|
5481
|
-
packets: z.number().int()
|
|
6009
|
+
var RegistryStatusSchema = z.object({
|
|
6010
|
+
brokerCount: z.number().int().nonnegative(),
|
|
6011
|
+
connectedCount: z.number().int().nonnegative()
|
|
5482
6012
|
});
|
|
5483
6013
|
/**
|
|
5484
|
-
*
|
|
5485
|
-
*
|
|
5486
|
-
*
|
|
5487
|
-
*
|
|
5488
|
-
*
|
|
5489
|
-
*
|
|
6014
|
+
* One entry per `broker` provider — which addon provides which broker
|
|
6015
|
+
* kind(s). The unified create picker reads this to know, for each
|
|
6016
|
+
* (addonId, kind) pair the operator can pick, where to route the
|
|
6017
|
+
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
6018
|
+
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
6019
|
+
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
6020
|
+
* offers a kind whose creation form can't be rendered).
|
|
5490
6021
|
*/
|
|
5491
|
-
var
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
/**
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
6022
|
+
var BrokerProviderInfoSchema = z.object({
|
|
6023
|
+
/** Addon id of the `broker` provider this entry describes. */
|
|
6024
|
+
addonId: z.string(),
|
|
6025
|
+
/** Broker kinds this provider can create, with a display label. */
|
|
6026
|
+
kinds: z.array(z.object({
|
|
6027
|
+
kind: z.string(),
|
|
6028
|
+
label: z.string()
|
|
6029
|
+
}))
|
|
5499
6030
|
});
|
|
5500
|
-
var
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
6031
|
+
var ListInputSchema = z.object({
|
|
6032
|
+
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
6033
|
+
* only HA brokers. Omit to list every broker. */
|
|
6034
|
+
kind: z.string().optional() });
|
|
6035
|
+
var GetInputSchema = z.object({ id: z.string() });
|
|
6036
|
+
var AddInputSchema = z.object({
|
|
6037
|
+
kind: z.string().min(1),
|
|
6038
|
+
name: z.string().min(1),
|
|
5505
6039
|
/**
|
|
5506
|
-
*
|
|
5507
|
-
*
|
|
5508
|
-
*
|
|
5509
|
-
*
|
|
5510
|
-
*
|
|
5511
|
-
*
|
|
5512
|
-
*
|
|
5513
|
-
*
|
|
6040
|
+
* ADOPT an existing id instead of minting a new one.
|
|
6041
|
+
*
|
|
6042
|
+
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
6043
|
+
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
6044
|
+
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
6045
|
+
* bound to a broker that no longer exists. Re-entering the password under the
|
|
6046
|
+
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
6047
|
+
* password.
|
|
6048
|
+
*
|
|
6049
|
+
* A provider MUST refuse an id that is already in use — adopting a live
|
|
6050
|
+
* broker's id would silently take it over.
|
|
5514
6051
|
*/
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
6052
|
+
id: z.string().min(1).optional(),
|
|
6053
|
+
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
6054
|
+
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
6055
|
+
* branch on receipt — invalid shape rejects the add. */
|
|
6056
|
+
settings: z.record(z.string(), z.unknown())
|
|
5520
6057
|
});
|
|
5521
|
-
var
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
6058
|
+
var AddResultSchema = z.object({ id: z.string() });
|
|
6059
|
+
var RemoveInputSchema = z.object({ id: z.string() });
|
|
6060
|
+
var TestConnectionResultSchema = z.discriminatedUnion("ok", [z.object({
|
|
6061
|
+
ok: z.literal(true),
|
|
6062
|
+
latencyMs: z.number().nonnegative()
|
|
6063
|
+
}), z.object({
|
|
6064
|
+
ok: z.literal(false),
|
|
6065
|
+
error: z.string()
|
|
6066
|
+
})]);
|
|
6067
|
+
var SettingsRecordSchema$1 = z.record(z.string(), z.unknown());
|
|
6068
|
+
var SettingsSchemaInputSchema = z.object({ kind: z.string() });
|
|
6069
|
+
var TestSettingsInputSchema = z.object({
|
|
6070
|
+
kind: z.string(),
|
|
6071
|
+
settings: SettingsRecordSchema$1
|
|
5533
6072
|
});
|
|
5534
|
-
var
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
6073
|
+
var TestSettingsResultSchema = z.discriminatedUnion("ok", [z.object({
|
|
6074
|
+
ok: z.literal(true),
|
|
6075
|
+
latencyMs: z.number().nonnegative().optional()
|
|
6076
|
+
}).strict(), z.object({
|
|
6077
|
+
ok: z.literal(false),
|
|
6078
|
+
error: z.string()
|
|
6079
|
+
})]);
|
|
6080
|
+
var SettingsSchemaResultSchema = z.unknown().nullable();
|
|
6081
|
+
var PublishInputSchema = z.object({
|
|
6082
|
+
brokerId: z.string(),
|
|
6083
|
+
/** Kind-specific routing target.
|
|
6084
|
+
* MQTT: `{ topic: string, qos?: 0|1|2, retain?: boolean }`.
|
|
6085
|
+
* HA: `{ domain: string, service: string, entityId?: string, area?: string }`. */
|
|
6086
|
+
target: z.record(z.string(), z.unknown()),
|
|
6087
|
+
/** Kind-specific payload.
|
|
6088
|
+
* MQTT: raw string / number / object (provider serialises).
|
|
6089
|
+
* HA: service-call `data` object (transition, brightness, …). */
|
|
6090
|
+
payload: z.unknown().optional()
|
|
5540
6091
|
});
|
|
5541
|
-
var
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
6092
|
+
var SubscribeInputSchema = z.object({
|
|
6093
|
+
brokerId: z.string(),
|
|
6094
|
+
/** Kind-specific filter.
|
|
6095
|
+
* MQTT: `{ topic: string, qos?: 0|1|2 }` — topic can include wildcards.
|
|
6096
|
+
* HA: `{ entityIds: string[] }` or `{ domain: string }`. */
|
|
6097
|
+
filter: z.record(z.string(), z.unknown())
|
|
6098
|
+
});
|
|
6099
|
+
var SubscribeResultSchema = z.object({
|
|
6100
|
+
/** Stable subscription id. Used to unsubscribe and to filter the
|
|
6101
|
+
* matching `broker.message` events on the bus. */
|
|
6102
|
+
subscriptionId: z.string() });
|
|
6103
|
+
var UnsubscribeInputSchema = z.object({
|
|
6104
|
+
brokerId: z.string(),
|
|
6105
|
+
subscriptionId: z.string()
|
|
6106
|
+
});
|
|
6107
|
+
var GetStateInputSchema = z.object({
|
|
6108
|
+
brokerId: z.string(),
|
|
6109
|
+
/** Kind-specific lookup key.
|
|
6110
|
+
* MQTT: topic string (returns the last retained message).
|
|
6111
|
+
* HA: entity_id (returns the cached entity state). */
|
|
6112
|
+
key: z.string()
|
|
5545
6113
|
});
|
|
6114
|
+
var brokerCapability = {
|
|
6115
|
+
name: "broker",
|
|
6116
|
+
scope: "system",
|
|
6117
|
+
mode: "collection",
|
|
6118
|
+
providerKind: "broker",
|
|
6119
|
+
status: {
|
|
6120
|
+
schema: RegistryStatusSchema,
|
|
6121
|
+
kind: "poll"
|
|
6122
|
+
},
|
|
6123
|
+
methods: {
|
|
6124
|
+
list: method(ListInputSchema, z.array(BrokerInfoSchema$1)),
|
|
6125
|
+
get: method(GetInputSchema, BrokerInfoSchema$1.nullable()),
|
|
6126
|
+
/** Enumerate which addon provides which broker kind(s) for the
|
|
6127
|
+
* unified create picker. The auto-mount fans this array across
|
|
6128
|
+
* every registered `broker` provider (array-output method), so the
|
|
6129
|
+
* picker sees every kind from every provider in one call. */
|
|
6130
|
+
listProviders: method(z.void(), z.array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
6131
|
+
add: method(AddInputSchema, AddResultSchema, {
|
|
6132
|
+
kind: "mutation",
|
|
6133
|
+
auth: "admin"
|
|
6134
|
+
}),
|
|
6135
|
+
remove: method(RemoveInputSchema, z.void(), {
|
|
6136
|
+
kind: "mutation",
|
|
6137
|
+
auth: "admin"
|
|
6138
|
+
}),
|
|
6139
|
+
testConnection: method(GetInputSchema, TestConnectionResultSchema, {
|
|
6140
|
+
kind: "mutation",
|
|
6141
|
+
auth: "admin"
|
|
6142
|
+
}),
|
|
6143
|
+
/** Read the persisted settings record for a broker (kind-specific
|
|
6144
|
+
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
6145
|
+
* when the broker id is unknown to the provider (the collection
|
|
6146
|
+
* fallback may route a foreign id to the first provider). */
|
|
6147
|
+
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
6148
|
+
/** Overwrite the persisted settings record. The kind-specific
|
|
6149
|
+
* provider validates the shape and applies the change (reconnects
|
|
6150
|
+
* if credentials changed). */
|
|
6151
|
+
setSettings: method(z.object({
|
|
6152
|
+
id: z.string(),
|
|
6153
|
+
settings: SettingsRecordSchema$1
|
|
6154
|
+
}), z.void(), {
|
|
6155
|
+
kind: "mutation",
|
|
6156
|
+
auth: "admin"
|
|
6157
|
+
}),
|
|
6158
|
+
/** Returns the kind-specific connection config the consumer needs
|
|
6159
|
+
* to open its own client (MQTT pattern: `{url, username, password,
|
|
6160
|
+
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
6161
|
+
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
6162
|
+
* Returns `null` when the broker id is unknown to the provider. */
|
|
6163
|
+
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
6164
|
+
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
6165
|
+
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
6166
|
+
kind: "mutation",
|
|
6167
|
+
auth: "admin"
|
|
6168
|
+
}),
|
|
6169
|
+
publish: method(PublishInputSchema, z.unknown(), {
|
|
6170
|
+
kind: "mutation",
|
|
6171
|
+
auth: "admin"
|
|
6172
|
+
}),
|
|
6173
|
+
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
6174
|
+
kind: "mutation",
|
|
6175
|
+
auth: "admin"
|
|
6176
|
+
}),
|
|
6177
|
+
unsubscribe: method(UnsubscribeInputSchema, z.void(), {
|
|
6178
|
+
kind: "mutation",
|
|
6179
|
+
auth: "admin"
|
|
6180
|
+
}),
|
|
6181
|
+
/** Read the broker's cached state for a key. Returns `null` when
|
|
6182
|
+
* unknown to the broker (never published / unknown entity). */
|
|
6183
|
+
getState: method(GetStateInputSchema, z.unknown().nullable()),
|
|
6184
|
+
/** Status method — explicit registration with a `z.void()` input so
|
|
6185
|
+
* the codegen-generated tRPC router types its input as
|
|
6186
|
+
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
6187
|
+
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
6188
|
+
getStatus: method(z.void(), RegistryStatusSchema)
|
|
6189
|
+
}
|
|
6190
|
+
};
|
|
5546
6191
|
/**
|
|
5547
|
-
*
|
|
5548
|
-
*
|
|
5549
|
-
*
|
|
5550
|
-
*
|
|
5551
|
-
*
|
|
6192
|
+
* camera-pipeline-config — device-scoped wrapper that carries the
|
|
6193
|
+
* pipeline-orchestrator's PER-DEVICE settings contribution into the
|
|
6194
|
+
* binding-driven device-detail aggregate (D12).
|
|
6195
|
+
*
|
|
6196
|
+
* Why this cap exists: the orchestrator's per-camera settings (motion
|
|
6197
|
+
* sources / fps / cooldown, detection + audio mode, cluster assignment,
|
|
6198
|
+
* the Detection-Zones top-tab widget, onboard-motion + onboard-object-
|
|
6199
|
+
* detection toggles) were historically contributed via the
|
|
6200
|
+
* `pipeline-orchestrator` cap's `exposesDeviceSettings` methods. But
|
|
6201
|
+
* `pipeline-orchestrator` is `scope:'system'` and never appears in
|
|
6202
|
+
* `getBindings(deviceId)`, so the D12 binding-driven aggregate
|
|
6203
|
+
* (`device-manager.getDeviceAggregate`) silently dropped the entire
|
|
6204
|
+
* contribution — the zones tab and the pipeline/motion settings vanished
|
|
6205
|
+
* from device-detail.
|
|
6206
|
+
*
|
|
6207
|
+
* `kind:'wrapper'` + `defaultActive:true` makes this cap auto-bind to every
|
|
6208
|
+
* camera (it appears in `getBindings` as `kind:'wrapped'`), so the aggregate
|
|
6209
|
+
* invokes the orchestrator's contribution methods through it — restoring the
|
|
6210
|
+
* tabs without violating D12 (contributions come only from bound providers).
|
|
6211
|
+
*
|
|
6212
|
+
* `exposesDeviceSettings:true` (NOT `deviceConfig`) is intentional: the
|
|
6213
|
+
* contribution is a dynamic, multi-section, per-device hand-built structure
|
|
6214
|
+
* (it probes bindings for onboard-motion / native-object-detection presence,
|
|
6215
|
+
* cluster nodes, etc.), which the derived-UI `deviceConfig` archetype cannot
|
|
6216
|
+
* express. This mirrors the `detection-pipeline` / `motion-detection`
|
|
6217
|
+
* wrapper pattern. The orchestrator class is the provider; it already
|
|
6218
|
+
* implements the three DeviceSettingsContribution methods.
|
|
5552
6219
|
*/
|
|
5553
|
-
var
|
|
5554
|
-
"
|
|
5555
|
-
"
|
|
5556
|
-
"
|
|
5557
|
-
"
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
"
|
|
6220
|
+
var cameraPipelineConfigCapability = {
|
|
6221
|
+
name: "camera-pipeline-config",
|
|
6222
|
+
scope: "device",
|
|
6223
|
+
mode: "singleton",
|
|
6224
|
+
kind: "wrapper",
|
|
6225
|
+
defaultActive: true,
|
|
6226
|
+
deviceTypes: [DeviceType.Camera],
|
|
6227
|
+
exposesDeviceSettings: true,
|
|
6228
|
+
methods: {}
|
|
6229
|
+
};
|
|
6230
|
+
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
6231
|
+
var StreamFormatSchema = z.enum([
|
|
6232
|
+
"webrtc",
|
|
6233
|
+
"hls",
|
|
6234
|
+
"mjpeg",
|
|
6235
|
+
"rtsp"
|
|
5566
6236
|
]);
|
|
5567
|
-
|
|
5568
|
-
|
|
6237
|
+
/** A container `produceEventMedia` can emit. */
|
|
6238
|
+
var EventMediaKindSchema = z.enum(["mp4", "gif"]);
|
|
6239
|
+
/**
|
|
6240
|
+
* One produced artifact, referenced by HANDLE.
|
|
6241
|
+
*
|
|
6242
|
+
* Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
|
|
6243
|
+
* method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
|
|
6244
|
+
* [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
|
|
6245
|
+
* on demand, compressed, by handle). `bytes` is here so a caller can decide
|
|
6246
|
+
* whether it wants the fetch at all.
|
|
6247
|
+
*/
|
|
6248
|
+
var EventMediaArtifactSchema = z.object({
|
|
6249
|
+
kind: EventMediaKindSchema,
|
|
6250
|
+
/** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
|
|
6251
|
+
handle: z.string(),
|
|
5569
6252
|
/**
|
|
5570
|
-
*
|
|
5571
|
-
*
|
|
5572
|
-
*
|
|
6253
|
+
* The node holding the bytes — the ROUTING key for `fetchEventMedia`.
|
|
6254
|
+
*
|
|
6255
|
+
* `stream-broker` is a singleton cap and an unpinned call never leaves the
|
|
6256
|
+
* hub, so a handle produced on an agent's broker would be redeemed against
|
|
6257
|
+
* the hub's store and come back `null`. Same contract, same field name and
|
|
6258
|
+
* the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
|
|
6259
|
+
* lives and the consumer pins to it.
|
|
6260
|
+
*/
|
|
6261
|
+
nodeId: z.string(),
|
|
6262
|
+
mime: z.string(),
|
|
6263
|
+
bytes: z.number().int(),
|
|
6264
|
+
width: z.number().int(),
|
|
6265
|
+
height: z.number().int()
|
|
6266
|
+
});
|
|
6267
|
+
/**
|
|
6268
|
+
* What a production actually covered — the answer to the only question an
|
|
6269
|
+
* operator asks about a notification clip.
|
|
6270
|
+
*
|
|
6271
|
+
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
6272
|
+
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
6273
|
+
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
6274
|
+
* event is a production with no pre-roll, and that is exactly the defect this
|
|
6275
|
+
* method exists to make visible rather than plausible.
|
|
6276
|
+
*/
|
|
6277
|
+
var EventMediaCoverageSchema = z.object({
|
|
6278
|
+
fromTs: z.number(),
|
|
6279
|
+
toTs: z.number(),
|
|
6280
|
+
/** Encoded packets in the muxed window. */
|
|
6281
|
+
packets: z.number().int()
|
|
6282
|
+
});
|
|
6283
|
+
/**
|
|
6284
|
+
* The result of ONE cut, in every container the caller asked for.
|
|
6285
|
+
*
|
|
6286
|
+
* Every artifact in `media` came out of the SAME window of the SAME rendition —
|
|
6287
|
+
* that is the whole reason this is one method rather than one call per format.
|
|
6288
|
+
* A consumer attaching a gif and a video can no longer show two different
|
|
6289
|
+
* moments, because it never chose two sources.
|
|
6290
|
+
*/
|
|
6291
|
+
var EventMediaProductionSchema = z.object({
|
|
6292
|
+
media: z.array(EventMediaArtifactSchema).readonly(),
|
|
6293
|
+
coverage: EventMediaCoverageSchema,
|
|
6294
|
+
/** The rendition actually cut from — what the default or the fallback chose. */
|
|
6295
|
+
profile: CamProfileSchema,
|
|
6296
|
+
/** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
|
|
6297
|
+
* source, a downscale, or a playback rate other than 1). */
|
|
6298
|
+
video: z.enum(["copy", "encode"])
|
|
6299
|
+
});
|
|
6300
|
+
var RtspRestreamEntrySchema = z.object({
|
|
6301
|
+
brokerId: z.string(),
|
|
6302
|
+
url: z.string(),
|
|
6303
|
+
mutedUrl: z.string(),
|
|
6304
|
+
enabled: z.boolean(),
|
|
6305
|
+
/**
|
|
6306
|
+
* Source-stream codec / resolution for the camStream this entry serves
|
|
6307
|
+
* (the broker's "high"/"mid"/"low" profile slot for this device).
|
|
6308
|
+
* Used by exporter pickers (`pickPreferredRtspEntry`) to resolve
|
|
6309
|
+
* `streamPreference: 'auto'` to the slot whose source is closest to
|
|
6310
|
+
* the consumer's target — Alexa wants ~720p, HomeKit wants ~1080p,
|
|
6311
|
+
* and there's no point dialling the 4K slot for an Echo Show. Absent
|
|
6312
|
+
* when the source publisher never advertised the field; pickers fall
|
|
6313
|
+
* back to first-enabled in that case.
|
|
6314
|
+
*/
|
|
6315
|
+
codec: z.string().optional(),
|
|
6316
|
+
resolution: z.object({
|
|
6317
|
+
width: z.number().int().positive(),
|
|
6318
|
+
height: z.number().int().positive()
|
|
6319
|
+
}).optional()
|
|
6320
|
+
});
|
|
6321
|
+
var BrokerRtspClientSchema = z.object({
|
|
6322
|
+
sessionId: z.string(),
|
|
6323
|
+
remoteAddr: z.string(),
|
|
6324
|
+
/** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
|
|
6325
|
+
* null/absent when the client sent none. Lets the UI label a consumer by
|
|
6326
|
+
* purpose. Optional so a client built against an older schema stays valid. */
|
|
6327
|
+
userAgent: z.string().nullish(),
|
|
6328
|
+
playing: z.boolean(),
|
|
6329
|
+
muted: z.boolean(),
|
|
6330
|
+
connectedAt: z.number(),
|
|
6331
|
+
lastRtpAt: z.number(),
|
|
6332
|
+
bytesSent: z.number()
|
|
6333
|
+
});
|
|
6334
|
+
var BrokerDecodedClientSchema = z.object({
|
|
6335
|
+
tag: z.string(),
|
|
6336
|
+
subscribedAt: z.number(),
|
|
6337
|
+
maxFps: z.number(),
|
|
6338
|
+
framesDelivered: z.number(),
|
|
6339
|
+
framesDropped: z.number()
|
|
6340
|
+
});
|
|
6341
|
+
var BrokerAudioClientSchema = z.object({
|
|
6342
|
+
tag: z.string(),
|
|
6343
|
+
subscribedAt: z.number(),
|
|
6344
|
+
chunksDelivered: z.number()
|
|
6345
|
+
});
|
|
6346
|
+
/**
|
|
6347
|
+
* Identifies who is holding an encoded / raw-RTP subscription open on
|
|
6348
|
+
* the broker. Populated by the subscribing addon when it attaches —
|
|
6349
|
+
* defaults to `{ kind: 'unknown' }` for callers that haven't been
|
|
6350
|
+
* migrated. The widget surfaces every field that's set so an operator
|
|
6351
|
+
* can answer "who is keeping cam X warm?" without grepping logs.
|
|
6352
|
+
*/
|
|
6353
|
+
var BrokerConsumerKindSchema = z.enum([
|
|
6354
|
+
"alexa",
|
|
6355
|
+
"homekit",
|
|
6356
|
+
"webrtc-browser",
|
|
6357
|
+
"webrtc-mobile",
|
|
6358
|
+
"webrtc-whep",
|
|
6359
|
+
"rtsp-listen",
|
|
6360
|
+
"derived-broker",
|
|
6361
|
+
"recording",
|
|
6362
|
+
"pipeline",
|
|
6363
|
+
"snapshot",
|
|
6364
|
+
"warmup",
|
|
6365
|
+
"unknown"
|
|
6366
|
+
]);
|
|
6367
|
+
var BrokerConsumerAttributionSchema = z.object({
|
|
6368
|
+
kind: BrokerConsumerKindSchema,
|
|
6369
|
+
/**
|
|
6370
|
+
* Free-form label intended to disambiguate consumers OF THE SAME kind
|
|
6371
|
+
* on the same broker — e.g. user name, device alias. Should NOT repeat
|
|
6372
|
+
* the kind / cam / cam-stream / sessionId (those are surfaced by
|
|
5573
6373
|
* dedicated fields). When empty the widget falls back to `${kind} ·
|
|
5574
6374
|
* <sessionId tail>`.
|
|
5575
6375
|
*/
|
|
@@ -6598,6 +7398,23 @@ function kebabToCamel(s) {
|
|
|
6598
7398
|
* too many — a drift would show as a logs pane that is simply always empty.
|
|
6599
7399
|
*/
|
|
6600
7400
|
var CORE_BLOCK_ADDON_PREFIX = "core-block-";
|
|
7401
|
+
/**
|
|
7402
|
+
* The builtin's own addon id — and, because of that, the OWNER of the
|
|
7403
|
+
* integration every block's devices hang from.
|
|
7404
|
+
*
|
|
7405
|
+
* A block cannot own one. Every integration read is gated on the installed-addon
|
|
7406
|
+
* set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
|
|
7407
|
+
* `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
|
|
7408
|
+
* null forever while the write succeeds — one invisible, undeletable row per
|
|
7409
|
+
* start. `core-blocks` IS installed, so it reconciles one shared integration and
|
|
7410
|
+
* a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
|
|
7411
|
+
*
|
|
7412
|
+
* Exported from the contract, next to the runner prefix and for the same reason:
|
|
7413
|
+
* a block reads it as
|
|
7414
|
+
* `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
|
|
7415
|
+
* so a second copy of the string is a block that silently declares into nowhere.
|
|
7416
|
+
*/
|
|
7417
|
+
var CORE_BLOCKS_ADDON_ID = "core-blocks";
|
|
6601
7418
|
/** The addon/runner id a block's process runs under. */
|
|
6602
7419
|
function coreBlockAddonId(blockId) {
|
|
6603
7420
|
return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
|
|
@@ -9728,7 +10545,7 @@ var motionDetectionCapability = {
|
|
|
9728
10545
|
* Why: pub/sub routing over the system event-bus loses fidelity
|
|
9729
10546
|
* (callback shape, QoS guarantees, will/retain semantics) and adds
|
|
9730
10547
|
* refcount bookkeeping that addons would rather own themselves. The
|
|
9731
|
-
* canonical consumer
|
|
10548
|
+
* canonical consumer needs raw `mqtt.js`
|
|
9732
10549
|
* features anyway — give it the connection config, get out of the way.
|
|
9733
10550
|
*
|
|
9734
10551
|
* Consumer flow:
|
|
@@ -12471,8 +13288,26 @@ var TrackSchema = z.object({
|
|
|
12471
13288
|
/** Periodic snapshots at snapshotIntervalMs cadence (subject to
|
|
12472
13289
|
* saveThumbnails policy). */
|
|
12473
13290
|
snapshots: z.array(TrackSnapshotSchema).readonly(),
|
|
12474
|
-
/** Deduplicated zones the track has entered at least once. */
|
|
13291
|
+
/** Deduplicated zones the track has entered at least once. Zone IDS. */
|
|
12475
13292
|
zonesVisited: z.array(z.string()).readonly(),
|
|
13293
|
+
/**
|
|
13294
|
+
* Human NAMES for {@link zonesVisited}, resolved at READ time against the
|
|
13295
|
+
* `zones` capability.
|
|
13296
|
+
*
|
|
13297
|
+
* `zonesVisited` persists ids (`cfeec78c-8d69-…`), which no operator can type
|
|
13298
|
+
* and no card can render — so every free-text search surface was structurally
|
|
13299
|
+
* unable to answer "show me the tracks in Uscio", and did not fail loudly, it
|
|
13300
|
+
* just returned nothing. Resolving here rather than in each client keeps ONE
|
|
13301
|
+
* derivation and costs the clients no extra call (the `zones` cap is
|
|
13302
|
+
* per-device, so a client-side resolve would be a per-camera fan-out on a
|
|
13303
|
+
* surface built to avoid exactly that).
|
|
13304
|
+
*
|
|
13305
|
+
* Resolved, never invented: a zone deleted since the track was written has no
|
|
13306
|
+
* name and is DROPPED, so this array can be shorter than `zonesVisited` — the
|
|
13307
|
+
* two are not positionally aligned. Absent when the track visited no zone, or
|
|
13308
|
+
* when the zone catalogue could not be read.
|
|
13309
|
+
*/
|
|
13310
|
+
zoneNames: z.array(z.string()).readonly().optional(),
|
|
12476
13311
|
/** Deduplicated set of detector classes observed for this track over its
|
|
12477
13312
|
* life (a track may be reclassified, e.g. person→vehicle). Absent on
|
|
12478
13313
|
* legacy rows written before class accumulation shipped. */
|
|
@@ -25752,723 +26587,278 @@ var AccessoryKind = {
|
|
|
25752
26587
|
Spotlight: DeviceRole.Spotlight,
|
|
25753
26588
|
PirSensor: DeviceRole.PirSensor,
|
|
25754
26589
|
Chime: DeviceRole.Chime,
|
|
25755
|
-
Autotrack: DeviceRole.Autotrack,
|
|
25756
|
-
Nightvision: DeviceRole.Nightvision,
|
|
25757
|
-
PrivacyMask: DeviceRole.PrivacyMask
|
|
25758
|
-
};
|
|
25759
|
-
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
25760
|
-
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
*
|
|
25765
|
-
|
|
25766
|
-
|
|
25767
|
-
|
|
25768
|
-
|
|
25769
|
-
|
|
25770
|
-
|
|
25771
|
-
|
|
25772
|
-
|
|
25773
|
-
|
|
25774
|
-
|
|
25775
|
-
|
|
25776
|
-
|
|
25777
|
-
|
|
25778
|
-
|
|
25779
|
-
|
|
25780
|
-
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
25802
|
-
});
|
|
25803
|
-
}
|
|
25804
|
-
return out;
|
|
25805
|
-
}
|
|
25806
|
-
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
25807
|
-
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
25808
|
-
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
25809
|
-
* source), each tagged `item: true` so the authoring UI collects an
|
|
25810
|
-
* `itemKey` alongside the field. Never throws. */
|
|
25811
|
-
function enumerateItemArrayFields(itemArray) {
|
|
25812
|
-
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
25813
|
-
...f,
|
|
25814
|
-
item: true
|
|
25815
|
-
}));
|
|
25816
|
-
}
|
|
25817
|
-
/**
|
|
25818
|
-
* Error types for the safe expression engine. Two distinct classes so callers
|
|
25819
|
-
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
25820
|
-
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
25821
|
-
*/
|
|
25822
|
-
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
25823
|
-
* the failure is anchored to a character (author-facing inline feedback). */
|
|
25824
|
-
var ExpressionParseError = class extends Error {
|
|
25825
|
-
position;
|
|
25826
|
-
constructor(message, position) {
|
|
25827
|
-
super(message);
|
|
25828
|
-
this.name = "ExpressionParseError";
|
|
25829
|
-
this.position = position;
|
|
25830
|
-
}
|
|
25831
|
-
};
|
|
25832
|
-
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
25833
|
-
* result, unknown builtin, step-budget exceeded). */
|
|
25834
|
-
var ExpressionEvalError = class extends Error {
|
|
25835
|
-
constructor(message) {
|
|
25836
|
-
super(message);
|
|
25837
|
-
this.name = "ExpressionEvalError";
|
|
25838
|
-
}
|
|
25839
|
-
};
|
|
25840
|
-
/**
|
|
25841
|
-
* Frozen, null-prototype builtin function table for the expression engine
|
|
25842
|
-
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
25843
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
25844
|
-
* own-property check against it.
|
|
25845
|
-
*
|
|
25846
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
25847
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
25848
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
25849
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
25850
|
-
* callable — they are simply "unknown function" at parse time.
|
|
25851
|
-
*
|
|
25852
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
25853
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
25854
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
25855
|
-
* closed rather than emitting a garbage value.
|
|
25856
|
-
*/
|
|
25857
|
-
function asFiniteNumber(value, name, index) {
|
|
25858
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
25859
|
-
return value;
|
|
25860
|
-
}
|
|
25861
|
-
function asString$1(value, name, index) {
|
|
25862
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
25863
|
-
return value;
|
|
25864
|
-
}
|
|
25865
|
-
function finiteResult(value, name) {
|
|
25866
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
25867
|
-
return value;
|
|
25868
|
-
}
|
|
25869
|
-
function allFiniteNumbers(args, name) {
|
|
25870
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
25871
|
-
}
|
|
25872
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
25873
|
-
var table = {
|
|
25874
|
-
min: {
|
|
25875
|
-
minArgs: 1,
|
|
25876
|
-
maxArgs: INF,
|
|
25877
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
25878
|
-
},
|
|
25879
|
-
max: {
|
|
25880
|
-
minArgs: 1,
|
|
25881
|
-
maxArgs: INF,
|
|
25882
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
25883
|
-
},
|
|
25884
|
-
abs: {
|
|
25885
|
-
minArgs: 1,
|
|
25886
|
-
maxArgs: 1,
|
|
25887
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
25888
|
-
},
|
|
25889
|
-
floor: {
|
|
25890
|
-
minArgs: 1,
|
|
25891
|
-
maxArgs: 1,
|
|
25892
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
25893
|
-
},
|
|
25894
|
-
ceil: {
|
|
25895
|
-
minArgs: 1,
|
|
25896
|
-
maxArgs: 1,
|
|
25897
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
25898
|
-
},
|
|
25899
|
-
sqrt: {
|
|
25900
|
-
minArgs: 1,
|
|
25901
|
-
maxArgs: 1,
|
|
25902
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
25903
|
-
},
|
|
25904
|
-
round: {
|
|
25905
|
-
minArgs: 1,
|
|
25906
|
-
maxArgs: 2,
|
|
25907
|
-
apply: (args) => {
|
|
25908
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
25909
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
25910
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
25911
|
-
const factor = 10 ** digits;
|
|
25912
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
25913
|
-
}
|
|
25914
|
-
},
|
|
25915
|
-
pow: {
|
|
25916
|
-
minArgs: 2,
|
|
25917
|
-
maxArgs: 2,
|
|
25918
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
25919
|
-
},
|
|
25920
|
-
clamp: {
|
|
25921
|
-
minArgs: 3,
|
|
25922
|
-
maxArgs: 3,
|
|
25923
|
-
apply: (args) => {
|
|
25924
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
25925
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
25926
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
25927
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
25928
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
25929
|
-
}
|
|
25930
|
-
},
|
|
25931
|
-
avg: {
|
|
25932
|
-
minArgs: 1,
|
|
25933
|
-
maxArgs: INF,
|
|
25934
|
-
apply: (args) => {
|
|
25935
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
25936
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
25937
|
-
}
|
|
25938
|
-
},
|
|
25939
|
-
sum: {
|
|
25940
|
-
minArgs: 1,
|
|
25941
|
-
maxArgs: INF,
|
|
25942
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
25943
|
-
},
|
|
25944
|
-
coalesce: {
|
|
25945
|
-
minArgs: 1,
|
|
25946
|
-
maxArgs: INF,
|
|
25947
|
-
apply: (args) => {
|
|
25948
|
-
for (const a of args) if (a !== null) return a;
|
|
25949
|
-
return null;
|
|
25950
|
-
}
|
|
25951
|
-
},
|
|
25952
|
-
age: {
|
|
25953
|
-
minArgs: 2,
|
|
25954
|
-
maxArgs: 2,
|
|
25955
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
25956
|
-
},
|
|
25957
|
-
convert: {
|
|
25958
|
-
minArgs: 3,
|
|
25959
|
-
maxArgs: 3,
|
|
25960
|
-
apply: (args, hooks) => {
|
|
25961
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
25962
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
25963
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
25964
|
-
if (hooks.convert) {
|
|
25965
|
-
const out = hooks.convert(x, from, to);
|
|
25966
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
25967
|
-
return finiteResult(out, "convert");
|
|
25968
|
-
}
|
|
25969
|
-
if (from === to) return x;
|
|
25970
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
25971
|
-
}
|
|
25972
|
-
}
|
|
25973
|
-
};
|
|
25974
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
25975
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
25976
|
-
* callees at parse time (immediate author feedback). */
|
|
25977
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
25978
|
-
/**
|
|
25979
|
-
* Resource-bound constants for the safe expression engine.
|
|
25980
|
-
*
|
|
25981
|
-
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
25982
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
25983
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
25984
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
25985
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
25986
|
-
*/
|
|
25987
|
-
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
25988
|
-
* rejected without allocation. */
|
|
25989
|
-
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
25990
|
-
/** A legal binding / identifier name. */
|
|
25991
|
-
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25992
|
-
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
25993
|
-
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
25994
|
-
var RESERVED_BINDING_NAMES = new Set([
|
|
25995
|
-
"now",
|
|
25996
|
-
"true",
|
|
25997
|
-
"false",
|
|
25998
|
-
"null"
|
|
25999
|
-
]);
|
|
26000
|
-
/**
|
|
26001
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
26002
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
26003
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
26004
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
26005
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
26006
|
-
* is a parse error with a source position, so member access / assignment /
|
|
26007
|
-
* template literals are lexically impossible.
|
|
26008
|
-
*/
|
|
26009
|
-
var KEYWORDS = new Set([
|
|
26010
|
-
"true",
|
|
26011
|
-
"false",
|
|
26012
|
-
"null"
|
|
26013
|
-
]);
|
|
26014
|
-
function isDigit(ch) {
|
|
26015
|
-
return ch >= "0" && ch <= "9";
|
|
26016
|
-
}
|
|
26017
|
-
function isIdentStart(ch) {
|
|
26018
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
26019
|
-
}
|
|
26020
|
-
function isIdentPart(ch) {
|
|
26021
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
26022
|
-
}
|
|
26023
|
-
function isWhitespace(ch) {
|
|
26024
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
26025
|
-
}
|
|
26026
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
26027
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
26028
|
-
* string. */
|
|
26029
|
-
function tokenize(source) {
|
|
26030
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
26031
|
-
const tokens = [];
|
|
26032
|
-
let i = 0;
|
|
26033
|
-
const n = source.length;
|
|
26034
|
-
while (i < n) {
|
|
26035
|
-
const ch = source[i];
|
|
26036
|
-
if (isWhitespace(ch)) {
|
|
26037
|
-
i += 1;
|
|
26038
|
-
continue;
|
|
26039
|
-
}
|
|
26040
|
-
if (isDigit(ch)) {
|
|
26041
|
-
const start = i;
|
|
26042
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
26043
|
-
if (i < n && source[i] === ".") {
|
|
26044
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
26045
|
-
i += 1;
|
|
26046
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
26047
|
-
}
|
|
26048
|
-
const text = source.slice(start, i);
|
|
26049
|
-
const value = Number(text);
|
|
26050
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
26051
|
-
tokens.push({
|
|
26052
|
-
type: "number",
|
|
26053
|
-
value,
|
|
26054
|
-
pos: start
|
|
26055
|
-
});
|
|
26056
|
-
continue;
|
|
26057
|
-
}
|
|
26058
|
-
if (ch === "'" || ch === "\"") {
|
|
26059
|
-
const quote = ch;
|
|
26060
|
-
const start = i;
|
|
26061
|
-
i += 1;
|
|
26062
|
-
let out = "";
|
|
26063
|
-
let closed = false;
|
|
26064
|
-
while (i < n) {
|
|
26065
|
-
const c = source[i];
|
|
26066
|
-
if (c === "\\") {
|
|
26067
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
26068
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
26069
|
-
out += next;
|
|
26070
|
-
i += 2;
|
|
26071
|
-
continue;
|
|
26072
|
-
}
|
|
26073
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
26074
|
-
}
|
|
26075
|
-
if (c === quote) {
|
|
26076
|
-
closed = true;
|
|
26077
|
-
i += 1;
|
|
26078
|
-
break;
|
|
26079
|
-
}
|
|
26080
|
-
out += c;
|
|
26081
|
-
i += 1;
|
|
26082
|
-
}
|
|
26083
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
26084
|
-
tokens.push({
|
|
26085
|
-
type: "string",
|
|
26086
|
-
value: out,
|
|
26087
|
-
pos: start
|
|
26088
|
-
});
|
|
26089
|
-
continue;
|
|
26090
|
-
}
|
|
26091
|
-
if (isIdentStart(ch)) {
|
|
26092
|
-
const start = i;
|
|
26093
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
26094
|
-
const text = source.slice(start, i);
|
|
26095
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
26096
|
-
type: "keyword",
|
|
26097
|
-
keyword: keywordOf(text),
|
|
26098
|
-
pos: start
|
|
26099
|
-
});
|
|
26100
|
-
else tokens.push({
|
|
26101
|
-
type: "identifier",
|
|
26102
|
-
name: text,
|
|
26103
|
-
pos: start
|
|
26104
|
-
});
|
|
26105
|
-
continue;
|
|
26106
|
-
}
|
|
26107
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
26108
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
26109
|
-
tokens.push({
|
|
26110
|
-
type: "punct",
|
|
26111
|
-
punct: two,
|
|
26112
|
-
pos: i
|
|
26113
|
-
});
|
|
26114
|
-
i += 2;
|
|
26115
|
-
continue;
|
|
26116
|
-
}
|
|
26117
|
-
if (isSinglePunct(ch)) {
|
|
26118
|
-
tokens.push({
|
|
26119
|
-
type: "punct",
|
|
26120
|
-
punct: ch,
|
|
26121
|
-
pos: i
|
|
26122
|
-
});
|
|
26123
|
-
i += 1;
|
|
26124
|
-
continue;
|
|
26125
|
-
}
|
|
26126
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
26127
|
-
}
|
|
26128
|
-
tokens.push({
|
|
26129
|
-
type: "eof",
|
|
26130
|
-
pos: n
|
|
26131
|
-
});
|
|
26132
|
-
return tokens;
|
|
26133
|
-
}
|
|
26134
|
-
function keywordOf(text) {
|
|
26135
|
-
if (text === "true") return "true";
|
|
26136
|
-
if (text === "false") return "false";
|
|
26137
|
-
return "null";
|
|
26138
|
-
}
|
|
26139
|
-
function isSinglePunct(ch) {
|
|
26140
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
26141
|
-
}
|
|
26142
|
-
/**
|
|
26143
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
26144
|
-
*
|
|
26145
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
26146
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
26147
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
26148
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
26149
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
26150
|
-
* that references a since-removed builtin degrades at read.
|
|
26151
|
-
*
|
|
26152
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
26153
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
26154
|
-
*/
|
|
26155
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
26156
|
-
var BINARY_PRECEDENCE = {
|
|
26157
|
-
"||": 1,
|
|
26158
|
-
"&&": 2,
|
|
26159
|
-
"==": 3,
|
|
26160
|
-
"!=": 3,
|
|
26161
|
-
"<": 4,
|
|
26162
|
-
"<=": 4,
|
|
26163
|
-
">": 4,
|
|
26164
|
-
">=": 4,
|
|
26165
|
-
"+": 5,
|
|
26166
|
-
"-": 5,
|
|
26167
|
-
"*": 6,
|
|
26168
|
-
"/": 6,
|
|
26169
|
-
"%": 6
|
|
26170
|
-
};
|
|
26171
|
-
function isLogicalOp(op) {
|
|
26172
|
-
return op === "&&" || op === "||";
|
|
26173
|
-
}
|
|
26174
|
-
function isBinaryOp(op) {
|
|
26175
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
26176
|
-
}
|
|
26177
|
-
var Parser = class {
|
|
26178
|
-
tokens;
|
|
26179
|
-
pos = 0;
|
|
26180
|
-
nodeCount = 0;
|
|
26181
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
26182
|
-
callees = /* @__PURE__ */ new Set();
|
|
26183
|
-
constructor(tokens) {
|
|
26184
|
-
this.tokens = tokens;
|
|
26185
|
-
}
|
|
26186
|
-
parse() {
|
|
26187
|
-
const ast = this.parseTernary();
|
|
26188
|
-
const tok = this.peek();
|
|
26189
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
26190
|
-
return {
|
|
26191
|
-
ast,
|
|
26192
|
-
identifiers: this.identifiers,
|
|
26193
|
-
callees: this.callees,
|
|
26194
|
-
nodeCount: this.nodeCount
|
|
26195
|
-
};
|
|
26196
|
-
}
|
|
26197
|
-
peek() {
|
|
26198
|
-
return this.tokens[this.pos];
|
|
26199
|
-
}
|
|
26200
|
-
next() {
|
|
26201
|
-
return this.tokens[this.pos++];
|
|
26202
|
-
}
|
|
26203
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
26204
|
-
expectPunct(punct) {
|
|
26205
|
-
const tok = this.peek();
|
|
26206
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
26207
|
-
this.pos += 1;
|
|
26208
|
-
}
|
|
26209
|
-
matchPunct(punct) {
|
|
26210
|
-
const tok = this.peek();
|
|
26211
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
26212
|
-
this.pos += 1;
|
|
26213
|
-
return true;
|
|
26214
|
-
}
|
|
26215
|
-
return false;
|
|
26216
|
-
}
|
|
26217
|
-
countNode() {
|
|
26218
|
-
this.nodeCount += 1;
|
|
26219
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
26220
|
-
}
|
|
26221
|
-
parseTernary() {
|
|
26222
|
-
const test = this.parseBinary(1);
|
|
26223
|
-
if (this.matchPunct("?")) {
|
|
26224
|
-
const consequent = this.parseTernary();
|
|
26225
|
-
this.expectPunct(":");
|
|
26226
|
-
const alternate = this.parseTernary();
|
|
26227
|
-
this.countNode();
|
|
26228
|
-
return {
|
|
26229
|
-
kind: "conditional",
|
|
26230
|
-
test,
|
|
26231
|
-
consequent,
|
|
26232
|
-
alternate
|
|
26233
|
-
};
|
|
26234
|
-
}
|
|
26235
|
-
return test;
|
|
26236
|
-
}
|
|
26237
|
-
parseBinary(minPrec) {
|
|
26238
|
-
let left = this.parseUnary();
|
|
26239
|
-
for (;;) {
|
|
26240
|
-
const tok = this.peek();
|
|
26241
|
-
if (tok.type !== "punct") break;
|
|
26242
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
26243
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
26244
|
-
const op = tok.punct;
|
|
26245
|
-
this.pos += 1;
|
|
26246
|
-
const right = this.parseBinary(prec + 1);
|
|
26247
|
-
this.countNode();
|
|
26248
|
-
if (isLogicalOp(op)) left = {
|
|
26249
|
-
kind: "logical",
|
|
26250
|
-
op,
|
|
26251
|
-
left,
|
|
26252
|
-
right
|
|
26253
|
-
};
|
|
26254
|
-
else if (isBinaryOp(op)) left = {
|
|
26255
|
-
kind: "binary",
|
|
26256
|
-
op,
|
|
26257
|
-
left,
|
|
26258
|
-
right
|
|
26259
|
-
};
|
|
26260
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
26261
|
-
}
|
|
26262
|
-
return left;
|
|
26263
|
-
}
|
|
26264
|
-
parseUnary() {
|
|
26265
|
-
const tok = this.peek();
|
|
26266
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
26267
|
-
const op = tok.punct;
|
|
26268
|
-
this.pos += 1;
|
|
26269
|
-
const operand = this.parseUnary();
|
|
26270
|
-
this.countNode();
|
|
26271
|
-
return {
|
|
26272
|
-
kind: "unary",
|
|
26273
|
-
op,
|
|
26274
|
-
operand
|
|
26275
|
-
};
|
|
26276
|
-
}
|
|
26277
|
-
return this.parsePrimary();
|
|
26278
|
-
}
|
|
26279
|
-
parsePrimary() {
|
|
26280
|
-
const tok = this.next();
|
|
26281
|
-
switch (tok.type) {
|
|
26282
|
-
case "number":
|
|
26283
|
-
this.countNode();
|
|
26284
|
-
return {
|
|
26285
|
-
kind: "literal",
|
|
26286
|
-
value: tok.value
|
|
26287
|
-
};
|
|
26288
|
-
case "string":
|
|
26289
|
-
this.countNode();
|
|
26290
|
-
return {
|
|
26291
|
-
kind: "literal",
|
|
26292
|
-
value: tok.value
|
|
26293
|
-
};
|
|
26294
|
-
case "keyword":
|
|
26295
|
-
this.countNode();
|
|
26296
|
-
return {
|
|
26297
|
-
kind: "literal",
|
|
26298
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
26299
|
-
};
|
|
26300
|
-
case "identifier": {
|
|
26301
|
-
const nextTok = this.peek();
|
|
26302
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
26303
|
-
this.identifiers.add(tok.name);
|
|
26304
|
-
this.countNode();
|
|
26590
|
+
Autotrack: DeviceRole.Autotrack,
|
|
26591
|
+
Nightvision: DeviceRole.Nightvision,
|
|
26592
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
26593
|
+
};
|
|
26594
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
26595
|
+
/** Marker written to a declared integration's `info`. */
|
|
26596
|
+
var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
|
|
26597
|
+
/**
|
|
26598
|
+
* Strip the `<node>/<addon>` suffix a forked child carries.
|
|
26599
|
+
*
|
|
26600
|
+
* Comparing `ctx.kernel.localNodeId` raw skipped EVERY node — including the one
|
|
26601
|
+
* that was supposed to act — because on the hub it reads `hub/<addon>`.
|
|
26602
|
+
*/
|
|
26603
|
+
function declarationOwnerNodeId(localNodeId) {
|
|
26604
|
+
const raw = localNodeId ?? "hub";
|
|
26605
|
+
if (!raw.includes("/")) return raw;
|
|
26606
|
+
return raw.split("/")[0] ?? "hub";
|
|
26607
|
+
}
|
|
26608
|
+
/**
|
|
26609
|
+
* The one way an addon owns a device it declares.
|
|
26610
|
+
*
|
|
26611
|
+
* Construct once with the addon's ports, then call {@link reconcile} on boot and
|
|
26612
|
+
* on every convergence tick. There is no second get-or-create helper — a guard
|
|
26613
|
+
* in `scripts/` enforces that.
|
|
26614
|
+
*/
|
|
26615
|
+
var DeclaredDevices = class {
|
|
26616
|
+
ports;
|
|
26617
|
+
constructor(ports) {
|
|
26618
|
+
const addonId = ports.addonId;
|
|
26619
|
+
if (typeof addonId !== "string" || addonId.length === 0) throw new Error(`DeclaredDevices: addonId must be the declaring addon's id, got ${JSON.stringify(addonId)}. On an addon context it is \`ctx.id\` — there is no \`ctx.addonId\`.`);
|
|
26620
|
+
this.ports = ports;
|
|
26621
|
+
}
|
|
26622
|
+
/**
|
|
26623
|
+
* Converge the declared set. Idempotent, and safe to call repeatedly.
|
|
26624
|
+
*
|
|
26625
|
+
* Throws only what the ports throw on the FIRST index read; every other
|
|
26626
|
+
* failure is per-device and logged, so one bad declaration never takes the
|
|
26627
|
+
* others down.
|
|
26628
|
+
*/
|
|
26629
|
+
async reconcile(spec) {
|
|
26630
|
+
if ((spec.placement ?? "hub") === "hub") {
|
|
26631
|
+
const nodeId = declarationOwnerNodeId(this.ports.localNodeId);
|
|
26632
|
+
if (nodeId !== "hub") {
|
|
26633
|
+
this.ports.logger.info("declared devices are hub-owned — skipping on this node", { meta: {
|
|
26634
|
+
nodeId,
|
|
26635
|
+
rawNodeId: this.ports.localNodeId ?? null
|
|
26636
|
+
} });
|
|
26305
26637
|
return {
|
|
26306
|
-
|
|
26307
|
-
|
|
26638
|
+
integrationId: null,
|
|
26639
|
+
devices: [],
|
|
26640
|
+
removed: [],
|
|
26641
|
+
owned: false
|
|
26308
26642
|
};
|
|
26309
26643
|
}
|
|
26310
|
-
case "punct":
|
|
26311
|
-
if (tok.punct === "(") {
|
|
26312
|
-
const inner = this.parseTernary();
|
|
26313
|
-
this.expectPunct(")");
|
|
26314
|
-
return inner;
|
|
26315
|
-
}
|
|
26316
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
26317
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
26318
26644
|
}
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
|
|
26323
|
-
|
|
26324
|
-
|
|
26325
|
-
args.push(this.parseTernary());
|
|
26326
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
26327
|
-
if (this.matchPunct(",")) continue;
|
|
26328
|
-
this.expectPunct(")");
|
|
26329
|
-
break;
|
|
26645
|
+
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
26646
|
+
const index = await this.readIndex();
|
|
26647
|
+
const outcomes = [];
|
|
26648
|
+
for (const declaration of spec.devices) {
|
|
26649
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
26650
|
+
if (outcome !== null) outcomes.push(outcome);
|
|
26330
26651
|
}
|
|
26331
|
-
this.callees.add(callee);
|
|
26332
|
-
this.countNode();
|
|
26333
26652
|
return {
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26653
|
+
integrationId,
|
|
26654
|
+
devices: outcomes,
|
|
26655
|
+
removed: await this.sweepWithdrawn(spec.devices, integrationId, index),
|
|
26656
|
+
owned: true
|
|
26337
26657
|
};
|
|
26338
26658
|
}
|
|
26339
|
-
|
|
26340
|
-
|
|
26341
|
-
*
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26359
|
-
|
|
26360
|
-
|
|
26361
|
-
|
|
26659
|
+
/**
|
|
26660
|
+
* Get-or-create the FIXED integration, and RE-ASSERT the flag every pass.
|
|
26661
|
+
*
|
|
26662
|
+
* The re-assertion is the fix for the defect the hand-rolled version shipped
|
|
26663
|
+
* with: writing `info.fixed` only on the create path left every pre-existing
|
|
26664
|
+
* install without it, and the kernel kept offering to delete an integration
|
|
26665
|
+
* the addon owns.
|
|
26666
|
+
*/
|
|
26667
|
+
async ensureIntegration(integrationName) {
|
|
26668
|
+
const existing = await this.ports.getIntegration(this.ports.addonId);
|
|
26669
|
+
if (existing === null) {
|
|
26670
|
+
const created = await this.ports.createIntegration({
|
|
26671
|
+
addonId: this.ports.addonId,
|
|
26672
|
+
name: integrationName,
|
|
26673
|
+
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
26674
|
+
});
|
|
26675
|
+
this.ports.logger.info("declared a fixed integration", { meta: {
|
|
26676
|
+
integrationId: created.id,
|
|
26677
|
+
name: integrationName
|
|
26678
|
+
} });
|
|
26679
|
+
return created.id;
|
|
26680
|
+
}
|
|
26681
|
+
if (existing.info?.["fixed"] !== true) {
|
|
26682
|
+
await this.ports.updateIntegration({
|
|
26683
|
+
id: existing.id,
|
|
26684
|
+
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
26685
|
+
});
|
|
26686
|
+
this.ports.logger.info("re-asserted `fixed` on a declared integration", { meta: { integrationId: existing.id } });
|
|
26687
|
+
}
|
|
26688
|
+
return existing.id;
|
|
26362
26689
|
}
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
|
|
26366
|
-
ok: true,
|
|
26367
|
-
parsed: parseExpression(source)
|
|
26368
|
-
};
|
|
26369
|
-
} catch (err) {
|
|
26370
|
-
result = {
|
|
26371
|
-
ok: false,
|
|
26372
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
26373
|
-
};
|
|
26690
|
+
async readIndex() {
|
|
26691
|
+
const rows = await this.ports.listOwnDevices();
|
|
26692
|
+
return new Map(rows.map((row) => [row.stableId, row]));
|
|
26374
26693
|
}
|
|
26375
|
-
|
|
26376
|
-
|
|
26377
|
-
|
|
26378
|
-
|
|
26694
|
+
/**
|
|
26695
|
+
* One declaration: adopt what exists, create what does not.
|
|
26696
|
+
*
|
|
26697
|
+
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
26698
|
+
* `initialMeta.name` lands as an unconditional `setName`. A transiently empty
|
|
26699
|
+
* index therefore looks exactly like a first boot and would silently re-stamp
|
|
26700
|
+
* the declared name over the operator's rename. D49: that branch needs a
|
|
26701
|
+
* second read to agree.
|
|
26702
|
+
*/
|
|
26703
|
+
async applyDeclaration(declaration, integrationId, index) {
|
|
26704
|
+
try {
|
|
26705
|
+
let existing = index.get(declaration.stableId);
|
|
26706
|
+
if (existing === void 0) {
|
|
26707
|
+
existing = (await this.readIndex()).get(declaration.stableId);
|
|
26708
|
+
if (existing !== void 0) this.ports.logger.warn("device index disagreed with itself — adopting instead of re-creating", {
|
|
26709
|
+
tags: { deviceId: existing.id },
|
|
26710
|
+
meta: {
|
|
26711
|
+
stableId: declaration.stableId,
|
|
26712
|
+
addonId: this.ports.addonId
|
|
26713
|
+
}
|
|
26714
|
+
});
|
|
26715
|
+
}
|
|
26716
|
+
if (existing !== void 0) {
|
|
26717
|
+
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, {}, null, void 0);
|
|
26718
|
+
this.ports.logger.info("declared device adopted", {
|
|
26719
|
+
tags: { deviceId: device.id },
|
|
26720
|
+
meta: {
|
|
26721
|
+
stableId: declaration.stableId,
|
|
26722
|
+
integrationId
|
|
26723
|
+
}
|
|
26724
|
+
});
|
|
26725
|
+
return {
|
|
26726
|
+
stableId: declaration.stableId,
|
|
26727
|
+
deviceId: device.id,
|
|
26728
|
+
device,
|
|
26729
|
+
created: false
|
|
26730
|
+
};
|
|
26731
|
+
}
|
|
26732
|
+
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, declaration.config ?? {}, null, {
|
|
26733
|
+
type: declaration.type,
|
|
26734
|
+
name: declaration.name,
|
|
26735
|
+
integrationId,
|
|
26736
|
+
...declaration.role === void 0 ? {} : { role: declaration.role }
|
|
26737
|
+
});
|
|
26738
|
+
this.ports.logger.info("declared device created", {
|
|
26739
|
+
tags: { deviceId: device.id },
|
|
26740
|
+
meta: {
|
|
26741
|
+
stableId: declaration.stableId,
|
|
26742
|
+
integrationId
|
|
26743
|
+
}
|
|
26744
|
+
});
|
|
26745
|
+
return {
|
|
26746
|
+
stableId: declaration.stableId,
|
|
26747
|
+
deviceId: device.id,
|
|
26748
|
+
device,
|
|
26749
|
+
created: true
|
|
26750
|
+
};
|
|
26751
|
+
} catch (err) {
|
|
26752
|
+
this.ports.logger.warn("a declared device could not be brought up", { meta: {
|
|
26753
|
+
stableId: declaration.stableId,
|
|
26754
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26755
|
+
} });
|
|
26756
|
+
return null;
|
|
26757
|
+
}
|
|
26379
26758
|
}
|
|
26380
|
-
|
|
26759
|
+
/**
|
|
26760
|
+
* Remove rows under the addon's FIXED integration whose declaration is gone.
|
|
26761
|
+
*
|
|
26762
|
+
* Bounded to that integration: a declared integration has no operator
|
|
26763
|
+
* add-flow, so every row under it got there by declaration. Devices this
|
|
26764
|
+
* addon owns OUTSIDE it (a provider's adopted devices) are never candidates.
|
|
26765
|
+
*
|
|
26766
|
+
* Bounded in count, and every deletion is logged with its `deviceId` — a
|
|
26767
|
+
* withdrawal that removes an operator-visible row silently is the failure
|
|
26768
|
+
* mode, not the removal itself.
|
|
26769
|
+
*/
|
|
26770
|
+
async sweepWithdrawn(declarations, integrationId, index) {
|
|
26771
|
+
const declared = new Set(declarations.map((d) => d.stableId));
|
|
26772
|
+
const candidates = [...index.values()].filter((row) => row.integrationId === integrationId && !declared.has(row.stableId));
|
|
26773
|
+
if (candidates.length === 0) return [];
|
|
26774
|
+
if (candidates.length > 32) {
|
|
26775
|
+
this.ports.logger.warn("withdrawal sweep exceeded its bound — removing nothing", { meta: {
|
|
26776
|
+
integrationId,
|
|
26777
|
+
candidates: candidates.length,
|
|
26778
|
+
bound: 32
|
|
26779
|
+
} });
|
|
26780
|
+
return [];
|
|
26781
|
+
}
|
|
26782
|
+
const removed = [];
|
|
26783
|
+
for (const row of candidates) try {
|
|
26784
|
+
await this.ports.devices.remove(row.id);
|
|
26785
|
+
removed.push(row.id);
|
|
26786
|
+
this.ports.logger.info("declared device removed — its declaration was withdrawn", {
|
|
26787
|
+
tags: { deviceId: row.id },
|
|
26788
|
+
meta: {
|
|
26789
|
+
stableId: row.stableId,
|
|
26790
|
+
integrationId
|
|
26791
|
+
}
|
|
26792
|
+
});
|
|
26793
|
+
} catch (err) {
|
|
26794
|
+
this.ports.logger.warn("a withdrawn declared device could not be removed", {
|
|
26795
|
+
tags: { deviceId: row.id },
|
|
26796
|
+
meta: {
|
|
26797
|
+
stableId: row.stableId,
|
|
26798
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26799
|
+
}
|
|
26800
|
+
});
|
|
26801
|
+
}
|
|
26802
|
+
return removed;
|
|
26803
|
+
}
|
|
26804
|
+
};
|
|
26805
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
26806
|
+
new Set(Object.values(DeviceType));
|
|
26807
|
+
DeviceFeature.BatteryOperated;
|
|
26808
|
+
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
26809
|
+
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
26810
|
+
function unwrap(schema) {
|
|
26811
|
+
let s = schema;
|
|
26812
|
+
while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
|
|
26813
|
+
return s;
|
|
26381
26814
|
}
|
|
26382
|
-
|
|
26383
|
-
|
|
26384
|
-
|
|
26385
|
-
|
|
26815
|
+
function leafKind(schema) {
|
|
26816
|
+
const s = unwrap(schema);
|
|
26817
|
+
if (s instanceof z.ZodEnum) return {
|
|
26818
|
+
kind: "enum",
|
|
26819
|
+
enumValues: s.options
|
|
26820
|
+
};
|
|
26821
|
+
if (s instanceof z.ZodNumber) return { kind: "number" };
|
|
26822
|
+
if (s instanceof z.ZodString) return { kind: "string" };
|
|
26823
|
+
if (s instanceof z.ZodBoolean) return { kind: "boolean" };
|
|
26824
|
+
return null;
|
|
26386
26825
|
}
|
|
26387
|
-
|
|
26388
|
-
|
|
26389
|
-
*
|
|
26390
|
-
*
|
|
26391
|
-
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26395
|
-
|
|
26396
|
-
const
|
|
26397
|
-
|
|
26398
|
-
|
|
26399
|
-
if (
|
|
26400
|
-
|
|
26401
|
-
|
|
26402
|
-
|
|
26403
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
|
|
26408
|
-
|
|
26826
|
+
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
26827
|
+
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
26828
|
+
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
26829
|
+
* are skipped — never throws. */
|
|
26830
|
+
function enumerateSchemaFields(schema, prefix = "") {
|
|
26831
|
+
const root = unwrap(schema);
|
|
26832
|
+
if (!(root instanceof z.ZodObject)) return [];
|
|
26833
|
+
const out = [];
|
|
26834
|
+
const shape = root.shape;
|
|
26835
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
26836
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
26837
|
+
const inner = unwrap(field);
|
|
26838
|
+
if (inner instanceof z.ZodObject) {
|
|
26839
|
+
out.push(...enumerateSchemaFields(inner, path));
|
|
26840
|
+
continue;
|
|
26841
|
+
}
|
|
26842
|
+
const leaf = leafKind(field);
|
|
26843
|
+
if (leaf) out.push({
|
|
26844
|
+
path,
|
|
26845
|
+
kind: leaf.kind,
|
|
26846
|
+
...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
|
|
26847
|
+
});
|
|
26409
26848
|
}
|
|
26410
|
-
return
|
|
26849
|
+
return out;
|
|
26850
|
+
}
|
|
26851
|
+
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
26852
|
+
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
26853
|
+
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
26854
|
+
* source), each tagged `item: true` so the authoring UI collects an
|
|
26855
|
+
* `itemKey` alongside the field. Never throws. */
|
|
26856
|
+
function enumerateItemArrayFields(itemArray) {
|
|
26857
|
+
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
26858
|
+
...f,
|
|
26859
|
+
item: true
|
|
26860
|
+
}));
|
|
26411
26861
|
}
|
|
26412
|
-
/**
|
|
26413
|
-
* What an expression's named bindings READ from.
|
|
26414
|
-
*
|
|
26415
|
-
* Salvaged verbatim from the deleted device-link mechanism. Wiring's source
|
|
26416
|
-
* kinds were the one part of it worth keeping: addressing a device field by
|
|
26417
|
-
* re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
|
|
26418
|
-
* read are the vocabulary any cross-device derivation needs, and they were
|
|
26419
|
-
* already correct. What wiring got wrong was the DESTINATION — a field on
|
|
26420
|
-
* somebody else's device, with no identity — not the source.
|
|
26421
|
-
*
|
|
26422
|
-
* These shapes are therefore kept, re-homed next to the engine that consumes
|
|
26423
|
-
* them, and are the binding type of a composition recipe (the source picker
|
|
26424
|
-
* stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
|
|
26425
|
-
* binding is a read, never another expression.
|
|
26426
|
-
*
|
|
26427
|
-
* Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
|
|
26428
|
-
* a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
|
|
26429
|
-
*/
|
|
26430
|
-
/** Read a sibling accessory's status field, addressed by the sibling's key.
|
|
26431
|
-
* `kind` is optional for wire compatibility — absent means `'field'`. */
|
|
26432
|
-
var ExpressionFieldBindingSchema = z.object({
|
|
26433
|
-
kind: z.literal("field").optional(),
|
|
26434
|
-
sourceKey: z.string(),
|
|
26435
|
-
cap: z.string(),
|
|
26436
|
-
fieldPath: z.string()
|
|
26437
|
-
});
|
|
26438
|
-
/** A constant. No device is read. */
|
|
26439
|
-
var ExpressionLiteralBindingSchema = z.object({
|
|
26440
|
-
kind: z.literal("literal"),
|
|
26441
|
-
value: z.union([
|
|
26442
|
-
z.string(),
|
|
26443
|
-
z.number(),
|
|
26444
|
-
z.boolean(),
|
|
26445
|
-
z.null()
|
|
26446
|
-
])
|
|
26447
|
-
});
|
|
26448
|
-
/** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
|
|
26449
|
-
* never by numeric id, which a re-adoption reissues. */
|
|
26450
|
-
var ExpressionGlobalBindingSchema = z.object({
|
|
26451
|
-
kind: z.literal("global"),
|
|
26452
|
-
sourceStableId: z.string(),
|
|
26453
|
-
cap: z.string(),
|
|
26454
|
-
fieldPath: z.string()
|
|
26455
|
-
});
|
|
26456
|
-
var ExpressionBindingSourceSchema = z.union([
|
|
26457
|
-
ExpressionFieldBindingSchema,
|
|
26458
|
-
ExpressionLiteralBindingSchema,
|
|
26459
|
-
ExpressionGlobalBindingSchema
|
|
26460
|
-
]);
|
|
26461
|
-
z.object({
|
|
26462
|
-
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
26463
|
-
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
26464
|
-
}).superRefine((src, ctx) => {
|
|
26465
|
-
const err = validateExpressionSource(src);
|
|
26466
|
-
if (err !== null) ctx.addIssue({
|
|
26467
|
-
code: "custom",
|
|
26468
|
-
message: err,
|
|
26469
|
-
path: ["expr"]
|
|
26470
|
-
});
|
|
26471
|
-
});
|
|
26472
26862
|
/**
|
|
26473
26863
|
* Runtime list of cap names with status. Used by the settings
|
|
26474
26864
|
* aggregator to enumerate caps whose `status` should be polled +
|
|
@@ -32477,4 +32867,4 @@ function enumerateInferenceDevices(hw) {
|
|
|
32477
32867
|
return out;
|
|
32478
32868
|
}
|
|
32479
32869
|
//#endregion
|
|
32480
|
-
export {
|
|
32870
|
+
export { objectInputDeclaresAddonId as $, deviceManagerCapability as A, hydrateSchema as At, isCollectionArrayMethod as B, bareAddonId as C, asJsonObject as Ct, coreBlocksCapability as D, emitDownForOwnedCaps as Dt, coreBlockIdFromAddonId as E, createEvent as Et, enumerateItemArrayFields as F, readinessKey as Ft, lifecycleJobSchema as G, isSameAddonId as H, enumerateSchemaFields as I, resolveCapMount as It, logLevelAtMost as J, localNetworkCapability as K, extractNestedAddonId as L, scopeKey as Lt, deviceStatusCapability as M, nodePin as Mt, doorbellCapability as N, parseJsonObject as Nt, dataStoreProviderCapability as O, emitReadiness as Ot, enumerateInferenceDevices as P, parseJsonUnknown as Pt, oauthIntegrationCapability as Q, filesystemBrowseCapability as R, sleep as Rt, backupCapability as S, WELL_KNOWN_TAB_MAP as St, coreBlockAddonId as T, asString as Tt, isVoidInput as U, isObjectInput as V, kebabToCamel as W, metricsProviderCapability as X, looseSchema as Y, normalizeUnit as Z, addonPagesCapability as _, DeviceFeature as _t, CAP_NAMES_WITH_STATUS as a, snapshotCapability as at, alertsCapability as b, ReadinessRegistry as bt, CoreBlockSchema as c, streamQualityLabel as ct, METHOD_ACCESS_MAP as d, vectorStoreCapability as dt, parseStreamParamsFormPatch as et, RUNTIME_DEFAULTS as f, errMsg as ft, UserRecordSchema as g, DEVICE_STATUS_METHOD as gt, StorageLocationTypeSchema as h, DEVICE_SETTINGS_CONTRIBUTION_METHODS as ht, BatteryStatusSchema as i, settingsStoreCapability as it, deviceStateCapability as j, isDeviceConfigCap as jt, decodeVectorBase64 as k, expandCapMethods as kt, DeclaredDevices as l, userManagementCapability as lt, ScopedTokenSchema as m, DATAPLANE_SECRET_HEADER as mt, AlertSchema as n, procedureAuthKey as nt, CORE_BLOCKS_ADDON_ID as o, storageCapability as ot, STREAM_PROFILE_META as p, BaseAddon as pt, logDestinationCapability as q, ApiKeyRecordSchema as r, scoreRuntimes as rt, CORE_BLOCK_ADDON_PREFIX as s, storageProviderCapability as st, ALL_CAPABILITY_DEFINITIONS as t, platformProbeCapability as tt, DeviceStatusSchema as u, vectorDimFromBase64 as ut, addonSettingsCapability as v, DeviceRole as vt, buildStreamParamsConfigSchema as w, asNumber as wt, authProviderCapability as x, ReadinessTimeoutError as xt, addonWidgetsCapability as y, DeviceType as yt, isArrayOutputSchema as z, EventCategory as zt };
|