@camstack/addon-post-analysis 1.2.54 → 1.2.56
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/{dist-BVOgkikr.mjs → dist-CAC3R7XW.mjs} +4519 -4281
- package/dist/{dist-BJ65Xxa1.js → dist-CcS9NwDl.js} +1422 -1169
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +12 -2
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-uW2bbQBi.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ABX4r2Sy.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DcqnDg2z.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-qqYR0iVH.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BC7FXncY.mjs → hostInit-5_dMMZLO.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +390 -18
- package/dist/pipeline-analytics/index.mjs +391 -18
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/node-Co0YpEFr.mjs +0 -148
- package/dist/node-DTrkngzv.js +0 -151
|
@@ -5,6 +5,15 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __exportAll = (all, no_symbols) => {
|
|
9
|
+
let target = {};
|
|
10
|
+
for (var name in all) __defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true
|
|
13
|
+
});
|
|
14
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
return target;
|
|
16
|
+
};
|
|
8
17
|
var __copyProps = (to, from, except, desc) => {
|
|
9
18
|
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
19
|
key = keys[i];
|
|
@@ -8145,372 +8154,1146 @@ var ConvertResultSchema = object({
|
|
|
8145
8154
|
})).readonly()
|
|
8146
8155
|
});
|
|
8147
8156
|
/**
|
|
8148
|
-
*
|
|
8149
|
-
*
|
|
8157
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
8158
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
8159
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
8160
|
+
*/
|
|
8161
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
8162
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
8163
|
+
var ExpressionParseError = class extends Error {
|
|
8164
|
+
position;
|
|
8165
|
+
constructor(message, position) {
|
|
8166
|
+
super(message);
|
|
8167
|
+
this.name = "ExpressionParseError";
|
|
8168
|
+
this.position = position;
|
|
8169
|
+
}
|
|
8170
|
+
};
|
|
8171
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
8172
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
8173
|
+
var ExpressionEvalError = class extends Error {
|
|
8174
|
+
constructor(message) {
|
|
8175
|
+
super(message);
|
|
8176
|
+
this.name = "ExpressionEvalError";
|
|
8177
|
+
}
|
|
8178
|
+
};
|
|
8179
|
+
/**
|
|
8180
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
8181
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
8182
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
8183
|
+
* own-property check against it.
|
|
8150
8184
|
*
|
|
8151
|
-
*
|
|
8152
|
-
*
|
|
8153
|
-
*
|
|
8154
|
-
*
|
|
8155
|
-
*
|
|
8185
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
8186
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
8187
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
8188
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
8189
|
+
* callable — they are simply "unknown function" at parse time.
|
|
8156
8190
|
*
|
|
8157
|
-
*
|
|
8158
|
-
*
|
|
8159
|
-
*
|
|
8160
|
-
*
|
|
8161
|
-
* `addon-pages.router.ts` wrapper anymore.
|
|
8191
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
8192
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
8193
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
8194
|
+
* closed rather than emitting a garbage value.
|
|
8162
8195
|
*/
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
});
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
}
|
|
8178
|
-
|
|
8196
|
+
function asFiniteNumber(value, name, index) {
|
|
8197
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
8198
|
+
return value;
|
|
8199
|
+
}
|
|
8200
|
+
function asString$1(value, name, index) {
|
|
8201
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
8202
|
+
return value;
|
|
8203
|
+
}
|
|
8204
|
+
function finiteResult(value, name) {
|
|
8205
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
8206
|
+
return value;
|
|
8207
|
+
}
|
|
8208
|
+
function allFiniteNumbers(args, name) {
|
|
8209
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
8210
|
+
}
|
|
8211
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
8212
|
+
var table = {
|
|
8213
|
+
min: {
|
|
8214
|
+
minArgs: 1,
|
|
8215
|
+
maxArgs: INF,
|
|
8216
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
8217
|
+
},
|
|
8218
|
+
max: {
|
|
8219
|
+
minArgs: 1,
|
|
8220
|
+
maxArgs: INF,
|
|
8221
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
8222
|
+
},
|
|
8223
|
+
abs: {
|
|
8224
|
+
minArgs: 1,
|
|
8225
|
+
maxArgs: 1,
|
|
8226
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
8227
|
+
},
|
|
8228
|
+
floor: {
|
|
8229
|
+
minArgs: 1,
|
|
8230
|
+
maxArgs: 1,
|
|
8231
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
8232
|
+
},
|
|
8233
|
+
ceil: {
|
|
8234
|
+
minArgs: 1,
|
|
8235
|
+
maxArgs: 1,
|
|
8236
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
8237
|
+
},
|
|
8238
|
+
sqrt: {
|
|
8239
|
+
minArgs: 1,
|
|
8240
|
+
maxArgs: 1,
|
|
8241
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
8242
|
+
},
|
|
8243
|
+
round: {
|
|
8244
|
+
minArgs: 1,
|
|
8245
|
+
maxArgs: 2,
|
|
8246
|
+
apply: (args) => {
|
|
8247
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
8248
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
8249
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
8250
|
+
const factor = 10 ** digits;
|
|
8251
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
8252
|
+
}
|
|
8253
|
+
},
|
|
8254
|
+
pow: {
|
|
8255
|
+
minArgs: 2,
|
|
8256
|
+
maxArgs: 2,
|
|
8257
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
8258
|
+
},
|
|
8259
|
+
clamp: {
|
|
8260
|
+
minArgs: 3,
|
|
8261
|
+
maxArgs: 3,
|
|
8262
|
+
apply: (args) => {
|
|
8263
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
8264
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
8265
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
8266
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
8267
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
8268
|
+
}
|
|
8269
|
+
},
|
|
8270
|
+
avg: {
|
|
8271
|
+
minArgs: 1,
|
|
8272
|
+
maxArgs: INF,
|
|
8273
|
+
apply: (args) => {
|
|
8274
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
8275
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
8276
|
+
}
|
|
8277
|
+
},
|
|
8278
|
+
sum: {
|
|
8279
|
+
minArgs: 1,
|
|
8280
|
+
maxArgs: INF,
|
|
8281
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
8282
|
+
},
|
|
8283
|
+
coalesce: {
|
|
8284
|
+
minArgs: 1,
|
|
8285
|
+
maxArgs: INF,
|
|
8286
|
+
apply: (args) => {
|
|
8287
|
+
for (const a of args) if (a !== null) return a;
|
|
8288
|
+
return null;
|
|
8289
|
+
}
|
|
8290
|
+
},
|
|
8291
|
+
age: {
|
|
8292
|
+
minArgs: 2,
|
|
8293
|
+
maxArgs: 2,
|
|
8294
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
8295
|
+
},
|
|
8296
|
+
convert: {
|
|
8297
|
+
minArgs: 3,
|
|
8298
|
+
maxArgs: 3,
|
|
8299
|
+
apply: (args, hooks) => {
|
|
8300
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
8301
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
8302
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
8303
|
+
if (hooks.convert) {
|
|
8304
|
+
const out = hooks.convert(x, from, to);
|
|
8305
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
8306
|
+
return finiteResult(out, "convert");
|
|
8307
|
+
}
|
|
8308
|
+
if (from === to) return x;
|
|
8309
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
};
|
|
8313
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
8314
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
8315
|
+
* callees at parse time (immediate author feedback). */
|
|
8316
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
8179
8317
|
/**
|
|
8180
|
-
*
|
|
8181
|
-
* declarations. Every addon that contributes a UI page registers a
|
|
8182
|
-
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
8183
|
-
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
8184
|
-
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
8185
|
-
* that admin-ui consumes.
|
|
8318
|
+
* Resource-bound constants for the safe expression engine.
|
|
8186
8319
|
*
|
|
8187
|
-
*
|
|
8188
|
-
*
|
|
8189
|
-
*
|
|
8320
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
8321
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
8322
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
8323
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
8324
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
8190
8325
|
*/
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
/**
|
|
8205
|
-
* Bundle filename inside the addon's `dist/` dir served at
|
|
8206
|
-
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
8207
|
-
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
8208
|
-
* the static-file route can compute an mtime-based cache-buster URL
|
|
8209
|
-
* without a separate filesystem stat.
|
|
8210
|
-
*/
|
|
8211
|
-
bundle: string(),
|
|
8212
|
-
/**
|
|
8213
|
-
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
8214
|
-
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
8215
|
-
* Any OTHER string creates (or joins) a custom section rendered after
|
|
8216
|
-
* the built-in groups; its label comes from `sectionLabel` (first
|
|
8217
|
-
* declaration wins), falling back to the id. Absent → the legacy
|
|
8218
|
-
* "Addon Pages" group.
|
|
8219
|
-
*/
|
|
8220
|
-
section: string().optional(),
|
|
8221
|
-
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
8222
|
-
sectionLabel: string().optional()
|
|
8223
|
-
});
|
|
8224
|
-
method(_void(), array(AddonPageDeclarationSchema).readonly());
|
|
8225
|
-
var AddonHttpRouteSchema = object({
|
|
8226
|
-
method: _enum([
|
|
8227
|
-
"GET",
|
|
8228
|
-
"POST",
|
|
8229
|
-
"PUT",
|
|
8230
|
-
"DELETE",
|
|
8231
|
-
"PATCH"
|
|
8232
|
-
]),
|
|
8233
|
-
path: string(),
|
|
8234
|
-
access: _enum([
|
|
8235
|
-
"public",
|
|
8236
|
-
"authenticated",
|
|
8237
|
-
"admin"
|
|
8238
|
-
]).optional(),
|
|
8239
|
-
description: string().optional()
|
|
8240
|
-
});
|
|
8326
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
8327
|
+
* rejected without allocation. */
|
|
8328
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
8329
|
+
/** A legal binding / identifier name. */
|
|
8330
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8331
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
8332
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
8333
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
8334
|
+
"now",
|
|
8335
|
+
"true",
|
|
8336
|
+
"false",
|
|
8337
|
+
"null"
|
|
8338
|
+
]);
|
|
8241
8339
|
/**
|
|
8242
|
-
*
|
|
8243
|
-
*
|
|
8244
|
-
*
|
|
8245
|
-
*
|
|
8246
|
-
*
|
|
8247
|
-
*
|
|
8340
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
8341
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
8342
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
8343
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
8344
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
8345
|
+
* is a parse error with a source position, so member access / assignment /
|
|
8346
|
+
* template literals are lexically impossible.
|
|
8248
8347
|
*/
|
|
8249
|
-
var
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
}
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
}
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
}
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
})
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8348
|
+
var KEYWORDS = new Set([
|
|
8349
|
+
"true",
|
|
8350
|
+
"false",
|
|
8351
|
+
"null"
|
|
8352
|
+
]);
|
|
8353
|
+
function isDigit(ch) {
|
|
8354
|
+
return ch >= "0" && ch <= "9";
|
|
8355
|
+
}
|
|
8356
|
+
function isIdentStart(ch) {
|
|
8357
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
8358
|
+
}
|
|
8359
|
+
function isIdentPart(ch) {
|
|
8360
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
8361
|
+
}
|
|
8362
|
+
function isWhitespace(ch) {
|
|
8363
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
8364
|
+
}
|
|
8365
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
8366
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
8367
|
+
* string. */
|
|
8368
|
+
function tokenize(source) {
|
|
8369
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
8370
|
+
const tokens = [];
|
|
8371
|
+
let i = 0;
|
|
8372
|
+
const n = source.length;
|
|
8373
|
+
while (i < n) {
|
|
8374
|
+
const ch = source[i];
|
|
8375
|
+
if (isWhitespace(ch)) {
|
|
8376
|
+
i += 1;
|
|
8377
|
+
continue;
|
|
8378
|
+
}
|
|
8379
|
+
if (isDigit(ch)) {
|
|
8380
|
+
const start = i;
|
|
8381
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
8382
|
+
if (i < n && source[i] === ".") {
|
|
8383
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
8384
|
+
i += 1;
|
|
8385
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
8386
|
+
}
|
|
8387
|
+
const text = source.slice(start, i);
|
|
8388
|
+
const value = Number(text);
|
|
8389
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
8390
|
+
tokens.push({
|
|
8391
|
+
type: "number",
|
|
8392
|
+
value,
|
|
8393
|
+
pos: start
|
|
8394
|
+
});
|
|
8395
|
+
continue;
|
|
8396
|
+
}
|
|
8397
|
+
if (ch === "'" || ch === "\"") {
|
|
8398
|
+
const quote = ch;
|
|
8399
|
+
const start = i;
|
|
8400
|
+
i += 1;
|
|
8401
|
+
let out = "";
|
|
8402
|
+
let closed = false;
|
|
8403
|
+
while (i < n) {
|
|
8404
|
+
const c = source[i];
|
|
8405
|
+
if (c === "\\") {
|
|
8406
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
8407
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
8408
|
+
out += next;
|
|
8409
|
+
i += 2;
|
|
8410
|
+
continue;
|
|
8411
|
+
}
|
|
8412
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
8413
|
+
}
|
|
8414
|
+
if (c === quote) {
|
|
8415
|
+
closed = true;
|
|
8416
|
+
i += 1;
|
|
8417
|
+
break;
|
|
8418
|
+
}
|
|
8419
|
+
out += c;
|
|
8420
|
+
i += 1;
|
|
8421
|
+
}
|
|
8422
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
8423
|
+
tokens.push({
|
|
8424
|
+
type: "string",
|
|
8425
|
+
value: out,
|
|
8426
|
+
pos: start
|
|
8427
|
+
});
|
|
8428
|
+
continue;
|
|
8429
|
+
}
|
|
8430
|
+
if (isIdentStart(ch)) {
|
|
8431
|
+
const start = i;
|
|
8432
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
8433
|
+
const text = source.slice(start, i);
|
|
8434
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
8435
|
+
type: "keyword",
|
|
8436
|
+
keyword: keywordOf(text),
|
|
8437
|
+
pos: start
|
|
8438
|
+
});
|
|
8439
|
+
else tokens.push({
|
|
8440
|
+
type: "identifier",
|
|
8441
|
+
name: text,
|
|
8442
|
+
pos: start
|
|
8443
|
+
});
|
|
8444
|
+
continue;
|
|
8445
|
+
}
|
|
8446
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
8447
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
8448
|
+
tokens.push({
|
|
8449
|
+
type: "punct",
|
|
8450
|
+
punct: two,
|
|
8451
|
+
pos: i
|
|
8452
|
+
});
|
|
8453
|
+
i += 2;
|
|
8454
|
+
continue;
|
|
8455
|
+
}
|
|
8456
|
+
if (isSinglePunct(ch)) {
|
|
8457
|
+
tokens.push({
|
|
8458
|
+
type: "punct",
|
|
8459
|
+
punct: ch,
|
|
8460
|
+
pos: i
|
|
8461
|
+
});
|
|
8462
|
+
i += 1;
|
|
8463
|
+
continue;
|
|
8464
|
+
}
|
|
8465
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
8466
|
+
}
|
|
8467
|
+
tokens.push({
|
|
8468
|
+
type: "eof",
|
|
8469
|
+
pos: n
|
|
8470
|
+
});
|
|
8471
|
+
return tokens;
|
|
8472
|
+
}
|
|
8473
|
+
function keywordOf(text) {
|
|
8474
|
+
if (text === "true") return "true";
|
|
8475
|
+
if (text === "false") return "false";
|
|
8476
|
+
return "null";
|
|
8477
|
+
}
|
|
8478
|
+
function isSinglePunct(ch) {
|
|
8479
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
8480
|
+
}
|
|
8332
8481
|
/**
|
|
8333
|
-
*
|
|
8334
|
-
* declarations. Mirrors the addon-pages split: every addon shipping
|
|
8335
|
-
* widgets registers a provider on this collection cap; the hub-local
|
|
8336
|
-
* aggregator (`addon-widgets`, see `addon-widgets.cap.ts`) walks the
|
|
8337
|
-
* collection, stamps versioned `bundleUrl`s onto each declaration, and
|
|
8338
|
-
* exposes the public listing surface that admin-ui consumes.
|
|
8482
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
8339
8483
|
*
|
|
8340
|
-
*
|
|
8341
|
-
*
|
|
8342
|
-
*
|
|
8484
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
8485
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
8486
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
8487
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
8488
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
8489
|
+
* that references a since-removed builtin degrades at read.
|
|
8343
8490
|
*
|
|
8344
|
-
*
|
|
8345
|
-
*
|
|
8346
|
-
* same `ContributionRenderer` / Module-Federation path as every other
|
|
8347
|
-
* contributed UI surface — no bespoke widget-rendering path. The widget-
|
|
8348
|
-
* only metadata (sizing hints, `requires`) lives as extra fields on the
|
|
8349
|
-
* descriptor; the `UiContribution` core (`tab` / `label` / `order` /
|
|
8350
|
-
* `kind` / `remote`) carries identity + placement + the MF remote.
|
|
8491
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
8492
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
8351
8493
|
*/
|
|
8352
|
-
/**
|
|
8353
|
-
var
|
|
8354
|
-
"
|
|
8355
|
-
"
|
|
8356
|
-
"
|
|
8494
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
8495
|
+
var BINARY_PRECEDENCE = {
|
|
8496
|
+
"||": 1,
|
|
8497
|
+
"&&": 2,
|
|
8498
|
+
"==": 3,
|
|
8499
|
+
"!=": 3,
|
|
8500
|
+
"<": 4,
|
|
8501
|
+
"<=": 4,
|
|
8502
|
+
">": 4,
|
|
8503
|
+
">=": 4,
|
|
8504
|
+
"+": 5,
|
|
8505
|
+
"-": 5,
|
|
8506
|
+
"*": 6,
|
|
8507
|
+
"/": 6,
|
|
8508
|
+
"%": 6
|
|
8509
|
+
};
|
|
8510
|
+
function isLogicalOp(op) {
|
|
8511
|
+
return op === "&&" || op === "||";
|
|
8512
|
+
}
|
|
8513
|
+
function isBinaryOp(op) {
|
|
8514
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
8515
|
+
}
|
|
8516
|
+
var Parser = class {
|
|
8517
|
+
tokens;
|
|
8518
|
+
pos = 0;
|
|
8519
|
+
nodeCount = 0;
|
|
8520
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
8521
|
+
callees = /* @__PURE__ */ new Set();
|
|
8522
|
+
constructor(tokens) {
|
|
8523
|
+
this.tokens = tokens;
|
|
8524
|
+
}
|
|
8525
|
+
parse() {
|
|
8526
|
+
const ast = this.parseTernary();
|
|
8527
|
+
const tok = this.peek();
|
|
8528
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
8529
|
+
return {
|
|
8530
|
+
ast,
|
|
8531
|
+
identifiers: this.identifiers,
|
|
8532
|
+
callees: this.callees,
|
|
8533
|
+
nodeCount: this.nodeCount
|
|
8534
|
+
};
|
|
8535
|
+
}
|
|
8536
|
+
peek() {
|
|
8537
|
+
return this.tokens[this.pos];
|
|
8538
|
+
}
|
|
8539
|
+
next() {
|
|
8540
|
+
return this.tokens[this.pos++];
|
|
8541
|
+
}
|
|
8542
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
8543
|
+
expectPunct(punct) {
|
|
8544
|
+
const tok = this.peek();
|
|
8545
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
8546
|
+
this.pos += 1;
|
|
8547
|
+
}
|
|
8548
|
+
matchPunct(punct) {
|
|
8549
|
+
const tok = this.peek();
|
|
8550
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
8551
|
+
this.pos += 1;
|
|
8552
|
+
return true;
|
|
8553
|
+
}
|
|
8554
|
+
return false;
|
|
8555
|
+
}
|
|
8556
|
+
countNode() {
|
|
8557
|
+
this.nodeCount += 1;
|
|
8558
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
8559
|
+
}
|
|
8560
|
+
parseTernary() {
|
|
8561
|
+
const test = this.parseBinary(1);
|
|
8562
|
+
if (this.matchPunct("?")) {
|
|
8563
|
+
const consequent = this.parseTernary();
|
|
8564
|
+
this.expectPunct(":");
|
|
8565
|
+
const alternate = this.parseTernary();
|
|
8566
|
+
this.countNode();
|
|
8567
|
+
return {
|
|
8568
|
+
kind: "conditional",
|
|
8569
|
+
test,
|
|
8570
|
+
consequent,
|
|
8571
|
+
alternate
|
|
8572
|
+
};
|
|
8573
|
+
}
|
|
8574
|
+
return test;
|
|
8575
|
+
}
|
|
8576
|
+
parseBinary(minPrec) {
|
|
8577
|
+
let left = this.parseUnary();
|
|
8578
|
+
for (;;) {
|
|
8579
|
+
const tok = this.peek();
|
|
8580
|
+
if (tok.type !== "punct") break;
|
|
8581
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
8582
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
8583
|
+
const op = tok.punct;
|
|
8584
|
+
this.pos += 1;
|
|
8585
|
+
const right = this.parseBinary(prec + 1);
|
|
8586
|
+
this.countNode();
|
|
8587
|
+
if (isLogicalOp(op)) left = {
|
|
8588
|
+
kind: "logical",
|
|
8589
|
+
op,
|
|
8590
|
+
left,
|
|
8591
|
+
right
|
|
8592
|
+
};
|
|
8593
|
+
else if (isBinaryOp(op)) left = {
|
|
8594
|
+
kind: "binary",
|
|
8595
|
+
op,
|
|
8596
|
+
left,
|
|
8597
|
+
right
|
|
8598
|
+
};
|
|
8599
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
8600
|
+
}
|
|
8601
|
+
return left;
|
|
8602
|
+
}
|
|
8603
|
+
parseUnary() {
|
|
8604
|
+
const tok = this.peek();
|
|
8605
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
8606
|
+
const op = tok.punct;
|
|
8607
|
+
this.pos += 1;
|
|
8608
|
+
const operand = this.parseUnary();
|
|
8609
|
+
this.countNode();
|
|
8610
|
+
return {
|
|
8611
|
+
kind: "unary",
|
|
8612
|
+
op,
|
|
8613
|
+
operand
|
|
8614
|
+
};
|
|
8615
|
+
}
|
|
8616
|
+
return this.parsePrimary();
|
|
8617
|
+
}
|
|
8618
|
+
parsePrimary() {
|
|
8619
|
+
const tok = this.next();
|
|
8620
|
+
switch (tok.type) {
|
|
8621
|
+
case "number":
|
|
8622
|
+
this.countNode();
|
|
8623
|
+
return {
|
|
8624
|
+
kind: "literal",
|
|
8625
|
+
value: tok.value
|
|
8626
|
+
};
|
|
8627
|
+
case "string":
|
|
8628
|
+
this.countNode();
|
|
8629
|
+
return {
|
|
8630
|
+
kind: "literal",
|
|
8631
|
+
value: tok.value
|
|
8632
|
+
};
|
|
8633
|
+
case "keyword":
|
|
8634
|
+
this.countNode();
|
|
8635
|
+
return {
|
|
8636
|
+
kind: "literal",
|
|
8637
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
8638
|
+
};
|
|
8639
|
+
case "identifier": {
|
|
8640
|
+
const nextTok = this.peek();
|
|
8641
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
8642
|
+
this.identifiers.add(tok.name);
|
|
8643
|
+
this.countNode();
|
|
8644
|
+
return {
|
|
8645
|
+
kind: "identifier",
|
|
8646
|
+
name: tok.name
|
|
8647
|
+
};
|
|
8648
|
+
}
|
|
8649
|
+
case "punct":
|
|
8650
|
+
if (tok.punct === "(") {
|
|
8651
|
+
const inner = this.parseTernary();
|
|
8652
|
+
this.expectPunct(")");
|
|
8653
|
+
return inner;
|
|
8654
|
+
}
|
|
8655
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
8656
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
parseCall(callee, pos) {
|
|
8660
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
8661
|
+
this.expectPunct("(");
|
|
8662
|
+
const args = [];
|
|
8663
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
8664
|
+
args.push(this.parseTernary());
|
|
8665
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
8666
|
+
if (this.matchPunct(",")) continue;
|
|
8667
|
+
this.expectPunct(")");
|
|
8668
|
+
break;
|
|
8669
|
+
}
|
|
8670
|
+
this.callees.add(callee);
|
|
8671
|
+
this.countNode();
|
|
8672
|
+
return {
|
|
8673
|
+
kind: "call",
|
|
8674
|
+
callee,
|
|
8675
|
+
args
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
};
|
|
8679
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
8680
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
8681
|
+
function parseExpression(source) {
|
|
8682
|
+
return new Parser(tokenize(source)).parse();
|
|
8683
|
+
}
|
|
8684
|
+
/**
|
|
8685
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
8686
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
8687
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
8688
|
+
* one per read on a hot resolve path.
|
|
8689
|
+
*
|
|
8690
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
8691
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
8692
|
+
* callers is safe and maximises hit rate.
|
|
8693
|
+
*/
|
|
8694
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8695
|
+
function getCached(source) {
|
|
8696
|
+
const hit = cache.get(source);
|
|
8697
|
+
if (hit !== void 0) {
|
|
8698
|
+
cache.delete(source);
|
|
8699
|
+
cache.set(source, hit);
|
|
8700
|
+
return hit;
|
|
8701
|
+
}
|
|
8702
|
+
let result;
|
|
8703
|
+
try {
|
|
8704
|
+
result = {
|
|
8705
|
+
ok: true,
|
|
8706
|
+
parsed: parseExpression(source)
|
|
8707
|
+
};
|
|
8708
|
+
} catch (err) {
|
|
8709
|
+
result = {
|
|
8710
|
+
ok: false,
|
|
8711
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
8712
|
+
};
|
|
8713
|
+
}
|
|
8714
|
+
cache.set(source, result);
|
|
8715
|
+
if (cache.size > 256) {
|
|
8716
|
+
const oldest = cache.keys().next().value;
|
|
8717
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
8718
|
+
}
|
|
8719
|
+
return result;
|
|
8720
|
+
}
|
|
8721
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
8722
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
8723
|
+
function compileExpressionSafe(source) {
|
|
8724
|
+
return getCached(source);
|
|
8725
|
+
}
|
|
8726
|
+
Object.freeze({});
|
|
8727
|
+
/**
|
|
8728
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
8729
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
8730
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
8731
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
8732
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
8733
|
+
*/
|
|
8734
|
+
function validateExpressionSource(src) {
|
|
8735
|
+
const names = Object.keys(src.bindings);
|
|
8736
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
8737
|
+
for (const name of names) {
|
|
8738
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
8739
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
8740
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
8741
|
+
}
|
|
8742
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
8743
|
+
if (!compiled.ok) return compiled.error;
|
|
8744
|
+
const bound = new Set(names);
|
|
8745
|
+
for (const id of compiled.parsed.identifiers) {
|
|
8746
|
+
if (id === "now") continue;
|
|
8747
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
8748
|
+
}
|
|
8749
|
+
return null;
|
|
8750
|
+
}
|
|
8751
|
+
var ExpressionBindingSourceSchema = union([
|
|
8752
|
+
object({
|
|
8753
|
+
kind: literal("field").optional(),
|
|
8754
|
+
sourceKey: string(),
|
|
8755
|
+
cap: string(),
|
|
8756
|
+
fieldPath: string()
|
|
8757
|
+
}),
|
|
8758
|
+
object({
|
|
8759
|
+
kind: literal("literal"),
|
|
8760
|
+
value: union([
|
|
8761
|
+
string(),
|
|
8762
|
+
number(),
|
|
8763
|
+
boolean(),
|
|
8764
|
+
_null()
|
|
8765
|
+
])
|
|
8766
|
+
}),
|
|
8767
|
+
object({
|
|
8768
|
+
kind: literal("global"),
|
|
8769
|
+
sourceStableId: string(),
|
|
8770
|
+
cap: string(),
|
|
8771
|
+
fieldPath: string()
|
|
8772
|
+
})
|
|
8357
8773
|
]);
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8774
|
+
object({
|
|
8775
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8776
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
8777
|
+
}).superRefine((src, ctx) => {
|
|
8778
|
+
const err = validateExpressionSource(src);
|
|
8779
|
+
if (err !== null) ctx.addIssue({
|
|
8780
|
+
code: "custom",
|
|
8781
|
+
message: err,
|
|
8782
|
+
path: ["expr"]
|
|
8783
|
+
});
|
|
8784
|
+
});
|
|
8785
|
+
/** How a leaf compares a device field to a value. Derived from the field's
|
|
8786
|
+
* `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
8787
|
+
var AutomationConditionOperatorSchema = _enum([
|
|
8788
|
+
"eq",
|
|
8789
|
+
"ne",
|
|
8790
|
+
"gt",
|
|
8791
|
+
"gte",
|
|
8792
|
+
"lt",
|
|
8793
|
+
"lte",
|
|
8794
|
+
"contains",
|
|
8795
|
+
"in"
|
|
8364
8796
|
]);
|
|
8797
|
+
var AutomationConditionLeafSchema = object({
|
|
8798
|
+
kind: literal("condition"),
|
|
8799
|
+
deviceId: number().int().nonnegative(),
|
|
8800
|
+
cap: string().min(1),
|
|
8801
|
+
fieldPath: string().min(1),
|
|
8802
|
+
operator: AutomationConditionOperatorSchema,
|
|
8803
|
+
value: union([
|
|
8804
|
+
string(),
|
|
8805
|
+
number(),
|
|
8806
|
+
boolean(),
|
|
8807
|
+
array(union([string(), number()]))
|
|
8808
|
+
])
|
|
8809
|
+
});
|
|
8365
8810
|
/**
|
|
8366
|
-
*
|
|
8367
|
-
*
|
|
8368
|
-
* `
|
|
8369
|
-
*
|
|
8370
|
-
*
|
|
8811
|
+
* The expression leaf, declared as a plain object rather than an intersection
|
|
8812
|
+
* with {@link ExpressionSourceSchema}: a discriminated union has to be able to
|
|
8813
|
+
* read `kind` off each option, and an intersection hides it. The author-time
|
|
8814
|
+
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
8815
|
+
* cannot drift — an expression that one accepts, the other accepts.
|
|
8371
8816
|
*/
|
|
8372
|
-
var
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8817
|
+
var AutomationConditionExpressionSchema = object({
|
|
8818
|
+
kind: literal("expression"),
|
|
8819
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8820
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
8821
|
+
}).superRefine((src, ctx) => {
|
|
8822
|
+
const err = validateExpressionSource(src);
|
|
8823
|
+
if (err !== null) ctx.addIssue({
|
|
8824
|
+
code: "custom",
|
|
8825
|
+
message: err,
|
|
8826
|
+
path: ["expr"]
|
|
8827
|
+
});
|
|
8376
8828
|
});
|
|
8829
|
+
var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
8830
|
+
object({
|
|
8831
|
+
kind: literal("all"),
|
|
8832
|
+
children: array(AutomationConditionSchema)
|
|
8833
|
+
}),
|
|
8834
|
+
object({
|
|
8835
|
+
kind: literal("any"),
|
|
8836
|
+
children: array(AutomationConditionSchema)
|
|
8837
|
+
}),
|
|
8838
|
+
object({
|
|
8839
|
+
kind: literal("not"),
|
|
8840
|
+
child: AutomationConditionSchema
|
|
8841
|
+
}),
|
|
8842
|
+
AutomationConditionLeafSchema,
|
|
8843
|
+
AutomationConditionExpressionSchema
|
|
8844
|
+
]));
|
|
8377
8845
|
/**
|
|
8378
|
-
*
|
|
8379
|
-
* widget-only metadata. The `UiContribution` core fields:
|
|
8846
|
+
* What starts a run.
|
|
8380
8847
|
*
|
|
8381
|
-
*
|
|
8382
|
-
*
|
|
8383
|
-
*
|
|
8384
|
-
*
|
|
8385
|
-
*
|
|
8386
|
-
*
|
|
8387
|
-
|
|
8388
|
-
|
|
8848
|
+
* D8 compliance, and it is the reason `device-state` is not merely an event
|
|
8849
|
+
* subscription: the trigger evaluates against the **state mirror**, which is
|
|
8850
|
+
* reconciled, and an event only WAKES the evaluation. A dropped event therefore
|
|
8851
|
+
* DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
|
|
8852
|
+
* already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
|
|
8853
|
+
* not.
|
|
8854
|
+
*/
|
|
8855
|
+
var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
8856
|
+
object({
|
|
8857
|
+
kind: literal("device-state"),
|
|
8858
|
+
deviceId: number().int().nonnegative(),
|
|
8859
|
+
cap: string().min(1),
|
|
8860
|
+
fieldPath: string().min(1),
|
|
8861
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
8862
|
+
becomes: union([
|
|
8863
|
+
string(),
|
|
8864
|
+
number(),
|
|
8865
|
+
boolean()
|
|
8866
|
+
]).optional(),
|
|
8867
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
8868
|
+
edge: boolean().optional(),
|
|
8869
|
+
/** The condition must hold this long before the run starts. */
|
|
8870
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
8871
|
+
/** Collapse a burst into one run. */
|
|
8872
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
8873
|
+
}),
|
|
8874
|
+
object({
|
|
8875
|
+
kind: literal("device-event"),
|
|
8876
|
+
/** An `EventCategory` value. */
|
|
8877
|
+
category: string().min(1),
|
|
8878
|
+
deviceId: number().int().nonnegative().optional()
|
|
8879
|
+
}),
|
|
8880
|
+
object({
|
|
8881
|
+
kind: literal("schedule"),
|
|
8882
|
+
cron: string().min(1).max(120)
|
|
8883
|
+
}),
|
|
8884
|
+
object({ kind: literal("manual") })
|
|
8885
|
+
]);
|
|
8886
|
+
/**
|
|
8887
|
+
* One action step.
|
|
8389
8888
|
*
|
|
8390
|
-
*
|
|
8889
|
+
* `wait` and `cap` are `NcRuleActionSchema`'s two members, kept structurally
|
|
8890
|
+
* identical so `NcRuleActionRunner` runs them unchanged — its device-scope
|
|
8891
|
+
* check, stop-at-first-failure and per-sequence throttle are the whole reason
|
|
8892
|
+
* to reuse it, and none of them are re-implemented here.
|
|
8391
8893
|
*
|
|
8392
|
-
*
|
|
8393
|
-
*
|
|
8394
|
-
*
|
|
8395
|
-
*
|
|
8396
|
-
*
|
|
8397
|
-
*
|
|
8398
|
-
*
|
|
8399
|
-
*
|
|
8400
|
-
*
|
|
8401
|
-
* picker filters on.
|
|
8402
|
-
* - `requires` — host-context requirements validated at mount.
|
|
8403
|
-
* - `defaultSize` / `allowedSizes` / `defaultColumns` / `defaultRows`
|
|
8404
|
-
* — dashboard placement hints.
|
|
8894
|
+
* **The one divergence, and it is forced.** `NcRuleActionSchema.cap.deviceId` is
|
|
8895
|
+
* a literal `z.number().int()`, and the NC runner's own `RunSequencesInput`
|
|
8896
|
+
* documents its subject device as *"for the log tag, never for routing"*. So an
|
|
8897
|
+
* NC action can never target the device that triggered it — which is fine for
|
|
8898
|
+
* the NC (its rules already scope to a device) and fatal for an automation
|
|
8899
|
+
* ("sound the siren of the camera that saw the person"). `deviceId` therefore
|
|
8900
|
+
* also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
|
|
8901
|
+
* is called. The runner still receives a number and is untouched; the
|
|
8902
|
+
* resolution is the recipe's job, not the runner's.
|
|
8405
8903
|
*/
|
|
8406
|
-
var
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
subTab: string().optional(),
|
|
8411
|
-
/** Operator-facing label. */
|
|
8412
|
-
label: string(),
|
|
8413
|
-
/** Ordering within `(tab, subTab)`, ascending. */
|
|
8414
|
-
order: number().optional(),
|
|
8415
|
-
/** Always `'remote'` — a widget is a Module Federation remote. */
|
|
8416
|
-
kind: literal("remote"),
|
|
8417
|
-
/** MF remote descriptor. */
|
|
8418
|
-
remote: WidgetRemoteSchema,
|
|
8419
|
-
/** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
|
|
8420
|
-
stableId: string(),
|
|
8421
|
-
description: string().optional(),
|
|
8422
|
-
icon: string().optional(),
|
|
8423
|
-
/**
|
|
8424
|
-
* Bundle filename inside the addon's `dist/` dir served at
|
|
8425
|
-
* `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
|
|
8426
|
-
* this is always `'remoteEntry.js'` — the value is kept on the
|
|
8427
|
-
* metadata so the static-file route can compute an mtime-based
|
|
8428
|
-
* cache-buster URL without a separate filesystem stat.
|
|
8429
|
-
*/
|
|
8430
|
-
bundle: string(),
|
|
8431
|
-
/** Every host the widget supports. The picker filters on this set. */
|
|
8432
|
-
hosts: array(WidgetHostEnum).readonly(),
|
|
8433
|
-
/** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
|
|
8434
|
-
requires: object({
|
|
8435
|
-
deviceContext: boolean().default(false),
|
|
8436
|
-
integrationContext: boolean().default(false)
|
|
8904
|
+
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
8905
|
+
object({
|
|
8906
|
+
kind: literal("wait"),
|
|
8907
|
+
seconds: number().min(0).max(300)
|
|
8437
8908
|
}),
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8909
|
+
object({
|
|
8910
|
+
kind: literal("cap"),
|
|
8911
|
+
deviceId: union([number().int(), object({ $var: string().min(1) })]),
|
|
8912
|
+
cap: string().min(1),
|
|
8913
|
+
method: string().min(1),
|
|
8914
|
+
/** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
|
|
8915
|
+
* evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
|
|
8916
|
+
args: record(string(), unknown()).optional()
|
|
8917
|
+
}),
|
|
8918
|
+
object({
|
|
8919
|
+
kind: literal("code"),
|
|
8920
|
+
/** Compiled into the automation's OWN block by esbuild — not a third
|
|
8921
|
+
* runtime, not a `vm`, and not dynamically evaluated. */
|
|
8922
|
+
code: string().min(1).max(2e4)
|
|
8923
|
+
})
|
|
8924
|
+
]);
|
|
8925
|
+
object({
|
|
8926
|
+
triggers: array(AutomationTriggerSchema),
|
|
8927
|
+
conditions: AutomationConditionSchema.optional(),
|
|
8928
|
+
actions: array(AutomationActionSchema)
|
|
8458
8929
|
});
|
|
8459
|
-
var addonWidgetsSourceCapability = {
|
|
8460
|
-
name: "addon-widgets-source",
|
|
8461
|
-
scope: "system",
|
|
8462
|
-
mode: "collection",
|
|
8463
|
-
internal: true,
|
|
8464
|
-
methods: { listWidgets: method(_void(), array(WidgetMetadataSchema).readonly()) }
|
|
8465
|
-
};
|
|
8466
8930
|
/**
|
|
8467
|
-
* `addon-
|
|
8468
|
-
* surface that admin-ui consumes through `
|
|
8931
|
+
* `addon-pages` — system-scoped singleton aggregator cap. Public-facing
|
|
8932
|
+
* surface that admin-ui consumes through `useAddonPagesListPages()`.
|
|
8469
8933
|
*
|
|
8470
|
-
* The provider iterates every `addon-
|
|
8471
|
-
*
|
|
8472
|
-
*
|
|
8473
|
-
*
|
|
8474
|
-
*
|
|
8475
|
-
* manual reload — same scheme used by `addon-pages`.
|
|
8934
|
+
* The provider iterates every `addon-pages-source` (collection) provider
|
|
8935
|
+
* and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
|
|
8936
|
+
* pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
|
|
8937
|
+
* filesystem `mtime` cache-buster lets the browser pick up addon
|
|
8938
|
+
* rebuilds without manual reload.
|
|
8476
8939
|
*
|
|
8477
|
-
* The hub-local builtin `addon-
|
|
8478
|
-
* `@camstack/system/builtins/addon-
|
|
8940
|
+
* The hub-local builtin `addon-pages-aggregator` (see
|
|
8941
|
+
* `@camstack/system/builtins/addon-pages-aggregator`) registers the
|
|
8479
8942
|
* provider. Splitting the public aggregator from the raw collection
|
|
8480
|
-
* keeps both ends in codegen — there's no hand-written
|
|
8943
|
+
* keeps both ends in codegen — there's no hand-written
|
|
8944
|
+
* `addon-pages.router.ts` wrapper anymore.
|
|
8481
8945
|
*/
|
|
8482
|
-
var
|
|
8946
|
+
var AddonPageDeclarationSchema$1 = object({
|
|
8947
|
+
id: string(),
|
|
8948
|
+
label: string(),
|
|
8949
|
+
icon: string(),
|
|
8950
|
+
path: string(),
|
|
8951
|
+
remoteName: string(),
|
|
8952
|
+
bundle: string(),
|
|
8953
|
+
section: string().optional(),
|
|
8954
|
+
sectionLabel: string().optional()
|
|
8955
|
+
});
|
|
8956
|
+
var AddonPageInfoSchema = object({
|
|
8483
8957
|
addonId: string(),
|
|
8958
|
+
page: AddonPageDeclarationSchema$1,
|
|
8484
8959
|
bundleUrl: string()
|
|
8485
8960
|
});
|
|
8486
|
-
method(_void(), array(
|
|
8961
|
+
method(_void(), array(AddonPageInfoSchema).readonly());
|
|
8487
8962
|
/**
|
|
8488
|
-
*
|
|
8963
|
+
* `addon-pages-source` — collection cap exposing per-provider raw page
|
|
8964
|
+
* declarations. Every addon that contributes a UI page registers a
|
|
8965
|
+
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
8966
|
+
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
8967
|
+
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
8968
|
+
* that admin-ui consumes.
|
|
8489
8969
|
*
|
|
8490
|
-
*
|
|
8491
|
-
*
|
|
8492
|
-
*
|
|
8970
|
+
* The split exists because the public listing has a different output
|
|
8971
|
+
* shape than the per-provider raw declarations, and we want both ends
|
|
8972
|
+
* to flow through codegen instead of relying on a hand-written wrapper.
|
|
8493
8973
|
*/
|
|
8494
|
-
var
|
|
8495
|
-
"info",
|
|
8496
|
-
"success",
|
|
8497
|
-
"warning",
|
|
8498
|
-
"error"
|
|
8499
|
-
]);
|
|
8500
|
-
var AlertStatusSchema = _enum([
|
|
8501
|
-
"active",
|
|
8502
|
-
"in-progress",
|
|
8503
|
-
"completed",
|
|
8504
|
-
"failed",
|
|
8505
|
-
"dismissed"
|
|
8506
|
-
]);
|
|
8507
|
-
var AlertSourceSchema = object({
|
|
8508
|
-
type: string(),
|
|
8509
|
-
id: string()
|
|
8510
|
-
});
|
|
8511
|
-
var AlertSchema = object({
|
|
8974
|
+
var AddonPageDeclarationSchema = object({
|
|
8512
8975
|
id: string(),
|
|
8513
|
-
|
|
8976
|
+
label: string(),
|
|
8977
|
+
icon: string(),
|
|
8978
|
+
path: string(),
|
|
8979
|
+
/**
|
|
8980
|
+
* Module Federation remote name — must match the `name` field on the
|
|
8981
|
+
* page addon's `federation()` plugin config. Used by admin-ui's
|
|
8982
|
+
* `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
|
|
8983
|
+
* Conventionally `addon_<id>_page` (snake_case; MF names cannot
|
|
8984
|
+
* contain hyphens).
|
|
8985
|
+
*/
|
|
8986
|
+
remoteName: string(),
|
|
8987
|
+
/**
|
|
8988
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
8989
|
+
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
8990
|
+
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
8991
|
+
* the static-file route can compute an mtime-based cache-buster URL
|
|
8992
|
+
* without a separate filesystem stat.
|
|
8993
|
+
*/
|
|
8994
|
+
bundle: string(),
|
|
8995
|
+
/**
|
|
8996
|
+
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
8997
|
+
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
8998
|
+
* Any OTHER string creates (or joins) a custom section rendered after
|
|
8999
|
+
* the built-in groups; its label comes from `sectionLabel` (first
|
|
9000
|
+
* declaration wins), falling back to the id. Absent → the legacy
|
|
9001
|
+
* "Addon Pages" group.
|
|
9002
|
+
*/
|
|
9003
|
+
section: string().optional(),
|
|
9004
|
+
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
9005
|
+
sectionLabel: string().optional()
|
|
9006
|
+
});
|
|
9007
|
+
method(_void(), array(AddonPageDeclarationSchema).readonly());
|
|
9008
|
+
var AddonHttpRouteSchema = object({
|
|
9009
|
+
method: _enum([
|
|
9010
|
+
"GET",
|
|
9011
|
+
"POST",
|
|
9012
|
+
"PUT",
|
|
9013
|
+
"DELETE",
|
|
9014
|
+
"PATCH"
|
|
9015
|
+
]),
|
|
9016
|
+
path: string(),
|
|
9017
|
+
access: _enum([
|
|
9018
|
+
"public",
|
|
9019
|
+
"authenticated",
|
|
9020
|
+
"admin"
|
|
9021
|
+
]).optional(),
|
|
9022
|
+
description: string().optional()
|
|
9023
|
+
});
|
|
9024
|
+
/**
|
|
9025
|
+
* Cross-process route invocation envelope. The hub captures the
|
|
9026
|
+
* request as plain data, ships it to the worker via Moleculer, and
|
|
9027
|
+
* the worker runs the local handler against a capturing reply. The
|
|
9028
|
+
* envelope returned describes what the handler intended (status,
|
|
9029
|
+
* headers, body, or a redirect) so the hub can translate it back to
|
|
9030
|
+
* the Fastify reply that's actually wired to the socket.
|
|
9031
|
+
*/
|
|
9032
|
+
var InvokeRequestSchema = object({
|
|
9033
|
+
method: string(),
|
|
9034
|
+
path: string(),
|
|
9035
|
+
params: record(string(), string()),
|
|
9036
|
+
query: record(string(), string()),
|
|
9037
|
+
body: unknown(),
|
|
9038
|
+
headers: record(string(), string()),
|
|
9039
|
+
user: object({
|
|
9040
|
+
id: string(),
|
|
9041
|
+
username: string(),
|
|
9042
|
+
isAdmin: boolean()
|
|
9043
|
+
}).optional(),
|
|
9044
|
+
scopedToken: unknown().optional()
|
|
9045
|
+
});
|
|
9046
|
+
var InvokeReplyEnvelopeSchema = object({
|
|
9047
|
+
status: number().int(),
|
|
9048
|
+
headers: record(string(), string()),
|
|
9049
|
+
/** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
|
|
9050
|
+
* sending `body`. Status defaults to 302 when this is set unless
|
|
9051
|
+
* the handler called `reply.code(...)` explicitly. */
|
|
9052
|
+
redirectUrl: string().nullable(),
|
|
9053
|
+
/** JSON-serializable body. `undefined` is treated as "no body". */
|
|
9054
|
+
body: unknown().optional(),
|
|
9055
|
+
/** Set when the handler called `reply.type(mime)`. */
|
|
9056
|
+
contentType: string().optional()
|
|
9057
|
+
});
|
|
9058
|
+
method(_void(), array(AddonHttpRouteSchema)), method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" });
|
|
9059
|
+
var ConfigTabDeclarationSchema = object({
|
|
9060
|
+
id: string(),
|
|
9061
|
+
label: string(),
|
|
9062
|
+
icon: string(),
|
|
9063
|
+
order: number().optional()
|
|
9064
|
+
});
|
|
9065
|
+
var ConfigSectionWithValuesSchema = object({
|
|
9066
|
+
id: string(),
|
|
9067
|
+
title: string(),
|
|
9068
|
+
description: string().optional(),
|
|
9069
|
+
style: _enum(["card", "accordion"]).optional(),
|
|
9070
|
+
defaultCollapsed: boolean().optional(),
|
|
9071
|
+
columns: union([
|
|
9072
|
+
literal(1),
|
|
9073
|
+
literal(2),
|
|
9074
|
+
literal(3),
|
|
9075
|
+
literal(4)
|
|
9076
|
+
]).optional(),
|
|
9077
|
+
tab: string().optional(),
|
|
9078
|
+
location: _enum(["settings", "top-tab"]).optional(),
|
|
9079
|
+
order: number().optional(),
|
|
9080
|
+
fields: array(any())
|
|
9081
|
+
});
|
|
9082
|
+
var SettingsSchemaWithValuesSchema = object({
|
|
9083
|
+
tabs: array(ConfigTabDeclarationSchema).optional(),
|
|
9084
|
+
sections: array(ConfigSectionWithValuesSchema)
|
|
9085
|
+
});
|
|
9086
|
+
/** Patch object — keys are field names, values are the new field values. */
|
|
9087
|
+
var SettingsPatchSchema = record(string(), unknown());
|
|
9088
|
+
/** Standard success response for update operations. */
|
|
9089
|
+
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
9090
|
+
method(object({
|
|
9091
|
+
addonId: string(),
|
|
9092
|
+
nodeId: string().optional(),
|
|
9093
|
+
overlay: record(string(), unknown()).optional(),
|
|
9094
|
+
cap: string().optional()
|
|
9095
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
9096
|
+
addonId: string(),
|
|
9097
|
+
nodeId: string().optional(),
|
|
9098
|
+
patch: SettingsPatchSchema
|
|
9099
|
+
}), SettingsUpdateResultSchema, {
|
|
9100
|
+
kind: "mutation",
|
|
9101
|
+
auth: "admin"
|
|
9102
|
+
}), method(object({
|
|
9103
|
+
addonId: string(),
|
|
9104
|
+
deviceId: number(),
|
|
9105
|
+
nodeId: string().optional()
|
|
9106
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
9107
|
+
addonId: string(),
|
|
9108
|
+
deviceId: number(),
|
|
9109
|
+
nodeId: string().optional(),
|
|
9110
|
+
patch: SettingsPatchSchema
|
|
9111
|
+
}), SettingsUpdateResultSchema, {
|
|
9112
|
+
kind: "mutation",
|
|
9113
|
+
auth: "admin"
|
|
9114
|
+
});
|
|
9115
|
+
/**
|
|
9116
|
+
* `addon-widgets-source` — collection cap exposing per-addon raw widget
|
|
9117
|
+
* declarations. Mirrors the addon-pages split: every addon shipping
|
|
9118
|
+
* widgets registers a provider on this collection cap; the hub-local
|
|
9119
|
+
* aggregator (`addon-widgets`, see `addon-widgets.cap.ts`) walks the
|
|
9120
|
+
* collection, stamps versioned `bundleUrl`s onto each declaration, and
|
|
9121
|
+
* exposes the public listing surface that admin-ui consumes.
|
|
9122
|
+
*
|
|
9123
|
+
* The split exists because the public listing has a different output
|
|
9124
|
+
* shape (flat enriched metadata with `addonId` + `bundleUrl`) than the
|
|
9125
|
+
* per-provider raw declarations. Both ends flow through codegen.
|
|
9126
|
+
*
|
|
9127
|
+
* Unified UI-contribution model (Task 10): a widget descriptor IS a
|
|
9128
|
+
* `UiContribution` with `kind:'remote'`. The host renders it through the
|
|
9129
|
+
* same `ContributionRenderer` / Module-Federation path as every other
|
|
9130
|
+
* contributed UI surface — no bespoke widget-rendering path. The widget-
|
|
9131
|
+
* only metadata (sizing hints, `requires`) lives as extra fields on the
|
|
9132
|
+
* descriptor; the `UiContribution` core (`tab` / `label` / `order` /
|
|
9133
|
+
* `kind` / `remote`) carries identity + placement + the MF remote.
|
|
9134
|
+
*/
|
|
9135
|
+
/** Where the widget makes sense to render — maps to a contribution `tab`. */
|
|
9136
|
+
var WidgetHostEnum = _enum([
|
|
9137
|
+
"device-tab",
|
|
9138
|
+
"dashboard",
|
|
9139
|
+
"integration-detail"
|
|
9140
|
+
]);
|
|
9141
|
+
var WidgetSizeEnum = _enum([
|
|
9142
|
+
"xs",
|
|
9143
|
+
"sm",
|
|
9144
|
+
"md",
|
|
9145
|
+
"lg",
|
|
9146
|
+
"xl"
|
|
9147
|
+
]);
|
|
9148
|
+
/**
|
|
9149
|
+
* MF remote descriptor — mirrors `UiContributionRemote` from
|
|
9150
|
+
* `capability-definition.ts`. Widget remotes expose a single
|
|
9151
|
+
* `'./widgets'` module whose default export is a
|
|
9152
|
+
* `Record<componentKey, Component>` map; `componentKey` (the widget
|
|
9153
|
+
* `stableId`) picks the entry the host mounts.
|
|
9154
|
+
*/
|
|
9155
|
+
var WidgetRemoteSchema = object({
|
|
9156
|
+
remoteName: string(),
|
|
9157
|
+
exposedModule: string(),
|
|
9158
|
+
componentKey: string().optional()
|
|
9159
|
+
});
|
|
9160
|
+
/**
|
|
9161
|
+
* One widget declaration — a `UiContribution` (`kind:'remote'`) plus
|
|
9162
|
+
* widget-only metadata. The `UiContribution` core fields:
|
|
9163
|
+
*
|
|
9164
|
+
* - `tab` — where the widget hosts. A widget that runs on the
|
|
9165
|
+
* dashboard declares `tab:'dashboard'`; a device-tab
|
|
9166
|
+
* widget declares the target device-detail tab id.
|
|
9167
|
+
* - `subTab` — optional sub-tab within `tab`.
|
|
9168
|
+
* - `label` — operator-facing label.
|
|
9169
|
+
* - `order` — ordering within `(tab, subTab)`.
|
|
9170
|
+
* - `kind` — always `'remote'` for widgets.
|
|
9171
|
+
* - `remote` — the MF remote `{ remoteName, exposedModule, componentKey }`.
|
|
9172
|
+
*
|
|
9173
|
+
* Widget-only fields retained alongside the contribution core:
|
|
9174
|
+
*
|
|
9175
|
+
* - `stableId` — stable identity within the addon (the MF
|
|
9176
|
+
* `componentKey`; kept top-level so consumers have
|
|
9177
|
+
* a stable key without reaching into `remote`).
|
|
9178
|
+
* - `description` / `icon` — picker metadata.
|
|
9179
|
+
* - `bundle` — entry filename inside the addon `dist/` dir; the
|
|
9180
|
+
* aggregator stamps a versioned `bundleUrl` from it.
|
|
9181
|
+
* - `hosts` — every host the widget supports (a widget can run
|
|
9182
|
+
* both on the dashboard and a device tab). `tab`
|
|
9183
|
+
* is the PRIMARY host; `hosts` is the full set the
|
|
9184
|
+
* picker filters on.
|
|
9185
|
+
* - `requires` — host-context requirements validated at mount.
|
|
9186
|
+
* - `defaultSize` / `allowedSizes` / `defaultColumns` / `defaultRows`
|
|
9187
|
+
* — dashboard placement hints.
|
|
9188
|
+
*/
|
|
9189
|
+
var WidgetMetadataSchema = object({
|
|
9190
|
+
/** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
|
|
9191
|
+
tab: string(),
|
|
9192
|
+
/** Optional sub-tab within `tab`. */
|
|
9193
|
+
subTab: string().optional(),
|
|
9194
|
+
/** Operator-facing label. */
|
|
9195
|
+
label: string(),
|
|
9196
|
+
/** Ordering within `(tab, subTab)`, ascending. */
|
|
9197
|
+
order: number().optional(),
|
|
9198
|
+
/** Always `'remote'` — a widget is a Module Federation remote. */
|
|
9199
|
+
kind: literal("remote"),
|
|
9200
|
+
/** MF remote descriptor. */
|
|
9201
|
+
remote: WidgetRemoteSchema,
|
|
9202
|
+
/** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
|
|
9203
|
+
stableId: string(),
|
|
9204
|
+
description: string().optional(),
|
|
9205
|
+
icon: string().optional(),
|
|
9206
|
+
/**
|
|
9207
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
9208
|
+
* `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
|
|
9209
|
+
* this is always `'remoteEntry.js'` — the value is kept on the
|
|
9210
|
+
* metadata so the static-file route can compute an mtime-based
|
|
9211
|
+
* cache-buster URL without a separate filesystem stat.
|
|
9212
|
+
*/
|
|
9213
|
+
bundle: string(),
|
|
9214
|
+
/** Every host the widget supports. The picker filters on this set. */
|
|
9215
|
+
hosts: array(WidgetHostEnum).readonly(),
|
|
9216
|
+
/** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
|
|
9217
|
+
requires: object({
|
|
9218
|
+
deviceContext: boolean().default(false),
|
|
9219
|
+
integrationContext: boolean().default(false)
|
|
9220
|
+
}),
|
|
9221
|
+
/**
|
|
9222
|
+
* Loadable BEFORE authentication. The normal widget registry listing
|
|
9223
|
+
* (`addon-widgets.listWidgets`) is auth-gated, so a pre-auth surface
|
|
9224
|
+
* (the login page) cannot discover a widget through it. A widget that
|
|
9225
|
+
* declares `preAuth: true` marks itself as safe to mount on a pre-auth
|
|
9226
|
+
* screen — it is surfaced through the PUBLIC `auth.listLoginMethods`
|
|
9227
|
+
* login-method contribution channel (see `login-method.cap.ts`) rather
|
|
9228
|
+
* than the authenticated registry, and its bundle is served by the
|
|
9229
|
+
* public `/api/addon-widgets/:addonId/*` static route. Defaults false.
|
|
9230
|
+
*/
|
|
9231
|
+
preAuth: boolean().optional().default(false),
|
|
9232
|
+
/** Dashboard placement HINTS (operator can override per instance). */
|
|
9233
|
+
defaultSize: WidgetSizeEnum.default("md"),
|
|
9234
|
+
allowedSizes: array(WidgetSizeEnum).readonly().default([
|
|
9235
|
+
"sm",
|
|
9236
|
+
"md",
|
|
9237
|
+
"lg"
|
|
9238
|
+
]),
|
|
9239
|
+
defaultColumns: number().int().min(1).max(12).default(6),
|
|
9240
|
+
defaultRows: number().int().min(1).max(12).default(1)
|
|
9241
|
+
});
|
|
9242
|
+
var addonWidgetsSourceCapability = {
|
|
9243
|
+
name: "addon-widgets-source",
|
|
9244
|
+
scope: "system",
|
|
9245
|
+
mode: "collection",
|
|
9246
|
+
internal: true,
|
|
9247
|
+
methods: { listWidgets: method(_void(), array(WidgetMetadataSchema).readonly()) }
|
|
9248
|
+
};
|
|
9249
|
+
/**
|
|
9250
|
+
* `addon-widgets` — system-scoped singleton aggregator cap. Public-facing
|
|
9251
|
+
* surface that admin-ui consumes through `useAddonWidgetsListWidgets()`.
|
|
9252
|
+
*
|
|
9253
|
+
* The provider iterates every `addon-widgets-source` (collection)
|
|
9254
|
+
* provider and emits `EnrichedWidgetMetadata[]` enriched with versioned
|
|
9255
|
+
* `bundleUrl` strings pointing at
|
|
9256
|
+
* `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem
|
|
9257
|
+
* `mtime` cache-buster lets the browser pick up addon rebuilds without
|
|
9258
|
+
* manual reload — same scheme used by `addon-pages`.
|
|
9259
|
+
*
|
|
9260
|
+
* The hub-local builtin `addon-widgets-aggregator` (see
|
|
9261
|
+
* `@camstack/system/builtins/addon-widgets-aggregator`) registers the
|
|
9262
|
+
* provider. Splitting the public aggregator from the raw collection
|
|
9263
|
+
* keeps both ends in codegen — there's no hand-written wrapper.
|
|
9264
|
+
*/
|
|
9265
|
+
var EnrichedWidgetMetadataSchema = WidgetMetadataSchema.extend({
|
|
9266
|
+
addonId: string(),
|
|
9267
|
+
bundleUrl: string()
|
|
9268
|
+
});
|
|
9269
|
+
method(_void(), array(EnrichedWidgetMetadataSchema).readonly());
|
|
9270
|
+
/**
|
|
9271
|
+
* Alerts capability — collection-based internal alert system.
|
|
9272
|
+
*
|
|
9273
|
+
* Multiple providers can register. Each provider filters by EventBus category
|
|
9274
|
+
* and creates/updates alerts. The built-in Alert Center addon persists alerts
|
|
9275
|
+
* in the DB and serves them to the admin UI.
|
|
9276
|
+
*/
|
|
9277
|
+
var AlertSeveritySchema = _enum([
|
|
9278
|
+
"info",
|
|
9279
|
+
"success",
|
|
9280
|
+
"warning",
|
|
9281
|
+
"error"
|
|
9282
|
+
]);
|
|
9283
|
+
var AlertStatusSchema = _enum([
|
|
9284
|
+
"active",
|
|
9285
|
+
"in-progress",
|
|
9286
|
+
"completed",
|
|
9287
|
+
"failed",
|
|
9288
|
+
"dismissed"
|
|
9289
|
+
]);
|
|
9290
|
+
var AlertSourceSchema = object({
|
|
9291
|
+
type: string(),
|
|
9292
|
+
id: string()
|
|
9293
|
+
});
|
|
9294
|
+
var AlertSchema = object({
|
|
9295
|
+
id: string(),
|
|
9296
|
+
category: string(),
|
|
8514
9297
|
severity: AlertSeveritySchema,
|
|
8515
9298
|
title: string(),
|
|
8516
9299
|
message: string(),
|
|
@@ -12177,7 +12960,7 @@ DeviceType.Camera, method(object({
|
|
|
12177
12960
|
* Why: pub/sub routing over the system event-bus loses fidelity
|
|
12178
12961
|
* (callback shape, QoS guarantees, will/retain semantics) and adds
|
|
12179
12962
|
* refcount bookkeeping that addons would rather own themselves. The
|
|
12180
|
-
* canonical consumer
|
|
12963
|
+
* canonical consumer needs raw `mqtt.js`
|
|
12181
12964
|
* features anyway — give it the connection config, get out of the way.
|
|
12182
12965
|
*
|
|
12183
12966
|
* Consumer flow:
|
|
@@ -15200,8 +15983,26 @@ var TrackSchema = object({
|
|
|
15200
15983
|
/** Periodic snapshots at snapshotIntervalMs cadence (subject to
|
|
15201
15984
|
* saveThumbnails policy). */
|
|
15202
15985
|
snapshots: array(TrackSnapshotSchema).readonly(),
|
|
15203
|
-
/** Deduplicated zones the track has entered at least once. */
|
|
15986
|
+
/** Deduplicated zones the track has entered at least once. Zone IDS. */
|
|
15204
15987
|
zonesVisited: array(string()).readonly(),
|
|
15988
|
+
/**
|
|
15989
|
+
* Human NAMES for {@link zonesVisited}, resolved at READ time against the
|
|
15990
|
+
* `zones` capability.
|
|
15991
|
+
*
|
|
15992
|
+
* `zonesVisited` persists ids (`cfeec78c-8d69-…`), which no operator can type
|
|
15993
|
+
* and no card can render — so every free-text search surface was structurally
|
|
15994
|
+
* unable to answer "show me the tracks in Uscio", and did not fail loudly, it
|
|
15995
|
+
* just returned nothing. Resolving here rather than in each client keeps ONE
|
|
15996
|
+
* derivation and costs the clients no extra call (the `zones` cap is
|
|
15997
|
+
* per-device, so a client-side resolve would be a per-camera fan-out on a
|
|
15998
|
+
* surface built to avoid exactly that).
|
|
15999
|
+
*
|
|
16000
|
+
* Resolved, never invented: a zone deleted since the track was written has no
|
|
16001
|
+
* name and is DROPPED, so this array can be shorter than `zonesVisited` — the
|
|
16002
|
+
* two are not positionally aligned. Absent when the track visited no zone, or
|
|
16003
|
+
* when the zone catalogue could not be read.
|
|
16004
|
+
*/
|
|
16005
|
+
zoneNames: array(string()).readonly().optional(),
|
|
15205
16006
|
/** Deduplicated set of detector classes observed for this track over its
|
|
15206
16007
|
* life (a track may be reclassified, e.g. person→vehicle). Absent on
|
|
15207
16008
|
* legacy rows written before class accumulation shipped. */
|
|
@@ -17695,6 +18496,23 @@ var CameraRecordingStatusSchema = object({
|
|
|
17695
18496
|
active: boolean(),
|
|
17696
18497
|
storageBytes: number()
|
|
17697
18498
|
});
|
|
18499
|
+
/** One stage of the fan-out that could NOT be read, and how long it cost. */
|
|
18500
|
+
var CameraStatusDegradationSchema = object({
|
|
18501
|
+
stage: _enum([
|
|
18502
|
+
"source",
|
|
18503
|
+
"broker",
|
|
18504
|
+
"detection",
|
|
18505
|
+
"recording",
|
|
18506
|
+
"switches"
|
|
18507
|
+
]),
|
|
18508
|
+
reason: _enum([
|
|
18509
|
+
"timeout",
|
|
18510
|
+
"error",
|
|
18511
|
+
"partial"
|
|
18512
|
+
]),
|
|
18513
|
+
/** Wall-clock ms spent on the stage before it was abandoned. */
|
|
18514
|
+
elapsedMs: number()
|
|
18515
|
+
});
|
|
17698
18516
|
/**
|
|
17699
18517
|
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
17700
18518
|
*
|
|
@@ -17725,9 +18543,28 @@ var CameraStatusSchema = object({
|
|
|
17725
18543
|
* differently — a quiet camera that looks identical to a dead one is the
|
|
17726
18544
|
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
17727
18545
|
*
|
|
17728
|
-
* Empty when nothing is off
|
|
18546
|
+
* Empty when nothing is off, and never contains a switch no provider offers
|
|
18547
|
+
* — but an empty list is only a POSITIVE claim when `degraded` does not name
|
|
18548
|
+
* `'switches'`. When it does, the switch set could not be read and nothing
|
|
18549
|
+
* here may be rendered as "the operator turned nothing off": that is the
|
|
18550
|
+
* D62 failure (a camera we could not read painted as broken) in the very
|
|
18551
|
+
* field that exists to prevent it.
|
|
17729
18552
|
*/
|
|
17730
18553
|
switchedOff: array(CameraSwitchIdSchema).readonly(),
|
|
18554
|
+
/**
|
|
18555
|
+
* Stages of the bounded fan-out that were CUT SHORT — a timeout or a
|
|
18556
|
+
* rejection — and whose block is therefore `null` because we could not
|
|
18557
|
+
* READ it, not because there is nothing there.
|
|
18558
|
+
*
|
|
18559
|
+
* Without this, three different facts arrive as the same `null`: "the stage
|
|
18560
|
+
* timed out", "the stage failed", and "this camera legitimately has no
|
|
18561
|
+
* decoder / no recording". Every surface that draws a conclusion from a null
|
|
18562
|
+
* block (or from an empty `switchedOff`) must consult this first; a stage
|
|
18563
|
+
* named here supports no conclusion at all, only "unknown".
|
|
18564
|
+
*
|
|
18565
|
+
* Empty on a clean read — the overwhelmingly common case.
|
|
18566
|
+
*/
|
|
18567
|
+
degraded: array(CameraStatusDegradationSchema).readonly(),
|
|
17731
18568
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
17732
18569
|
fetchedAt: number()
|
|
17733
18570
|
});
|
|
@@ -18275,6 +19112,37 @@ DeviceType.Camera, method(object({
|
|
|
18275
19112
|
lastCapturedAt: number().nullable(),
|
|
18276
19113
|
cacheAgeMs: number().nullable(),
|
|
18277
19114
|
etag: string().nullable()
|
|
19115
|
+
}))), systemMethod(object({
|
|
19116
|
+
/** The tiles a surface is actually rendering. One entry per (device,
|
|
19117
|
+
* width) the caller will paint — the width is snapped to the server's
|
|
19118
|
+
* ladder and becomes part of the link's SIGNED identity. */
|
|
19119
|
+
targets: array(object({
|
|
19120
|
+
deviceId: number(),
|
|
19121
|
+
/** Target width in px. Omit for the frame as captured — correct
|
|
19122
|
+
* for a full-bleed surface, wrong (and expensive) for a grid. */
|
|
19123
|
+
width: number().int().positive().optional()
|
|
19124
|
+
})).min(1).max(200) }), array(object({
|
|
19125
|
+
deviceId: number(),
|
|
19126
|
+
/** Root-relative signed path, or null when the link plane is not
|
|
19127
|
+
* served (no data-plane facility). Present even for a device that has
|
|
19128
|
+
* never captured — the request is what triggers the first one (D94). */
|
|
19129
|
+
url: string().nullable(),
|
|
19130
|
+
/** Epoch ms of the frame this link serves. Null = never captured.
|
|
19131
|
+
* THE honest age: the tRPC path carried none before this. */
|
|
19132
|
+
capturedAt: number().nullable(),
|
|
19133
|
+
/** Age of that frame at the moment the answer was built. */
|
|
19134
|
+
ageMs: number().nullable(),
|
|
19135
|
+
/** Epoch ms after which `url` stops verifying. */
|
|
19136
|
+
expiresAt: number().nullable(),
|
|
19137
|
+
/** Ladder rung the bytes are at; null = the frame as captured. */
|
|
19138
|
+
width: number().nullable(),
|
|
19139
|
+
/** The device has never produced a frame. An empty state, not a
|
|
19140
|
+
* failure — and never a reason to withhold the link (D94). */
|
|
19141
|
+
neverCaptured: boolean(),
|
|
19142
|
+
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
19143
|
+
* NOT refresh in the background. A surface should say so rather than
|
|
19144
|
+
* present it as current. */
|
|
19145
|
+
sleeping: boolean()
|
|
18278
19146
|
})));
|
|
18279
19147
|
/**
|
|
18280
19148
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -26990,864 +27858,237 @@ var BaseDevice = class {
|
|
|
26990
27858
|
getAccessoryChildren() {
|
|
26991
27859
|
return [];
|
|
26992
27860
|
}
|
|
26993
|
-
/**
|
|
26994
|
-
* Read the current feature-probe flag bag with a typed cast. Helper
|
|
26995
|
-
* for `getAccessoryChildren()` and `features` getters that derive
|
|
26996
|
-
* outputs from the probe results.
|
|
26997
|
-
*/
|
|
26998
|
-
getProbeFlags() {
|
|
26999
|
-
return this.runtimeState.getCapState("feature-probe")?.flags ?? {};
|
|
27000
|
-
}
|
|
27001
|
-
/**
|
|
27002
|
-
* Returns true once `onProbe` has completed at least once
|
|
27003
|
-
* (`lastProbedAt > 0`). Drivers gate `getAccessoryChildren()` on this
|
|
27004
|
-
* to avoid spawning stale accessories on a fresh device whose probe
|
|
27005
|
-
* hasn't landed yet.
|
|
27006
|
-
*/
|
|
27007
|
-
hasProbed() {
|
|
27008
|
-
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
27009
|
-
}
|
|
27010
|
-
};
|
|
27011
|
-
/** Marker written to a declared integration's `info`. */
|
|
27012
|
-
var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
|
|
27013
|
-
/**
|
|
27014
|
-
* Strip the `<node>/<addon>` suffix a forked child carries.
|
|
27015
|
-
*
|
|
27016
|
-
* Comparing `ctx.kernel.localNodeId` raw skipped EVERY node — including the one
|
|
27017
|
-
* that was supposed to act — because on the hub it reads `hub/<addon>`.
|
|
27018
|
-
*/
|
|
27019
|
-
function declarationOwnerNodeId(localNodeId) {
|
|
27020
|
-
const raw = localNodeId ?? "hub";
|
|
27021
|
-
if (!raw.includes("/")) return raw;
|
|
27022
|
-
return raw.split("/")[0] ?? "hub";
|
|
27023
|
-
}
|
|
27024
|
-
/**
|
|
27025
|
-
* The one way an addon owns a device it declares.
|
|
27026
|
-
*
|
|
27027
|
-
* Construct once with the addon's ports, then call {@link reconcile} on boot and
|
|
27028
|
-
* on every convergence tick. There is no second get-or-create helper — a guard
|
|
27029
|
-
* in `scripts/` enforces that.
|
|
27030
|
-
*/
|
|
27031
|
-
var DeclaredDevices = class {
|
|
27032
|
-
ports;
|
|
27033
|
-
constructor(ports) {
|
|
27034
|
-
this.ports = ports;
|
|
27035
|
-
}
|
|
27036
|
-
/**
|
|
27037
|
-
* Converge the declared set. Idempotent, and safe to call repeatedly.
|
|
27038
|
-
*
|
|
27039
|
-
* Throws only what the ports throw on the FIRST index read; every other
|
|
27040
|
-
* failure is per-device and logged, so one bad declaration never takes the
|
|
27041
|
-
* others down.
|
|
27042
|
-
*/
|
|
27043
|
-
async reconcile(spec) {
|
|
27044
|
-
if ((spec.placement ?? "hub") === "hub") {
|
|
27045
|
-
const nodeId = declarationOwnerNodeId(this.ports.localNodeId);
|
|
27046
|
-
if (nodeId !== "hub") {
|
|
27047
|
-
this.ports.logger.info("declared devices are hub-owned — skipping on this node", { meta: {
|
|
27048
|
-
nodeId,
|
|
27049
|
-
rawNodeId: this.ports.localNodeId ?? null
|
|
27050
|
-
} });
|
|
27051
|
-
return {
|
|
27052
|
-
integrationId: null,
|
|
27053
|
-
devices: [],
|
|
27054
|
-
removed: [],
|
|
27055
|
-
owned: false
|
|
27056
|
-
};
|
|
27057
|
-
}
|
|
27058
|
-
}
|
|
27059
|
-
const integrationId = await this.ensureIntegration(spec.integrationName);
|
|
27060
|
-
const index = await this.readIndex();
|
|
27061
|
-
const outcomes = [];
|
|
27062
|
-
for (const declaration of spec.devices) {
|
|
27063
|
-
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
27064
|
-
if (outcome !== null) outcomes.push(outcome);
|
|
27065
|
-
}
|
|
27066
|
-
return {
|
|
27067
|
-
integrationId,
|
|
27068
|
-
devices: outcomes,
|
|
27069
|
-
removed: await this.sweepWithdrawn(spec.devices, integrationId, index),
|
|
27070
|
-
owned: true
|
|
27071
|
-
};
|
|
27072
|
-
}
|
|
27073
|
-
/**
|
|
27074
|
-
* Get-or-create the FIXED integration, and RE-ASSERT the flag every pass.
|
|
27075
|
-
*
|
|
27076
|
-
* The re-assertion is the fix for the defect the hand-rolled version shipped
|
|
27077
|
-
* with: writing `info.fixed` only on the create path left every pre-existing
|
|
27078
|
-
* install without it, and the kernel kept offering to delete an integration
|
|
27079
|
-
* the addon owns.
|
|
27080
|
-
*/
|
|
27081
|
-
async ensureIntegration(integrationName) {
|
|
27082
|
-
const existing = await this.ports.getIntegration(this.ports.addonId);
|
|
27083
|
-
if (existing === null) {
|
|
27084
|
-
const created = await this.ports.createIntegration({
|
|
27085
|
-
addonId: this.ports.addonId,
|
|
27086
|
-
name: integrationName,
|
|
27087
|
-
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
27088
|
-
});
|
|
27089
|
-
this.ports.logger.info("declared a fixed integration", { meta: {
|
|
27090
|
-
integrationId: created.id,
|
|
27091
|
-
name: integrationName
|
|
27092
|
-
} });
|
|
27093
|
-
return created.id;
|
|
27094
|
-
}
|
|
27095
|
-
if (existing.info?.["fixed"] !== true) {
|
|
27096
|
-
await this.ports.updateIntegration({
|
|
27097
|
-
id: existing.id,
|
|
27098
|
-
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
27099
|
-
});
|
|
27100
|
-
this.ports.logger.info("re-asserted `fixed` on a declared integration", { meta: { integrationId: existing.id } });
|
|
27101
|
-
}
|
|
27102
|
-
return existing.id;
|
|
27103
|
-
}
|
|
27104
|
-
async readIndex() {
|
|
27105
|
-
const rows = await this.ports.listOwnDevices();
|
|
27106
|
-
return new Map(rows.map((row) => [row.stableId, row]));
|
|
27107
|
-
}
|
|
27108
|
-
/**
|
|
27109
|
-
* One declaration: adopt what exists, create what does not.
|
|
27110
|
-
*
|
|
27111
|
-
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
27112
|
-
* `initialMeta.name` lands as an unconditional `setName`. A transiently empty
|
|
27113
|
-
* index therefore looks exactly like a first boot and would silently re-stamp
|
|
27114
|
-
* the declared name over the operator's rename. D49: that branch needs a
|
|
27115
|
-
* second read to agree.
|
|
27116
|
-
*/
|
|
27117
|
-
async applyDeclaration(declaration, integrationId, index) {
|
|
27118
|
-
try {
|
|
27119
|
-
let existing = index.get(declaration.stableId);
|
|
27120
|
-
if (existing === void 0) {
|
|
27121
|
-
existing = (await this.readIndex()).get(declaration.stableId);
|
|
27122
|
-
if (existing !== void 0) this.ports.logger.warn("device index disagreed with itself — adopting instead of re-creating", {
|
|
27123
|
-
tags: { deviceId: existing.id },
|
|
27124
|
-
meta: {
|
|
27125
|
-
stableId: declaration.stableId,
|
|
27126
|
-
addonId: this.ports.addonId
|
|
27127
|
-
}
|
|
27128
|
-
});
|
|
27129
|
-
}
|
|
27130
|
-
if (existing !== void 0) {
|
|
27131
|
-
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, {}, null, void 0);
|
|
27132
|
-
this.ports.logger.info("declared device adopted", {
|
|
27133
|
-
tags: { deviceId: device.id },
|
|
27134
|
-
meta: {
|
|
27135
|
-
stableId: declaration.stableId,
|
|
27136
|
-
integrationId
|
|
27137
|
-
}
|
|
27138
|
-
});
|
|
27139
|
-
return {
|
|
27140
|
-
stableId: declaration.stableId,
|
|
27141
|
-
deviceId: device.id,
|
|
27142
|
-
device,
|
|
27143
|
-
created: false
|
|
27144
|
-
};
|
|
27145
|
-
}
|
|
27146
|
-
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, declaration.config ?? {}, null, {
|
|
27147
|
-
type: declaration.type,
|
|
27148
|
-
name: declaration.name,
|
|
27149
|
-
integrationId,
|
|
27150
|
-
...declaration.role === void 0 ? {} : { role: declaration.role }
|
|
27151
|
-
});
|
|
27152
|
-
this.ports.logger.info("declared device created", {
|
|
27153
|
-
tags: { deviceId: device.id },
|
|
27154
|
-
meta: {
|
|
27155
|
-
stableId: declaration.stableId,
|
|
27156
|
-
integrationId
|
|
27157
|
-
}
|
|
27158
|
-
});
|
|
27159
|
-
return {
|
|
27160
|
-
stableId: declaration.stableId,
|
|
27161
|
-
deviceId: device.id,
|
|
27162
|
-
device,
|
|
27163
|
-
created: true
|
|
27164
|
-
};
|
|
27165
|
-
} catch (err) {
|
|
27166
|
-
this.ports.logger.warn("a declared device could not be brought up", { meta: {
|
|
27167
|
-
stableId: declaration.stableId,
|
|
27168
|
-
error: err instanceof Error ? err.message : String(err)
|
|
27169
|
-
} });
|
|
27170
|
-
return null;
|
|
27171
|
-
}
|
|
27172
|
-
}
|
|
27173
|
-
/**
|
|
27174
|
-
* Remove rows under the addon's FIXED integration whose declaration is gone.
|
|
27175
|
-
*
|
|
27176
|
-
* Bounded to that integration: a declared integration has no operator
|
|
27177
|
-
* add-flow, so every row under it got there by declaration. Devices this
|
|
27178
|
-
* addon owns OUTSIDE it (a provider's adopted devices) are never candidates.
|
|
27179
|
-
*
|
|
27180
|
-
* Bounded in count, and every deletion is logged with its `deviceId` — a
|
|
27181
|
-
* withdrawal that removes an operator-visible row silently is the failure
|
|
27182
|
-
* mode, not the removal itself.
|
|
27183
|
-
*/
|
|
27184
|
-
async sweepWithdrawn(declarations, integrationId, index) {
|
|
27185
|
-
const declared = new Set(declarations.map((d) => d.stableId));
|
|
27186
|
-
const candidates = [...index.values()].filter((row) => row.integrationId === integrationId && !declared.has(row.stableId));
|
|
27187
|
-
if (candidates.length === 0) return [];
|
|
27188
|
-
if (candidates.length > 32) {
|
|
27189
|
-
this.ports.logger.warn("withdrawal sweep exceeded its bound — removing nothing", { meta: {
|
|
27190
|
-
integrationId,
|
|
27191
|
-
candidates: candidates.length,
|
|
27192
|
-
bound: 32
|
|
27193
|
-
} });
|
|
27194
|
-
return [];
|
|
27195
|
-
}
|
|
27196
|
-
const removed = [];
|
|
27197
|
-
for (const row of candidates) try {
|
|
27198
|
-
await this.ports.devices.remove(row.id);
|
|
27199
|
-
removed.push(row.id);
|
|
27200
|
-
this.ports.logger.info("declared device removed — its declaration was withdrawn", {
|
|
27201
|
-
tags: { deviceId: row.id },
|
|
27202
|
-
meta: {
|
|
27203
|
-
stableId: row.stableId,
|
|
27204
|
-
integrationId
|
|
27205
|
-
}
|
|
27206
|
-
});
|
|
27207
|
-
} catch (err) {
|
|
27208
|
-
this.ports.logger.warn("a withdrawn declared device could not be removed", {
|
|
27209
|
-
tags: { deviceId: row.id },
|
|
27210
|
-
meta: {
|
|
27211
|
-
stableId: row.stableId,
|
|
27212
|
-
error: err instanceof Error ? err.message : String(err)
|
|
27213
|
-
}
|
|
27214
|
-
});
|
|
27215
|
-
}
|
|
27216
|
-
return removed;
|
|
27217
|
-
}
|
|
27218
|
-
};
|
|
27219
|
-
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;
|
|
27220
|
-
new Set(Object.values(DeviceType));
|
|
27221
|
-
DeviceFeature.BatteryOperated;
|
|
27222
|
-
/**
|
|
27223
|
-
* Error types for the safe expression engine. Two distinct classes so callers
|
|
27224
|
-
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
27225
|
-
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
27226
|
-
*/
|
|
27227
|
-
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
27228
|
-
* the failure is anchored to a character (author-facing inline feedback). */
|
|
27229
|
-
var ExpressionParseError = class extends Error {
|
|
27230
|
-
position;
|
|
27231
|
-
constructor(message, position) {
|
|
27232
|
-
super(message);
|
|
27233
|
-
this.name = "ExpressionParseError";
|
|
27234
|
-
this.position = position;
|
|
27235
|
-
}
|
|
27236
|
-
};
|
|
27237
|
-
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
27238
|
-
* result, unknown builtin, step-budget exceeded). */
|
|
27239
|
-
var ExpressionEvalError = class extends Error {
|
|
27240
|
-
constructor(message) {
|
|
27241
|
-
super(message);
|
|
27242
|
-
this.name = "ExpressionEvalError";
|
|
27243
|
-
}
|
|
27244
|
-
};
|
|
27245
|
-
/**
|
|
27246
|
-
* Frozen, null-prototype builtin function table for the expression engine
|
|
27247
|
-
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
27248
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
27249
|
-
* own-property check against it.
|
|
27250
|
-
*
|
|
27251
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
27252
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
27253
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
27254
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
27255
|
-
* callable — they are simply "unknown function" at parse time.
|
|
27256
|
-
*
|
|
27257
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
27258
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
27259
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
27260
|
-
* closed rather than emitting a garbage value.
|
|
27261
|
-
*/
|
|
27262
|
-
function asFiniteNumber(value, name, index) {
|
|
27263
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
27264
|
-
return value;
|
|
27265
|
-
}
|
|
27266
|
-
function asString$1(value, name, index) {
|
|
27267
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
27268
|
-
return value;
|
|
27269
|
-
}
|
|
27270
|
-
function finiteResult(value, name) {
|
|
27271
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
27272
|
-
return value;
|
|
27273
|
-
}
|
|
27274
|
-
function allFiniteNumbers(args, name) {
|
|
27275
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
27276
|
-
}
|
|
27277
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
27278
|
-
var table = {
|
|
27279
|
-
min: {
|
|
27280
|
-
minArgs: 1,
|
|
27281
|
-
maxArgs: INF,
|
|
27282
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
27283
|
-
},
|
|
27284
|
-
max: {
|
|
27285
|
-
minArgs: 1,
|
|
27286
|
-
maxArgs: INF,
|
|
27287
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
27288
|
-
},
|
|
27289
|
-
abs: {
|
|
27290
|
-
minArgs: 1,
|
|
27291
|
-
maxArgs: 1,
|
|
27292
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
27293
|
-
},
|
|
27294
|
-
floor: {
|
|
27295
|
-
minArgs: 1,
|
|
27296
|
-
maxArgs: 1,
|
|
27297
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
27298
|
-
},
|
|
27299
|
-
ceil: {
|
|
27300
|
-
minArgs: 1,
|
|
27301
|
-
maxArgs: 1,
|
|
27302
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
27303
|
-
},
|
|
27304
|
-
sqrt: {
|
|
27305
|
-
minArgs: 1,
|
|
27306
|
-
maxArgs: 1,
|
|
27307
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
27308
|
-
},
|
|
27309
|
-
round: {
|
|
27310
|
-
minArgs: 1,
|
|
27311
|
-
maxArgs: 2,
|
|
27312
|
-
apply: (args) => {
|
|
27313
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
27314
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
27315
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
27316
|
-
const factor = 10 ** digits;
|
|
27317
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
27318
|
-
}
|
|
27319
|
-
},
|
|
27320
|
-
pow: {
|
|
27321
|
-
minArgs: 2,
|
|
27322
|
-
maxArgs: 2,
|
|
27323
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
27324
|
-
},
|
|
27325
|
-
clamp: {
|
|
27326
|
-
minArgs: 3,
|
|
27327
|
-
maxArgs: 3,
|
|
27328
|
-
apply: (args) => {
|
|
27329
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
27330
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
27331
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
27332
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
27333
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
27334
|
-
}
|
|
27335
|
-
},
|
|
27336
|
-
avg: {
|
|
27337
|
-
minArgs: 1,
|
|
27338
|
-
maxArgs: INF,
|
|
27339
|
-
apply: (args) => {
|
|
27340
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
27341
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
27342
|
-
}
|
|
27343
|
-
},
|
|
27344
|
-
sum: {
|
|
27345
|
-
minArgs: 1,
|
|
27346
|
-
maxArgs: INF,
|
|
27347
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
27348
|
-
},
|
|
27349
|
-
coalesce: {
|
|
27350
|
-
minArgs: 1,
|
|
27351
|
-
maxArgs: INF,
|
|
27352
|
-
apply: (args) => {
|
|
27353
|
-
for (const a of args) if (a !== null) return a;
|
|
27354
|
-
return null;
|
|
27355
|
-
}
|
|
27356
|
-
},
|
|
27357
|
-
age: {
|
|
27358
|
-
minArgs: 2,
|
|
27359
|
-
maxArgs: 2,
|
|
27360
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
27361
|
-
},
|
|
27362
|
-
convert: {
|
|
27363
|
-
minArgs: 3,
|
|
27364
|
-
maxArgs: 3,
|
|
27365
|
-
apply: (args, hooks) => {
|
|
27366
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
27367
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
27368
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
27369
|
-
if (hooks.convert) {
|
|
27370
|
-
const out = hooks.convert(x, from, to);
|
|
27371
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
27372
|
-
return finiteResult(out, "convert");
|
|
27373
|
-
}
|
|
27374
|
-
if (from === to) return x;
|
|
27375
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
27376
|
-
}
|
|
27377
|
-
}
|
|
27378
|
-
};
|
|
27379
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
27380
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
27381
|
-
* callees at parse time (immediate author feedback). */
|
|
27382
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
27383
|
-
/**
|
|
27384
|
-
* Resource-bound constants for the safe expression engine.
|
|
27385
|
-
*
|
|
27386
|
-
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
27387
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
27388
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
27389
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
27390
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
27391
|
-
*/
|
|
27392
|
-
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
27393
|
-
* rejected without allocation. */
|
|
27394
|
-
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
27395
|
-
/** A legal binding / identifier name. */
|
|
27396
|
-
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
27397
|
-
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
27398
|
-
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
27399
|
-
var RESERVED_BINDING_NAMES = new Set([
|
|
27400
|
-
"now",
|
|
27401
|
-
"true",
|
|
27402
|
-
"false",
|
|
27403
|
-
"null"
|
|
27404
|
-
]);
|
|
27405
|
-
/**
|
|
27406
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
27407
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
27408
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
27409
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
27410
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
27411
|
-
* is a parse error with a source position, so member access / assignment /
|
|
27412
|
-
* template literals are lexically impossible.
|
|
27413
|
-
*/
|
|
27414
|
-
var KEYWORDS = new Set([
|
|
27415
|
-
"true",
|
|
27416
|
-
"false",
|
|
27417
|
-
"null"
|
|
27418
|
-
]);
|
|
27419
|
-
function isDigit(ch) {
|
|
27420
|
-
return ch >= "0" && ch <= "9";
|
|
27421
|
-
}
|
|
27422
|
-
function isIdentStart(ch) {
|
|
27423
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
27424
|
-
}
|
|
27425
|
-
function isIdentPart(ch) {
|
|
27426
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
27427
|
-
}
|
|
27428
|
-
function isWhitespace(ch) {
|
|
27429
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
27430
|
-
}
|
|
27431
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
27432
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
27433
|
-
* string. */
|
|
27434
|
-
function tokenize(source) {
|
|
27435
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
27436
|
-
const tokens = [];
|
|
27437
|
-
let i = 0;
|
|
27438
|
-
const n = source.length;
|
|
27439
|
-
while (i < n) {
|
|
27440
|
-
const ch = source[i];
|
|
27441
|
-
if (isWhitespace(ch)) {
|
|
27442
|
-
i += 1;
|
|
27443
|
-
continue;
|
|
27444
|
-
}
|
|
27445
|
-
if (isDigit(ch)) {
|
|
27446
|
-
const start = i;
|
|
27447
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
27448
|
-
if (i < n && source[i] === ".") {
|
|
27449
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
27450
|
-
i += 1;
|
|
27451
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
27452
|
-
}
|
|
27453
|
-
const text = source.slice(start, i);
|
|
27454
|
-
const value = Number(text);
|
|
27455
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
27456
|
-
tokens.push({
|
|
27457
|
-
type: "number",
|
|
27458
|
-
value,
|
|
27459
|
-
pos: start
|
|
27460
|
-
});
|
|
27461
|
-
continue;
|
|
27462
|
-
}
|
|
27463
|
-
if (ch === "'" || ch === "\"") {
|
|
27464
|
-
const quote = ch;
|
|
27465
|
-
const start = i;
|
|
27466
|
-
i += 1;
|
|
27467
|
-
let out = "";
|
|
27468
|
-
let closed = false;
|
|
27469
|
-
while (i < n) {
|
|
27470
|
-
const c = source[i];
|
|
27471
|
-
if (c === "\\") {
|
|
27472
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
27473
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
27474
|
-
out += next;
|
|
27475
|
-
i += 2;
|
|
27476
|
-
continue;
|
|
27477
|
-
}
|
|
27478
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
27479
|
-
}
|
|
27480
|
-
if (c === quote) {
|
|
27481
|
-
closed = true;
|
|
27482
|
-
i += 1;
|
|
27483
|
-
break;
|
|
27484
|
-
}
|
|
27485
|
-
out += c;
|
|
27486
|
-
i += 1;
|
|
27487
|
-
}
|
|
27488
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
27489
|
-
tokens.push({
|
|
27490
|
-
type: "string",
|
|
27491
|
-
value: out,
|
|
27492
|
-
pos: start
|
|
27493
|
-
});
|
|
27494
|
-
continue;
|
|
27495
|
-
}
|
|
27496
|
-
if (isIdentStart(ch)) {
|
|
27497
|
-
const start = i;
|
|
27498
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
27499
|
-
const text = source.slice(start, i);
|
|
27500
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
27501
|
-
type: "keyword",
|
|
27502
|
-
keyword: keywordOf(text),
|
|
27503
|
-
pos: start
|
|
27504
|
-
});
|
|
27505
|
-
else tokens.push({
|
|
27506
|
-
type: "identifier",
|
|
27507
|
-
name: text,
|
|
27508
|
-
pos: start
|
|
27509
|
-
});
|
|
27510
|
-
continue;
|
|
27511
|
-
}
|
|
27512
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
27513
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
27514
|
-
tokens.push({
|
|
27515
|
-
type: "punct",
|
|
27516
|
-
punct: two,
|
|
27517
|
-
pos: i
|
|
27518
|
-
});
|
|
27519
|
-
i += 2;
|
|
27520
|
-
continue;
|
|
27521
|
-
}
|
|
27522
|
-
if (isSinglePunct(ch)) {
|
|
27523
|
-
tokens.push({
|
|
27524
|
-
type: "punct",
|
|
27525
|
-
punct: ch,
|
|
27526
|
-
pos: i
|
|
27527
|
-
});
|
|
27528
|
-
i += 1;
|
|
27529
|
-
continue;
|
|
27530
|
-
}
|
|
27531
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
27861
|
+
/**
|
|
27862
|
+
* Read the current feature-probe flag bag with a typed cast. Helper
|
|
27863
|
+
* for `getAccessoryChildren()` and `features` getters that derive
|
|
27864
|
+
* outputs from the probe results.
|
|
27865
|
+
*/
|
|
27866
|
+
getProbeFlags() {
|
|
27867
|
+
return this.runtimeState.getCapState("feature-probe")?.flags ?? {};
|
|
27532
27868
|
}
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
27546
|
-
}
|
|
27869
|
+
/**
|
|
27870
|
+
* Returns true once `onProbe` has completed at least once
|
|
27871
|
+
* (`lastProbedAt > 0`). Drivers gate `getAccessoryChildren()` on this
|
|
27872
|
+
* to avoid spawning stale accessories on a fresh device whose probe
|
|
27873
|
+
* hasn't landed yet.
|
|
27874
|
+
*/
|
|
27875
|
+
hasProbed() {
|
|
27876
|
+
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
27877
|
+
}
|
|
27878
|
+
};
|
|
27879
|
+
/** Marker written to a declared integration's `info`. */
|
|
27880
|
+
var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
|
|
27547
27881
|
/**
|
|
27548
|
-
*
|
|
27549
|
-
*
|
|
27550
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
27551
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
27552
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
27553
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
27554
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
27555
|
-
* that references a since-removed builtin degrades at read.
|
|
27882
|
+
* Strip the `<node>/<addon>` suffix a forked child carries.
|
|
27556
27883
|
*
|
|
27557
|
-
*
|
|
27558
|
-
*
|
|
27884
|
+
* Comparing `ctx.kernel.localNodeId` raw skipped EVERY node — including the one
|
|
27885
|
+
* that was supposed to act — because on the hub it reads `hub/<addon>`.
|
|
27559
27886
|
*/
|
|
27560
|
-
|
|
27561
|
-
|
|
27562
|
-
"
|
|
27563
|
-
"
|
|
27564
|
-
"==": 3,
|
|
27565
|
-
"!=": 3,
|
|
27566
|
-
"<": 4,
|
|
27567
|
-
"<=": 4,
|
|
27568
|
-
">": 4,
|
|
27569
|
-
">=": 4,
|
|
27570
|
-
"+": 5,
|
|
27571
|
-
"-": 5,
|
|
27572
|
-
"*": 6,
|
|
27573
|
-
"/": 6,
|
|
27574
|
-
"%": 6
|
|
27575
|
-
};
|
|
27576
|
-
function isLogicalOp(op) {
|
|
27577
|
-
return op === "&&" || op === "||";
|
|
27578
|
-
}
|
|
27579
|
-
function isBinaryOp(op) {
|
|
27580
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
27887
|
+
function declarationOwnerNodeId(localNodeId) {
|
|
27888
|
+
const raw = localNodeId ?? "hub";
|
|
27889
|
+
if (!raw.includes("/")) return raw;
|
|
27890
|
+
return raw.split("/")[0] ?? "hub";
|
|
27581
27891
|
}
|
|
27582
|
-
|
|
27583
|
-
|
|
27584
|
-
|
|
27585
|
-
|
|
27586
|
-
|
|
27587
|
-
|
|
27588
|
-
|
|
27589
|
-
|
|
27892
|
+
/**
|
|
27893
|
+
* The one way an addon owns a device it declares.
|
|
27894
|
+
*
|
|
27895
|
+
* Construct once with the addon's ports, then call {@link reconcile} on boot and
|
|
27896
|
+
* on every convergence tick. There is no second get-or-create helper — a guard
|
|
27897
|
+
* in `scripts/` enforces that.
|
|
27898
|
+
*/
|
|
27899
|
+
var DeclaredDevices = class {
|
|
27900
|
+
ports;
|
|
27901
|
+
constructor(ports) {
|
|
27902
|
+
const addonId = ports.addonId;
|
|
27903
|
+
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\`.`);
|
|
27904
|
+
this.ports = ports;
|
|
27590
27905
|
}
|
|
27591
|
-
|
|
27592
|
-
|
|
27593
|
-
|
|
27594
|
-
|
|
27906
|
+
/**
|
|
27907
|
+
* Converge the declared set. Idempotent, and safe to call repeatedly.
|
|
27908
|
+
*
|
|
27909
|
+
* Throws only what the ports throw on the FIRST index read; every other
|
|
27910
|
+
* failure is per-device and logged, so one bad declaration never takes the
|
|
27911
|
+
* others down.
|
|
27912
|
+
*/
|
|
27913
|
+
async reconcile(spec) {
|
|
27914
|
+
if ((spec.placement ?? "hub") === "hub") {
|
|
27915
|
+
const nodeId = declarationOwnerNodeId(this.ports.localNodeId);
|
|
27916
|
+
if (nodeId !== "hub") {
|
|
27917
|
+
this.ports.logger.info("declared devices are hub-owned — skipping on this node", { meta: {
|
|
27918
|
+
nodeId,
|
|
27919
|
+
rawNodeId: this.ports.localNodeId ?? null
|
|
27920
|
+
} });
|
|
27921
|
+
return {
|
|
27922
|
+
integrationId: null,
|
|
27923
|
+
devices: [],
|
|
27924
|
+
removed: [],
|
|
27925
|
+
owned: false
|
|
27926
|
+
};
|
|
27927
|
+
}
|
|
27928
|
+
}
|
|
27929
|
+
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
27930
|
+
const index = await this.readIndex();
|
|
27931
|
+
const outcomes = [];
|
|
27932
|
+
for (const declaration of spec.devices) {
|
|
27933
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
27934
|
+
if (outcome !== null) outcomes.push(outcome);
|
|
27935
|
+
}
|
|
27595
27936
|
return {
|
|
27596
|
-
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27937
|
+
integrationId,
|
|
27938
|
+
devices: outcomes,
|
|
27939
|
+
removed: await this.sweepWithdrawn(spec.devices, integrationId, index),
|
|
27940
|
+
owned: true
|
|
27600
27941
|
};
|
|
27601
27942
|
}
|
|
27602
|
-
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
|
|
27611
|
-
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
27620
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
this.nodeCount += 1;
|
|
27624
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
27625
|
-
}
|
|
27626
|
-
parseTernary() {
|
|
27627
|
-
const test = this.parseBinary(1);
|
|
27628
|
-
if (this.matchPunct("?")) {
|
|
27629
|
-
const consequent = this.parseTernary();
|
|
27630
|
-
this.expectPunct(":");
|
|
27631
|
-
const alternate = this.parseTernary();
|
|
27632
|
-
this.countNode();
|
|
27633
|
-
return {
|
|
27634
|
-
kind: "conditional",
|
|
27635
|
-
test,
|
|
27636
|
-
consequent,
|
|
27637
|
-
alternate
|
|
27638
|
-
};
|
|
27943
|
+
/**
|
|
27944
|
+
* Get-or-create the FIXED integration, and RE-ASSERT the flag every pass.
|
|
27945
|
+
*
|
|
27946
|
+
* The re-assertion is the fix for the defect the hand-rolled version shipped
|
|
27947
|
+
* with: writing `info.fixed` only on the create path left every pre-existing
|
|
27948
|
+
* install without it, and the kernel kept offering to delete an integration
|
|
27949
|
+
* the addon owns.
|
|
27950
|
+
*/
|
|
27951
|
+
async ensureIntegration(integrationName) {
|
|
27952
|
+
const existing = await this.ports.getIntegration(this.ports.addonId);
|
|
27953
|
+
if (existing === null) {
|
|
27954
|
+
const created = await this.ports.createIntegration({
|
|
27955
|
+
addonId: this.ports.addonId,
|
|
27956
|
+
name: integrationName,
|
|
27957
|
+
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
27958
|
+
});
|
|
27959
|
+
this.ports.logger.info("declared a fixed integration", { meta: {
|
|
27960
|
+
integrationId: created.id,
|
|
27961
|
+
name: integrationName
|
|
27962
|
+
} });
|
|
27963
|
+
return created.id;
|
|
27639
27964
|
}
|
|
27640
|
-
|
|
27641
|
-
|
|
27642
|
-
|
|
27643
|
-
|
|
27644
|
-
|
|
27645
|
-
|
|
27646
|
-
if (tok.type !== "punct") break;
|
|
27647
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
27648
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
27649
|
-
const op = tok.punct;
|
|
27650
|
-
this.pos += 1;
|
|
27651
|
-
const right = this.parseBinary(prec + 1);
|
|
27652
|
-
this.countNode();
|
|
27653
|
-
if (isLogicalOp(op)) left = {
|
|
27654
|
-
kind: "logical",
|
|
27655
|
-
op,
|
|
27656
|
-
left,
|
|
27657
|
-
right
|
|
27658
|
-
};
|
|
27659
|
-
else if (isBinaryOp(op)) left = {
|
|
27660
|
-
kind: "binary",
|
|
27661
|
-
op,
|
|
27662
|
-
left,
|
|
27663
|
-
right
|
|
27664
|
-
};
|
|
27665
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
27965
|
+
if (existing.info?.["fixed"] !== true) {
|
|
27966
|
+
await this.ports.updateIntegration({
|
|
27967
|
+
id: existing.id,
|
|
27968
|
+
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
27969
|
+
});
|
|
27970
|
+
this.ports.logger.info("re-asserted `fixed` on a declared integration", { meta: { integrationId: existing.id } });
|
|
27666
27971
|
}
|
|
27667
|
-
return
|
|
27972
|
+
return existing.id;
|
|
27668
27973
|
}
|
|
27669
|
-
|
|
27670
|
-
const
|
|
27671
|
-
|
|
27672
|
-
const op = tok.punct;
|
|
27673
|
-
this.pos += 1;
|
|
27674
|
-
const operand = this.parseUnary();
|
|
27675
|
-
this.countNode();
|
|
27676
|
-
return {
|
|
27677
|
-
kind: "unary",
|
|
27678
|
-
op,
|
|
27679
|
-
operand
|
|
27680
|
-
};
|
|
27681
|
-
}
|
|
27682
|
-
return this.parsePrimary();
|
|
27974
|
+
async readIndex() {
|
|
27975
|
+
const rows = await this.ports.listOwnDevices();
|
|
27976
|
+
return new Map(rows.map((row) => [row.stableId, row]));
|
|
27683
27977
|
}
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27700
|
-
|
|
27701
|
-
|
|
27702
|
-
|
|
27703
|
-
|
|
27704
|
-
};
|
|
27705
|
-
|
|
27706
|
-
|
|
27707
|
-
|
|
27708
|
-
this.
|
|
27709
|
-
|
|
27978
|
+
/**
|
|
27979
|
+
* One declaration: adopt what exists, create what does not.
|
|
27980
|
+
*
|
|
27981
|
+
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
27982
|
+
* `initialMeta.name` lands as an unconditional `setName`. A transiently empty
|
|
27983
|
+
* index therefore looks exactly like a first boot and would silently re-stamp
|
|
27984
|
+
* the declared name over the operator's rename. D49: that branch needs a
|
|
27985
|
+
* second read to agree.
|
|
27986
|
+
*/
|
|
27987
|
+
async applyDeclaration(declaration, integrationId, index) {
|
|
27988
|
+
try {
|
|
27989
|
+
let existing = index.get(declaration.stableId);
|
|
27990
|
+
if (existing === void 0) {
|
|
27991
|
+
existing = (await this.readIndex()).get(declaration.stableId);
|
|
27992
|
+
if (existing !== void 0) this.ports.logger.warn("device index disagreed with itself — adopting instead of re-creating", {
|
|
27993
|
+
tags: { deviceId: existing.id },
|
|
27994
|
+
meta: {
|
|
27995
|
+
stableId: declaration.stableId,
|
|
27996
|
+
addonId: this.ports.addonId
|
|
27997
|
+
}
|
|
27998
|
+
});
|
|
27999
|
+
}
|
|
28000
|
+
if (existing !== void 0) {
|
|
28001
|
+
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, {}, null, void 0);
|
|
28002
|
+
this.ports.logger.info("declared device adopted", {
|
|
28003
|
+
tags: { deviceId: device.id },
|
|
28004
|
+
meta: {
|
|
28005
|
+
stableId: declaration.stableId,
|
|
28006
|
+
integrationId
|
|
28007
|
+
}
|
|
28008
|
+
});
|
|
27710
28009
|
return {
|
|
27711
|
-
|
|
27712
|
-
|
|
28010
|
+
stableId: declaration.stableId,
|
|
28011
|
+
deviceId: device.id,
|
|
28012
|
+
device,
|
|
28013
|
+
created: false
|
|
27713
28014
|
};
|
|
27714
28015
|
}
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
|
|
28016
|
+
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, declaration.config ?? {}, null, {
|
|
28017
|
+
type: declaration.type,
|
|
28018
|
+
name: declaration.name,
|
|
28019
|
+
integrationId,
|
|
28020
|
+
...declaration.role === void 0 ? {} : { role: declaration.role }
|
|
28021
|
+
});
|
|
28022
|
+
this.ports.logger.info("declared device created", {
|
|
28023
|
+
tags: { deviceId: device.id },
|
|
28024
|
+
meta: {
|
|
28025
|
+
stableId: declaration.stableId,
|
|
28026
|
+
integrationId
|
|
27720
28027
|
}
|
|
27721
|
-
|
|
27722
|
-
|
|
28028
|
+
});
|
|
28029
|
+
return {
|
|
28030
|
+
stableId: declaration.stableId,
|
|
28031
|
+
deviceId: device.id,
|
|
28032
|
+
device,
|
|
28033
|
+
created: true
|
|
28034
|
+
};
|
|
28035
|
+
} catch (err) {
|
|
28036
|
+
this.ports.logger.warn("a declared device could not be brought up", { meta: {
|
|
28037
|
+
stableId: declaration.stableId,
|
|
28038
|
+
error: err instanceof Error ? err.message : String(err)
|
|
28039
|
+
} });
|
|
28040
|
+
return null;
|
|
27723
28041
|
}
|
|
27724
28042
|
}
|
|
27725
|
-
|
|
27726
|
-
|
|
27727
|
-
|
|
27728
|
-
|
|
27729
|
-
|
|
27730
|
-
|
|
27731
|
-
|
|
27732
|
-
|
|
27733
|
-
|
|
27734
|
-
|
|
28043
|
+
/**
|
|
28044
|
+
* Remove rows under the addon's FIXED integration whose declaration is gone.
|
|
28045
|
+
*
|
|
28046
|
+
* Bounded to that integration: a declared integration has no operator
|
|
28047
|
+
* add-flow, so every row under it got there by declaration. Devices this
|
|
28048
|
+
* addon owns OUTSIDE it (a provider's adopted devices) are never candidates.
|
|
28049
|
+
*
|
|
28050
|
+
* Bounded in count, and every deletion is logged with its `deviceId` — a
|
|
28051
|
+
* withdrawal that removes an operator-visible row silently is the failure
|
|
28052
|
+
* mode, not the removal itself.
|
|
28053
|
+
*/
|
|
28054
|
+
async sweepWithdrawn(declarations, integrationId, index) {
|
|
28055
|
+
const declared = new Set(declarations.map((d) => d.stableId));
|
|
28056
|
+
const candidates = [...index.values()].filter((row) => row.integrationId === integrationId && !declared.has(row.stableId));
|
|
28057
|
+
if (candidates.length === 0) return [];
|
|
28058
|
+
if (candidates.length > 32) {
|
|
28059
|
+
this.ports.logger.warn("withdrawal sweep exceeded its bound — removing nothing", { meta: {
|
|
28060
|
+
integrationId,
|
|
28061
|
+
candidates: candidates.length,
|
|
28062
|
+
bound: 32
|
|
28063
|
+
} });
|
|
28064
|
+
return [];
|
|
27735
28065
|
}
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
28066
|
+
const removed = [];
|
|
28067
|
+
for (const row of candidates) try {
|
|
28068
|
+
await this.ports.devices.remove(row.id);
|
|
28069
|
+
removed.push(row.id);
|
|
28070
|
+
this.ports.logger.info("declared device removed — its declaration was withdrawn", {
|
|
28071
|
+
tags: { deviceId: row.id },
|
|
28072
|
+
meta: {
|
|
28073
|
+
stableId: row.stableId,
|
|
28074
|
+
integrationId
|
|
28075
|
+
}
|
|
28076
|
+
});
|
|
28077
|
+
} catch (err) {
|
|
28078
|
+
this.ports.logger.warn("a withdrawn declared device could not be removed", {
|
|
28079
|
+
tags: { deviceId: row.id },
|
|
28080
|
+
meta: {
|
|
28081
|
+
stableId: row.stableId,
|
|
28082
|
+
error: err instanceof Error ? err.message : String(err)
|
|
28083
|
+
}
|
|
28084
|
+
});
|
|
28085
|
+
}
|
|
28086
|
+
return removed;
|
|
27743
28087
|
}
|
|
27744
28088
|
};
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
|
|
27748
|
-
return new Parser(tokenize(source)).parse();
|
|
27749
|
-
}
|
|
27750
|
-
/**
|
|
27751
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
27752
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
27753
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
27754
|
-
* one per read on a hot resolve path.
|
|
27755
|
-
*
|
|
27756
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
27757
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
27758
|
-
* callers is safe and maximises hit rate.
|
|
27759
|
-
*/
|
|
27760
|
-
var cache = /* @__PURE__ */ new Map();
|
|
27761
|
-
function getCached(source) {
|
|
27762
|
-
const hit = cache.get(source);
|
|
27763
|
-
if (hit !== void 0) {
|
|
27764
|
-
cache.delete(source);
|
|
27765
|
-
cache.set(source, hit);
|
|
27766
|
-
return hit;
|
|
27767
|
-
}
|
|
27768
|
-
let result;
|
|
27769
|
-
try {
|
|
27770
|
-
result = {
|
|
27771
|
-
ok: true,
|
|
27772
|
-
parsed: parseExpression(source)
|
|
27773
|
-
};
|
|
27774
|
-
} catch (err) {
|
|
27775
|
-
result = {
|
|
27776
|
-
ok: false,
|
|
27777
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
27778
|
-
};
|
|
27779
|
-
}
|
|
27780
|
-
cache.set(source, result);
|
|
27781
|
-
if (cache.size > 256) {
|
|
27782
|
-
const oldest = cache.keys().next().value;
|
|
27783
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
27784
|
-
}
|
|
27785
|
-
return result;
|
|
27786
|
-
}
|
|
27787
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
27788
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
27789
|
-
function compileExpressionSafe(source) {
|
|
27790
|
-
return getCached(source);
|
|
27791
|
-
}
|
|
27792
|
-
Object.freeze({});
|
|
27793
|
-
/**
|
|
27794
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
27795
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
27796
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
27797
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
27798
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
27799
|
-
*/
|
|
27800
|
-
function validateExpressionSource(src) {
|
|
27801
|
-
const names = Object.keys(src.bindings);
|
|
27802
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
27803
|
-
for (const name of names) {
|
|
27804
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
27805
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
27806
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
27807
|
-
}
|
|
27808
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
27809
|
-
if (!compiled.ok) return compiled.error;
|
|
27810
|
-
const bound = new Set(names);
|
|
27811
|
-
for (const id of compiled.parsed.identifiers) {
|
|
27812
|
-
if (id === "now") continue;
|
|
27813
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
27814
|
-
}
|
|
27815
|
-
return null;
|
|
27816
|
-
}
|
|
27817
|
-
var ExpressionBindingSourceSchema = union([
|
|
27818
|
-
object({
|
|
27819
|
-
kind: literal("field").optional(),
|
|
27820
|
-
sourceKey: string(),
|
|
27821
|
-
cap: string(),
|
|
27822
|
-
fieldPath: string()
|
|
27823
|
-
}),
|
|
27824
|
-
object({
|
|
27825
|
-
kind: literal("literal"),
|
|
27826
|
-
value: union([
|
|
27827
|
-
string(),
|
|
27828
|
-
number(),
|
|
27829
|
-
boolean(),
|
|
27830
|
-
_null()
|
|
27831
|
-
])
|
|
27832
|
-
}),
|
|
27833
|
-
object({
|
|
27834
|
-
kind: literal("global"),
|
|
27835
|
-
sourceStableId: string(),
|
|
27836
|
-
cap: string(),
|
|
27837
|
-
fieldPath: string()
|
|
27838
|
-
})
|
|
27839
|
-
]);
|
|
27840
|
-
object({
|
|
27841
|
-
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
27842
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
27843
|
-
}).superRefine((src, ctx) => {
|
|
27844
|
-
const err = validateExpressionSource(src);
|
|
27845
|
-
if (err !== null) ctx.addIssue({
|
|
27846
|
-
code: "custom",
|
|
27847
|
-
message: err,
|
|
27848
|
-
path: ["expr"]
|
|
27849
|
-
});
|
|
27850
|
-
});
|
|
28089
|
+
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;
|
|
28090
|
+
new Set(Object.values(DeviceType));
|
|
28091
|
+
DeviceFeature.BatteryOperated;
|
|
27851
28092
|
Object.freeze({
|
|
27852
28093
|
"accessories.setChildHidden": {
|
|
27853
28094
|
capName: "accessories",
|
|
@@ -32181,6 +32422,12 @@ Object.freeze({
|
|
|
32181
32422
|
addonId: null,
|
|
32182
32423
|
access: "view"
|
|
32183
32424
|
},
|
|
32425
|
+
"snapshot.getSnapshotLinks": {
|
|
32426
|
+
capName: "snapshot",
|
|
32427
|
+
capScope: "device",
|
|
32428
|
+
addonId: null,
|
|
32429
|
+
access: "view"
|
|
32430
|
+
},
|
|
32184
32431
|
"snapshot.getSnapshotOverview": {
|
|
32185
32432
|
capName: "snapshot",
|
|
32186
32433
|
capScope: "device",
|
|
@@ -33712,6 +33959,12 @@ Object.defineProperty(exports, "TrackSourceSchema", {
|
|
|
33712
33959
|
return TrackSourceSchema;
|
|
33713
33960
|
}
|
|
33714
33961
|
});
|
|
33962
|
+
Object.defineProperty(exports, "__exportAll", {
|
|
33963
|
+
enumerable: true,
|
|
33964
|
+
get: function() {
|
|
33965
|
+
return __exportAll;
|
|
33966
|
+
}
|
|
33967
|
+
});
|
|
33715
33968
|
Object.defineProperty(exports, "__toESM", {
|
|
33716
33969
|
enumerable: true,
|
|
33717
33970
|
get: function() {
|