@camstack/types 1.2.25 → 1.2.27

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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Which capability says what a device's STATE is, and how to read it.
3
+ *
4
+ * `deviceManager.loadRuntimeState` returns every cap's slice for a device,
5
+ * keyed by the kebab-case cap name:
6
+ *
7
+ * { 'device-status': { online: true }, switch: { on: true, … } }
8
+ *
9
+ * "The state of this device" is not a thing the platform models — a switch has
10
+ * `on`, a contact has `entryOpen`, an alarm panel has `state`. So the mapping
11
+ * is a TABLE, declared here, rather than a guess made at read time. An
12
+ * unmapped cap yields no state, and no state never matches a gate: adding a cap
13
+ * cannot silently change what an existing rule does.
14
+ *
15
+ * ORDER MATTERS. A device usually carries several of these slices (a switch is
16
+ * also `device-status`), and the first match wins — so the list runs from most
17
+ * specific to least. `device-status` is deliberately absent: "online" is not a
18
+ * state a rule should gate on, and having it here would make every device match
19
+ * the first entry and mask its real one.
20
+ */
21
+ /** How one capability's slice collapses to a single comparable string. */
22
+ interface StateReader {
23
+ /** kebab-case cap name, as `loadRuntimeState` keys it. */
24
+ readonly cap: string;
25
+ /** Field inside that slice. */
26
+ readonly field: string;
27
+ /**
28
+ * Vocabulary a rule author writes. A boolean field becomes these two words,
29
+ * in this order — `[whenTrue, whenFalse]`. Absent for a field that is already
30
+ * a string (the alarm's `state`, the cover's `state`).
31
+ */
32
+ readonly booleanWords?: readonly [string, string];
33
+ }
34
+ /**
35
+ * Most specific first. Extending this list is how a new device kind becomes
36
+ * gateable; nothing else needs to change.
37
+ */
38
+ export declare const DEVICE_STATE_READERS: readonly StateReader[];
39
+ /**
40
+ * Collapse a device's full runtime state to the one string a rule compares
41
+ * against, or `undefined` when nothing in the table applies.
42
+ *
43
+ * `undefined` is the safe answer everywhere: the gate treats it as "does not
44
+ * match", so a device whose kind we cannot read simply never arms a rule.
45
+ */
46
+ export declare function readDeviceStateFrom(runtimeState: Readonly<Record<string, unknown>>): string | undefined;
47
+ /**
48
+ * The states a rule editor can offer for a device, given its runtime state.
49
+ * Same table, same order — so the picker can never offer a value the gate
50
+ * would not recognise.
51
+ */
52
+ export declare function stateVocabularyFor(runtimeState: Readonly<Record<string, unknown>>, alarmStates?: readonly string[]): readonly string[];
53
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * AUTO-GENERATED by scripts/generate-device-scoped-caps.ts — DO NOT EDIT.
3
+ *
4
+ * Every `scope: 'device'` capability name, as plain data — so a forked runner
5
+ * can answer "may a rule actuate this?" without importing the schema barrel
6
+ * (~144MB RSS per runner, D28).
7
+ *
8
+ * Coverage: 80 device-scoped capabilities.
9
+ */
10
+ export declare const DEVICE_SCOPED_CAPS: ReadonlySet<string>;
11
+ /**
12
+ * True when `capName` is a device capability.
13
+ *
14
+ * This is the ONLY boundary on what a notification rule may actuate. A rule can
15
+ * be authored by a non-admin and the runner executes with the addon's
16
+ * privileges, so an unbounded action would be an arbitrary RPC channel with a
17
+ * privilege escalation attached. Device scope excludes the system caps
18
+ * (`device-manager.removeDevice` and friends) by construction.
19
+ */
20
+ export declare function isDeviceScopedCap(capName: string): boolean;
package/dist/index.d.ts CHANGED
@@ -121,6 +121,8 @@ export { COCO_80_LABELS, COCO_TO_MACRO, MACRO_LABELS, } from './catalogs/coco-cl
121
121
  export { AUDIO_MACRO_LABELS, YAMNET_TO_MACRO, APPLE_SA_TO_MACRO, mapAudioLabelToMacro, getAudioMacroClassIds, } from './catalogs/audio-classmap.js';
122
122
  export { EVENT_TAXONOMY, TAXONOMY_COLORS, DEFAULT_EVENT_COLOR, getTaxonomyEntry, colorForKind, subKindsOf, type EventTaxonomyEntry, type EventTaxonomyCategory, type EventTaxonomyLevel, } from './catalogs/event-taxonomy.js';
123
123
  export { NcTaxonomyEntrySchema, NcTaxonomySchema, buildNcTaxonomy, NC_TAXONOMY, type NcTaxonomyEntry, type NcTaxonomy, } from './catalogs/nc-taxonomy.js';
124
+ export { DEVICE_SCOPED_CAPS, isDeviceScopedCap } from './generated/device-scoped-caps.js';
125
+ export { DEVICE_STATE_READERS, readDeviceStateFrom, stateVocabularyFor, } from './catalogs/device-state-vocabulary.js';
124
126
  export type { CameraMotionConfig, CameraNativeDetectionConfig, CameraDetectionCapabilities, } from './types/camera-detection.js';
125
127
  export { type DeviceTypeInfo, DEVICE_TYPE_INFO } from './types/device-type.js';
126
128
  export type { DeviceBinding, DeviceBindingEntry } from './device/device-binding.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-BvSjpsfC.js");
2
+ const require_sleep = require("./sleep-6oDIDtYV.js");
3
3
  const require_event_category = require("./event-category-BE4PDZ_3.js");
4
4
  const require_enums = require("./enums.js");
5
5
  const require_err_msg = require("./err-msg-COpsHMw2.js");
@@ -2973,6 +2973,78 @@ function buildNcTaxonomy() {
2973
2973
  /** The frozen NC taxonomy, derived once from the taxonomy dictionary. */
2974
2974
  var NC_TAXONOMY = Object.freeze(buildNcTaxonomy());
2975
2975
  //#endregion
2976
+ //#region src/catalogs/device-state-vocabulary.ts
2977
+ /**
2978
+ * Most specific first. Extending this list is how a new device kind becomes
2979
+ * gateable; nothing else needs to change.
2980
+ */
2981
+ var DEVICE_STATE_READERS = [
2982
+ {
2983
+ cap: "alarm-panel",
2984
+ field: "state"
2985
+ },
2986
+ {
2987
+ cap: "cover",
2988
+ field: "state"
2989
+ },
2990
+ {
2991
+ cap: "presence",
2992
+ field: "state"
2993
+ },
2994
+ {
2995
+ cap: "lock",
2996
+ field: "locked",
2997
+ booleanWords: ["locked", "unlocked"]
2998
+ },
2999
+ {
3000
+ cap: "contact",
3001
+ field: "entryOpen",
3002
+ booleanWords: ["open", "closed"]
3003
+ },
3004
+ {
3005
+ cap: "switch",
3006
+ field: "on",
3007
+ booleanWords: ["on", "off"]
3008
+ },
3009
+ {
3010
+ cap: "binary",
3011
+ field: "on",
3012
+ booleanWords: ["on", "off"]
3013
+ }
3014
+ ];
3015
+ /**
3016
+ * Collapse a device's full runtime state to the one string a rule compares
3017
+ * against, or `undefined` when nothing in the table applies.
3018
+ *
3019
+ * `undefined` is the safe answer everywhere: the gate treats it as "does not
3020
+ * match", so a device whose kind we cannot read simply never arms a rule.
3021
+ */
3022
+ function readDeviceStateFrom(runtimeState) {
3023
+ for (const reader of DEVICE_STATE_READERS) {
3024
+ const slice = runtimeState[reader.cap];
3025
+ if (slice === null || typeof slice !== "object") continue;
3026
+ const value = slice[reader.field];
3027
+ if (typeof value === "string" && value.length > 0) return value;
3028
+ if (typeof value === "boolean" && reader.booleanWords !== void 0) return value ? reader.booleanWords[0] : reader.booleanWords[1];
3029
+ }
3030
+ }
3031
+ /**
3032
+ * The states a rule editor can offer for a device, given its runtime state.
3033
+ * Same table, same order — so the picker can never offer a value the gate
3034
+ * would not recognise.
3035
+ */
3036
+ function stateVocabularyFor(runtimeState, alarmStates = []) {
3037
+ for (const reader of DEVICE_STATE_READERS) {
3038
+ const slice = runtimeState[reader.cap];
3039
+ if (slice === null || typeof slice !== "object") continue;
3040
+ if (reader.booleanWords !== void 0) return [...reader.booleanWords];
3041
+ if (reader.cap === "alarm-panel") return [...alarmStates];
3042
+ const current = slice[reader.field];
3043
+ return typeof current === "string" ? [current] : [];
3044
+ }
3045
+ return [];
3046
+ }
3047
+ //#endregion
2976
3048
  //#region src/types/device-type.ts
2977
3049
  var DEVICE_TYPE_INFO = { ["camera"]: {
2978
3050
  type: "camera",
@@ -4784,7 +4856,87 @@ var NcZoneConditionSchema = zod.z.object({
4784
4856
  * The P1 condition set — a flat AND of groups; absent group = pass;
4785
4857
  * membership lists are OR within the list (spec §2.3).
4786
4858
  */
4859
+ /**
4860
+ * What a rule may actuate.
4861
+ *
4862
+ * **No hand-maintained allowlist** (operator decision, and the right one — a
4863
+ * written list of methods is a third parallel map to keep aligned, and this
4864
+ * repo has paid for those). The boundary instead comes from a property the
4865
+ * capabilities already carry: an action may target only a **device-scoped**
4866
+ * capability method.
4867
+ *
4868
+ * That is not decoration. A rule can be authored by a NON-ADMIN — personal
4869
+ * rules are a supported flow — and the executor runs with the addon's
4870
+ * privileges, so an unbounded action is an arbitrary RPC channel with a
4871
+ * privilege escalation attached. Restricting to device scope excludes the
4872
+ * system caps (`device-manager.removeDevice` and friends) by construction,
4873
+ * costs nothing to maintain, and cannot rot: a cap that stops being
4874
+ * device-scoped stops being actuatable in the same change.
4875
+ *
4876
+ * The executor enforces it; {@link NcRuleActionSchema} carries the intent.
4877
+ */
4878
+ /**
4879
+ * One step of a sequence.
4880
+ *
4881
+ * `wait` is a first-class step rather than a property of the next action: it is
4882
+ * what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
4883
+ * cannot be expressed otherwise.
4884
+ */
4885
+ var NcRuleActionSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
4886
+ kind: zod.z.literal("wait"),
4887
+ seconds: zod.z.number().min(0).max(300)
4888
+ }), zod.z.object({
4889
+ kind: zod.z.literal("cap"),
4890
+ deviceId: zod.z.number().int(),
4891
+ /** Capability name, e.g. `alarm-panel`. */
4892
+ cap: zod.z.string().min(1),
4893
+ /** Method on it. The executor refuses a non-device-scoped cap. */
4894
+ method: zod.z.string().min(1),
4895
+ /** Method arguments, minus `deviceId` (the executor injects it). */
4896
+ args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
4897
+ })]);
4898
+ /**
4899
+ * A named, ordered run of steps with its own throttle.
4900
+ *
4901
+ * `minDelaySec` exists because a noisy rule otherwise hammers a physical
4902
+ * actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
4903
+ * different budget from "how often may this gate actually open".
4904
+ */
4905
+ var NcRuleActionSequenceSchema = zod.z.object({
4906
+ name: zod.z.string().min(1).max(120),
4907
+ enabled: zod.z.boolean(),
4908
+ minDelaySec: zod.z.number().int().min(0).max(86400).optional(),
4909
+ actions: zod.z.array(NcRuleActionSchema).min(1)
4910
+ });
4911
+ /**
4912
+ * Sequences a rule runs, by hook point.
4913
+ *
4914
+ * ONLY `onTrigger` is here, deliberately. The reference also has activation /
4915
+ * deactivation / reset / post-generation hooks, and they are wanted — but this
4916
+ * repo's expensive failure mode is declaring a surface nothing produces, so a
4917
+ * hook appears here in the same change that produces its edge, never before.
4918
+ */
4919
+ var NcRuleActionsSchema = zod.z.object({
4920
+ /** Runs when the rule MATCHES. */
4921
+ onTrigger: zod.z.array(NcRuleActionSequenceSchema).optional() });
4922
+ /**
4923
+ * "This rule applies only while `deviceId` is in one of `states`."
4924
+ *
4925
+ * The states are the DEVICE's own vocabulary — `AlarmState` for a panel,
4926
+ * `on`/`off` for a switch — not a normalised set, because normalising would
4927
+ * make the condition lie about devices whose states have no equivalent.
4928
+ *
4929
+ * An unreadable state does NOT match: see the engine's fail-closed gate. A
4930
+ * condition that fired on "I could not read it" would be worse than no gate.
4931
+ */
4932
+ var NcDeviceStateConditionSchema = zod.z.object({
4933
+ deviceId: zod.z.number().int(),
4934
+ /** Any of these matches. */
4935
+ states: zod.z.array(zod.z.string().min(1)).min(1)
4936
+ });
4787
4937
  var NcConditionsSchema = zod.z.object({
4938
+ /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
4939
+ deviceState: NcDeviceStateConditionSchema.optional(),
4788
4940
  /** Device scope — absent = all devices. */
4789
4941
  devices: zod.z.array(zod.z.number()).optional(),
4790
4942
  /** Detector class names (any overlap with the record's class set). */
@@ -5078,7 +5230,16 @@ var NcRuleInputSchema = zod.z.object({
5078
5230
  * read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
5079
5231
  * by this flag — see the scope rules on that function.
5080
5232
  */
5081
- snoozeAllowGlobal: zod.z.boolean().optional()
5233
+ snoozeAllowGlobal: zod.z.boolean().optional(),
5234
+ /**
5235
+ * Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
5236
+ *
5237
+ * This is what makes the rule set the alarm's trigger set without the alarm
5238
+ * being a special case: arming is
5239
+ * `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
5240
+ * shape as every other actuation.
5241
+ */
5242
+ actions: NcRuleActionsSchema.optional()
5082
5243
  });
5083
5244
  /**
5084
5245
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -5149,7 +5310,8 @@ var NcConditionDescriptorSchema = zod.z.object({
5149
5310
  "packagePhase",
5150
5311
  "crossingSelect",
5151
5312
  "polygonDraw",
5152
- "occupancy"
5313
+ "occupancy",
5314
+ "deviceState"
5153
5315
  ]),
5154
5316
  operator: zod.z.enum([
5155
5317
  "in",
@@ -5408,6 +5570,21 @@ var NC_CONDITION_CATALOG = [
5408
5570
  phase: "P1",
5409
5571
  description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
5410
5572
  },
5573
+ {
5574
+ id: "deviceState",
5575
+ group: "scope",
5576
+ label: "Device state",
5577
+ valueType: "deviceState",
5578
+ operator: "in",
5579
+ appliesTo: [
5580
+ "immediate",
5581
+ "track-end",
5582
+ "device-event",
5583
+ "package-event"
5584
+ ],
5585
+ phase: "P2",
5586
+ description: "Only fire while another device is in one of the chosen states — the alarm armed, a switch on, a contact closed. A state that cannot be read does NOT fire."
5587
+ },
5411
5588
  {
5412
5589
  id: "sensorKinds",
5413
5590
  group: "device",
@@ -34284,7 +34461,9 @@ exports.DEFAULT_SCRUB_THUMBNAIL_PRESET = DEFAULT_SCRUB_THUMBNAIL_PRESET;
34284
34461
  exports.DEVICE_BACKEND_TO_FORMAT = DEVICE_BACKEND_TO_FORMAT;
34285
34462
  exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
34286
34463
  exports.DEVICE_PROFILES = DEVICE_PROFILES;
34464
+ exports.DEVICE_SCOPED_CAPS = require_sleep.DEVICE_SCOPED_CAPS;
34287
34465
  exports.DEVICE_SETTINGS_CONTRIBUTION_METHODS = require_sleep.DEVICE_SETTINGS_CONTRIBUTION_METHODS;
34466
+ exports.DEVICE_STATE_READERS = DEVICE_STATE_READERS;
34288
34467
  exports.DEVICE_STATUS_METHOD = require_sleep.DEVICE_STATUS_METHOD;
34289
34468
  exports.DEVICE_TYPE_CONTROL_KIND = DEVICE_TYPE_CONTROL_KIND;
34290
34469
  exports.DEVICE_TYPE_INFO = DEVICE_TYPE_INFO;
@@ -34496,6 +34675,7 @@ exports.NcConditionDescriptorSchema = NcConditionDescriptorSchema;
34496
34675
  exports.NcConditionsSchema = NcConditionsSchema;
34497
34676
  exports.NcCrossingSchema = NcCrossingSchema;
34498
34677
  exports.NcDeliverySchema = NcDeliverySchema;
34678
+ exports.NcDeviceStateConditionSchema = NcDeviceStateConditionSchema;
34499
34679
  exports.NcHistoryEntrySchema = NcHistoryEntrySchema;
34500
34680
  exports.NcHistoryFilterSchema = NcHistoryFilterSchema;
34501
34681
  exports.NcHistoryRecordKindSchema = NcHistoryRecordKindSchema;
@@ -34505,6 +34685,9 @@ exports.NcMediaFrameSchema = NcMediaFrameSchema;
34505
34685
  exports.NcMediaPolicySchema = NcMediaPolicySchema;
34506
34686
  exports.NcOccupancyConditionSchema = NcOccupancyConditionSchema;
34507
34687
  exports.NcPlateMatcherSchema = NcPlateMatcherSchema;
34688
+ exports.NcRuleActionSchema = NcRuleActionSchema;
34689
+ exports.NcRuleActionSequenceSchema = NcRuleActionSequenceSchema;
34690
+ exports.NcRuleActionsSchema = NcRuleActionsSchema;
34508
34691
  exports.NcRuleInputSchema = NcRuleInputSchema;
34509
34692
  exports.NcRulePatchSchema = NcRulePatchSchema;
34510
34693
  exports.NcRuleSchema = NcRuleSchema;
@@ -34921,6 +35104,7 @@ exports.isBaseConditionKey = isBaseConditionKey;
34921
35104
  exports.isCollectionArrayMethod = isCollectionArrayMethod;
34922
35105
  exports.isDeployableToAgent = isDeployableToAgent;
34923
35106
  exports.isDeviceConfigCap = require_sleep.isDeviceConfigCap;
35107
+ exports.isDeviceScopedCap = require_sleep.isDeviceScopedCap;
34924
35108
  exports.isEvent = require_sleep.isEvent;
34925
35109
  exports.isNode = isNode;
34926
35110
  exports.isObjectInput = isObjectInput;
@@ -35001,6 +35185,7 @@ exports.procedureAuthKey = procedureAuthKey;
35001
35185
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
35002
35186
  exports.ptzCapability = ptzCapability;
35003
35187
  exports.pythonScriptForBackend = pythonScriptForBackend;
35188
+ exports.readDeviceStateFrom = readDeviceStateFrom;
35004
35189
  exports.readNodePin = require_sleep.readNodePin;
35005
35190
  exports.readinessKey = require_sleep.readinessKey;
35006
35191
  exports.rebootCapability = rebootCapability;
@@ -35040,6 +35225,7 @@ exports.smtpProviderCapability = smtpProviderCapability;
35040
35225
  exports.snapshotCapability = snapshotCapability;
35041
35226
  exports.ssoBridgeCapability = ssoBridgeCapability;
35042
35227
  exports.startReachabilityPoll = startReachabilityPoll;
35228
+ exports.stateVocabularyFor = stateVocabularyFor;
35043
35229
  exports.storageCapability = storageCapability;
35044
35230
  exports.storageEvictableCapability = storageEvictableCapability;
35045
35231
  exports.storageProviderCapability = storageProviderCapability;