@camstack/types 1.2.429 → 1.2.431

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/addon.js +4 -3
  3. package/dist/addon.mjs +4 -3
  4. package/dist/automation/action.d.ts +210 -0
  5. package/dist/automation/condition-eval.d.ts +64 -0
  6. package/dist/automation/detection-trigger.d.ts +66 -0
  7. package/dist/automation/index.d.ts +12 -2
  8. package/dist/automation/recipe.d.ts +103 -1
  9. package/dist/automation/run-gate.d.ts +43 -0
  10. package/dist/automation/step-runner.d.ts +82 -0
  11. package/dist/automation/trace.d.ts +325 -0
  12. package/dist/capabilities/action-source.cap.d.ts +74 -0
  13. package/dist/capabilities/actions.cap.d.ts +459 -0
  14. package/dist/capabilities/core-blocks.cap.d.ts +761 -0
  15. package/dist/capabilities/index.d.ts +5 -1
  16. package/dist/client.js +73 -49
  17. package/dist/client.mjs +73 -49
  18. package/dist/enums/event-category.d.ts +5 -0
  19. package/dist/enums.js +1 -1
  20. package/dist/enums.mjs +1 -1
  21. package/dist/{event-category-Ave2VSxY.js → event-category-BhzwWQCH.js} +5 -0
  22. package/dist/{event-category-CT-hz5Rb.mjs → event-category-hdMR7CTW.mjs} +5 -0
  23. package/dist/generated/addon-api.d.ts +135 -0
  24. package/dist/generated/capability-router-map.d.ts +10 -4
  25. package/dist/generated/collection-array-methods.d.ts +1 -1
  26. package/dist/generated/device-proxy.d.ts +2 -0
  27. package/dist/generated/method-access-map.d.ts +1 -1
  28. package/dist/generated/method-device-selectors.d.ts +2 -2
  29. package/dist/generated/system-proxy.d.ts +5 -1
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.js +2849 -579
  32. package/dist/index.mjs +2792 -580
  33. package/dist/interfaces/config-ui.d.ts +7 -0
  34. package/dist/{sleep-hRhTSDlN.js → sleep-5zRztaHa.js} +12 -2
  35. package/dist/{sleep-BE7IAR2p.mjs → sleep-BHjNez61.mjs} +12 -2
  36. package/dist/vendor-login-proxy/index.d.ts +2 -0
  37. package/dist/vendor-login-proxy/login-proxy.d.ts +55 -0
  38. package/dist/vendor-login-proxy/url-rewrite.d.ts +68 -0
  39. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-hRhTSDlN.js");
3
- const require_event_category = require("./event-category-Ave2VSxY.js");
2
+ const require_sleep = require("./sleep-5zRztaHa.js");
3
+ const require_event_category = require("./event-category-BhzwWQCH.js");
4
4
  const require_composition = require("./composition-CpAc0YfQ.js");
5
5
  const require_node_color = require("./node-color.js");
6
6
  const require_err_msg = require("./err-msg-U7lLUMNz.js");
@@ -8074,7 +8074,7 @@ var ExpressionSourceSchema = zod.z.object({
8074
8074
  *
8075
8075
  * Three vocabularies, each salvaged rather than invented:
8076
8076
  *
8077
- * - **Triggers** — the only genuinely new surface. Four members, none of which
8077
+ * - **Triggers** — the only genuinely new surface. Six members, none of which
8078
8078
  * touches notification delivery. The NC keeps its four *pipeline* triggers;
8079
8079
  * these are device/time triggers and the two sets do not overlap.
8080
8080
  * - **Conditions** — `ALL`/`ANY`/`NOT` over two leaf kinds, bounded. The
@@ -8198,7 +8198,23 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
8198
8198
  kind: zod.z.literal("schedule"),
8199
8199
  cron: zod.z.string().min(1).max(120)
8200
8200
  }),
8201
- zod.z.object({ kind: zod.z.literal("manual") })
8201
+ zod.z.object({ kind: zod.z.literal("manual") }),
8202
+ zod.z.object({
8203
+ kind: zod.z.literal("detection"),
8204
+ /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
8205
+ * matches every raw class that maps to it; any other name matches
8206
+ * exactly. */
8207
+ classes: zod.z.array(zod.z.string().min(1)).min(1),
8208
+ deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
8209
+ /** The track must have visited at least one of these zones. */
8210
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8211
+ /** Best-so-far confidence the track must reach. */
8212
+ minScore: zod.z.number().min(0).max(1).optional()
8213
+ }),
8214
+ zod.z.object({
8215
+ kind: zod.z.literal("interval"),
8216
+ minutes: zod.z.number().int().min(1).max(1440)
8217
+ })
8202
8218
  ]);
8203
8219
  /**
8204
8220
  * One action step.
@@ -8218,28 +8234,47 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
8218
8234
  * is called. The runner still receives a number and is untouched; the
8219
8235
  * resolution is the recipe's job, not the runner's.
8220
8236
  */
8237
+ var AutomationWaitStepSchema = zod.z.object({
8238
+ kind: zod.z.literal("wait"),
8239
+ seconds: zod.z.number().min(0).max(300)
8240
+ });
8241
+ var AutomationCapStepSchema = zod.z.object({
8242
+ kind: zod.z.literal("cap"),
8243
+ /**
8244
+ * One device, several same-family devices (locks / climate family / …),
8245
+ * or `{ $var }` for the device that triggered the run. An array is
8246
+ * fan-out at codegen — each id gets the same cap.method call.
8247
+ */
8248
+ deviceId: zod.z.union([
8249
+ zod.z.number().int(),
8250
+ zod.z.array(zod.z.number().int()).min(1),
8251
+ zod.z.object({ $var: zod.z.string().min(1) })
8252
+ ]),
8253
+ cap: zod.z.string().min(1),
8254
+ method: zod.z.string().min(1),
8255
+ /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
8256
+ * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
8257
+ * the expression leaf. */
8258
+ args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
8259
+ });
8260
+ /**
8261
+ * The steps an internal Action may contain: cap and wait, and nothing that can
8262
+ * call back into an Action — so an Action can never reach itself (no cycles).
8263
+ */
8264
+ var ActionStepSchema = zod.z.discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
8221
8265
  var AutomationActionSchema = zod.z.discriminatedUnion("kind", [
8266
+ AutomationWaitStepSchema,
8267
+ AutomationCapStepSchema,
8222
8268
  zod.z.object({
8223
- kind: zod.z.literal("wait"),
8224
- seconds: zod.z.number().min(0).max(300)
8225
- }),
8226
- zod.z.object({
8227
- kind: zod.z.literal("cap"),
8228
- /**
8229
- * One device, several same-family devices (locks / climate family / …),
8230
- * or `{ $var }` for the device that triggered the run. An array is
8231
- * fan-out at codegen — each id gets the same cap.method call.
8232
- */
8233
- deviceId: zod.z.union([
8234
- zod.z.number().int(),
8235
- zod.z.array(zod.z.number().int()).min(1),
8236
- zod.z.object({ $var: zod.z.string().min(1) })
8237
- ]),
8238
- cap: zod.z.string().min(1),
8239
- method: zod.z.string().min(1),
8240
- /** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
8241
- * evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
8242
- args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
8269
+ kind: zod.z.literal("action"),
8270
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
8271
+ actionId: zod.z.string().min(1),
8272
+ /** Values for the action's declared variables; the others take their default. */
8273
+ variables: zod.z.record(zod.z.string(), zod.z.union([
8274
+ zod.z.string(),
8275
+ zod.z.number(),
8276
+ zod.z.boolean()
8277
+ ])).optional()
8243
8278
  }),
8244
8279
  zod.z.object({
8245
8280
  kind: zod.z.literal("code"),
@@ -8248,10 +8283,45 @@ var AutomationActionSchema = zod.z.discriminatedUnion("kind", [
8248
8283
  code: zod.z.string().min(1).max(2e4)
8249
8284
  })
8250
8285
  ]);
8286
+ /**
8287
+ * How a trigger that arrives while the automation is busy (or cooling down) is
8288
+ * handled. One of the automation's execution modes, never a property of its
8289
+ * actions. Absent means `single`.
8290
+ *
8291
+ * - `single` — drop the trigger while a run is in progress (traced + logged).
8292
+ * - `restart` — abort the run in progress (at its next step boundary), start fresh.
8293
+ * - `queued` — queue up to `max` triggers, drop the rest (traced).
8294
+ * - `parallel` — up to `max` runs at once, drop the rest (traced).
8295
+ * - `cooldown` — `single`, and no new run until `minutes` after the last run
8296
+ * STARTED; persisted across a restart. A manual trigger bypasses only this
8297
+ * wait, never the single/queued limits.
8298
+ */
8299
+ var AutomationRunModeSchema = zod.z.discriminatedUnion("mode", [
8300
+ zod.z.object({ mode: zod.z.literal("single") }),
8301
+ zod.z.object({ mode: zod.z.literal("restart") }),
8302
+ zod.z.object({
8303
+ mode: zod.z.literal("queued"),
8304
+ max: zod.z.number().int().min(1).max(10)
8305
+ }),
8306
+ zod.z.object({
8307
+ mode: zod.z.literal("parallel"),
8308
+ max: zod.z.number().int().min(1).max(10)
8309
+ }),
8310
+ zod.z.object({
8311
+ mode: zod.z.literal("cooldown"),
8312
+ minutes: zod.z.number().int().min(1).max(1440)
8313
+ })
8314
+ ]);
8315
+ /** Run traces kept per automation when the recipe does not say. */
8316
+ var DEFAULT_TRACE_KEEP = 20;
8317
+ var MAX_TRACE_KEEP = 100;
8251
8318
  var AutomationRecipeSchema = zod.z.object({
8252
8319
  triggers: zod.z.array(AutomationTriggerSchema),
8253
8320
  conditions: AutomationConditionSchema.optional(),
8254
- actions: zod.z.array(AutomationActionSchema)
8321
+ actions: zod.z.array(AutomationActionSchema),
8322
+ runMode: AutomationRunModeSchema.optional(),
8323
+ /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
8324
+ keepTraces: zod.z.number().int().min(1).max(100).optional()
8255
8325
  });
8256
8326
  /** Depth of a tree, root container = 1. A bare leaf is depth 1. */
8257
8327
  function conditionDepth(cond) {
@@ -8303,19 +8373,23 @@ function validateRecipeBounds(recipe) {
8303
8373
  /** How often the state mirror is re-read as the backstop for a dropped event
8304
8374
  * (D8: an event WAKES the evaluation, the reconcile guarantees it). */
8305
8375
  var RECONCILE_MS = 15e3;
8376
+ /** What a step trace shows before the step runs. */
8377
+ function stepMetaOf(action) {
8378
+ switch (action.kind) {
8379
+ case "wait": return { kind: "wait" };
8380
+ case "code": return { kind: "code" };
8381
+ case "action": return { kind: "action" };
8382
+ case "cap": return {
8383
+ kind: "cap",
8384
+ cap: action.cap,
8385
+ method: action.method,
8386
+ ...typeof action.deviceId === "number" ? { deviceId: action.deviceId } : {}
8387
+ };
8388
+ }
8389
+ }
8306
8390
  function j(value) {
8307
8391
  return JSON.stringify(value);
8308
8392
  }
8309
- /** Emit the boolean tree as calls into the generated combinators. */
8310
- function emitCondition(cond, indent) {
8311
- switch (cond.kind) {
8312
- case "all": return `allOf([\n${cond.children.map((c) => `${indent} () => ${emitCondition(c, `${indent} `)}`).join(",\n")}\n${indent}])`;
8313
- case "any": return `anyOf([\n${cond.children.map((c) => `${indent} () => ${emitCondition(c, `${indent} `)}`).join(",\n")}\n${indent}])`;
8314
- case "not": return `notOf(() => ${emitCondition(cond.child, indent)})`;
8315
- case "condition": return `leaf(${j(cond.deviceId)}, ${j(cond.cap)}, ${j(cond.fieldPath)}, ${j(cond.operator)}, ${j(cond.value)})`;
8316
- case "expression": return `exprLeaf(${j(cond.expr)}, ${j(cond.bindings)})`;
8317
- }
8318
- }
8319
8393
  function emitTrigger(trigger, index) {
8320
8394
  switch (trigger.kind) {
8321
8395
  case "schedule": return ` // trigger ${index}: schedule — armed to the next minute boundary and
@@ -8391,31 +8465,79 @@ function emitTrigger(trigger, index) {
8391
8465
  }`;
8392
8466
  }
8393
8467
  case "manual": return ` // trigger ${index}: manual — fired by automation-control.trigger
8394
- // ("Run actions"), which runs the THEN half with an EMPTY vars bag.`;
8468
+ // ("Run actions"), which runs the THEN half with an EMPTY vars bag. A manual
8469
+ // run bypasses only the cooldown wait, never the single/queued limits.`;
8470
+ case "interval": return ` // trigger ${index}: interval — every ${trigger.minutes} min, first run jittered so
8471
+ // blocks restarted together do not all fire on the same second.
8472
+ {
8473
+ let interval: ReturnType<typeof setInterval> | undefined
8474
+ const first = setTimeout(() => {
8475
+ void run({ kind: 'interval', at: Date.now() })
8476
+ interval = setInterval(() => { void run({ kind: 'interval', at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
8477
+ }, Math.floor(Math.random() * Math.min(${j(trigger.minutes * 6e4)}, 30_000)) + 1)
8478
+ disposers.push(() => {
8479
+ clearTimeout(first)
8480
+ if (interval !== undefined) clearInterval(interval)
8481
+ })
8482
+ }`;
8483
+ case "detection": return ` // trigger ${index}: detection — ONE fire per (device, track); a rejected event is
8484
+ // traced with its reason and never fires.
8485
+ {
8486
+ const filter = new DetectionTriggerFilter(${j(trigger)})
8487
+ disposers.push(
8488
+ ctx.eventBus.subscribe(
8489
+ { category: EventCategory.PipelineAnalyticsTrackLifecycle },
8490
+ (event) => {
8491
+ const decision = filter.evaluate(event.data, Date.now())
8492
+ if (decision.kind === 'ignore') return
8493
+ if (decision.kind === 'reject') {
8494
+ ctx.logger.debug('detection trigger rejected an event', {
8495
+ tags: { deviceId: decision.vars.deviceId },
8496
+ reason: decision.reason,
8497
+ })
8498
+ }
8499
+ void run(
8500
+ { kind: 'detection', at: Date.now(), ...decision.vars },
8501
+ decision.reason,
8502
+ decision.kind === 'fire',
8503
+ )
8504
+ },
8505
+ ),
8506
+ )
8507
+ }`;
8395
8508
  }
8396
8509
  }
8397
8510
  function emitAction(action, index) {
8511
+ const step = (body) => ` {
8512
+ meta: STEP_META[${index}]!,
8513
+ run: async (scope) => {
8514
+ ${body}
8515
+ },
8516
+ },`;
8398
8517
  switch (action.kind) {
8399
- case "wait": return ` case ${index}: await sleep(${j(action.seconds * 1e3)}); break`;
8518
+ case "wait": return step(` await sleep(${j(action.seconds * 1e3)})`);
8400
8519
  case "cap":
8401
- if (Array.isArray(action.deviceId)) return ` case ${index}: {
8402
- for (const deviceId of ${j(action.deviceId)}) {
8520
+ if (Array.isArray(action.deviceId)) return step(` for (const deviceId of ${j(action.deviceId)}) {
8521
+ scope.setDeviceId(deviceId)
8403
8522
  await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
8404
- }
8405
- break
8406
- }`;
8407
- return ` case ${index}: {
8408
- const deviceId = ${typeof action.deviceId === "number" ? j(action.deviceId) : `resolveTarget(vars, ${j(action.deviceId.$var)})`}
8409
- await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
8410
- break
8411
- }`;
8412
- case "code": return ` case ${index}: {
8413
- // inline code action — a fragment of THIS block, bundled by esbuild
8523
+ }`);
8524
+ return step(` const deviceId = ${typeof action.deviceId === "number" ? j(action.deviceId) : `resolveTarget(vars, ${j(action.deviceId.$var)})`}
8525
+ scope.setDeviceId(deviceId)
8526
+ await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
8527
+ case "action": return step(` // an Action run, awaited: the step takes THAT run's outcome
8528
+ const result = await ctx.api.actions.run.mutate({
8529
+ actionId: ${j(action.actionId)},
8530
+ variables: substituteVariables(${j(action.variables ?? {})}, vars),
8531
+ wait: true,
8532
+ origin: { blockId: BLOCK_ID ?? undefined },
8533
+ })
8534
+ if (result.outcome !== 'ran') {
8535
+ throw new Error('action ' + ${j(action.actionId)} + ' ' + result.outcome + (result.message === undefined ? '' : ': ' + result.message))
8536
+ }`);
8537
+ case "code": return step(` // inline code action — a fragment of THIS block, bundled by esbuild
8414
8538
  await (async () => {
8415
8539
  ${action.code.split("\n").map((line) => ` ${line}`).join("\n")}
8416
- })()
8417
- break
8418
- }`;
8540
+ })()`);
8419
8541
  }
8420
8542
  }
8421
8543
  /**
@@ -8429,9 +8551,31 @@ function generateAutomationBlock(input) {
8429
8551
  const boundsError = validateRecipeBounds(input.recipe);
8430
8552
  if (boundsError !== null) throw new Error(`cannot generate an automation from an invalid recipe: ${boundsError}`);
8431
8553
  const { name, recipe } = input;
8432
- const conditionExpr = recipe.conditions === void 0 ? "true" : `await ${emitCondition(recipe.conditions, " ")}`;
8433
- return `import type { AddonContext } from '@camstack/types'
8434
- import { evaluateExpressionSource, toExpressionValue } from '@camstack/types'
8554
+ const runMode = recipe.runMode ?? { mode: "single" };
8555
+ const stepMeta = recipe.actions.map(stepMetaOf);
8556
+ return `import type {
8557
+ AddonContext,
8558
+ AutomationConditionTraceNode,
8559
+ AutomationRunTrace,
8560
+ AutomationRunVerdict,
8561
+ AutomationStepTrace,
8562
+ RunnableStep,
8563
+ } from '@camstack/types'
8564
+ import {
8565
+ ConditionMirror,
8566
+ DetectionTriggerFilter,
8567
+ EventCategory,
8568
+ RunGate,
8569
+ buildTriggerTrace,
8570
+ collectMirrorSources,
8571
+ coreBlockIdFromAddonId,
8572
+ evaluateConditionTree,
8573
+ isSkippedTrace,
8574
+ mergeSkipTraces,
8575
+ resolveDeviceTarget,
8576
+ resolveStepArgs,
8577
+ runSteps,
8578
+ } from '@camstack/types'
8435
8579
 
8436
8580
  /**
8437
8581
  * ${name}
@@ -8446,6 +8590,20 @@ const AUTOMATION_RECIPE = ${JSON.stringify(recipe, null, 2)} as const
8446
8590
 
8447
8591
  const RECONCILE_MS = ${RECONCILE_MS}
8448
8592
 
8593
+ /** How triggers that arrive while busy (or cooling down) are handled. */
8594
+ const RUN_MODE = ${j(runMode)} as const
8595
+
8596
+ /** Run traces kept (a ring, held in the hub's runs ledger). */
8597
+ const KEEP_TRACES = ${recipe.keepTraces ?? 20}
8598
+
8599
+ const CONDITIONS = ${recipe.conditions === void 0 ? "undefined" : "AUTOMATION_RECIPE.conditions"}
8600
+
8601
+ /** Skips that repeat are folded into one trace; it goes to the hub after this. */
8602
+ const SKIP_FLUSH_MS = 5_000
8603
+
8604
+ /** Static facts about each action step, for its trace. */
8605
+ const STEP_META: ReadonlyArray<{ kind: string; cap?: string; method?: string; deviceId?: number }> = ${j(stepMeta)}
8606
+
8449
8607
  /** A cap is named \`alarm-panel\`; \`ctx.api\` is keyed by the ROUTER name,
8450
8608
  * \`alarmPanel\`. Indexing ctx.api with the cap name yields undefined and every
8451
8609
  * action throws, so the conversion is not cosmetic. */
@@ -8496,137 +8654,170 @@ function matchField(field: string, value: number): boolean {
8496
8654
 
8497
8655
  export default function block(ctx: AddonContext) {
8498
8656
  const disposers: Array<() => void> = []
8499
- const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))
8657
+ const gate = new RunGate(RUN_MODE)
8658
+ const mirror = new ConditionMirror(collectMirrorSources(CONDITIONS))
8659
+ const BLOCK_ID = coreBlockIdFromAddonId(ctx.id)
8660
+ /** The cooldown stamp: memory is the authority at fire time (D49); the hub ledger is read once at start. */
8661
+ let lastRunStartedAt: number | null = null
8662
+ /** True when the stamp could not be read at start — the first run is let through and says so. */
8663
+ let stampUnknown = false
8664
+ let sendChain: Promise<void> = Promise.resolve()
8665
+ let pendingSkip: AutomationRunTrace | undefined
8666
+ let skipTimer: ReturnType<typeof setTimeout> | undefined
8667
+ let stopped = false
8668
+ let runCounter = 0
8669
+ const runningTokens = new Set<RunToken>()
8670
+ const queue: QueuedRun[] = []
8500
8671
 
8501
- /** Read one cap field off the state mirror. Returns undefined when the device,
8502
- * the cap or the field cannot be resolved — the caller decides what that
8503
- * means, and it always logs. */
8504
- async function readField(
8505
- deviceId: number,
8506
- cap: string,
8507
- fieldPath: string,
8508
- ): Promise<unknown> {
8509
- try {
8510
- const slice = await ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap })
8511
- let cursor: unknown = slice
8512
- for (const part of fieldPath.split('.')) {
8513
- if (cursor === null || typeof cursor !== 'object') return undefined
8514
- cursor = (cursor as Record<string, unknown>)[part]
8672
+ interface RunToken {
8673
+ aborted: boolean
8674
+ wake?: () => void
8675
+ }
8676
+ interface QueuedRun {
8677
+ readonly trigger: Record<string, unknown>
8678
+ readonly reason: string
8679
+ readonly queuedAt: number
8680
+ }
8681
+
8682
+ /** Sleep that a restart can cut short. */
8683
+ const abortableSleep = (ms: number, token: RunToken): Promise<void> =>
8684
+ new Promise((resolve) => {
8685
+ const timer = setTimeout(resolve, ms)
8686
+ token.wake = () => {
8687
+ clearTimeout(timer)
8688
+ resolve()
8515
8689
  }
8516
- return cursor
8517
- } catch {
8518
- return undefined
8690
+ })
8691
+
8692
+ // ── the hub's runs ledger: the stamp is read ONCE here, the ring is written through RPC ──
8693
+ async function loadState(): Promise<void> {
8694
+ if (BLOCK_ID === null) {
8695
+ ctx.logger.warn('this is not a core-block addon id — no run ledger, cooldown and traces will not survive a restart', { addonId: ctx.id })
8696
+ return
8697
+ }
8698
+ try {
8699
+ const doc = await ctx.api.coreBlocks.getAutomationRunState.query({ blockId: BLOCK_ID })
8700
+ lastRunStartedAt = doc.lastRunStartedAt
8701
+ } catch (err: unknown) {
8702
+ stampUnknown = true
8703
+ ctx.logger.warn('could not read the stored cooldown stamp — the first run is let through', {
8704
+ blockId: BLOCK_ID,
8705
+ error: err instanceof Error ? err.message : String(err),
8706
+ })
8519
8707
  }
8520
8708
  }
8521
8709
 
8522
- function compare(actual: unknown, operator: string, expected: unknown): boolean {
8523
- switch (operator) {
8524
- case 'eq': return actual === expected
8525
- case 'ne': return actual !== expected
8526
- case 'gt': return typeof actual === 'number' && typeof expected === 'number' && actual > expected
8527
- case 'gte': return typeof actual === 'number' && typeof expected === 'number' && actual >= expected
8528
- case 'lt': return typeof actual === 'number' && typeof expected === 'number' && actual < expected
8529
- case 'lte': return typeof actual === 'number' && typeof expected === 'number' && actual <= expected
8530
- case 'contains': return typeof actual === 'string' && typeof expected === 'string' && actual.includes(expected)
8531
- case 'in': return Array.isArray(expected) && expected.some((v) => v === actual)
8532
- default: return false
8533
- }
8710
+ function send(payload: { trace?: AutomationRunTrace; runStartedAt?: number }): Promise<void> {
8711
+ if (BLOCK_ID === null) return Promise.resolve()
8712
+ const blockId = BLOCK_ID
8713
+ sendChain = sendChain.then(async () => {
8714
+ try {
8715
+ await ctx.api.coreBlocks.reportAutomationRun.mutate({ blockId, keepTraces: KEEP_TRACES, ...payload })
8716
+ } catch (err: unknown) {
8717
+ ctx.logger.warn('could not report the run to the hub ledger', {
8718
+ blockId,
8719
+ error: err instanceof Error ? err.message : String(err),
8720
+ })
8721
+ }
8722
+ })
8723
+ return sendChain
8534
8724
  }
8535
8725
 
8536
- /** A leaf whose device or cap is currently unresolvable is FALSE and logs.
8537
- * It never throws the pass away, and it never counts as true. */
8538
- async function leaf(
8539
- deviceId: number,
8540
- cap: string,
8541
- fieldPath: string,
8542
- operator: string,
8543
- expected: unknown,
8544
- ): Promise<boolean> {
8545
- const actual = await readField(deviceId, cap, fieldPath)
8546
- if (actual === undefined) {
8547
- ctx.logger.warn('condition leaf unresolvable — treated as false', {
8548
- tags: { deviceId },
8549
- cap,
8550
- fieldPath,
8551
- })
8552
- return false
8726
+ function flushSkip(): void {
8727
+ if (skipTimer !== undefined) {
8728
+ clearTimeout(skipTimer)
8729
+ skipTimer = undefined
8553
8730
  }
8554
- return compare(actual, operator, expected)
8731
+ if (pendingSkip === undefined) return
8732
+ const trace = pendingSkip
8733
+ pendingSkip = undefined
8734
+ void send({ trace })
8555
8735
  }
8556
8736
 
8557
- /** The expression leaf goes through the bounded engine — parsed AST, no
8558
- * member access, no loops, hard step/node limits. Never inline JS. */
8559
- async function exprLeaf(
8560
- expr: string,
8561
- bindings: Record<string, { kind?: string; cap?: string; fieldPath?: string; value?: unknown; sourceStableId?: string }>,
8562
- ): Promise<boolean> {
8563
- const values: Record<string, string | number | boolean | null> = {}
8564
- for (const [alias, binding] of Object.entries(bindings)) {
8565
- if (binding.kind === 'literal') {
8566
- values[alias] = toExpressionValue(binding.value) ?? null
8567
- continue
8568
- }
8569
- const deviceId = await stableIdToDeviceId(binding.sourceStableId)
8570
- if (deviceId === undefined) {
8571
- ctx.logger.warn('condition leaf unresolvable — treated as false', {
8572
- tags: { deviceId: -1 },
8573
- binding: alias,
8574
- })
8575
- return false
8737
+ /** A skip identical to the one waiting is COUNTED on it; anything else flushes it first. */
8738
+ function record(trace: AutomationRunTrace): void {
8739
+ if (isSkippedTrace(trace)) {
8740
+ if (pendingSkip !== undefined) {
8741
+ const merged = mergeSkipTraces(pendingSkip, trace)
8742
+ if (merged !== null) {
8743
+ pendingSkip = merged
8744
+ return
8745
+ }
8746
+ flushSkip()
8576
8747
  }
8577
- const raw = await readField(deviceId, binding.cap ?? '', binding.fieldPath ?? '')
8578
- values[alias] = toExpressionValue(raw) ?? null
8748
+ pendingSkip = trace
8749
+ skipTimer = setTimeout(flushSkip, SKIP_FLUSH_MS)
8750
+ return
8579
8751
  }
8580
- const result = evaluateExpressionSource(expr, values, Date.now())
8581
- if (!result.ok) {
8582
- ctx.logger.warn('expression leaf failed — treated as false', { expr, error: result.error })
8583
- return false
8752
+ flushSkip()
8753
+ void send({ trace })
8754
+ }
8755
+
8756
+ // ── the condition mirror (D49): memory only at fire time ──
8757
+ async function refreshMirror(): Promise<void> {
8758
+ const report = await mirror.refresh({
8759
+ fetchSlice: (deviceId, cap) => ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap }),
8760
+ fetchStableIds: async () => {
8761
+ const all = await ctx.api.deviceManager.listAll.query({})
8762
+ return new Map(all.devices.flatMap((d: { stableId?: string; id: number }) =>
8763
+ d.stableId === undefined ? [] : [[d.stableId, d.id] as [string, number]]))
8764
+ },
8765
+ })
8766
+ if (report.kept > 0) {
8767
+ ctx.logger.debug('condition mirror kept stale values for sources it could not read', report)
8584
8768
  }
8585
- return result.value === true
8586
8769
  }
8587
8770
 
8588
- async function stableIdToDeviceId(stableId: string | undefined): Promise<number | undefined> {
8589
- if (stableId === undefined) return undefined
8771
+ /** Read one cap field off the state mirror. Returns undefined when the device,
8772
+ * the cap or the field cannot be resolved — the caller decides what that
8773
+ * means, and it always logs. (Used by the device-state TRIGGER, which the
8774
+ * reconcile backs; conditions read the ConditionMirror instead.) */
8775
+ async function readField(
8776
+ deviceId: number,
8777
+ cap: string,
8778
+ fieldPath: string,
8779
+ ): Promise<unknown> {
8590
8780
  try {
8591
- const all = await ctx.api.deviceManager.listAll.query({})
8592
- const hit = all.devices.find((d: { stableId?: string; id: number }) => d.stableId === stableId)
8593
- return hit?.id
8781
+ const slice = await ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap })
8782
+ let cursor: unknown = slice
8783
+ for (const part of fieldPath.split('.')) {
8784
+ if (cursor === null || typeof cursor !== 'object') return undefined
8785
+ cursor = (cursor as Record<string, unknown>)[part]
8786
+ }
8787
+ return cursor
8594
8788
  } catch {
8595
8789
  return undefined
8596
8790
  }
8597
8791
  }
8598
8792
 
8599
- async function allOf(children: Array<() => Promise<boolean>>): Promise<boolean> {
8600
- for (const c of children) { if (!(await c())) return false }
8601
- return true
8602
- }
8603
- async function anyOf(children: Array<() => Promise<boolean>>): Promise<boolean> {
8604
- for (const c of children) { if (await c()) return true }
8605
- return false
8606
- }
8607
- async function notOf(child: () => Promise<boolean>): Promise<boolean> {
8608
- return !(await child())
8609
- }
8610
-
8611
8793
  /** A {$var} action target, resolved from the run's bag BEFORE dispatch — the
8612
8794
  * action runner only ever sees a number. */
8613
8795
  function resolveTarget(vars: Record<string, unknown>, name: string): number {
8614
- const raw = vars[name]
8615
- if (typeof raw === 'number') return raw
8616
- throw new Error('action target var "' + name + '" is not a device id')
8796
+ return resolveDeviceTarget(vars, name)
8617
8797
  }
8618
8798
 
8619
- /** Substitute {{vars.x}} slots. Substitution only — never evaluation. */
8799
+ function warnMissingVar(name: string): void {
8800
+ ctx.logger.warn('template referenced a var that was never set', { var: name })
8801
+ }
8802
+
8803
+ /** Substitute {{vars.x}} slots and { $var } values. Substitution only — never evaluation. */
8620
8804
  function substitute(value: unknown, vars: Record<string, unknown>): unknown {
8621
- if (typeof value !== 'string') return value
8622
- return value.replace(/\\{\\{\\s*vars\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\}\\}/g, (_m, key: string) => {
8623
- const v = vars[key]
8624
- if (v === undefined) {
8625
- ctx.logger.warn('template referenced a var that was never set', { var: key })
8626
- return ''
8805
+ return resolveStepArgs(value, vars, warnMissingVar)
8806
+ }
8807
+
8808
+ /** The variables handed to an Action step, substituted and kept to primitives. */
8809
+ function substituteVariables(
8810
+ raw: Record<string, string | number | boolean>,
8811
+ vars: Record<string, unknown>,
8812
+ ): Record<string, string | number | boolean> {
8813
+ const out: Record<string, string | number | boolean> = {}
8814
+ for (const [key, value] of Object.entries(raw)) {
8815
+ const resolved = substitute(value, vars)
8816
+ if (typeof resolved === 'string' || typeof resolved === 'number' || typeof resolved === 'boolean') {
8817
+ out[key] = resolved
8627
8818
  }
8628
- return String(v)
8629
- })
8819
+ }
8820
+ return out
8630
8821
  }
8631
8822
 
8632
8823
  async function actuate(
@@ -8636,8 +8827,9 @@ export default function block(ctx: AddonContext) {
8636
8827
  args: Record<string, unknown>,
8637
8828
  vars: Record<string, unknown>,
8638
8829
  ): Promise<void> {
8639
- const resolved: Record<string, unknown> = {}
8640
- for (const [k, v] of Object.entries(args)) resolved[k] = substitute(v, vars)
8830
+ const substituted = substitute(args, vars)
8831
+ const resolved: Record<string, unknown> =
8832
+ substituted !== null && typeof substituted === 'object' ? { ...substituted } : {}
8641
8833
  // Forked-addon ctx.api namespaces are tRPC Proxies over a noop function
8642
8834
  // (typeof === 'function'), not plain objects — reject only null/undefined.
8643
8835
  const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
@@ -8651,48 +8843,1693 @@ export default function block(ctx: AddonContext) {
8651
8843
  await mutate({ ...resolved, deviceId })
8652
8844
  }
8653
8845
 
8654
- async function evaluateConditions(): Promise<boolean> {
8655
- return ${conditionExpr}
8656
- }
8657
-
8658
- /** The THEN half. Stops at the first failure — continuing past a failed step
8659
- * executes an intent the operator never expressed. */
8660
- async function runActions(vars: Record<string, unknown>): Promise<void> {
8661
- for (let step = 0; step < ${recipe.actions.length}; step += 1) {
8662
- try {
8663
- switch (step) {
8846
+ /** The THEN half, through the ONE shared step runner: stops at the first
8847
+ * failure (continuing past a failed step executes an intent the operator
8848
+ * never expressed); every step is traced, the ones after a failure (or an
8849
+ * abort) are marked not-run. */
8850
+ async function runActions(
8851
+ vars: Record<string, unknown>,
8852
+ token: RunToken,
8853
+ ): Promise<{ steps: readonly AutomationStepTrace[]; failed: boolean }> {
8854
+ const sleep = (ms: number): Promise<void> => abortableSleep(ms, token)
8855
+ const steps: RunnableStep[] = [
8664
8856
  ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
8665
- }
8666
- } catch (err: unknown) {
8857
+ ]
8858
+ return runSteps(steps, {
8859
+ pathPrefix: 'actions',
8860
+ isAborted: () => token.aborted,
8861
+ abortReason: 'aborted by a restart',
8862
+ now: () => Date.now(),
8863
+ onFailure: (failure) => {
8667
8864
  ctx.logger.warn('action failed — stopping the sequence', {
8668
- step,
8669
- error: err instanceof Error ? err.message : String(err),
8865
+ ...(failure.deviceId === undefined ? {} : { tags: { deviceId: failure.deviceId } }),
8866
+ step: failure.index,
8867
+ error: failure.message,
8670
8868
  })
8869
+ },
8870
+ })
8871
+ }
8872
+
8873
+ function deviceTags(trigger: Record<string, unknown>): { tags?: { deviceId: number } } {
8874
+ const deviceId = trigger['deviceId']
8875
+ return typeof deviceId === 'number' ? { tags: { deviceId } } : {}
8876
+ }
8877
+
8878
+ function newRunId(at: number): string {
8879
+ runCounter += 1
8880
+ return at.toString(36) + '-' + runCounter.toString(36)
8881
+ }
8882
+
8883
+ /** A trigger that never became a run (rejected, or dropped by the run mode). */
8884
+ function skipTrace(
8885
+ trigger: Record<string, unknown>,
8886
+ reason: string,
8887
+ outcome: AutomationRunTrace['outcome'],
8888
+ verdict: AutomationRunVerdict | undefined,
8889
+ accepted: boolean,
8890
+ ): AutomationRunTrace {
8891
+ const now = Date.now()
8892
+ return {
8893
+ runId: newRunId(now),
8894
+ startedAt: now,
8895
+ endedAt: now,
8896
+ trigger: buildTriggerTrace(trigger, accepted ? 'pass' : 'fail', reason),
8897
+ conditions: [],
8898
+ ...(verdict === undefined ? {} : { verdict }),
8899
+ steps: [],
8900
+ outcome,
8901
+ repeat: 1,
8902
+ lastAt: now,
8903
+ }
8904
+ }
8905
+
8906
+ /** One admitted run: conditions (memory only), the cooldown, the actions. */
8907
+ async function execute(
8908
+ trigger: Record<string, unknown>,
8909
+ reason: string,
8910
+ admission: AutomationRunVerdict,
8911
+ ): Promise<void> {
8912
+ const token: RunToken = { aborted: false }
8913
+ runningTokens.add(token)
8914
+ const startedAt = Date.now()
8915
+ const runId = newRunId(startedAt)
8916
+ let verdict = admission
8917
+ let outcome: AutomationRunTrace['outcome'] = 'ran'
8918
+ let nodes: AutomationConditionTraceNode[] = []
8919
+ let steps: AutomationStepTrace[] = []
8920
+ try {
8921
+ const evaluation = evaluateConditionTree(CONDITIONS, mirror, Date.now())
8922
+ nodes = [...evaluation.nodes]
8923
+ for (const node of nodes) {
8924
+ if (node.kind === 'condition' && node.observed === null && node.deviceId !== undefined) {
8925
+ ctx.logger.warn('condition leaf value unknown — treated as false', {
8926
+ tags: { deviceId: node.deviceId },
8927
+ path: node.path,
8928
+ cap: node.cap,
8929
+ fieldPath: node.fieldPath,
8930
+ })
8931
+ }
8932
+ }
8933
+ if (!evaluation.pass) {
8934
+ outcome = 'skipped-conditions'
8935
+ ctx.logger.debug('automation did not run — conditions were false', { trigger, ...deviceTags(trigger) })
8671
8936
  return
8672
8937
  }
8938
+ const letThrough = stampUnknown && RUN_MODE.mode === 'cooldown'
8939
+ const cooldown = letThrough
8940
+ ? { ok: true, bypassed: true, reason: 'cooldown stamp unreadable at start — run let through' }
8941
+ : gate.cooldown({
8942
+ manual: trigger['kind'] === 'manual',
8943
+ now: Date.now(),
8944
+ lastRunStartedAt,
8945
+ })
8946
+ if (!cooldown.ok) {
8947
+ outcome = 'skipped-cooldown'
8948
+ verdict = { mode: RUN_MODE, decision: 'cooldown', reason: cooldown.reason }
8949
+ ctx.logger.info('automation did not run — ' + cooldown.reason, deviceTags(trigger))
8950
+ return
8951
+ }
8952
+ if (cooldown.bypassed) verdict = { mode: RUN_MODE, decision: 'bypass', reason: cooldown.reason }
8953
+ // Stamped when the actions START, and reported to the hub before they run,
8954
+ // so a restart mid-run cannot fire again inside the window.
8955
+ lastRunStartedAt = startedAt
8956
+ stampUnknown = false
8957
+ await send({ runStartedAt: startedAt })
8958
+ ctx.logger.info('automation running', { trigger, ...deviceTags(trigger) })
8959
+ const vars: Record<string, unknown> = { ...trigger }
8960
+ const result = await runActions(vars, token)
8961
+ steps = result.steps
8962
+ outcome = token.aborted ? 'aborted' : result.failed ? 'failed' : 'ran'
8963
+ } catch (err: unknown) {
8964
+ outcome = 'failed'
8965
+ ctx.logger.warn('automation run failed', { error: err instanceof Error ? err.message : String(err), ...deviceTags(trigger) })
8966
+ } finally {
8967
+ runningTokens.delete(token)
8968
+ const endedAt = Date.now()
8969
+ record({
8970
+ runId,
8971
+ startedAt,
8972
+ endedAt,
8973
+ trigger: buildTriggerTrace(trigger, 'pass', reason),
8974
+ conditions: nodes,
8975
+ verdict,
8976
+ steps,
8977
+ outcome,
8978
+ repeat: 1,
8979
+ lastAt: endedAt,
8980
+ })
8981
+ if (gate.release()) {
8982
+ const next = queue.shift()
8983
+ if (next !== undefined && !stopped) {
8984
+ void execute(next.trigger, next.reason, {
8985
+ mode: RUN_MODE,
8986
+ decision: 'queued',
8987
+ reason: 'queued, then ran after waiting ' + (Date.now() - next.queuedAt) + ' ms',
8988
+ })
8989
+ }
8990
+ }
8673
8991
  }
8674
8992
  }
8675
8993
 
8676
- async function run(trigger: Record<string, unknown>): Promise<void> {
8677
- const vars: Record<string, unknown> = { ...trigger }
8678
- const passed = await evaluateConditions()
8679
- if (!passed) {
8680
- ctx.logger.debug('automation did not run — conditions were false', { trigger })
8994
+ /** Every trigger lands here. \`accepted\` = the trigger itself was satisfied
8995
+ * (a rejected detection event is only traced). */
8996
+ async function run(
8997
+ trigger: Record<string, unknown>,
8998
+ reason = 'fired',
8999
+ accepted = true,
9000
+ ): Promise<void> {
9001
+ await ready
9002
+ if (stopped) return
9003
+ if (!accepted) {
9004
+ record(skipTrace(trigger, reason, 'skipped-trigger', undefined, false))
8681
9005
  return
8682
9006
  }
8683
- ctx.logger.info('automation running', { trigger })
8684
- await runActions(vars)
9007
+ const admission = gate.admit()
9008
+ const verdict: AutomationRunVerdict = { mode: RUN_MODE, decision: admission.decision, reason: admission.reason }
9009
+ switch (admission.decision) {
9010
+ case 'drop':
9011
+ ctx.logger.info('automation trigger dropped — ' + admission.reason, deviceTags(trigger))
9012
+ record(skipTrace(trigger, reason, 'skipped-overlap', verdict, true))
9013
+ return
9014
+ case 'queued':
9015
+ ctx.logger.info('automation trigger queued — ' + admission.reason, deviceTags(trigger))
9016
+ queue.push({ trigger, reason, queuedAt: Date.now() })
9017
+ return
9018
+ case 'restart':
9019
+ ctx.logger.info('automation restarting — ' + admission.reason, deviceTags(trigger))
9020
+ for (const running of runningTokens) {
9021
+ running.aborted = true
9022
+ running.wake?.()
9023
+ }
9024
+ break
9025
+ case 'run':
9026
+ break
9027
+ }
9028
+ await execute(trigger, reason, verdict)
9029
+ }
9030
+
9031
+ const ready: Promise<void> = (async () => {
9032
+ await loadState()
9033
+ await refreshMirror()
9034
+ })()
9035
+
9036
+ if (CONDITIONS !== undefined) {
9037
+ disposers.push(
9038
+ ctx.eventBus.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
9039
+ const data = event.data
9040
+ const deviceId = data['deviceId']
9041
+ const capName = data['capName']
9042
+ if (typeof deviceId === 'number' && typeof capName === 'string') {
9043
+ mirror.apply(deviceId, capName, data['slice'])
9044
+ }
9045
+ }),
9046
+ )
9047
+ const mirrorReconcile = setInterval(() => { void refreshMirror() }, RECONCILE_MS)
9048
+ disposers.push(() => clearInterval(mirrorReconcile))
8685
9049
  }
8686
9050
 
8687
9051
  ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
8688
9052
 
8689
9053
  return () => {
9054
+ stopped = true
9055
+ queue.length = 0
9056
+ flushSkip()
9057
+ for (const running of runningTokens) {
9058
+ running.aborted = true
9059
+ running.wake?.()
9060
+ }
8690
9061
  for (const dispose of disposers) dispose()
8691
9062
  }
8692
9063
  }
8693
9064
  `;
8694
9065
  }
8695
9066
  //#endregion
9067
+ //#region src/automation/condition-eval.ts
9068
+ /**
9069
+ * Evaluating an automation's condition tree against the ConditionMirror.
9070
+ *
9071
+ * D49: a trigger is a one-shot event, so its gate must never hang on a read
9072
+ * that can fail. Every field a condition leaf names is therefore kept in
9073
+ * memory — filled at start, updated off device state-changed events, reconciled
9074
+ * on a timer — and evaluation reads that memory ONLY. A value the mirror does
9075
+ * not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
9076
+ * `pass`. A failed refresh changes nothing: the previous value stays.
9077
+ *
9078
+ * The evaluator returns a node per tree position (containers, leaves, and the
9079
+ * ones a short-circuit never reached) with a stable path and a human reason, so
9080
+ * the editor can paint each block.
9081
+ */
9082
+ function collectInto(cond, out) {
9083
+ switch (cond.kind) {
9084
+ case "all":
9085
+ case "any":
9086
+ for (const child of cond.children) collectInto(child, out);
9087
+ return;
9088
+ case "not":
9089
+ collectInto(cond.child, out);
9090
+ return;
9091
+ case "condition":
9092
+ out.push({
9093
+ kind: "device",
9094
+ deviceId: cond.deviceId,
9095
+ cap: cond.cap
9096
+ });
9097
+ return;
9098
+ case "expression":
9099
+ for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
9100
+ kind: "stable",
9101
+ stableId: binding.sourceStableId,
9102
+ cap: binding.cap
9103
+ });
9104
+ return;
9105
+ }
9106
+ }
9107
+ /** Every (device, cap) a tree reads, deduplicated. */
9108
+ function collectMirrorSources(cond) {
9109
+ if (cond === void 0) return [];
9110
+ const all = [];
9111
+ collectInto(cond, all);
9112
+ const seen = /* @__PURE__ */ new Set();
9113
+ return all.filter((s) => {
9114
+ const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
9115
+ if (seen.has(key)) return false;
9116
+ seen.add(key);
9117
+ return true;
9118
+ });
9119
+ }
9120
+ function sliceKey(deviceId, cap) {
9121
+ return `${deviceId}|${cap}`;
9122
+ }
9123
+ function pick$1(slice, fieldPath) {
9124
+ let cursor = slice;
9125
+ for (const part of fieldPath.split(".")) {
9126
+ if (cursor === null || typeof cursor !== "object") return void 0;
9127
+ cursor = Reflect.get(cursor, part);
9128
+ }
9129
+ return cursor;
9130
+ }
9131
+ function isSliceObject(value) {
9132
+ return typeof value === "object" && value !== null;
9133
+ }
9134
+ var ConditionMirror = class {
9135
+ sources;
9136
+ slices = /* @__PURE__ */ new Map();
9137
+ stableIds = /* @__PURE__ */ new Map();
9138
+ watched;
9139
+ constructor(sources) {
9140
+ this.sources = sources;
9141
+ this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
9142
+ }
9143
+ /** A `device.state-changed` event. Slices nobody reads are ignored. */
9144
+ apply(deviceId, cap, slice) {
9145
+ const key = sliceKey(deviceId, cap);
9146
+ if (!isSliceObject(slice)) return;
9147
+ if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
9148
+ this.slices.set(key, slice);
9149
+ }
9150
+ isStableTarget(deviceId, cap) {
9151
+ for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
9152
+ return false;
9153
+ }
9154
+ read(deviceId, cap, fieldPath) {
9155
+ const slice = this.slices.get(sliceKey(deviceId, cap));
9156
+ return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
9157
+ }
9158
+ readStable(stableId, cap, fieldPath) {
9159
+ const deviceId = this.stableIds.get(stableId);
9160
+ return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
9161
+ }
9162
+ /** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
9163
+ async refresh(fetchers) {
9164
+ let updated = 0;
9165
+ let kept = 0;
9166
+ if (this.sources.some((s) => s.kind === "stable")) try {
9167
+ this.stableIds = await fetchers.fetchStableIds();
9168
+ } catch {}
9169
+ const targets = /* @__PURE__ */ new Map();
9170
+ for (const s of this.sources) {
9171
+ const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
9172
+ if (deviceId === void 0) {
9173
+ kept += 1;
9174
+ continue;
9175
+ }
9176
+ targets.set(sliceKey(deviceId, s.cap), {
9177
+ deviceId,
9178
+ cap: s.cap
9179
+ });
9180
+ }
9181
+ for (const [key, target] of targets) try {
9182
+ const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
9183
+ if (isSliceObject(slice)) {
9184
+ this.slices.set(key, slice);
9185
+ updated += 1;
9186
+ } else kept += 1;
9187
+ } catch {
9188
+ kept += 1;
9189
+ }
9190
+ return {
9191
+ updated,
9192
+ kept
9193
+ };
9194
+ }
9195
+ };
9196
+ var OPERATOR_SYMBOL = {
9197
+ eq: "=",
9198
+ ne: "≠",
9199
+ gt: ">",
9200
+ gte: "≥",
9201
+ lt: "<",
9202
+ lte: "≤",
9203
+ contains: "contains",
9204
+ in: "in"
9205
+ };
9206
+ function compare(actual, operator, expected) {
9207
+ switch (operator) {
9208
+ case "eq": return actual === expected;
9209
+ case "ne": return actual !== expected;
9210
+ case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
9211
+ case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
9212
+ case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
9213
+ case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
9214
+ case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
9215
+ case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
9216
+ }
9217
+ }
9218
+ function toTraceValue(value) {
9219
+ if (value === void 0 || value === null) return null;
9220
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9221
+ if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
9222
+ return JSON.stringify(value);
9223
+ }
9224
+ function show(value) {
9225
+ return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
9226
+ }
9227
+ function markSkipped(cond, path, why, out) {
9228
+ out.push({
9229
+ path,
9230
+ kind: cond.kind,
9231
+ outcome: "not-evaluated",
9232
+ reason: why
9233
+ });
9234
+ if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
9235
+ markSkipped(c, `${path}.${i}`, why, out);
9236
+ });
9237
+ else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
9238
+ }
9239
+ function evalNode(cond, path, reader, now, out) {
9240
+ switch (cond.kind) {
9241
+ case "condition": {
9242
+ const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
9243
+ const base = {
9244
+ path,
9245
+ kind: cond.kind,
9246
+ deviceId: cond.deviceId,
9247
+ cap: cond.cap,
9248
+ fieldPath: cond.fieldPath,
9249
+ operator: cond.operator,
9250
+ expected: toTraceValue(cond.value),
9251
+ observed: toTraceValue(observed)
9252
+ };
9253
+ if (observed === void 0) {
9254
+ out.push({
9255
+ ...base,
9256
+ outcome: "fail",
9257
+ reason: `${cond.fieldPath} value unknown → fail`
9258
+ });
9259
+ return false;
9260
+ }
9261
+ const ok = compare(observed, cond.operator, cond.value);
9262
+ out.push({
9263
+ ...base,
9264
+ outcome: ok ? "pass" : "fail",
9265
+ reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
9266
+ });
9267
+ return ok;
9268
+ }
9269
+ case "expression": {
9270
+ const values = {};
9271
+ for (const [alias, binding] of Object.entries(cond.bindings)) {
9272
+ if (binding.kind === "literal") {
9273
+ values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
9274
+ continue;
9275
+ }
9276
+ const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
9277
+ if (raw === void 0) {
9278
+ out.push({
9279
+ path,
9280
+ kind: cond.kind,
9281
+ outcome: "fail",
9282
+ reason: `${alias} value unknown → fail`
9283
+ });
9284
+ return false;
9285
+ }
9286
+ values[alias] = require_composition.toExpressionValue(raw) ?? null;
9287
+ }
9288
+ const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
9289
+ if (!result.ok) {
9290
+ out.push({
9291
+ path,
9292
+ kind: cond.kind,
9293
+ outcome: "fail",
9294
+ reason: `${cond.expr}: ${result.error} → fail`
9295
+ });
9296
+ return false;
9297
+ }
9298
+ const ok = result.value === true;
9299
+ out.push({
9300
+ path,
9301
+ kind: cond.kind,
9302
+ outcome: ok ? "pass" : "fail",
9303
+ reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
9304
+ });
9305
+ return ok;
9306
+ }
9307
+ case "not": {
9308
+ const at = out.length;
9309
+ out.push({
9310
+ path,
9311
+ kind: "not",
9312
+ outcome: "not-evaluated",
9313
+ reason: ""
9314
+ });
9315
+ const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
9316
+ const ok = !inner;
9317
+ out.splice(at, 1, {
9318
+ path,
9319
+ kind: "not",
9320
+ outcome: ok ? "pass" : "fail",
9321
+ reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
9322
+ });
9323
+ return ok;
9324
+ }
9325
+ case "all":
9326
+ case "any": {
9327
+ const isAll = cond.kind === "all";
9328
+ const at = out.length;
9329
+ out.push({
9330
+ path,
9331
+ kind: cond.kind,
9332
+ outcome: "not-evaluated",
9333
+ reason: ""
9334
+ });
9335
+ let decided = null;
9336
+ for (let i = 0; i < cond.children.length; i += 1) {
9337
+ const child = cond.children[i];
9338
+ if (child === void 0) continue;
9339
+ if (decided !== null) {
9340
+ markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
9341
+ continue;
9342
+ }
9343
+ const ok = evalNode(child, `${path}.${i}`, reader, now, out);
9344
+ if (isAll ? !ok : ok) decided = i;
9345
+ }
9346
+ const ok = isAll ? decided === null : decided !== null;
9347
+ const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
9348
+ out.splice(at, 1, {
9349
+ path,
9350
+ kind: cond.kind,
9351
+ outcome: ok ? "pass" : "fail",
9352
+ reason
9353
+ });
9354
+ return ok;
9355
+ }
9356
+ }
9357
+ }
9358
+ /** No condition tree is a pass with no nodes. */
9359
+ function evaluateConditionTree(cond, reader, now) {
9360
+ if (cond === void 0) return {
9361
+ pass: true,
9362
+ nodes: []
9363
+ };
9364
+ const nodes = [];
9365
+ return {
9366
+ pass: evalNode(cond, "conditions", reader, now, nodes),
9367
+ nodes
9368
+ };
9369
+ }
9370
+ //#endregion
9371
+ //#region src/catalogs/coco-classmap.ts
9372
+ var COCO_80_LABELS = [
9373
+ {
9374
+ id: "person",
9375
+ name: "Person"
9376
+ },
9377
+ {
9378
+ id: "bicycle",
9379
+ name: "Bicycle"
9380
+ },
9381
+ {
9382
+ id: "car",
9383
+ name: "Car"
9384
+ },
9385
+ {
9386
+ id: "motorcycle",
9387
+ name: "Motorcycle"
9388
+ },
9389
+ {
9390
+ id: "airplane",
9391
+ name: "Airplane"
9392
+ },
9393
+ {
9394
+ id: "bus",
9395
+ name: "Bus"
9396
+ },
9397
+ {
9398
+ id: "train",
9399
+ name: "Train"
9400
+ },
9401
+ {
9402
+ id: "truck",
9403
+ name: "Truck"
9404
+ },
9405
+ {
9406
+ id: "boat",
9407
+ name: "Boat"
9408
+ },
9409
+ {
9410
+ id: "traffic light",
9411
+ name: "Traffic Light"
9412
+ },
9413
+ {
9414
+ id: "fire hydrant",
9415
+ name: "Fire Hydrant"
9416
+ },
9417
+ {
9418
+ id: "stop sign",
9419
+ name: "Stop Sign"
9420
+ },
9421
+ {
9422
+ id: "parking meter",
9423
+ name: "Parking Meter"
9424
+ },
9425
+ {
9426
+ id: "bench",
9427
+ name: "Bench"
9428
+ },
9429
+ {
9430
+ id: "bird",
9431
+ name: "Bird"
9432
+ },
9433
+ {
9434
+ id: "cat",
9435
+ name: "Cat"
9436
+ },
9437
+ {
9438
+ id: "dog",
9439
+ name: "Dog"
9440
+ },
9441
+ {
9442
+ id: "horse",
9443
+ name: "Horse"
9444
+ },
9445
+ {
9446
+ id: "sheep",
9447
+ name: "Sheep"
9448
+ },
9449
+ {
9450
+ id: "cow",
9451
+ name: "Cow"
9452
+ },
9453
+ {
9454
+ id: "elephant",
9455
+ name: "Elephant"
9456
+ },
9457
+ {
9458
+ id: "bear",
9459
+ name: "Bear"
9460
+ },
9461
+ {
9462
+ id: "zebra",
9463
+ name: "Zebra"
9464
+ },
9465
+ {
9466
+ id: "giraffe",
9467
+ name: "Giraffe"
9468
+ },
9469
+ {
9470
+ id: "backpack",
9471
+ name: "Backpack"
9472
+ },
9473
+ {
9474
+ id: "umbrella",
9475
+ name: "Umbrella"
9476
+ },
9477
+ {
9478
+ id: "handbag",
9479
+ name: "Handbag"
9480
+ },
9481
+ {
9482
+ id: "tie",
9483
+ name: "Tie"
9484
+ },
9485
+ {
9486
+ id: "suitcase",
9487
+ name: "Suitcase"
9488
+ },
9489
+ {
9490
+ id: "frisbee",
9491
+ name: "Frisbee"
9492
+ },
9493
+ {
9494
+ id: "skis",
9495
+ name: "Skis"
9496
+ },
9497
+ {
9498
+ id: "snowboard",
9499
+ name: "Snowboard"
9500
+ },
9501
+ {
9502
+ id: "sports ball",
9503
+ name: "Sports Ball"
9504
+ },
9505
+ {
9506
+ id: "kite",
9507
+ name: "Kite"
9508
+ },
9509
+ {
9510
+ id: "baseball bat",
9511
+ name: "Baseball Bat"
9512
+ },
9513
+ {
9514
+ id: "baseball glove",
9515
+ name: "Baseball Glove"
9516
+ },
9517
+ {
9518
+ id: "skateboard",
9519
+ name: "Skateboard"
9520
+ },
9521
+ {
9522
+ id: "surfboard",
9523
+ name: "Surfboard"
9524
+ },
9525
+ {
9526
+ id: "tennis racket",
9527
+ name: "Tennis Racket"
9528
+ },
9529
+ {
9530
+ id: "bottle",
9531
+ name: "Bottle"
9532
+ },
9533
+ {
9534
+ id: "wine glass",
9535
+ name: "Wine Glass"
9536
+ },
9537
+ {
9538
+ id: "cup",
9539
+ name: "Cup"
9540
+ },
9541
+ {
9542
+ id: "fork",
9543
+ name: "Fork"
9544
+ },
9545
+ {
9546
+ id: "knife",
9547
+ name: "Knife"
9548
+ },
9549
+ {
9550
+ id: "spoon",
9551
+ name: "Spoon"
9552
+ },
9553
+ {
9554
+ id: "bowl",
9555
+ name: "Bowl"
9556
+ },
9557
+ {
9558
+ id: "banana",
9559
+ name: "Banana"
9560
+ },
9561
+ {
9562
+ id: "apple",
9563
+ name: "Apple"
9564
+ },
9565
+ {
9566
+ id: "sandwich",
9567
+ name: "Sandwich"
9568
+ },
9569
+ {
9570
+ id: "orange",
9571
+ name: "Orange"
9572
+ },
9573
+ {
9574
+ id: "broccoli",
9575
+ name: "Broccoli"
9576
+ },
9577
+ {
9578
+ id: "carrot",
9579
+ name: "Carrot"
9580
+ },
9581
+ {
9582
+ id: "hot dog",
9583
+ name: "Hot Dog"
9584
+ },
9585
+ {
9586
+ id: "pizza",
9587
+ name: "Pizza"
9588
+ },
9589
+ {
9590
+ id: "donut",
9591
+ name: "Donut"
9592
+ },
9593
+ {
9594
+ id: "cake",
9595
+ name: "Cake"
9596
+ },
9597
+ {
9598
+ id: "chair",
9599
+ name: "Chair"
9600
+ },
9601
+ {
9602
+ id: "couch",
9603
+ name: "Couch"
9604
+ },
9605
+ {
9606
+ id: "potted plant",
9607
+ name: "Potted Plant"
9608
+ },
9609
+ {
9610
+ id: "bed",
9611
+ name: "Bed"
9612
+ },
9613
+ {
9614
+ id: "dining table",
9615
+ name: "Dining Table"
9616
+ },
9617
+ {
9618
+ id: "toilet",
9619
+ name: "Toilet"
9620
+ },
9621
+ {
9622
+ id: "tv",
9623
+ name: "TV"
9624
+ },
9625
+ {
9626
+ id: "laptop",
9627
+ name: "Laptop"
9628
+ },
9629
+ {
9630
+ id: "mouse",
9631
+ name: "Mouse"
9632
+ },
9633
+ {
9634
+ id: "remote",
9635
+ name: "Remote"
9636
+ },
9637
+ {
9638
+ id: "keyboard",
9639
+ name: "Keyboard"
9640
+ },
9641
+ {
9642
+ id: "cell phone",
9643
+ name: "Cell Phone"
9644
+ },
9645
+ {
9646
+ id: "microwave",
9647
+ name: "Microwave"
9648
+ },
9649
+ {
9650
+ id: "oven",
9651
+ name: "Oven"
9652
+ },
9653
+ {
9654
+ id: "toaster",
9655
+ name: "Toaster"
9656
+ },
9657
+ {
9658
+ id: "sink",
9659
+ name: "Sink"
9660
+ },
9661
+ {
9662
+ id: "refrigerator",
9663
+ name: "Refrigerator"
9664
+ },
9665
+ {
9666
+ id: "book",
9667
+ name: "Book"
9668
+ },
9669
+ {
9670
+ id: "clock",
9671
+ name: "Clock"
9672
+ },
9673
+ {
9674
+ id: "vase",
9675
+ name: "Vase"
9676
+ },
9677
+ {
9678
+ id: "scissors",
9679
+ name: "Scissors"
9680
+ },
9681
+ {
9682
+ id: "teddy bear",
9683
+ name: "Teddy Bear"
9684
+ },
9685
+ {
9686
+ id: "hair drier",
9687
+ name: "Hair Drier"
9688
+ },
9689
+ {
9690
+ id: "toothbrush",
9691
+ name: "Toothbrush"
9692
+ }
9693
+ ];
9694
+ var MACRO_LABELS = [
9695
+ {
9696
+ id: "person",
9697
+ name: "Person"
9698
+ },
9699
+ {
9700
+ id: "vehicle",
9701
+ name: "Vehicle"
9702
+ },
9703
+ {
9704
+ id: "animal",
9705
+ name: "Animal"
9706
+ },
9707
+ {
9708
+ id: "package",
9709
+ name: "Package"
9710
+ }
9711
+ ];
9712
+ /**
9713
+ * The class names a per-class COUNT can ever be keyed on — the macro ids, and
9714
+ * only those.
9715
+ *
9716
+ * `preserveOriginal: false` below is the whole reason this set exists: the
9717
+ * executor maps each raw COCO label onto its macro and DROPS everything else,
9718
+ * so a tracked detection's `className` is always one of {@link MACRO_LABELS}.
9719
+ * Any surface that indexes counts by class name — the ZoneAnalytics occupancy
9720
+ * snapshot's `byClass`, and therefore every occupancy rule — can match nothing
9721
+ * else. Derived from the catalog, never hand-listed, so a new macro joins it by
9722
+ * being declared once.
9723
+ *
9724
+ * Cost of not having it, 2026-08-12: the notification rule editor offered the
9725
+ * full taxonomy for an occupancy class (occupancy is the one condition that
9726
+ * does not go through the engine's macro-expanding `expandClassSelector`), two
9727
+ * live rules were authored on `car`, and both counters read 0 forever.
9728
+ */
9729
+ var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
9730
+ /** True when `className` is a macro the detector can actually emit (and hence a
9731
+ * class a count can be keyed on) — see {@link DETECTION_MACRO_CLASSES}. */
9732
+ function isDetectionMacroClass(className) {
9733
+ return DETECTION_MACRO_CLASSES.has(className.trim().toLowerCase());
9734
+ }
9735
+ var COCO_TO_MACRO = {
9736
+ mapping: {
9737
+ person: "person",
9738
+ bicycle: "vehicle",
9739
+ car: "vehicle",
9740
+ motorcycle: "vehicle",
9741
+ airplane: "vehicle",
9742
+ bus: "vehicle",
9743
+ train: "vehicle",
9744
+ truck: "vehicle",
9745
+ boat: "vehicle",
9746
+ bird: "animal",
9747
+ cat: "animal",
9748
+ dog: "animal",
9749
+ horse: "animal",
9750
+ sheep: "animal",
9751
+ cow: "animal",
9752
+ elephant: "animal",
9753
+ bear: "animal",
9754
+ zebra: "animal",
9755
+ giraffe: "animal",
9756
+ suitcase: "package",
9757
+ backpack: "package",
9758
+ handbag: "package"
9759
+ },
9760
+ preserveOriginal: false
9761
+ };
9762
+ var MACRO_NAMES = new Set(Object.values(COCO_TO_MACRO.mapping));
9763
+ function macroOf(className) {
9764
+ return COCO_TO_MACRO.mapping[className] ?? className;
9765
+ }
9766
+ /** `animal` matches a dog; `dog` matches only a dog (a cat is also an animal). */
9767
+ function detectionClassMatches(wanted, actual) {
9768
+ const w = wanted.toLowerCase();
9769
+ const a = actual.toLowerCase();
9770
+ if (w === a) return true;
9771
+ return MACRO_NAMES.has(w) && macroOf(a) === w;
9772
+ }
9773
+ function isPhase(value) {
9774
+ return value === "start" || value === "update" || value === "end";
9775
+ }
9776
+ var DetectionTriggerFilter = class {
9777
+ trigger;
9778
+ /** `${deviceId}|${trackId}` → fired at. Insertion-ordered, so the oldest is first. */
9779
+ fired = /* @__PURE__ */ new Map();
9780
+ constructor(trigger) {
9781
+ this.trigger = trigger;
9782
+ }
9783
+ get firedCount() {
9784
+ return this.fired.size;
9785
+ }
9786
+ evaluate(event, now) {
9787
+ const { deviceId, trackId, phase } = event;
9788
+ if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase(phase)) return {
9789
+ kind: "ignore",
9790
+ why: "malformed track-lifecycle event"
9791
+ };
9792
+ if (!this.trigger.deviceIds.includes(deviceId)) return {
9793
+ kind: "ignore",
9794
+ why: `device ${deviceId} is not one of the trigger's devices`
9795
+ };
9796
+ const key = `${deviceId}|${trackId}`;
9797
+ if (phase === "end") {
9798
+ this.fired.delete(key);
9799
+ return {
9800
+ kind: "ignore",
9801
+ why: "track ended"
9802
+ };
9803
+ }
9804
+ this.expire(now);
9805
+ const className = typeof event.bestClassName === "string" ? event.bestClassName : "";
9806
+ const score = typeof event.bestConfidence === "number" ? event.bestConfidence : 0;
9807
+ const vars = {
9808
+ deviceId,
9809
+ className,
9810
+ trackId,
9811
+ score
9812
+ };
9813
+ if (this.fired.has(key)) return {
9814
+ kind: "reject",
9815
+ reason: `already fired for track ${trackId}`,
9816
+ vars
9817
+ };
9818
+ if (!this.trigger.classes.some((c) => detectionClassMatches(c, className))) return {
9819
+ kind: "reject",
9820
+ reason: `class ${className || "unknown"} not in [${this.trigger.classes.join(", ")}]`,
9821
+ vars
9822
+ };
9823
+ if (this.trigger.minScore !== void 0 && score < this.trigger.minScore) return {
9824
+ kind: "reject",
9825
+ reason: `score ${score} < ${this.trigger.minScore}`,
9826
+ vars
9827
+ };
9828
+ if (this.trigger.zones !== void 0) {
9829
+ const visited = Array.isArray(event.zonesVisited) ? event.zonesVisited.filter((z) => typeof z === "string") : [];
9830
+ if (!this.trigger.zones.some((z) => visited.includes(z))) return {
9831
+ kind: "reject",
9832
+ reason: `zone mismatch: visited [${visited.join(", ")}], wanted [${this.trigger.zones.join(", ")}]`,
9833
+ vars
9834
+ };
9835
+ }
9836
+ this.remember(key, now);
9837
+ return {
9838
+ kind: "fire",
9839
+ reason: `${className} ${score} on device ${deviceId}`,
9840
+ vars
9841
+ };
9842
+ }
9843
+ remember(key, now) {
9844
+ this.fired.set(key, now);
9845
+ while (this.fired.size > 512) {
9846
+ const oldest = this.fired.keys().next();
9847
+ if (oldest.done === true) break;
9848
+ this.fired.delete(oldest.value);
9849
+ }
9850
+ }
9851
+ expire(now) {
9852
+ for (const [key, at] of this.fired) {
9853
+ if (now - at < 9e5) break;
9854
+ this.fired.delete(key);
9855
+ }
9856
+ }
9857
+ };
9858
+ //#endregion
9859
+ //#region src/automation/run-gate.ts
9860
+ var MINUTE_MS$1 = 6e4;
9861
+ function minutes(ms) {
9862
+ return (ms / MINUTE_MS$1).toFixed(1);
9863
+ }
9864
+ var RunGate = class {
9865
+ mode;
9866
+ active = 0;
9867
+ pending = 0;
9868
+ constructor(mode) {
9869
+ this.mode = mode;
9870
+ }
9871
+ get activeRuns() {
9872
+ return this.active;
9873
+ }
9874
+ get queuedTriggers() {
9875
+ return this.pending;
9876
+ }
9877
+ admit() {
9878
+ const mode = this.mode;
9879
+ switch (mode.mode) {
9880
+ case "single":
9881
+ case "cooldown":
9882
+ if (this.active > 0) return {
9883
+ decision: "drop",
9884
+ reason: `${mode.mode}: a run is already in progress`
9885
+ };
9886
+ this.active += 1;
9887
+ return {
9888
+ decision: "run",
9889
+ reason: `${mode.mode}: no run in progress`
9890
+ };
9891
+ case "restart":
9892
+ if (this.active > 0) {
9893
+ this.active += 1;
9894
+ return {
9895
+ decision: "restart",
9896
+ reason: `restart: aborting ${this.active - 1} run(s) in progress`
9897
+ };
9898
+ }
9899
+ this.active += 1;
9900
+ return {
9901
+ decision: "run",
9902
+ reason: "restart: no run in progress"
9903
+ };
9904
+ case "queued":
9905
+ if (this.active === 0) {
9906
+ this.active += 1;
9907
+ return {
9908
+ decision: "run",
9909
+ reason: "queued: no run in progress"
9910
+ };
9911
+ }
9912
+ if (this.pending < mode.max) {
9913
+ this.pending += 1;
9914
+ return {
9915
+ decision: "queued",
9916
+ reason: `queued: a run is in progress, waiting ${this.pending} of ${mode.max}`
9917
+ };
9918
+ }
9919
+ return {
9920
+ decision: "drop",
9921
+ reason: `queued: a run is in progress and the queue is full (${mode.max})`
9922
+ };
9923
+ case "parallel":
9924
+ if (this.active < mode.max) {
9925
+ this.active += 1;
9926
+ return {
9927
+ decision: "run",
9928
+ reason: `parallel: ${this.active} of ${mode.max} running`
9929
+ };
9930
+ }
9931
+ return {
9932
+ decision: "drop",
9933
+ reason: `parallel: ${mode.max} runs already in progress`
9934
+ };
9935
+ }
9936
+ }
9937
+ /** The run holding a slot ended (any outcome). True = a queued trigger took the slot. */
9938
+ release() {
9939
+ if (this.active > 0) this.active -= 1;
9940
+ if (this.mode.mode === "queued" && this.pending > 0) {
9941
+ this.pending -= 1;
9942
+ this.active += 1;
9943
+ return true;
9944
+ }
9945
+ return false;
9946
+ }
9947
+ /** Checked AFTER the conditions pass. Always `ok` outside `cooldown` mode. */
9948
+ cooldown(input) {
9949
+ if (this.mode.mode !== "cooldown") return {
9950
+ ok: true,
9951
+ bypassed: false,
9952
+ reason: ""
9953
+ };
9954
+ const waitMs = this.mode.minutes * MINUTE_MS$1;
9955
+ if (input.lastRunStartedAt === null) return {
9956
+ ok: true,
9957
+ bypassed: false,
9958
+ reason: `cooldown ${this.mode.minutes} min: no earlier run`
9959
+ };
9960
+ const elapsed = input.now - input.lastRunStartedAt;
9961
+ if (elapsed >= waitMs) return {
9962
+ ok: true,
9963
+ bypassed: false,
9964
+ reason: `cooldown ${this.mode.minutes} min: last run started ${minutes(elapsed)} min ago`
9965
+ };
9966
+ const left = `${minutes(waitMs - elapsed)} min left`;
9967
+ if (input.manual) return {
9968
+ ok: true,
9969
+ bypassed: true,
9970
+ reason: `cooldown ${this.mode.minutes} min bypassed by a manual trigger (${left})`
9971
+ };
9972
+ return {
9973
+ ok: false,
9974
+ bypassed: false,
9975
+ reason: `cooldown ${this.mode.minutes} min: last run started ${minutes(elapsed)} min ago, ${left}`
9976
+ };
9977
+ }
9978
+ };
9979
+ //#endregion
9980
+ //#region src/automation/trace.ts
9981
+ /**
9982
+ * Run traces — what an automation (or, later, an Action run) DID, node by node,
9983
+ * so the editor can paint every block green or red with its reason.
9984
+ *
9985
+ * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
9986
+ * every condition node (containers and leaves, including the ones a
9987
+ * short-circuit never reached), the run-mode verdict and every action step —
9988
+ * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
9989
+ * for an Action run). Skipped
9990
+ * evaluations are traced too, and identical consecutive skips collapse into a
9991
+ * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
9992
+ *
9993
+ * {@link AutomationStepTraceSchema} is deliberately free of automation
9994
+ * vocabulary so an Action run records the same step shape.
9995
+ */
9996
+ /** A value a trace may show: JSON primitives and short lists, nothing deeper. */
9997
+ var TraceValueSchema = zod.z.union([
9998
+ zod.z.string(),
9999
+ zod.z.number(),
10000
+ zod.z.boolean(),
10001
+ zod.z.null(),
10002
+ zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))
10003
+ ]);
10004
+ var AutomationRunOutcomeSchema = zod.z.enum([
10005
+ "ran",
10006
+ "skipped-trigger",
10007
+ "skipped-conditions",
10008
+ "skipped-cooldown",
10009
+ "skipped-overlap",
10010
+ "failed",
10011
+ "aborted"
10012
+ ]);
10013
+ /** `not-evaluated` = a short-circuit never reached the node. */
10014
+ var TraceNodeOutcomeSchema = zod.z.enum([
10015
+ "pass",
10016
+ "fail",
10017
+ "not-evaluated"
10018
+ ]);
10019
+ var AutomationTriggerTraceSchema = zod.z.object({
10020
+ kind: zod.z.string().min(1),
10021
+ outcome: zod.z.enum(["pass", "fail"]),
10022
+ /** Why an event was rejected ("class person not in [dog]") or what fired. */
10023
+ reason: zod.z.string(),
10024
+ /** deviceId, className, trackId, score … — only what the trigger exposes. */
10025
+ vars: zod.z.record(zod.z.string(), TraceValueSchema)
10026
+ });
10027
+ var AutomationConditionTraceNodeSchema = zod.z.object({
10028
+ /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
10029
+ path: zod.z.string().min(1),
10030
+ kind: zod.z.enum([
10031
+ "all",
10032
+ "any",
10033
+ "not",
10034
+ "condition",
10035
+ "expression"
10036
+ ]),
10037
+ outcome: TraceNodeOutcomeSchema,
10038
+ reason: zod.z.string(),
10039
+ deviceId: zod.z.number().int().optional(),
10040
+ cap: zod.z.string().optional(),
10041
+ fieldPath: zod.z.string().optional(),
10042
+ operator: zod.z.string().optional(),
10043
+ expected: TraceValueSchema.optional(),
10044
+ /** What the mirror held; `null` when it held nothing (unknown). */
10045
+ observed: TraceValueSchema.optional()
10046
+ });
10047
+ var AutomationRunVerdictSchema = zod.z.object({
10048
+ mode: AutomationRunModeSchema,
10049
+ decision: zod.z.enum([
10050
+ "run",
10051
+ "queued",
10052
+ "restart",
10053
+ "drop",
10054
+ "cooldown",
10055
+ "bypass"
10056
+ ]),
10057
+ reason: zod.z.string()
10058
+ });
10059
+ /** One executed (or not-run) step — shared by automations and Action runs. */
10060
+ var AutomationStepTraceSchema = zod.z.object({
10061
+ /** `actions.<i>` */
10062
+ path: zod.z.string().min(1),
10063
+ kind: zod.z.string().min(1),
10064
+ cap: zod.z.string().optional(),
10065
+ method: zod.z.string().optional(),
10066
+ deviceId: zod.z.number().int().optional(),
10067
+ startedAt: zod.z.number().optional(),
10068
+ endedAt: zod.z.number().optional(),
10069
+ outcome: zod.z.enum([
10070
+ "ok",
10071
+ "error",
10072
+ "not-run"
10073
+ ]),
10074
+ message: zod.z.string().optional()
10075
+ });
10076
+ var AutomationRunTraceSchema = zod.z.object({
10077
+ runId: zod.z.string().min(1),
10078
+ startedAt: zod.z.number(),
10079
+ endedAt: zod.z.number(),
10080
+ trigger: AutomationTriggerTraceSchema,
10081
+ conditions: zod.z.array(AutomationConditionTraceNodeSchema),
10082
+ verdict: AutomationRunVerdictSchema.optional(),
10083
+ steps: zod.z.array(AutomationStepTraceSchema),
10084
+ outcome: AutomationRunOutcomeSchema,
10085
+ /** How many identical consecutive skips this trace stands for (>= 1). */
10086
+ repeat: zod.z.number().int().min(1),
10087
+ /** When the latest of those repeats happened. */
10088
+ lastAt: zod.z.number()
10089
+ });
10090
+ /**
10091
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
10092
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
10093
+ */
10094
+ var AutomationStateDocSchema = zod.z.object({
10095
+ version: zod.z.literal(1),
10096
+ lastRunStartedAt: zod.z.number().nullable(),
10097
+ traces: zod.z.array(AutomationRunTraceSchema)
10098
+ });
10099
+ /** The state of an owner that never reported. */
10100
+ var EMPTY_AUTOMATION_STATE = {
10101
+ version: 1,
10102
+ lastRunStartedAt: null,
10103
+ traces: []
10104
+ };
10105
+ function isSkippedTrace(trace) {
10106
+ return trace.outcome.startsWith("skipped-");
10107
+ }
10108
+ /** Numbers vary between repeats of the same cause ("0.41 < 0.6" / "0.43 < 0.6"). */
10109
+ function normaliseReason(reason) {
10110
+ return reason.replace(/\d+(\.\d+)?/g, "#");
10111
+ }
10112
+ /** Two skips with the same signature are the same event repeating. */
10113
+ function skipSignature(trace) {
10114
+ return JSON.stringify([
10115
+ trace.outcome,
10116
+ trace.trigger.kind,
10117
+ trace.trigger.outcome,
10118
+ normaliseReason(trace.trigger.reason),
10119
+ trace.verdict === void 0 ? null : [trace.verdict.decision, normaliseReason(trace.verdict.reason)],
10120
+ trace.conditions.map((n) => [n.path, n.outcome])
10121
+ ]);
10122
+ }
10123
+ /**
10124
+ * `next` folded into `prev` when both are skips of the same signature (the
10125
+ * repeat count and the latest time move on), else null. Used by the block
10126
+ * BEFORE it sends, so a trigger that fails every few seconds costs one RPC per
10127
+ * flush, not one per skip; the ledger applies the same rule through
10128
+ * {@link appendTrace}.
10129
+ */
10130
+ function mergeSkipTraces(prev, next) {
10131
+ if (!isSkippedTrace(prev) || !isSkippedTrace(next)) return null;
10132
+ if (skipSignature(prev) !== skipSignature(next)) return null;
10133
+ return {
10134
+ ...prev,
10135
+ repeat: prev.repeat + next.repeat,
10136
+ lastAt: next.lastAt,
10137
+ endedAt: next.endedAt
10138
+ };
10139
+ }
10140
+ /**
10141
+ * Append a trace to a ring of at most `keep` entries, without mutating it.
10142
+ *
10143
+ * - A skip identical to the previous trace is counted on that trace
10144
+ * (`repeat`, `lastAt`), not appended.
10145
+ * - When the ring is full, the oldest SKIPPED trace goes first; a real run is
10146
+ * evicted only when every trace is a run. A noisy skip stream therefore
10147
+ * cannot push the runs the operator is looking for out of the ring.
10148
+ */
10149
+ function appendTrace(ring, trace, keep) {
10150
+ const last = ring[ring.length - 1];
10151
+ const merged = last === void 0 ? null : mergeSkipTraces(last, trace);
10152
+ if (merged !== null) return [...ring.slice(0, -1), merged];
10153
+ const next = [...ring, trace];
10154
+ while (next.length > keep) {
10155
+ const skipAt = next.findIndex((t, i) => i < next.length - 1 && isSkippedTrace(t));
10156
+ next.splice(skipAt === -1 ? 0 : skipAt, 1);
10157
+ }
10158
+ return next;
10159
+ }
10160
+ /**
10161
+ * The trigger stage of a trace from the bag a trigger handed to `run()`:
10162
+ * primitives only (a device-state `to` that is an object is left out), minus the
10163
+ * bookkeeping keys `kind` and `at`.
10164
+ */
10165
+ function buildTriggerTrace(trigger, outcome, reason) {
10166
+ const vars = {};
10167
+ for (const [key, value] of Object.entries(trigger)) {
10168
+ if (key === "kind" || key === "at") continue;
10169
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) vars[key] = value;
10170
+ }
10171
+ return {
10172
+ kind: typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown",
10173
+ outcome,
10174
+ reason,
10175
+ vars
10176
+ };
10177
+ }
10178
+ //#endregion
10179
+ //#region src/automation/action.ts
10180
+ /**
10181
+ * Actions — named, runnable things an operator (or an automation) can start,
10182
+ * declared as LIST ENTRIES, never as devices.
10183
+ *
10184
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
10185
+ * hub's `actions` record kind) or declared by an addon through the
10186
+ * `action-source` collection (a Home Assistant script, a scene …). The one
10187
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
10188
+ * catalog of its own (D3) — every `list` asks the providers live.
10189
+ *
10190
+ * This file is the data half: the entry schema, the variables an entry may
10191
+ * declare, and the pure resolution of the values a run was given. It holds no
10192
+ * runtime.
10193
+ */
10194
+ /** What a variable's value is. `select` takes one of `options`. */
10195
+ var ActionVariableTypeSchema = zod.z.enum([
10196
+ "string",
10197
+ "number",
10198
+ "boolean",
10199
+ "select"
10200
+ ]);
10201
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
10202
+ var ActionVariableValueSchema = zod.z.union([
10203
+ zod.z.string(),
10204
+ zod.z.number(),
10205
+ zod.z.boolean()
10206
+ ]);
10207
+ /**
10208
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
10209
+ * value) or `{{vars.name}}` (inside a string) in their args.
10210
+ */
10211
+ var ActionVariableSchema = zod.z.object({
10212
+ name: zod.z.string().regex(ACTION_VARIABLE_NAME_RE),
10213
+ label: zod.z.string().min(1).max(120).optional(),
10214
+ type: ActionVariableTypeSchema,
10215
+ required: zod.z.boolean().optional(),
10216
+ default: ActionVariableValueSchema.optional(),
10217
+ /** The allowed values of a `select`. */
10218
+ options: zod.z.array(zod.z.string().min(1)).min(1).optional()
10219
+ }).superRefine((v, ctx) => {
10220
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
10221
+ code: "custom",
10222
+ message: "a select variable needs options",
10223
+ path: ["options"]
10224
+ });
10225
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
10226
+ code: "custom",
10227
+ message: variableValueError(v, v.default) ?? "invalid default",
10228
+ path: ["default"]
10229
+ });
10230
+ });
10231
+ /** Why `value` is not a legal value for the variable, or null when it is. */
10232
+ function variableValueError(variable, value) {
10233
+ switch (variable.type) {
10234
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
10235
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
10236
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
10237
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
10238
+ }
10239
+ }
10240
+ /**
10241
+ * The values a run starts with: what the caller gave, checked against the
10242
+ * declaration, with defaults filled. A value the entry did not declare is
10243
+ * REFUSED — a typo in a variable name must not silently run with the default.
10244
+ */
10245
+ function resolveActionVariables(declared, given) {
10246
+ const decls = declared ?? [];
10247
+ const provided = given ?? {};
10248
+ for (const name of Object.keys(provided)) if (!decls.some((d) => d.name === name)) return {
10249
+ ok: false,
10250
+ error: `unknown variable "${name}"`
10251
+ };
10252
+ const values = {};
10253
+ for (const decl of decls) {
10254
+ const value = provided[decl.name] ?? decl.default;
10255
+ if (value === void 0) {
10256
+ if (decl.required === true) return {
10257
+ ok: false,
10258
+ error: `variable "${decl.name}" is required`
10259
+ };
10260
+ continue;
10261
+ }
10262
+ const error = variableValueError(decl, value);
10263
+ if (error !== null) return {
10264
+ ok: false,
10265
+ error
10266
+ };
10267
+ values[decl.name] = value;
10268
+ }
10269
+ return {
10270
+ ok: true,
10271
+ values
10272
+ };
10273
+ }
10274
+ var ActionSourceKindSchema = zod.z.enum(["internal", "addon"]);
10275
+ /** Prefix of an internal action's id. */
10276
+ var INTERNAL_ACTION_PREFIX = "internal:";
10277
+ function internalActionId(uuid) {
10278
+ return `${INTERNAL_ACTION_PREFIX}${uuid}`;
10279
+ }
10280
+ function isInternalActionId(actionId) {
10281
+ return actionId.startsWith("internal:") && actionId.length > 9;
10282
+ }
10283
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
10284
+ function addonIdOfActionId(actionId) {
10285
+ if (isInternalActionId(actionId)) return null;
10286
+ const at = actionId.indexOf(":");
10287
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
10288
+ }
10289
+ var ActionStatusSchema = zod.z.object({
10290
+ isRunning: zod.z.boolean(),
10291
+ /** Ms epoch the latest run STARTED; null when it never ran. */
10292
+ lastRunAt: zod.z.number().nullable(),
10293
+ /** The latest run's failure; null on success or when it never ran. */
10294
+ lastError: zod.z.string().nullable()
10295
+ });
10296
+ var IDLE_ACTION_STATUS = {
10297
+ isRunning: false,
10298
+ lastRunAt: null,
10299
+ lastError: null
10300
+ };
10301
+ var ActionEntrySchema = zod.z.object({
10302
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
10303
+ id: zod.z.string().min(3),
10304
+ name: zod.z.string().min(1).max(120),
10305
+ /** The addon that declared it; `core-blocks` for an internal one. */
10306
+ addonId: zod.z.string().min(1),
10307
+ source: ActionSourceKindSchema,
10308
+ /** An addon-declared entry is edited where it was declared. */
10309
+ readOnly: zod.z.boolean(),
10310
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
10311
+ steps: zod.z.array(ActionStepSchema).min(1).max(50).optional(),
10312
+ variables: zod.z.array(ActionVariableSchema).max(20).optional(),
10313
+ status: ActionStatusSchema
10314
+ }).superRefine((entry, ctx) => {
10315
+ if (entry.source === "internal") {
10316
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
10317
+ code: "custom",
10318
+ message: "an internal action id is internal:<uuid>",
10319
+ path: ["id"]
10320
+ });
10321
+ return;
10322
+ }
10323
+ if (entry.steps !== void 0) ctx.addIssue({
10324
+ code: "custom",
10325
+ message: "only an internal action carries steps",
10326
+ path: ["steps"]
10327
+ });
10328
+ if (!entry.readOnly) ctx.addIssue({
10329
+ code: "custom",
10330
+ message: "an addon action is read-only here",
10331
+ path: ["readOnly"]
10332
+ });
10333
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
10334
+ code: "custom",
10335
+ message: "an addon action id is <addonId>:<localId>",
10336
+ path: ["id"]
10337
+ });
10338
+ });
10339
+ /** What an operator writes for an internal action. */
10340
+ var ActionInputSchema = zod.z.object({
10341
+ name: zod.z.string().min(1).max(120),
10342
+ steps: zod.z.array(ActionStepSchema).min(1).max(50),
10343
+ variables: zod.z.array(ActionVariableSchema).max(20).optional()
10344
+ });
10345
+ /** Who started a run. Absent = the operator pressed it. */
10346
+ var ActionRunOriginSchema = zod.z.object({
10347
+ /** The automation block whose step started it. */
10348
+ blockId: zod.z.string().min(1).optional() });
10349
+ /** `started` = the caller did not ask to wait. */
10350
+ var ActionRunOutcomeSchema = zod.z.union([AutomationRunOutcomeSchema, zod.z.literal("started")]);
10351
+ var ActionRunResultSchema = zod.z.object({
10352
+ runId: zod.z.string().min(1),
10353
+ outcome: ActionRunOutcomeSchema,
10354
+ /** Why it did not run, or what failed. */
10355
+ message: zod.z.string().optional()
10356
+ });
10357
+ var ActionErrorCodeSchema = zod.z.enum([
10358
+ "not-found",
10359
+ "read-only",
10360
+ "invalid-variables",
10361
+ "invalid-steps",
10362
+ "not-linked",
10363
+ "busy"
10364
+ ]);
10365
+ /**
10366
+ * A refusal with a reason a caller can branch on. The code leads the message
10367
+ * (`[read-only] …`) because a cap call flattens an error to its text.
10368
+ */
10369
+ var ActionError = class extends Error {
10370
+ code;
10371
+ constructor(code, message) {
10372
+ super(`[${code}] ${message}`);
10373
+ this.name = "ActionError";
10374
+ this.code = code;
10375
+ }
10376
+ };
10377
+ /** The run-state change `actions.onRunChanged` carries. */
10378
+ var ActionRunChangedSchema = zod.z.object({
10379
+ actionId: zod.z.string(),
10380
+ runId: zod.z.string(),
10381
+ isRunning: zod.z.boolean(),
10382
+ /** Set when the run ended. */
10383
+ outcome: AutomationRunOutcomeSchema.optional(),
10384
+ lastRunAt: zod.z.number().nullable(),
10385
+ lastError: zod.z.string().nullable()
10386
+ });
10387
+ //#endregion
10388
+ //#region src/automation/step-runner.ts
10389
+ function errorMessage(err) {
10390
+ return err instanceof Error ? err.message : String(err);
10391
+ }
10392
+ async function runSteps(steps, options) {
10393
+ const traces = [];
10394
+ let failed = false;
10395
+ let aborted = false;
10396
+ for (const [index, step] of steps.entries()) {
10397
+ const path = `${options.pathPrefix}.${index}`;
10398
+ if (failed || aborted || options.isAborted()) {
10399
+ if (!failed) aborted = true;
10400
+ traces.push({
10401
+ ...step.meta,
10402
+ path,
10403
+ outcome: "not-run",
10404
+ message: failed ? "an earlier step failed" : options.abortReason
10405
+ });
10406
+ continue;
10407
+ }
10408
+ const startedAt = options.now();
10409
+ let deviceId = step.meta.deviceId;
10410
+ const scope = { setDeviceId: (id) => {
10411
+ deviceId = id;
10412
+ } };
10413
+ try {
10414
+ await step.run(scope);
10415
+ traces.push({
10416
+ ...step.meta,
10417
+ ...deviceId === void 0 ? {} : { deviceId },
10418
+ path,
10419
+ startedAt,
10420
+ endedAt: options.now(),
10421
+ outcome: "ok"
10422
+ });
10423
+ } catch (err) {
10424
+ failed = true;
10425
+ const message = errorMessage(err);
10426
+ options.onFailure({
10427
+ index,
10428
+ path,
10429
+ message,
10430
+ deviceId
10431
+ });
10432
+ traces.push({
10433
+ ...step.meta,
10434
+ ...deviceId === void 0 ? {} : { deviceId },
10435
+ path,
10436
+ startedAt,
10437
+ endedAt: options.now(),
10438
+ outcome: "error",
10439
+ message
10440
+ });
10441
+ }
10442
+ }
10443
+ return {
10444
+ steps: traces,
10445
+ failed,
10446
+ aborted: aborted && !failed
10447
+ };
10448
+ }
10449
+ function isVarRef(value) {
10450
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
10451
+ const keys = Object.keys(value);
10452
+ return keys.length === 1 && keys[0] === "$var" && typeof Reflect.get(value, "$var") === "string";
10453
+ }
10454
+ var TEMPLATE_RE = /\{\{\s*vars\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
10455
+ /**
10456
+ * Substitute a step's args from the run's variables. SUBSTITUTION only, never
10457
+ * evaluation:
10458
+ * - a value that is exactly `{ $var: 'name' }` becomes the variable's value,
10459
+ * keeping its type (a number stays a number);
10460
+ * - `{{vars.name}}` inside a string becomes its text.
10461
+ * A variable that is not set THROWS — a step must not run with a hole where
10462
+ * the operator meant a value. (`{{vars.x}}` keeps the automation behaviour:
10463
+ * the hole is reported through `onMissing` and rendered empty.)
10464
+ */
10465
+ function resolveStepArgs(value, vars, onMissing) {
10466
+ if (isVarRef(value)) {
10467
+ const found = vars[value.$var];
10468
+ if (found === void 0) throw new Error(`variable "${value.$var}" is not set`);
10469
+ return found;
10470
+ }
10471
+ if (typeof value === "string") return value.replace(TEMPLATE_RE, (_m, key) => {
10472
+ const v = vars[key];
10473
+ if (v === void 0) {
10474
+ onMissing(key);
10475
+ return "";
10476
+ }
10477
+ return String(v);
10478
+ });
10479
+ if (Array.isArray(value)) return value.map((v) => resolveStepArgs(v, vars, onMissing));
10480
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveStepArgs(v, vars, onMissing)]));
10481
+ return value;
10482
+ }
10483
+ /** A `{ $var }` device target, resolved from the bag BEFORE dispatch. */
10484
+ function resolveDeviceTarget(vars, name) {
10485
+ const raw = vars[name];
10486
+ if (typeof raw === "number") return raw;
10487
+ throw new Error(`action target var "${name}" is not a device id`);
10488
+ }
10489
+ function stepMetaOfAction(step) {
10490
+ if (step.kind === "wait") return { kind: "wait" };
10491
+ return {
10492
+ kind: "cap",
10493
+ cap: step.cap,
10494
+ method: step.method,
10495
+ ...typeof step.deviceId === "number" ? { deviceId: step.deviceId } : {}
10496
+ };
10497
+ }
10498
+ function isPlainRecord(value) {
10499
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10500
+ }
10501
+ /**
10502
+ * The runnable form of an internal Action's steps, bound to one run's
10503
+ * variables. Variables are substituted per step, at the moment it runs, so a
10504
+ * missing one fails THAT step (traced) and leaves the later ones not-run.
10505
+ */
10506
+ function buildActionSteps(steps, vars, deps) {
10507
+ return steps.map((step) => {
10508
+ const meta = stepMetaOfAction(step);
10509
+ if (step.kind === "wait") return {
10510
+ meta,
10511
+ run: () => deps.sleep(step.seconds * 1e3)
10512
+ };
10513
+ return {
10514
+ meta,
10515
+ run: async (scope) => {
10516
+ const resolved = resolveStepArgs(step.args ?? {}, vars, deps.onMissingVar);
10517
+ const args = isPlainRecord(resolved) ? resolved : {};
10518
+ const targets = typeof step.deviceId === "number" ? [step.deviceId] : Array.isArray(step.deviceId) ? step.deviceId : [resolveDeviceTarget(vars, step.deviceId.$var)];
10519
+ for (const deviceId of targets) {
10520
+ scope.setDeviceId(deviceId);
10521
+ await deps.invokeCap({
10522
+ deviceId,
10523
+ cap: step.cap,
10524
+ method: step.method,
10525
+ args
10526
+ });
10527
+ }
10528
+ }
10529
+ };
10530
+ });
10531
+ }
10532
+ //#endregion
8696
10533
  //#region src/capabilities/addon-pages.cap.ts
8697
10534
  /**
8698
10535
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
@@ -16238,7 +18075,29 @@ var coreBlocksCapability = {
16238
18075
  kind: "mutation",
16239
18076
  auth: "admin",
16240
18077
  access: "view"
16241
- })
18078
+ }),
18079
+ /**
18080
+ * An automation block reports one run (or one coalesced skip) to the hub's
18081
+ * `runs` ledger. Only the block itself may: the transport stamps
18082
+ * `callerAddonId`, and anything but `core-block-<blockId>` is refused.
18083
+ * `keepTraces` bounds the ring; `runStartedAt` moves the cooldown stamp.
18084
+ */
18085
+ reportAutomationRun: require_sleep.method(zod.z.object({
18086
+ blockId: zod.z.string().min(1),
18087
+ trace: AutomationRunTraceSchema.optional(),
18088
+ runStartedAt: zod.z.number().optional(),
18089
+ keepTraces: zod.z.number().int().min(1).max(100).optional()
18090
+ }), zod.z.void(), {
18091
+ kind: "mutation",
18092
+ auth: "admin"
18093
+ }),
18094
+ /** The cooldown stamp and trace ring a block reads ONCE, at start. Owner-only. */
18095
+ getAutomationRunState: require_sleep.method(zod.z.object({ blockId: zod.z.string().min(1) }), AutomationStateDocSchema, { auth: "admin" }),
18096
+ /** An automation's run traces, newest last. The operator's read. */
18097
+ getTraces: require_sleep.method(zod.z.object({ blockId: zod.z.string().min(1) }), zod.z.object({
18098
+ traces: zod.z.array(AutomationRunTraceSchema),
18099
+ lastError: zod.z.string().nullable()
18100
+ }), { auth: "admin" })
16242
18101
  }
16243
18102
  };
16244
18103
  //#endregion
@@ -41835,7 +43694,7 @@ function autoRankFor(role, endpoint) {
41835
43694
  if (isPlausibleLan(endpoint)) return 3;
41836
43695
  return null;
41837
43696
  }
41838
- var LOOPBACK_HOSTS = new Set([
43697
+ var LOOPBACK_HOSTS$1 = new Set([
41839
43698
  "localhost",
41840
43699
  "0.0.0.0",
41841
43700
  "::1",
@@ -41843,9 +43702,9 @@ var LOOPBACK_HOSTS = new Set([
41843
43702
  "::",
41844
43703
  "[::]"
41845
43704
  ]);
41846
- function isLoopbackHost(hostname) {
43705
+ function isLoopbackHost$1(hostname) {
41847
43706
  const h = hostname.toLowerCase();
41848
- if (LOOPBACK_HOSTS.has(h) || h.endsWith(".localhost")) return true;
43707
+ if (LOOPBACK_HOSTS$1.has(h) || h.endsWith(".localhost")) return true;
41849
43708
  return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
41850
43709
  }
41851
43710
  /**
@@ -41880,7 +43739,7 @@ function validateCustomAddress(raw) {
41880
43739
  ok: false,
41881
43740
  reason: "The address has no host."
41882
43741
  };
41883
- if (isLoopbackHost(parsed.hostname)) return {
43742
+ if (isLoopbackHost$1(parsed.hostname)) return {
41884
43743
  ok: false,
41885
43744
  reason: "A loopback address names whoever dials it, never this hub — use an address other machines reach."
41886
43745
  };
@@ -44542,398 +46401,6 @@ var notificationOutputCapability = {
44542
46401
  }
44543
46402
  };
44544
46403
  //#endregion
44545
- //#region src/catalogs/coco-classmap.ts
44546
- var COCO_80_LABELS = [
44547
- {
44548
- id: "person",
44549
- name: "Person"
44550
- },
44551
- {
44552
- id: "bicycle",
44553
- name: "Bicycle"
44554
- },
44555
- {
44556
- id: "car",
44557
- name: "Car"
44558
- },
44559
- {
44560
- id: "motorcycle",
44561
- name: "Motorcycle"
44562
- },
44563
- {
44564
- id: "airplane",
44565
- name: "Airplane"
44566
- },
44567
- {
44568
- id: "bus",
44569
- name: "Bus"
44570
- },
44571
- {
44572
- id: "train",
44573
- name: "Train"
44574
- },
44575
- {
44576
- id: "truck",
44577
- name: "Truck"
44578
- },
44579
- {
44580
- id: "boat",
44581
- name: "Boat"
44582
- },
44583
- {
44584
- id: "traffic light",
44585
- name: "Traffic Light"
44586
- },
44587
- {
44588
- id: "fire hydrant",
44589
- name: "Fire Hydrant"
44590
- },
44591
- {
44592
- id: "stop sign",
44593
- name: "Stop Sign"
44594
- },
44595
- {
44596
- id: "parking meter",
44597
- name: "Parking Meter"
44598
- },
44599
- {
44600
- id: "bench",
44601
- name: "Bench"
44602
- },
44603
- {
44604
- id: "bird",
44605
- name: "Bird"
44606
- },
44607
- {
44608
- id: "cat",
44609
- name: "Cat"
44610
- },
44611
- {
44612
- id: "dog",
44613
- name: "Dog"
44614
- },
44615
- {
44616
- id: "horse",
44617
- name: "Horse"
44618
- },
44619
- {
44620
- id: "sheep",
44621
- name: "Sheep"
44622
- },
44623
- {
44624
- id: "cow",
44625
- name: "Cow"
44626
- },
44627
- {
44628
- id: "elephant",
44629
- name: "Elephant"
44630
- },
44631
- {
44632
- id: "bear",
44633
- name: "Bear"
44634
- },
44635
- {
44636
- id: "zebra",
44637
- name: "Zebra"
44638
- },
44639
- {
44640
- id: "giraffe",
44641
- name: "Giraffe"
44642
- },
44643
- {
44644
- id: "backpack",
44645
- name: "Backpack"
44646
- },
44647
- {
44648
- id: "umbrella",
44649
- name: "Umbrella"
44650
- },
44651
- {
44652
- id: "handbag",
44653
- name: "Handbag"
44654
- },
44655
- {
44656
- id: "tie",
44657
- name: "Tie"
44658
- },
44659
- {
44660
- id: "suitcase",
44661
- name: "Suitcase"
44662
- },
44663
- {
44664
- id: "frisbee",
44665
- name: "Frisbee"
44666
- },
44667
- {
44668
- id: "skis",
44669
- name: "Skis"
44670
- },
44671
- {
44672
- id: "snowboard",
44673
- name: "Snowboard"
44674
- },
44675
- {
44676
- id: "sports ball",
44677
- name: "Sports Ball"
44678
- },
44679
- {
44680
- id: "kite",
44681
- name: "Kite"
44682
- },
44683
- {
44684
- id: "baseball bat",
44685
- name: "Baseball Bat"
44686
- },
44687
- {
44688
- id: "baseball glove",
44689
- name: "Baseball Glove"
44690
- },
44691
- {
44692
- id: "skateboard",
44693
- name: "Skateboard"
44694
- },
44695
- {
44696
- id: "surfboard",
44697
- name: "Surfboard"
44698
- },
44699
- {
44700
- id: "tennis racket",
44701
- name: "Tennis Racket"
44702
- },
44703
- {
44704
- id: "bottle",
44705
- name: "Bottle"
44706
- },
44707
- {
44708
- id: "wine glass",
44709
- name: "Wine Glass"
44710
- },
44711
- {
44712
- id: "cup",
44713
- name: "Cup"
44714
- },
44715
- {
44716
- id: "fork",
44717
- name: "Fork"
44718
- },
44719
- {
44720
- id: "knife",
44721
- name: "Knife"
44722
- },
44723
- {
44724
- id: "spoon",
44725
- name: "Spoon"
44726
- },
44727
- {
44728
- id: "bowl",
44729
- name: "Bowl"
44730
- },
44731
- {
44732
- id: "banana",
44733
- name: "Banana"
44734
- },
44735
- {
44736
- id: "apple",
44737
- name: "Apple"
44738
- },
44739
- {
44740
- id: "sandwich",
44741
- name: "Sandwich"
44742
- },
44743
- {
44744
- id: "orange",
44745
- name: "Orange"
44746
- },
44747
- {
44748
- id: "broccoli",
44749
- name: "Broccoli"
44750
- },
44751
- {
44752
- id: "carrot",
44753
- name: "Carrot"
44754
- },
44755
- {
44756
- id: "hot dog",
44757
- name: "Hot Dog"
44758
- },
44759
- {
44760
- id: "pizza",
44761
- name: "Pizza"
44762
- },
44763
- {
44764
- id: "donut",
44765
- name: "Donut"
44766
- },
44767
- {
44768
- id: "cake",
44769
- name: "Cake"
44770
- },
44771
- {
44772
- id: "chair",
44773
- name: "Chair"
44774
- },
44775
- {
44776
- id: "couch",
44777
- name: "Couch"
44778
- },
44779
- {
44780
- id: "potted plant",
44781
- name: "Potted Plant"
44782
- },
44783
- {
44784
- id: "bed",
44785
- name: "Bed"
44786
- },
44787
- {
44788
- id: "dining table",
44789
- name: "Dining Table"
44790
- },
44791
- {
44792
- id: "toilet",
44793
- name: "Toilet"
44794
- },
44795
- {
44796
- id: "tv",
44797
- name: "TV"
44798
- },
44799
- {
44800
- id: "laptop",
44801
- name: "Laptop"
44802
- },
44803
- {
44804
- id: "mouse",
44805
- name: "Mouse"
44806
- },
44807
- {
44808
- id: "remote",
44809
- name: "Remote"
44810
- },
44811
- {
44812
- id: "keyboard",
44813
- name: "Keyboard"
44814
- },
44815
- {
44816
- id: "cell phone",
44817
- name: "Cell Phone"
44818
- },
44819
- {
44820
- id: "microwave",
44821
- name: "Microwave"
44822
- },
44823
- {
44824
- id: "oven",
44825
- name: "Oven"
44826
- },
44827
- {
44828
- id: "toaster",
44829
- name: "Toaster"
44830
- },
44831
- {
44832
- id: "sink",
44833
- name: "Sink"
44834
- },
44835
- {
44836
- id: "refrigerator",
44837
- name: "Refrigerator"
44838
- },
44839
- {
44840
- id: "book",
44841
- name: "Book"
44842
- },
44843
- {
44844
- id: "clock",
44845
- name: "Clock"
44846
- },
44847
- {
44848
- id: "vase",
44849
- name: "Vase"
44850
- },
44851
- {
44852
- id: "scissors",
44853
- name: "Scissors"
44854
- },
44855
- {
44856
- id: "teddy bear",
44857
- name: "Teddy Bear"
44858
- },
44859
- {
44860
- id: "hair drier",
44861
- name: "Hair Drier"
44862
- },
44863
- {
44864
- id: "toothbrush",
44865
- name: "Toothbrush"
44866
- }
44867
- ];
44868
- var MACRO_LABELS = [
44869
- {
44870
- id: "person",
44871
- name: "Person"
44872
- },
44873
- {
44874
- id: "vehicle",
44875
- name: "Vehicle"
44876
- },
44877
- {
44878
- id: "animal",
44879
- name: "Animal"
44880
- },
44881
- {
44882
- id: "package",
44883
- name: "Package"
44884
- }
44885
- ];
44886
- /**
44887
- * The class names a per-class COUNT can ever be keyed on — the macro ids, and
44888
- * only those.
44889
- *
44890
- * `preserveOriginal: false` below is the whole reason this set exists: the
44891
- * executor maps each raw COCO label onto its macro and DROPS everything else,
44892
- * so a tracked detection's `className` is always one of {@link MACRO_LABELS}.
44893
- * Any surface that indexes counts by class name — the ZoneAnalytics occupancy
44894
- * snapshot's `byClass`, and therefore every occupancy rule — can match nothing
44895
- * else. Derived from the catalog, never hand-listed, so a new macro joins it by
44896
- * being declared once.
44897
- *
44898
- * Cost of not having it, 2026-08-12: the notification rule editor offered the
44899
- * full taxonomy for an occupancy class (occupancy is the one condition that
44900
- * does not go through the engine's macro-expanding `expandClassSelector`), two
44901
- * live rules were authored on `car`, and both counters read 0 forever.
44902
- */
44903
- var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
44904
- /** True when `className` is a macro the detector can actually emit (and hence a
44905
- * class a count can be keyed on) — see {@link DETECTION_MACRO_CLASSES}. */
44906
- function isDetectionMacroClass(className) {
44907
- return DETECTION_MACRO_CLASSES.has(className.trim().toLowerCase());
44908
- }
44909
- var COCO_TO_MACRO = {
44910
- mapping: {
44911
- person: "person",
44912
- bicycle: "vehicle",
44913
- car: "vehicle",
44914
- motorcycle: "vehicle",
44915
- airplane: "vehicle",
44916
- bus: "vehicle",
44917
- train: "vehicle",
44918
- truck: "vehicle",
44919
- boat: "vehicle",
44920
- bird: "animal",
44921
- cat: "animal",
44922
- dog: "animal",
44923
- horse: "animal",
44924
- sheep: "animal",
44925
- cow: "animal",
44926
- elephant: "animal",
44927
- bear: "animal",
44928
- zebra: "animal",
44929
- giraffe: "animal",
44930
- suitcase: "package",
44931
- backpack: "package",
44932
- handbag: "package"
44933
- },
44934
- preserveOriginal: false
44935
- };
44936
- //#endregion
44937
46404
  //#region src/catalogs/audio-classmap.ts
44938
46405
  var AUDIO_MACRO_LABELS = [
44939
46406
  {
@@ -62877,6 +64344,152 @@ var audioMetricsCapability = {
62877
64344
  history: "none"
62878
64345
  };
62879
64346
  //#endregion
64347
+ //#region src/capabilities/action-source.cap.ts
64348
+ /**
64349
+ * action-source — an addon DECLARES runnable actions (a Home Assistant script,
64350
+ * a scene, a vendor routine) as list entries. Never as devices.
64351
+ *
64352
+ * It mirrors `notification-output`: `listActions` returns an array, so the
64353
+ * generated cap mount concat-fans it across every provider; `runAction` and
64354
+ * `stopAction` route to ONE provider by the `addonId` the generated collection
64355
+ * router extracts from the call input.
64356
+ *
64357
+ * Nobody but the `actions` surface (the core-blocks builtin) calls this cap.
64358
+ * The UI and automations talk to `actions`, which fans out here LIVE on every
64359
+ * `list` and keeps no catalog of its own (D3: no shadow registry).
64360
+ *
64361
+ * `listActions` entries carry `source: 'addon'`, `readOnly: true`, an id of
64362
+ * `<addonId>:<localId>` and an idle `status` (the provider may fill what it
64363
+ * knows). `actionId` on `runAction` / `stopAction` is the full entry id.
64364
+ */
64365
+ var actionSourceCapability = {
64366
+ name: "action-source",
64367
+ scope: "system",
64368
+ mode: "collection",
64369
+ methods: {
64370
+ listActions: require_sleep.method(zod.z.object({}), zod.z.array(ActionEntrySchema), { auth: "admin" }),
64371
+ runAction: require_sleep.method(zod.z.object({
64372
+ actionId: zod.z.string().min(1),
64373
+ variables: zod.z.record(zod.z.string(), ActionVariableValueSchema).optional()
64374
+ }), zod.z.void(), {
64375
+ kind: "mutation",
64376
+ auth: "admin"
64377
+ }),
64378
+ stopAction: require_sleep.method(zod.z.object({ actionId: zod.z.string().min(1) }), zod.z.void(), {
64379
+ kind: "mutation",
64380
+ auth: "admin"
64381
+ })
64382
+ }
64383
+ };
64384
+ //#endregion
64385
+ //#region src/capabilities/actions.cap.ts
64386
+ /**
64387
+ * actions — the ONE surface for runnable actions, for the UI and for
64388
+ * automations. Provided by the core-blocks builtin on the hub.
64389
+ *
64390
+ * An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
64391
+ * steps, kept in the hub's `actions` record kind) or declared by an addon over
64392
+ * `action-source`. `list` asks every `action-source` provider LIVE and overlays
64393
+ * the run status from the hub's `runs` ledger; there is no catalog here to
64394
+ * drift (D3).
64395
+ *
64396
+ * **Permissions follow the camera, as linked devices do.** An action is linked
64397
+ * to cameras by the operator (`setLinks`, D356: nothing links itself). A
64398
+ * non-admin may `get` / `run` an action only through a camera they may see that
64399
+ * links it: they name the camera (`deviceId`, a top-level field so the scope
64400
+ * gate sees it) and the provider refuses an action that camera does not link.
64401
+ * An admin, and an in-process caller (an automation block), may run any action.
64402
+ * Every refusal is typed (`[not-linked] …`) and logged.
64403
+ *
64404
+ * A second `run` of an action that is already running is REFUSED — traced and
64405
+ * logged — never queued.
64406
+ */
64407
+ var ActionIdInput = zod.z.object({ actionId: zod.z.string().min(1) });
64408
+ var ActionLinksSchema = zod.z.object({
64409
+ deviceId: zod.z.number().int().nonnegative(),
64410
+ actionIds: zod.z.array(zod.z.string().min(1)).max(100)
64411
+ });
64412
+ var actionsCapability = {
64413
+ name: "actions",
64414
+ scope: "system",
64415
+ mode: "singleton",
64416
+ methods: {
64417
+ /** Every action, live from the providers, with its run status. Admin. */
64418
+ list: require_sleep.method(zod.z.object({}), zod.z.array(ActionEntrySchema), { auth: "admin" }),
64419
+ /**
64420
+ * One action. A non-admin names the camera (`deviceId`) that links it.
64421
+ * `null` when it does not exist.
64422
+ */
64423
+ get: require_sleep.method(zod.z.object({
64424
+ actionId: zod.z.string().min(1),
64425
+ deviceId: zod.z.number().int().nonnegative().optional()
64426
+ }), zod.z.object({ action: ActionEntrySchema.nullable() }), {
64427
+ role: "viewer",
64428
+ auth: "protected",
64429
+ kind: "query",
64430
+ caller: "required"
64431
+ }),
64432
+ /**
64433
+ * Start an action. `wait` holds the call until the run ended (an automation
64434
+ * step does); without it the call returns `started`. A non-admin names the
64435
+ * camera (`deviceId`) that links the action.
64436
+ */
64437
+ run: require_sleep.method(zod.z.object({
64438
+ actionId: zod.z.string().min(1),
64439
+ variables: zod.z.record(zod.z.string(), ActionVariableValueSchema).optional(),
64440
+ wait: zod.z.boolean().optional(),
64441
+ origin: ActionRunOriginSchema.optional(),
64442
+ deviceId: zod.z.number().int().nonnegative().optional()
64443
+ }), ActionRunResultSchema, {
64444
+ role: "viewer",
64445
+ auth: "protected",
64446
+ kind: "mutation",
64447
+ caller: "required"
64448
+ }),
64449
+ stop: require_sleep.method(ActionIdInput, zod.z.void(), {
64450
+ kind: "mutation",
64451
+ auth: "admin"
64452
+ }),
64453
+ getTraces: require_sleep.method(ActionIdInput, zod.z.object({ traces: zod.z.array(AutomationRunTraceSchema) }), { auth: "admin" }),
64454
+ /** Internal actions only. A read-only (addon) entry is refused with `[read-only]`. */
64455
+ create: require_sleep.method(zod.z.object({ action: ActionInputSchema }), zod.z.object({ action: ActionEntrySchema }), {
64456
+ kind: "mutation",
64457
+ auth: "admin"
64458
+ }),
64459
+ update: require_sleep.method(zod.z.object({
64460
+ actionId: zod.z.string().min(1),
64461
+ action: ActionInputSchema.partial()
64462
+ }), zod.z.object({ action: ActionEntrySchema }), {
64463
+ kind: "mutation",
64464
+ auth: "admin"
64465
+ }),
64466
+ delete: require_sleep.method(ActionIdInput, zod.z.void(), {
64467
+ kind: "mutation",
64468
+ auth: "admin"
64469
+ }),
64470
+ /**
64471
+ * What a viewer sees under a camera: the actions this camera LINKS, with
64472
+ * their variables, and only when the caller may see the camera. The scope
64473
+ * gate admits the camera; the answer is cut to the linked ones.
64474
+ */
64475
+ listForCamera: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.array(ActionEntrySchema), {
64476
+ role: "viewer",
64477
+ auth: "protected",
64478
+ kind: "query"
64479
+ }),
64480
+ /** The actions a camera links. Admin (the picker). */
64481
+ getLinks: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }),
64482
+ /** Replace a camera's linked actions with what the operator picked. */
64483
+ setLinks: require_sleep.method(ActionLinksSchema, ActionLinksSchema, {
64484
+ kind: "mutation",
64485
+ auth: "admin"
64486
+ })
64487
+ },
64488
+ events: {
64489
+ /** A run started or ended. Telemetry (D8): `list` is the authority. */
64490
+ onRunChanged: { data: ActionRunChangedSchema } }
64491
+ };
64492
+ //#endregion
62880
64493
  //#region src/capabilities/automation-control.cap.ts
62881
64494
  /**
62882
64495
  * Automation-control cap. Models HA `automation.*` entities on
@@ -85469,6 +87082,104 @@ var METHOD_ACCESS_MAP = Object.freeze({
85469
87082
  access: "create",
85470
87083
  role: "admin"
85471
87084
  },
87085
+ "actions.create": {
87086
+ capName: "actions",
87087
+ capScope: "system",
87088
+ addonId: null,
87089
+ access: "create",
87090
+ role: "admin"
87091
+ },
87092
+ "actions.delete": {
87093
+ capName: "actions",
87094
+ capScope: "system",
87095
+ addonId: null,
87096
+ access: "delete",
87097
+ role: "admin"
87098
+ },
87099
+ "actions.get": {
87100
+ capName: "actions",
87101
+ capScope: "system",
87102
+ addonId: null,
87103
+ access: "view",
87104
+ role: "viewer"
87105
+ },
87106
+ "actions.getLinks": {
87107
+ capName: "actions",
87108
+ capScope: "system",
87109
+ addonId: null,
87110
+ access: "view",
87111
+ role: "admin"
87112
+ },
87113
+ "actions.getTraces": {
87114
+ capName: "actions",
87115
+ capScope: "system",
87116
+ addonId: null,
87117
+ access: "view",
87118
+ role: "admin"
87119
+ },
87120
+ "actions.list": {
87121
+ capName: "actions",
87122
+ capScope: "system",
87123
+ addonId: null,
87124
+ access: "view",
87125
+ role: "admin"
87126
+ },
87127
+ "actions.listForCamera": {
87128
+ capName: "actions",
87129
+ capScope: "system",
87130
+ addonId: null,
87131
+ access: "view",
87132
+ role: "viewer"
87133
+ },
87134
+ "actions.run": {
87135
+ capName: "actions",
87136
+ capScope: "system",
87137
+ addonId: null,
87138
+ access: "create",
87139
+ role: "viewer"
87140
+ },
87141
+ "actions.setLinks": {
87142
+ capName: "actions",
87143
+ capScope: "system",
87144
+ addonId: null,
87145
+ access: "create",
87146
+ role: "admin"
87147
+ },
87148
+ "actions.stop": {
87149
+ capName: "actions",
87150
+ capScope: "system",
87151
+ addonId: null,
87152
+ access: "create",
87153
+ role: "admin"
87154
+ },
87155
+ "actions.update": {
87156
+ capName: "actions",
87157
+ capScope: "system",
87158
+ addonId: null,
87159
+ access: "create",
87160
+ role: "admin"
87161
+ },
87162
+ "actionSource.listActions": {
87163
+ capName: "action-source",
87164
+ capScope: "system",
87165
+ addonId: null,
87166
+ access: "view",
87167
+ role: "admin"
87168
+ },
87169
+ "actionSource.runAction": {
87170
+ capName: "action-source",
87171
+ capScope: "system",
87172
+ addonId: null,
87173
+ access: "create",
87174
+ role: "admin"
87175
+ },
87176
+ "actionSource.stopAction": {
87177
+ capName: "action-source",
87178
+ capScope: "system",
87179
+ addonId: null,
87180
+ access: "create",
87181
+ role: "admin"
87182
+ },
85472
87183
  "addonPages.listPages": {
85473
87184
  capName: "addon-pages",
85474
87185
  capScope: "system",
@@ -86750,6 +88461,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
86750
88461
  access: "view",
86751
88462
  role: "admin"
86752
88463
  },
88464
+ "coreBlocks.getAutomationRunState": {
88465
+ capName: "core-blocks",
88466
+ capScope: "system",
88467
+ addonId: null,
88468
+ access: "view",
88469
+ role: "admin"
88470
+ },
86753
88471
  "coreBlocks.getBlockSettings": {
86754
88472
  capName: "core-blocks",
86755
88473
  capScope: "system",
@@ -86771,6 +88489,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
86771
88489
  access: "view",
86772
88490
  role: "admin"
86773
88491
  },
88492
+ "coreBlocks.getTraces": {
88493
+ capName: "core-blocks",
88494
+ capScope: "system",
88495
+ addonId: null,
88496
+ access: "view",
88497
+ role: "admin"
88498
+ },
86774
88499
  "coreBlocks.getTypeDefs": {
86775
88500
  capName: "core-blocks",
86776
88501
  capScope: "system",
@@ -86806,6 +88531,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
86806
88531
  access: "create",
86807
88532
  role: "admin"
86808
88533
  },
88534
+ "coreBlocks.reportAutomationRun": {
88535
+ capName: "core-blocks",
88536
+ capScope: "system",
88537
+ addonId: null,
88538
+ access: "create",
88539
+ role: "admin"
88540
+ },
86809
88541
  "coreBlocks.restart": {
86810
88542
  capName: "core-blocks",
86811
88543
  capScope: "system",
@@ -95168,6 +96900,8 @@ var METHOD_ACCESS_MAP = Object.freeze({
95168
96900
  /** Set of all known cap names — handy for the scope picker. */
95169
96901
  var KNOWN_CAP_NAMES = [
95170
96902
  "accessories",
96903
+ "action-source",
96904
+ "actions",
95171
96905
  "addon-pages",
95172
96906
  "addon-pages-source",
95173
96907
  "addon-routes",
@@ -95453,6 +97187,8 @@ var DEVICE_CAP_NAMES = [
95453
97187
  ];
95454
97188
  /** Every cap declared as `scope: 'system'` — drives the `category:system` matcher. */
95455
97189
  var SYSTEM_CAP_NAMES = [
97190
+ "action-source",
97191
+ "actions",
95456
97192
  "addon-pages",
95457
97193
  "addon-pages-source",
95458
97194
  "addon-routes",
@@ -99351,6 +101087,8 @@ var ML = "model and training machinery — no device action";
99351
101087
  var INTERNAL = "internal plumbing between addons — never operator-facing";
99352
101088
  /** Every system-scoped cap, with why it is not a device's vocabulary. */
99353
101089
  var SYSTEM_CAP_VOCABULARY = {
101090
+ "action-source": system(INTERNAL),
101091
+ actions: system("declared actions — run from the Actions page and from the cameras that link them"),
99354
101092
  "addon-pages": system(UI_PLUMBING),
99355
101093
  "addon-pages-source": system(UI_PLUMBING),
99356
101094
  "addon-routes": system(UI_PLUMBING),
@@ -101257,6 +102995,8 @@ var CAP_NAMES_WITH_STATUS = [
101257
102995
  //#region src/generated/capability-router-map.ts
101258
102996
  var CAPABILITY_NAMES = {
101259
102997
  accessories: "accessories",
102998
+ actionSource: "action-source",
102999
+ actions: "actions",
101260
103000
  addonPages: "addon-pages",
101261
103001
  addonPagesSource: "addon-pages-source",
101262
103002
  addonRoutes: "addon-routes",
@@ -101448,6 +103188,14 @@ var CAPABILITY_ROUTER_KEYS = [
101448
103188
  key: "accessories",
101449
103189
  name: "accessories"
101450
103190
  },
103191
+ {
103192
+ key: "actionSource",
103193
+ name: "action-source"
103194
+ },
103195
+ {
103196
+ key: "actions",
103197
+ name: "actions"
103198
+ },
101451
103199
  {
101452
103200
  key: "addonPages",
101453
103201
  name: "addon-pages"
@@ -102196,6 +103944,8 @@ var CAPABILITY_ROUTER_KEYS = [
102196
103944
  */
102197
103945
  var ALL_CAPABILITY_DEFINITIONS = [
102198
103946
  accessoriesCapability,
103947
+ actionSourceCapability,
103948
+ actionsCapability,
102199
103949
  addonPagesCapability,
102200
103950
  addonPagesSourceCapability,
102201
103951
  addonRoutesCapability,
@@ -102398,6 +104148,31 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
102398
104148
  form: "single",
102399
104149
  optional: false
102400
104150
  }],
104151
+ "actions.get": [{
104152
+ name: "deviceId",
104153
+ form: "single",
104154
+ optional: true
104155
+ }],
104156
+ "actions.getLinks": [{
104157
+ name: "deviceId",
104158
+ form: "single",
104159
+ optional: false
104160
+ }],
104161
+ "actions.listForCamera": [{
104162
+ name: "deviceId",
104163
+ form: "single",
104164
+ optional: false
104165
+ }],
104166
+ "actions.run": [{
104167
+ name: "deviceId",
104168
+ form: "single",
104169
+ optional: true
104170
+ }],
104171
+ "actions.setLinks": [{
104172
+ name: "deviceId",
104173
+ form: "single",
104174
+ optional: false
104175
+ }],
102401
104176
  "addonSettings.getDeviceSettings": [{
102402
104177
  name: "deviceId",
102403
104178
  form: "single",
@@ -105169,6 +106944,11 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
105169
106944
  /** Cap-method paths that carry a device reference AND live on a system-scope
105170
106945
  * cap — the set the v3 device check newly reaches. */
105171
106946
  var SYSTEM_SCOPE_DEVICE_METHODS = [
106947
+ "actions.get",
106948
+ "actions.getLinks",
106949
+ "actions.listForCamera",
106950
+ "actions.run",
106951
+ "actions.setLinks",
105172
106952
  "addonSettings.getDeviceSettings",
105173
106953
  "addonSettings.updateDeviceSettings",
105174
106954
  "audioAnalyzer.attachDevice",
@@ -106383,6 +108163,20 @@ function createSystemProxy(api) {
106383
108163
  return require_sleep.invokeTrpcLeaf(api, capProp, method, kind, input, push);
106384
108164
  }
106385
108165
  return {
108166
+ actions: {
108167
+ list: (input) => dispatch("actions", "list", "query", input),
108168
+ stop: (input) => dispatch("actions", "stop", "mutation", input),
108169
+ getTraces: (input) => dispatch("actions", "getTraces", "query", input),
108170
+ create: (input) => dispatch("actions", "create", "mutation", input),
108171
+ update: (input) => dispatch("actions", "update", "mutation", input),
108172
+ delete: (input) => dispatch("actions", "delete", "mutation", input),
108173
+ setLinks: (input) => dispatch("actions", "setLinks", "mutation", input)
108174
+ },
108175
+ actionSource: {
108176
+ listActions: (input) => dispatch("actionSource", "listActions", "query", input),
108177
+ runAction: (input) => dispatch("actionSource", "runAction", "mutation", input),
108178
+ stopAction: (input) => dispatch("actionSource", "stopAction", "mutation", input)
108179
+ },
106386
108180
  addonPages: { listPages: (input) => dispatch("addonPages", "listPages", "query", input) },
106387
108181
  addons: {
106388
108182
  list: (input) => dispatch("addons", "list", "query", input),
@@ -106526,7 +108320,10 @@ function createSystemProxy(api) {
106526
108320
  listCompositions: (input) => dispatch("coreBlocks", "listCompositions", "query", input),
106527
108321
  getCustomization: (input) => dispatch("coreBlocks", "getCustomization", "query", input),
106528
108322
  listComposableCaps: (input) => dispatch("coreBlocks", "listComposableCaps", "query", input),
106529
- validateComposition: (input) => dispatch("coreBlocks", "validateComposition", "mutation", input)
108323
+ validateComposition: (input) => dispatch("coreBlocks", "validateComposition", "mutation", input),
108324
+ reportAutomationRun: (input) => dispatch("coreBlocks", "reportAutomationRun", "mutation", input),
108325
+ getAutomationRunState: (input) => dispatch("coreBlocks", "getAutomationRunState", "query", input),
108326
+ getTraces: (input) => dispatch("coreBlocks", "getTraces", "query", input)
106530
108327
  },
106531
108328
  decoder: {
106532
108329
  getInfo: (input) => dispatch("decoder", "getInfo", "query", input),
@@ -116048,6 +117845,421 @@ function parseOauthLandingFragment(hash) {
116048
117845
  };
116049
117846
  }
116050
117847
  //#endregion
117848
+ //#region src/vendor-login-proxy/url-rewrite.ts
117849
+ var LOOPBACK_HOSTS = new Set([
117850
+ "127.0.0.1",
117851
+ "localhost",
117852
+ "[::1]"
117853
+ ]);
117854
+ function hostWithoutPort(host) {
117855
+ if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
117856
+ const colon = host.indexOf(":");
117857
+ return colon === -1 ? host : host.slice(0, colon);
117858
+ }
117859
+ function isLoopbackHost(host) {
117860
+ return LOOPBACK_HOSTS.has(hostWithoutPort(host.toLowerCase()));
117861
+ }
117862
+ /** An allow-list entry is valid when it is a bare host[:port] and `http` only targets loopback. */
117863
+ function isValidUpstream(upstream) {
117864
+ if (upstream.host.length === 0 || !/^[a-z0-9.[\]:-]+$/.test(upstream.host)) return false;
117865
+ return upstream.scheme === "https" || isLoopbackHost(upstream.host);
117866
+ }
117867
+ function findUpstream(route, host) {
117868
+ const wanted = host.toLowerCase();
117869
+ return route.upstreams.find((upstream) => upstream.host === wanted);
117870
+ }
117871
+ /** `<basePath>/<host><pathname><search>` — what the browser is sent to. */
117872
+ function proxyPathFor(route, host, pathname, search = "") {
117873
+ return `${route.basePath}/${host}${pathname.startsWith("/") ? pathname : `/${pathname}`}${search}`;
117874
+ }
117875
+ /** Split `/<attemptId>/<host>/<rest…>[?query]` (the data-plane view of a request). */
117876
+ function parseProxyPath(rawUrl) {
117877
+ const queryAt = rawUrl.indexOf("?");
117878
+ const path = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
117879
+ const search = queryAt === -1 ? "" : rawUrl.slice(queryAt);
117880
+ const match = /^\/([A-Za-z0-9_-]{8,128})\/([^/]+)(\/.*)?$/.exec(path);
117881
+ if (match === null) return null;
117882
+ const attemptId = match[1];
117883
+ const host = match[2];
117884
+ if (attemptId === void 0 || host === void 0) return null;
117885
+ return {
117886
+ attemptId,
117887
+ host: host.toLowerCase(),
117888
+ pathname: match[3] ?? "/",
117889
+ search
117890
+ };
117891
+ }
117892
+ /** The vendor URL a proxied request targets, or null when the host would not stay `host`. */
117893
+ function upstreamUrlFor(upstream, parsed) {
117894
+ try {
117895
+ const url = new URL(`${upstream.scheme}://${upstream.host}${parsed.pathname}${parsed.search}`);
117896
+ return url.host.toLowerCase() === upstream.host ? url : null;
117897
+ } catch {
117898
+ return null;
117899
+ }
117900
+ }
117901
+ function escapeRegex(value) {
117902
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
117903
+ }
117904
+ /** A Location (or any URL value) to a vendor host becomes a proxy path; anything else is unchanged. */
117905
+ function rewriteLocation(location, currentUpstream, route) {
117906
+ let resolved;
117907
+ try {
117908
+ resolved = new URL(location, `${currentUpstream.scheme}://${currentUpstream.host}/`);
117909
+ } catch {
117910
+ return location;
117911
+ }
117912
+ if (resolved.protocol !== "https:" && resolved.protocol !== "http:") return location;
117913
+ const target = findUpstream(route, resolved.host);
117914
+ if (target === void 0) return location;
117915
+ return `${proxyPathFor(route, target.host, resolved.pathname, resolved.search)}${resolved.hash}`;
117916
+ }
117917
+ /** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
117918
+ var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
117919
+ /**
117920
+ * Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
117921
+ * (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
117922
+ * HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
117923
+ * escape the proxy mount and land on the hub.
117924
+ */
117925
+ function rewriteBody(body, kind, currentUpstream, route) {
117926
+ let out = body;
117927
+ for (const upstream of route.upstreams) {
117928
+ const host = escapeRegex(upstream.host);
117929
+ const plain = new RegExp(`${VALUE_START}(?:https?:)?//${host}(?=[/"'?#\\s)\\]},<]|$)`, "g");
117930
+ out = out.replace(plain, `${route.basePath}/${upstream.host}`);
117931
+ const escapedBase = `${route.basePath}/${upstream.host}`.replace(/\//g, "\\/");
117932
+ const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
117933
+ out = out.replace(escaped, escapedBase);
117934
+ }
117935
+ const current = `${route.basePath}/${currentUpstream.host}/`;
117936
+ const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
117937
+ if (kind === "html") {
117938
+ const attribute = String.raw`(\b(?:href|src|action|formaction|poster|data-href|data-src)\s*=\s*["'])/`;
117939
+ out = out.replace(new RegExp(`${attribute}${notMounted}`, "gi"), `$1${current}`);
117940
+ }
117941
+ if (kind === "html" || kind === "css") {
117942
+ const cssUrl = String.raw`(url\(\s*["']?)/`;
117943
+ out = out.replace(new RegExp(`${cssUrl}${notMounted}`, "gi"), `$1${current}`);
117944
+ }
117945
+ return out;
117946
+ }
117947
+ /**
117948
+ * A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
117949
+ * origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
117950
+ */
117951
+ function rewriteSetCookie(cookie, route) {
117952
+ const kept = cookie.split(";").map((part) => part.trim()).filter((part, index) => {
117953
+ if (index === 0) return true;
117954
+ const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
117955
+ return name !== "domain" && name !== "path";
117956
+ });
117957
+ kept.push(`Path=${route.basePath}/`);
117958
+ return kept.join("; ");
117959
+ }
117960
+ /**
117961
+ * A browser `Referer` that points into this proxy, turned back into the vendor URL it
117962
+ * stands for. A Referer that is not ours is dropped (null): it names the hub.
117963
+ */
117964
+ function rewriteReferer(referer, route) {
117965
+ let url;
117966
+ try {
117967
+ url = new URL(referer, "http://hub.invalid");
117968
+ } catch {
117969
+ return null;
117970
+ }
117971
+ if (!url.pathname.startsWith(`${route.basePath}/`)) return null;
117972
+ const rest = /^\/([^/]+)(\/.*)?$/.exec(url.pathname.slice(route.basePath.length));
117973
+ const host = rest?.[1]?.toLowerCase();
117974
+ if (host === void 0) return null;
117975
+ const upstream = findUpstream(route, host);
117976
+ if (upstream === void 0) return null;
117977
+ return `${upstream.scheme}://${upstream.host}${rest?.[2] ?? "/"}${url.search}`;
117978
+ }
117979
+ /** Cookies the browser holds for the hub itself must never reach a vendor. */
117980
+ function filterForwardedCookies(cookieHeader) {
117981
+ return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
117982
+ }
117983
+ //#endregion
117984
+ //#region src/vendor-login-proxy/login-proxy.ts
117985
+ var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
117986
+ var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
117987
+ /** Request headers that are never forwarded to the vendor. */
117988
+ var DROP_REQUEST_HEADERS = new Set([
117989
+ "host",
117990
+ "connection",
117991
+ "keep-alive",
117992
+ "proxy-authorization",
117993
+ "proxy-connection",
117994
+ "te",
117995
+ "trailer",
117996
+ "transfer-encoding",
117997
+ "upgrade",
117998
+ "content-length",
117999
+ "accept-encoding",
118000
+ "authorization",
118001
+ "cookie",
118002
+ "origin",
118003
+ "referer",
118004
+ "x-camstack-dataplane-secret",
118005
+ "x-forwarded-for",
118006
+ "x-forwarded-host",
118007
+ "x-forwarded-proto",
118008
+ "x-forwarded-prefix",
118009
+ "x-real-ip",
118010
+ "forwarded"
118011
+ ]);
118012
+ /**
118013
+ * Response headers dropped: framing/CSP/HSTS that break a page under another origin, and
118014
+ * the ones that no longer describe the (decoded, possibly rewritten) body.
118015
+ */
118016
+ var DROP_RESPONSE_HEADERS = new Set([
118017
+ "content-security-policy",
118018
+ "content-security-policy-report-only",
118019
+ "x-frame-options",
118020
+ "strict-transport-security",
118021
+ "public-key-pins",
118022
+ "report-to",
118023
+ "nel",
118024
+ "content-encoding",
118025
+ "content-length",
118026
+ "transfer-encoding",
118027
+ "connection",
118028
+ "keep-alive",
118029
+ "set-cookie",
118030
+ "location"
118031
+ ]);
118032
+ function bodyKindOf(contentType) {
118033
+ const type = contentType.toLowerCase();
118034
+ if (type.includes("text/html") || type.includes("application/xhtml")) return "html";
118035
+ if (type.includes("text/css")) return "css";
118036
+ if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml")) return "other";
118037
+ return null;
118038
+ }
118039
+ function firstHeader(value) {
118040
+ return Array.isArray(value) ? value[0] : value;
118041
+ }
118042
+ async function readRequestBody(req) {
118043
+ const chunks = [];
118044
+ let size = 0;
118045
+ for await (const chunk of req) {
118046
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
118047
+ size += buf.length;
118048
+ if (size > MAX_REQUEST_BODY_BYTES) return "too-large";
118049
+ chunks.push(buf);
118050
+ }
118051
+ return Buffer.concat(chunks);
118052
+ }
118053
+ function sendPlain(res, status, text) {
118054
+ res.writeHead(status, {
118055
+ "content-type": "text/plain; charset=utf-8",
118056
+ "cache-control": "no-store"
118057
+ });
118058
+ res.end(text);
118059
+ }
118060
+ var VendorLoginProxy = class {
118061
+ #addonId;
118062
+ #prefix;
118063
+ #logger;
118064
+ #now;
118065
+ #fetch;
118066
+ #attempts = /* @__PURE__ */ new Map();
118067
+ constructor(options) {
118068
+ this.#addonId = options.addonId;
118069
+ this.#prefix = options.prefix;
118070
+ this.#logger = options.logger;
118071
+ this.#now = options.now ?? (() => Date.now());
118072
+ this.#fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
118073
+ }
118074
+ /** The `DataPlaneHandler` to pass to `ctx.dataPlane.serve({ prefix, access: 'admin', … })`. */
118075
+ handler = (req, res) => this.#handle(req, res);
118076
+ /** Register an attempt. A one-shot capability: refused hosts are rejected up front. */
118077
+ open(spec) {
118078
+ for (const upstream of spec.upstreams) if (!isValidUpstream(upstream)) throw new Error(`vendor login proxy: upstream ${upstream.host} is not allowed`);
118079
+ this.#attempts.set(spec.attemptId, {
118080
+ expiresAtMs: spec.expiresAtMs,
118081
+ upstreams: spec.upstreams,
118082
+ evaluate: async (view) => {
118083
+ const verdict = await spec.terminal(view);
118084
+ if (verdict.kind === "continue") return verdict;
118085
+ return {
118086
+ kind: "done",
118087
+ landing: await spec.onDone(verdict.payload)
118088
+ };
118089
+ }
118090
+ });
118091
+ }
118092
+ close(attemptId) {
118093
+ this.#attempts.delete(attemptId);
118094
+ }
118095
+ isOpen(attemptId) {
118096
+ const attempt = this.#attempts.get(attemptId);
118097
+ return attempt !== void 0 && this.#now() <= attempt.expiresAtMs;
118098
+ }
118099
+ /**
118100
+ * The hub-relative path that opens `vendorUrl` through the proxy for this attempt:
118101
+ * the URL the browser is sent to. Null when the host is not in the attempt's list.
118102
+ */
118103
+ entryPath(attemptId, vendorUrl) {
118104
+ const attempt = this.#attempts.get(attemptId);
118105
+ if (attempt === void 0) return null;
118106
+ const url = new URL(vendorUrl);
118107
+ if (findUpstream({
118108
+ basePath: "",
118109
+ upstreams: attempt.upstreams
118110
+ }, url.host) === void 0) return null;
118111
+ return `/addon/${this.#addonId}/${this.#prefix}/${attemptId}/${url.host}${url.pathname}${url.search}`;
118112
+ }
118113
+ #routeFor(attemptId, attempt, forwardedPrefix) {
118114
+ return {
118115
+ basePath: `${forwardedPrefix}/addon/${this.#addonId}/${this.#prefix}/${attemptId}`,
118116
+ upstreams: attempt.upstreams
118117
+ };
118118
+ }
118119
+ async #handle(req, res) {
118120
+ try {
118121
+ await this.#serve(req, res);
118122
+ } catch (err) {
118123
+ this.#logger.warn("vendor login proxy: request failed", { meta: {
118124
+ addonId: this.#addonId,
118125
+ error: err instanceof Error ? err.name : "unknown"
118126
+ } });
118127
+ if (!res.headersSent) sendPlain(res, 502, "The sign-in page could not be reached.");
118128
+ else res.end();
118129
+ }
118130
+ }
118131
+ async #serve(req, res) {
118132
+ const parsed = parseProxyPath(req.url ?? "/");
118133
+ if (parsed === null) {
118134
+ sendPlain(res, 404, "Not found");
118135
+ return;
118136
+ }
118137
+ const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
118138
+ const attempt = this.#attempts.get(parsed.attemptId);
118139
+ if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
118140
+ if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
118141
+ this.#logger.info("vendor login proxy: attempt refused", { meta: {
118142
+ addonId: this.#addonId,
118143
+ reason: attempt === void 0 ? "missing" : "expired"
118144
+ } });
118145
+ this.#redirect(res, forwardedPrefix, {
118146
+ status: "error",
118147
+ addonId: this.#addonId,
118148
+ reason: "invalid_state"
118149
+ });
118150
+ return;
118151
+ }
118152
+ const route = this.#routeFor(parsed.attemptId, attempt, forwardedPrefix);
118153
+ const upstream = findUpstream(route, parsed.host);
118154
+ const target = upstream === void 0 ? null : upstreamUrlFor(upstream, parsed);
118155
+ if (upstream === void 0 || target === null) {
118156
+ this.#logger.warn("vendor login proxy: host refused", { meta: {
118157
+ addonId: this.#addonId,
118158
+ host: parsed.host
118159
+ } });
118160
+ sendPlain(res, 404, "Not found");
118161
+ return;
118162
+ }
118163
+ const method = (req.method ?? "GET").toUpperCase();
118164
+ let body;
118165
+ if (method !== "GET" && method !== "HEAD") {
118166
+ const read = await readRequestBody(req);
118167
+ if (read === "too-large") {
118168
+ sendPlain(res, 413, "Request too large");
118169
+ return;
118170
+ }
118171
+ body = read;
118172
+ }
118173
+ const headers = this.#requestHeaders(req, route, upstream);
118174
+ const upstreamRes = await this.#fetch(target, {
118175
+ method,
118176
+ headers,
118177
+ redirect: "manual",
118178
+ ...body !== void 0 && body.length > 0 ? { body: new Uint8Array(body) } : {}
118179
+ });
118180
+ const location = upstreamRes.headers.get("location");
118181
+ const setCookies = upstreamRes.headers.getSetCookie();
118182
+ const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
118183
+ const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
118184
+ const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
118185
+ this.#logger.debug("vendor login proxy: upstream answered", { meta: {
118186
+ addonId: this.#addonId,
118187
+ host: upstream.host,
118188
+ status: upstreamRes.status,
118189
+ method
118190
+ } });
118191
+ const settled = await attempt.evaluate({
118192
+ status: upstreamRes.status,
118193
+ url: target.toString(),
118194
+ location,
118195
+ setCookies,
118196
+ bodyText: text
118197
+ });
118198
+ if (settled.kind === "done") {
118199
+ this.#attempts.delete(parsed.attemptId);
118200
+ this.#logger.info("vendor login proxy: sign-in finished", { meta: {
118201
+ addonId: this.#addonId,
118202
+ status: settled.landing.status
118203
+ } });
118204
+ this.#redirect(res, forwardedPrefix, settled.landing);
118205
+ return;
118206
+ }
118207
+ const out = {};
118208
+ upstreamRes.headers.forEach((value, name) => {
118209
+ if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
118210
+ });
118211
+ if (setCookies.length > 0) out["set-cookie"] = setCookies.map((c) => rewriteSetCookie(c, route));
118212
+ if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
118213
+ out["cache-control"] = "no-store";
118214
+ if (text !== null && kind !== null) {
118215
+ const rewritten = rewriteBody(text, kind, upstream, route);
118216
+ res.writeHead(upstreamRes.status, out);
118217
+ res.end(rewritten);
118218
+ return;
118219
+ }
118220
+ res.writeHead(upstreamRes.status, out);
118221
+ await this.#pipeBody(upstreamRes, res);
118222
+ }
118223
+ #requestHeaders(req, route, upstream) {
118224
+ const headers = {};
118225
+ for (const [name, value] of Object.entries(req.headers)) {
118226
+ const lower = name.toLowerCase();
118227
+ if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
118228
+ headers[lower] = Array.isArray(value) ? value.join(", ") : value;
118229
+ }
118230
+ const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
118231
+ if (cookie.length > 0) headers["cookie"] = cookie;
118232
+ if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
118233
+ const referer = firstHeader(req.headers["referer"]);
118234
+ if (referer !== void 0) {
118235
+ const back = rewriteReferer(referer, route);
118236
+ if (back !== null) headers["referer"] = back;
118237
+ }
118238
+ return headers;
118239
+ }
118240
+ async #pipeBody(upstreamRes, res) {
118241
+ if (upstreamRes.body === null) {
118242
+ res.end();
118243
+ return;
118244
+ }
118245
+ for await (const chunk of upstreamRes.body) {
118246
+ if (!res.write(chunk)) await new Promise((resolve) => {
118247
+ res.once("drain", resolve);
118248
+ res.once("close", resolve);
118249
+ });
118250
+ if (res.destroyed) return;
118251
+ }
118252
+ res.end();
118253
+ }
118254
+ #redirect(res, forwardedPrefix, outcome) {
118255
+ res.writeHead(302, {
118256
+ location: `${forwardedPrefix}${oauthLandingUrl(null, outcome)}`,
118257
+ "cache-control": "no-store"
118258
+ });
118259
+ res.end();
118260
+ }
118261
+ };
118262
+ //#endregion
116051
118263
  //#region src/utils/pool-memory-watchdog.ts
116052
118264
  var MB = 1024 * 1024;
116053
118265
  var DEFAULT_POOL_MEMORY_POLICY = {
@@ -117149,6 +119361,21 @@ exports.AcceleratorComputeSchema = AcceleratorComputeSchema;
117149
119361
  exports.AcceleratorKindSchema = AcceleratorKindSchema;
117150
119362
  exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
117151
119363
  exports.AccessoryKind = AccessoryKind;
119364
+ exports.ActionEntrySchema = ActionEntrySchema;
119365
+ exports.ActionError = ActionError;
119366
+ exports.ActionErrorCodeSchema = ActionErrorCodeSchema;
119367
+ exports.ActionInputSchema = ActionInputSchema;
119368
+ exports.ActionLinksSchema = ActionLinksSchema;
119369
+ exports.ActionRunChangedSchema = ActionRunChangedSchema;
119370
+ exports.ActionRunOriginSchema = ActionRunOriginSchema;
119371
+ exports.ActionRunOutcomeSchema = ActionRunOutcomeSchema;
119372
+ exports.ActionRunResultSchema = ActionRunResultSchema;
119373
+ exports.ActionSourceKindSchema = ActionSourceKindSchema;
119374
+ exports.ActionStatusSchema = ActionStatusSchema;
119375
+ exports.ActionStepSchema = ActionStepSchema;
119376
+ exports.ActionVariableSchema = ActionVariableSchema;
119377
+ exports.ActionVariableTypeSchema = ActionVariableTypeSchema;
119378
+ exports.ActionVariableValueSchema = ActionVariableValueSchema;
117152
119379
  exports.ActiveCriterionSchema = require_composition.ActiveCriterionSchema;
117153
119380
  exports.AddBrokerInputSchema = AddBrokerInputSchema;
117154
119381
  exports.AddRetrainFrameFromRecordingResultSchema = AddRetrainFrameFromRecordingResultSchema;
@@ -117229,11 +119456,21 @@ exports.AutoTunePlanSchema = AutoTunePlanSchema;
117229
119456
  exports.AutoTuneWarningSchema = AutoTuneWarningSchema;
117230
119457
  exports.AutoUpdateSettingsSchema = AutoUpdateSettingsSchema;
117231
119458
  exports.AutomationActionSchema = AutomationActionSchema;
119459
+ exports.AutomationCapStepSchema = AutomationCapStepSchema;
117232
119460
  exports.AutomationConditionOperatorSchema = AutomationConditionOperatorSchema;
117233
119461
  exports.AutomationConditionSchema = AutomationConditionSchema;
119462
+ exports.AutomationConditionTraceNodeSchema = AutomationConditionTraceNodeSchema;
117234
119463
  exports.AutomationControlStatusSchema = AutomationControlStatusSchema;
117235
119464
  exports.AutomationRecipeSchema = AutomationRecipeSchema;
119465
+ exports.AutomationRunModeSchema = AutomationRunModeSchema;
119466
+ exports.AutomationRunOutcomeSchema = AutomationRunOutcomeSchema;
119467
+ exports.AutomationRunTraceSchema = AutomationRunTraceSchema;
119468
+ exports.AutomationRunVerdictSchema = AutomationRunVerdictSchema;
119469
+ exports.AutomationStateDocSchema = AutomationStateDocSchema;
119470
+ exports.AutomationStepTraceSchema = AutomationStepTraceSchema;
117236
119471
  exports.AutomationTriggerSchema = AutomationTriggerSchema;
119472
+ exports.AutomationTriggerTraceSchema = AutomationTriggerTraceSchema;
119473
+ exports.AutomationWaitStepSchema = AutomationWaitStepSchema;
117237
119474
  exports.AvailableIntegrationTypeSchema = AvailableIntegrationTypeSchema;
117238
119475
  exports.BASE_DEVICE_CAPS = BASE_DEVICE_CAPS;
117239
119476
  exports.BASE_LIVE_EGRESS_PROFILE = BASE_LIVE_EGRESS_PROFILE;
@@ -117495,6 +119732,7 @@ exports.CompositionTargetSchema = require_composition.CompositionTargetSchema;
117495
119732
  exports.CompositionUnavailableModeSchema = CompositionUnavailableModeSchema;
117496
119733
  exports.CompositionValidationSchema = CompositionValidationSchema;
117497
119734
  exports.CompositionViewSchema = CompositionViewSchema;
119735
+ exports.ConditionMirror = ConditionMirror;
117498
119736
  exports.ConfigEntrySchema = ConfigEntrySchema;
117499
119737
  exports.ConfigSectionWithValuesSchema = ConfigSectionWithValuesSchema;
117500
119738
  exports.ConfigTabDeclarationSchema = ConfigTabDeclarationSchema;
@@ -117574,6 +119812,7 @@ exports.DEFAULT_SESSION_PLACEMENT_POLICY = DEFAULT_SESSION_PLACEMENT_POLICY;
117574
119812
  exports.DEFAULT_SHARE_SOURCE = DEFAULT_SHARE_SOURCE;
117575
119813
  exports.DEFAULT_TIMELAPSE_PREVIEW_TEXT = DEFAULT_TIMELAPSE_PREVIEW_TEXT;
117576
119814
  exports.DEFAULT_TOKEN_EXPIRY = require_sleep.DEFAULT_TOKEN_EXPIRY;
119815
+ exports.DEFAULT_TRACE_KEEP = DEFAULT_TRACE_KEEP;
117577
119816
  exports.DEFAULT_VIEWER_SCOPE = DEFAULT_VIEWER_SCOPE;
117578
119817
  exports.DEFINITIVE_DEVICE_REFUSALS = DEFINITIVE_DEVICE_REFUSALS;
117579
119818
  exports.DERIVATIVE_UNITS = require_composition.DERIVATIVE_UNITS;
@@ -117641,6 +119880,7 @@ exports.DetailCropConventionSchema = DetailCropConventionSchema;
117641
119880
  exports.DetectionCatalogClassMapSchema = DetectionCatalogClassMapSchema;
117642
119881
  exports.DetectionSourceSchema = DetectionSourceSchema;
117643
119882
  exports.DetectionSourcesSchema = DetectionSourcesSchema;
119883
+ exports.DetectionTriggerFilter = DetectionTriggerFilter;
117644
119884
  exports.DeviceAdminLinkSchema = DeviceAdminLinkSchema;
117645
119885
  exports.DeviceCaStatusSchema = DeviceCaStatusSchema;
117646
119886
  exports.DeviceCertificateLoginMethodSchema = DeviceCertificateLoginMethodSchema;
@@ -117707,6 +119947,7 @@ exports.DoorbellStatusSchema = DoorbellStatusSchema;
117707
119947
  exports.EMBEDDING_MODEL_FAILED = EMBEDDING_MODEL_FAILED;
117708
119948
  exports.EMBEDDING_REQUIREMENTS_FAILED = EMBEDDING_REQUIREMENTS_FAILED;
117709
119949
  exports.EMBEDDING_REQUIREMENTS_UNAVAILABLE = EMBEDDING_REQUIREMENTS_UNAVAILABLE;
119950
+ exports.EMPTY_AUTOMATION_STATE = EMPTY_AUTOMATION_STATE;
117710
119951
  exports.ESTIMATE_COMPUTE_RATIO_MAX = ESTIMATE_COMPUTE_RATIO_MAX;
117711
119952
  exports.ESTIMATE_COMPUTE_RATIO_MIN = ESTIMATE_COMPUTE_RATIO_MIN;
117712
119953
  exports.EVENTFUL_CAP_NAMES = EVENTFUL_CAP_NAMES;
@@ -117895,10 +120136,12 @@ exports.ICON_CATEGORIES = ICON_CATEGORIES;
117895
120136
  exports.ICON_ENTRIES = ICON_ENTRIES;
117896
120137
  exports.ICON_KEYS = ICON_KEYS;
117897
120138
  exports.ICON_VIEWER_MODULES = ICON_VIEWER_MODULES;
120139
+ exports.IDLE_ACTION_STATUS = IDLE_ACTION_STATUS;
117898
120140
  exports.INFERENCE_DEVICE_EXCLUSION_REASONS = INFERENCE_DEVICE_EXCLUSION_REASONS;
117899
120141
  exports.INITIAL_PULSE = INITIAL_PULSE;
117900
120142
  exports.INITIAL_SPAN = INITIAL_SPAN;
117901
120143
  exports.INTEGRAL_OUTPUTS = require_composition.INTEGRAL_OUTPUTS;
120144
+ exports.INTERNAL_ACTION_PREFIX = INTERNAL_ACTION_PREFIX;
117902
120145
  exports.INVERTIBLE_CAPS = INVERTIBLE_CAPS;
117903
120146
  exports.IconKeySchema = IconKeySchema;
117904
120147
  exports.ImageContractSchema = ImageContractSchema;
@@ -118066,6 +120309,7 @@ exports.MAX_KEYS = MAX_KEYS;
118066
120309
  exports.MAX_REASONS_PER_KEY = MAX_REASONS_PER_KEY;
118067
120310
  exports.MAX_REQUEUED_BUCKETS_PER_SERIES = MAX_REQUEUED_BUCKETS_PER_SERIES;
118068
120311
  exports.MAX_SENSOR_TRIGGER_DEVICES = MAX_SENSOR_TRIGGER_DEVICES;
120312
+ exports.MAX_TRACE_KEEP = MAX_TRACE_KEEP;
118069
120313
  exports.MAX_TRACK_DEBUG_NOTE_LEN = MAX_TRACK_DEBUG_NOTE_LEN;
118070
120314
  exports.MAX_TRACK_PURITIES = MAX_TRACK_PURITIES;
118071
120315
  exports.MAX_TRACK_RELATIONS = MAX_TRACK_RELATIONS;
@@ -118789,6 +121033,7 @@ exports.RollingWindowSchema = require_composition.RollingWindowSchema;
118789
121033
  exports.RootStepSupplySchema = RootStepSupplySchema;
118790
121034
  exports.RtpSourceSchema = RtpSourceSchema;
118791
121035
  exports.RtspRestreamEntrySchema = RtspRestreamEntrySchema;
121036
+ exports.RunGate = RunGate;
118792
121037
  exports.RunnerBootCheckSchema = RunnerBootCheckSchema;
118793
121038
  exports.RunnerCameraConfigSchema = RunnerCameraConfigSchema;
118794
121039
  exports.RunnerCameraDeviceUIFields = RunnerCameraDeviceUIFields;
@@ -119014,6 +121259,8 @@ exports.TokenScopeSchema = TokenScopeSchema;
119014
121259
  exports.TopologyNodeSchema = TopologyNodeSchema;
119015
121260
  exports.TopologyProcessSchema = TopologyProcessSchema;
119016
121261
  exports.TopologyServiceSchema = TopologyServiceSchema;
121262
+ exports.TraceNodeOutcomeSchema = TraceNodeOutcomeSchema;
121263
+ exports.TraceValueSchema = TraceValueSchema;
119017
121264
  exports.TrackCascadeCountsSchema = TrackCascadeCountsSchema;
119018
121265
  exports.TrackEnvelopeSchema = TrackEnvelopeSchema;
119019
121266
  exports.TrackFlagsPatchSchema = TrackFlagsPatchSchema;
@@ -119120,6 +121367,7 @@ exports.VectorStatsInputSchema = VectorStatsInputSchema;
119120
121367
  exports.VectorStatsResultSchema = VectorStatsResultSchema;
119121
121368
  exports.VectorUpsertInputSchema = VectorUpsertInputSchema;
119122
121369
  exports.VectorUpsertResultSchema = VectorUpsertResultSchema;
121370
+ exports.VendorLoginProxy = VendorLoginProxy;
119123
121371
  exports.VibrationStatusSchema = VibrationStatusSchema;
119124
121372
  exports.VideoEncodeSchema = VideoEncodeSchema;
119125
121373
  exports.ViewerDeviceAllowSchema = ViewerDeviceAllowSchema;
@@ -119165,6 +121413,8 @@ exports.accessoriesCapability = accessoriesCapability;
119165
121413
  exports.accessoryChildrenComplete = accessoryChildrenComplete;
119166
121414
  exports.accessoryChildrenUnknown = accessoryChildrenUnknown;
119167
121415
  exports.accessoryStableId = accessoryStableId;
121416
+ exports.actionSourceCapability = actionSourceCapability;
121417
+ exports.actionsCapability = actionsCapability;
119168
121418
  exports.actionsForBoundCaps = actionsForBoundCaps;
119169
121419
  exports.actionsForDevice = actionsForDevice;
119170
121420
  exports.actionsForDeviceType = actionsForDeviceType;
@@ -119173,6 +121423,7 @@ exports.activateAlarmProfile = activateAlarmProfile;
119173
121423
  exports.activeForReducer = activeForReducer;
119174
121424
  exports.actuatorMethodRow = actuatorMethodRow;
119175
121425
  exports.addonCatalogHref = addonCatalogHref;
121426
+ exports.addonIdOfActionId = addonIdOfActionId;
119176
121427
  exports.addonPagesCapability = addonPagesCapability;
119177
121428
  exports.addonPagesSourceCapability = addonPagesSourceCapability;
119178
121429
  exports.addonRoutesCapability = addonRoutesCapability;
@@ -119199,6 +121450,7 @@ exports.allowedSuppliedEffect = allowedSuppliedEffect;
119199
121450
  exports.ambientLightSensorCapability = ambientLightSensorCapability;
119200
121451
  exports.annotateClipsWithEventLabels = annotateClipsWithEventLabels;
119201
121452
  exports.appFlowLocation = appFlowLocation;
121453
+ exports.appendTrace = appendTrace;
119202
121454
  exports.applianceDeviceType = require_composition.applianceDeviceType;
119203
121455
  exports.applianceExport = applianceExport;
119204
121456
  exports.applyBlockingEdit = applyBlockingEdit;
@@ -119246,6 +121498,7 @@ exports.bindAddonActions = bindAddonActions;
119246
121498
  exports.brightnessCapability = brightnessCapability;
119247
121499
  exports.brokerCapability = brokerCapability;
119248
121500
  exports.bucketStartOf = bucketStartOf;
121501
+ exports.buildActionSteps = buildActionSteps;
119249
121502
  exports.buildAddonRouteProvider = buildAddonRouteProvider;
119250
121503
  exports.buildAudioArgs = require_composition.buildAudioArgs;
119251
121504
  exports.buildBenchStepTree = buildBenchStepTree;
@@ -119264,6 +121517,7 @@ exports.buildSecretRef = require_sleep.buildSecretRef;
119264
121517
  exports.buildSessionLostAlert = buildSessionLostAlert;
119265
121518
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
119266
121519
  exports.buildSubjectPath = buildSubjectPath;
121520
+ exports.buildTriggerTrace = buildTriggerTrace;
119267
121521
  exports.buildVideoArgs = require_composition.buildVideoArgs;
119268
121522
  exports.buildViewerLink = buildViewerLink;
119269
121523
  exports.builtCapabilityBlockers = builtCapabilityBlockers;
@@ -119318,6 +121572,7 @@ exports.codecIsAccepted = codecIsAccepted;
119318
121572
  exports.collectHydratedFieldEntries = require_sleep.collectHydratedFieldEntries;
119319
121573
  exports.collectHydratedFieldValues = require_sleep.collectHydratedFieldValues;
119320
121574
  exports.collectKeychainStoredKeys = require_sleep.collectKeychainStoredKeys;
121575
+ exports.collectMirrorSources = collectMirrorSources;
119321
121576
  exports.collectModelBoundKeys = collectModelBoundKeys;
119322
121577
  exports.collectSecretConfigKeys = require_sleep.collectSecretConfigKeys;
119323
121578
  exports.collectSecretLeaves = require_sleep.collectSecretLeaves;
@@ -119438,6 +121693,7 @@ exports.describeSystemEventDeliveryProblem = describeSystemEventDeliveryProblem;
119438
121693
  exports.describeSystemEventFamilyProblem = describeSystemEventFamilyProblem;
119439
121694
  exports.describeTopLevelStateFields = describeTopLevelStateFields;
119440
121695
  exports.detectAccessRole = detectAccessRole;
121696
+ exports.detectionClassMatches = detectionClassMatches;
119441
121697
  exports.detectionPipelineCapability = detectionPipelineCapability;
119442
121698
  exports.deviceAdminLinkCapability = deviceAdminLinkCapability;
119443
121699
  exports.deviceAdoptionCapability = deviceAdoptionCapability;
@@ -119498,6 +121754,7 @@ exports.estimateNodeBenchmarkDurationMs = estimateNodeBenchmarkDurationMs;
119498
121754
  exports.evaluateAggregate = evaluateAggregate;
119499
121755
  exports.evaluateAst = require_composition.evaluateAst;
119500
121756
  exports.evaluateCompositionField = evaluateCompositionField;
121757
+ exports.evaluateConditionTree = evaluateConditionTree;
119501
121758
  exports.evaluateCount = require_composition.evaluateCount;
119502
121759
  exports.evaluateDerivative = require_composition.evaluateDerivative;
119503
121760
  exports.evaluateExpressionSource = require_composition.evaluateExpressionSource;
@@ -119608,6 +121865,7 @@ exports.integralDraft = integralDraft;
119608
121865
  exports.integrateReading = require_composition.integrateReading;
119609
121866
  exports.integrationsCapability = integrationsCapability;
119610
121867
  exports.intercomCapability = intercomCapability;
121868
+ exports.internalActionId = internalActionId;
119611
121869
  exports.interpolate = interpolate;
119612
121870
  exports.interpretStoredClusterModel = interpretStoredClusterModel;
119613
121871
  exports.invalidClusterStepModelEntries = invalidClusterStepModelEntries;
@@ -119649,6 +121907,7 @@ exports.isFirstLevelMacroClass = isFirstLevelMacroClass;
119649
121907
  exports.isHeldBackTemplateVar = isHeldBackTemplateVar;
119650
121908
  exports.isHistoryHelper = require_composition.isHistoryHelper;
119651
121909
  exports.isIconKey = isIconKey;
121910
+ exports.isInternalActionId = isInternalActionId;
119652
121911
  exports.isIsolatedBuiltin = isIsolatedBuiltin;
119653
121912
  exports.isKeychainStoredField = require_sleep.isKeychainStoredField;
119654
121913
  exports.isKnownTemplateVar = isKnownTemplateVar;
@@ -119677,6 +121936,7 @@ exports.isSecretMarkerLike = require_sleep.isSecretMarkerLike;
119677
121936
  exports.isSecretRef = require_sleep.isSecretRef;
119678
121937
  exports.isSecretRefOwnedBy = require_sleep.isSecretRefOwnedBy;
119679
121938
  exports.isSingleHubAddressRole = isSingleHubAddressRole;
121939
+ exports.isSkippedTrace = isSkippedTrace;
119680
121940
  exports.isSoftwareDecode = require_composition.isSoftwareDecode;
119681
121941
  exports.isSourceCap = require_composition.isSourceCap;
119682
121942
  exports.isStateLabelKey = isStateLabelKey;
@@ -119749,6 +122009,7 @@ exports.mergeOverrides = mergeOverrides;
119749
122009
  exports.mergeScalarTarget = mergeScalarTarget;
119750
122010
  exports.mergeSecretStatePaths = require_sleep.mergeSecretStatePaths;
119751
122011
  exports.mergeSettingsBags = mergeSettingsBags;
122012
+ exports.mergeSkipTraces = mergeSkipTraces;
119752
122013
  exports.mergeSourceInfo = mergeSourceInfo;
119753
122014
  exports.meshNetworkCapability = meshNetworkCapability;
119754
122015
  exports.meterCapability = meterCapability;
@@ -119954,6 +122215,7 @@ exports.requiredTopLevelKeys = requiredTopLevelKeys;
119954
122215
  exports.requiresPython = requiresPython;
119955
122216
  exports.reservedSourceOwner = reservedSourceOwner;
119956
122217
  exports.resetPoolBaseline = resetPoolBaseline;
122218
+ exports.resolveActionVariables = resolveActionVariables;
119957
122219
  exports.resolveAddonExecution = resolveAddonExecution;
119958
122220
  exports.resolveAddonGroup = resolveAddonGroup;
119959
122221
  exports.resolveAddonPlacement = resolveAddonPlacement;
@@ -119968,6 +122230,7 @@ exports.resolveCoreBlockFiles = resolveCoreBlockFiles;
119968
122230
  exports.resolveCreateExportSubject = resolveCreateExportSubject;
119969
122231
  exports.resolveDeviceControlKind = resolveDeviceControlKind;
119970
122232
  exports.resolveDeviceProfile = resolveDeviceProfile;
122233
+ exports.resolveDeviceTarget = resolveDeviceTarget;
119971
122234
  exports.resolveEffectiveShares = resolveEffectiveShares;
119972
122235
  exports.resolveEgressDecodeHwAccel = resolveEgressDecodeHwAccel;
119973
122236
  exports.resolveExportProfiles = resolveExportProfiles;
@@ -119994,6 +122257,7 @@ exports.resolveRunnerId = resolveRunnerId;
119994
122257
  exports.resolveSecretMarkers = require_sleep.resolveSecretMarkers;
119995
122258
  exports.resolveSecretViewsIn = require_sleep.resolveSecretViewsIn;
119996
122259
  exports.resolveServerName = resolveServerName;
122260
+ exports.resolveStepArgs = resolveStepArgs;
119997
122261
  exports.resolveStreamMaps = require_composition.resolveStreamMaps;
119998
122262
  exports.resolveVariantModelId = resolveVariantModelId;
119999
122263
  exports.resolveViewableDeviceIds = resolveViewableDeviceIds;
@@ -120001,6 +122265,10 @@ exports.resultCellKey = resultCellKey;
120001
122265
  exports.resultStandings = resultStandings;
120002
122266
  exports.retrainBoxIou = retrainBoxIou;
120003
122267
  exports.retrainRevisionConflictMessage = retrainRevisionConflictMessage;
122268
+ exports.rewriteLoginProxyBody = rewriteBody;
122269
+ exports.rewriteLoginProxyLocation = rewriteLocation;
122270
+ exports.rewriteLoginProxyReferer = rewriteReferer;
122271
+ exports.rewriteLoginProxySetCookie = rewriteSetCookie;
120004
122272
  exports.roleSpec = roleSpec;
120005
122273
  exports.rollingDurationMs = require_composition.rollingDurationMs;
120006
122274
  exports.roomCompositionExample = roomCompositionExample;
@@ -120022,6 +122290,7 @@ exports.ruleSeedForSection = ruleSeedForSection;
120022
122290
  exports.ruleTriggerZoneIds = ruleTriggerZoneIds;
120023
122291
  exports.runFanOut = runFanOut;
120024
122292
  exports.runInferenceStep = runInferenceStep;
122293
+ exports.runSteps = runSteps;
120025
122294
  exports.runningFromDishwasherOperation = runningFromDishwasherOperation;
120026
122295
  exports.runningFromLawnMower = runningFromLawnMower;
120027
122296
  exports.runningFromMediaPlayer = runningFromMediaPlayer;
@@ -120080,6 +122349,7 @@ exports.shortSecretRefHash = require_sleep.shortSecretRefHash;
120080
122349
  exports.signalPercentFromBars = signalPercentFromBars;
120081
122350
  exports.signalPercentFromRssi = signalPercentFromRssi;
120082
122351
  exports.siteZoneKey = siteZoneKey;
122352
+ exports.skipSignature = skipSignature;
120083
122353
  exports.sleep = require_sleep.sleep;
120084
122354
  exports.sleepCancellable = require_sleep.sleepCancellable;
120085
122355
  exports.sliceActiveValue = require_composition.sliceActiveValue;