@camstack/addon-export-alexa 1.2.133 → 1.2.135

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.
@@ -1,5 +1,6 @@
1
1
  import * as crypto$1 from "node:crypto";
2
- //#region ../types/dist/event-category-BGmz_oxW.mjs
2
+ import { randomUUID } from "node:crypto";
3
+ //#region ../types/dist/event-category-B_0XEcLF.mjs
3
4
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
5
  EventCategory["SystemBoot"] = "system.boot";
5
6
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -500,15 +501,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
500
501
  EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
501
502
  EventCategory["AgentBackupActivated"] = "agent.backup.activated";
502
503
  EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
503
- /**
504
- * Per-agent hwaccel preference changed (user override set, cleared, or
505
- * re-probed). Observability event — decoders pull the current pref at
506
- * `createSession`, so running sessions keep their current backend until
507
- * they rotate naturally (camera add/remove, stream restart). Future
508
- * work can wire a listener in stream-broker that force-rotates live
509
- * sessions; for now this event powers logs + admin-UI toast feedback.
510
- */
511
- EventCategory["PipelineAgentHwaccelChanged"] = "pipeline.agent-hwaccel-changed";
512
504
  EventCategory["MotionAnalysis"] = "detection.motion-analysis";
513
505
  /** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
514
506
  EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
@@ -5379,7 +5371,7 @@ var ZodIssueCode = {
5379
5371
  var ZodFirstPartyTypeKind;
5380
5372
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5381
5373
  //#endregion
5382
- //#region ../types/dist/sleep-DAJ9Er57.mjs
5374
+ //#region ../types/dist/sleep-BNeXenvr.mjs
5383
5375
  /**
5384
5376
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5385
5377
  * window to float samples (D455).
@@ -6745,32 +6737,12 @@ object({
6745
6737
  audioCodec: string().optional(),
6746
6738
  metadata: record(string(), unknown()).readonly().optional()
6747
6739
  });
6748
- var EncodedPacketSchema = object({
6749
- type: _enum(["video", "audio"]),
6750
- data: _instanceof(Uint8Array),
6751
- pts: number(),
6752
- dts: number(),
6753
- keyframe: boolean(),
6754
- codec: string()
6755
- });
6756
- var DecodedFrameSchema = object({
6757
- data: _instanceof(Uint8Array),
6758
- width: number(),
6759
- height: number(),
6760
- format: _enum([
6761
- "jpeg",
6762
- "rgb",
6763
- "bgr",
6764
- "yuv420",
6765
- "gray"
6766
- ]),
6767
- timestamp: number()
6768
- });
6769
6740
  /**
6770
6741
  * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
6771
6742
  * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
6772
- * interface in `interfaces/frame-handle.ts` field-for-field so a decoder cap
6773
- * method (`pullHandles`) can carry handles over tRPC / Moleculer.
6743
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
6744
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
6745
+ * over tRPC / Moleculer.
6774
6746
  *
6775
6747
  * The `satisfies` assertion below pins this schema to the interface: if a
6776
6748
  * field is added to / removed from `FrameHandle` without a matching schema
@@ -6796,50 +6768,6 @@ var FrameHandleSchema = object({
6796
6768
  slotCount: number().int().positive()
6797
6769
  });
6798
6770
  /**
6799
- * Pixel format a frame-handle subscriber can request from the broker
6800
- * (Phase 5 / D9). The packed, raster subset of `FrameFormat` — the formats a
6801
- * `frameSink: 'shm'` decoder session can write into a `FrameRing` slot.
6802
- * `jpeg` is excluded: a variable-length stream is not a fixed-stride ring
6803
- * payload, and no decoded-frame consumer requests it over the shm plane.
6804
- */
6805
- var FrameHandleFormatSchema = _enum([
6806
- "rgb",
6807
- "bgr",
6808
- "yuv420",
6809
- "gray"
6810
- ]);
6811
- /**
6812
- * Input for `stream-broker.subscribeFrames` (Phase 5 / D9). A consumer asks the
6813
- * broker for a `FrameHandle` stream of one `format`; the broker maintains one
6814
- * shared-memory ring per `(brokerId, format)` actually requested, so a `gray`
6815
- * subscriber (motion) and an `rgb` subscriber (detection) each read the format
6816
- * they asked for with no broker-side conversion.
6817
- */
6818
- var SubscribeFramesInputSchema = object({
6819
- brokerId: string(),
6820
- format: FrameHandleFormatSchema,
6821
- /**
6822
- * Optional reader-side cadence hint in frames per second. The broker does
6823
- * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
6824
- * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
6825
- * the consumer can pace its own `pullFrameHandles` polling.
6826
- */
6827
- maxFps: number().positive().optional(),
6828
- /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
6829
- tag: string().optional()
6830
- });
6831
- /**
6832
- * Result of `stream-broker.subscribeFrames`. The consumer then polls
6833
- * `pullFrameHandles({ subscriptionId, maxCount })` and feeds each returned
6834
- * `FrameHandle` to a `FrameRingReader`.
6835
- */
6836
- var SubscribeFramesResultSchema = object({
6837
- /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
6838
- subscriptionId: string(),
6839
- /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6840
- maxFps: number().nonnegative()
6841
- });
6842
- /**
6843
6771
  * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
6844
6772
  * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
6845
6773
  * sample bytes plus the track parameters a consumer needs to interpret them.
@@ -6847,8 +6775,8 @@ var SubscribeFramesResultSchema = object({
6847
6775
  * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
6848
6776
  * Float32 PCM), so unlike video frames they ship their bytes INLINE over
6849
6777
  * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
6850
- * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
6851
- * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
6778
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
6779
+ * it.
6852
6780
  */
6853
6781
  var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
6854
6782
  var DecodedAudioChunkSchema = object({
@@ -6866,11 +6794,10 @@ var DecodedAudioChunkSchema = object({
6866
6794
  format: AudioChunkFormatSchema.optional()
6867
6795
  });
6868
6796
  /**
6869
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
6870
- * handle-free, ship-bytes-inline analogue of `subscribeFrames`: a consumer
6871
- * asks the broker for the decoded audio-chunk stream of one broker; the
6872
- * broker keeps a per-subscription bounded FIFO queue the consumer drains via
6873
- * `pullAudioChunks`.
6797
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
6798
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
6799
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
6800
+ * the consumer drains via `pullAudioChunks`.
6874
6801
  */
6875
6802
  var SubscribeAudioChunksInputSchema = object({
6876
6803
  brokerId: string(),
@@ -6901,9 +6828,7 @@ var BrokerStatsSchema = object({
6901
6828
  "stopped"
6902
6829
  ]),
6903
6830
  inputFps: number(),
6904
- decodeFps: number(),
6905
6831
  encodedSubscribers: number(),
6906
- decodedSubscribers: number(),
6907
6832
  uptimeMs: number(),
6908
6833
  bitrateKbps: number(),
6909
6834
  idrIntervalMs: number(),
@@ -6916,14 +6841,6 @@ var BrokerStatsSchema = object({
6916
6841
  preBufferMs: number(),
6917
6842
  preBufferPackets: number(),
6918
6843
  /**
6919
- * Moleculer node id of the decoder provider currently servicing this
6920
- * stream's decoded subscribers. `null` until the deferred decoder is
6921
- * created (no decoded clients yet, or codec not detected). Surfaces the
6922
- * runtime decoder placement so the UI can show "Decoder: <agent>" without
6923
- * a separate cap call per broker.
6924
- */
6925
- decoderNodeId: string().nullable(),
6926
- /**
6927
6844
  * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
6928
6845
  * when the stream has no audio track or the broker is in cold start.
6929
6846
  * `supported = false` means the codec was detected but the local
@@ -10446,70 +10363,6 @@ var StorageLocationDeclarationSchema = object({
10446
10363
  "backup"
10447
10364
  ]).optional()
10448
10365
  });
10449
- var DecoderStatsSchema = object({
10450
- inputFps: number(),
10451
- outputFps: number(),
10452
- avgDecodeTimeMs: number(),
10453
- droppedFrames: number(),
10454
- /**
10455
- * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
10456
- * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
10457
- * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
10458
- * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
10459
- */
10460
- lagMs: number().optional(),
10461
- effectiveFps: number().optional(),
10462
- adaptiveFps: number().optional()
10463
- });
10464
- var DecoderSessionConfigSchema = object({
10465
- codec: string(),
10466
- maxFps: number().default(0),
10467
- outputFormat: _enum([
10468
- "jpeg",
10469
- "rgb",
10470
- "bgr",
10471
- "yuv420",
10472
- "gray"
10473
- ]).default("jpeg"),
10474
- scale: number().default(1),
10475
- width: number().optional(),
10476
- height: number().optional(),
10477
- /**
10478
- * Identifier of the camera this decoder session serves. Optional
10479
- * because the cap is generic (any caller could request decode), but
10480
- * stream-broker passes it so decoder logs include `deviceId` for
10481
- * per-camera filtering when diagnosing failures (e.g. node-av
10482
- * sendPacket errors on a single hung camera).
10483
- */
10484
- deviceId: number().int().nonnegative().optional(),
10485
- /**
10486
- * Free-form tag for log scoping. Stream-broker uses
10487
- * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
10488
- * on every line so `grep tag=broker:5/high` filters one camera
10489
- * profile cleanly.
10490
- */
10491
- tag: string().optional(),
10492
- /**
10493
- * Where the session delivers decoded frames (Phase 5 / D9):
10494
- *
10495
- * - `'callback'` (default) — the legacy pixel path: decoded frames are
10496
- * buffered as `DecodedFrame`s and drained via `pullFrames`.
10497
- * - `'shm'` — the shared-memory frame plane: decoded frames are written
10498
- * into an OS shared-memory ring and drained as zero-pixel
10499
- * `FrameHandle`s via `pullHandles`. A session is one mode or the
10500
- * other — `pullFrames` returns nothing for an `'shm'` session and
10501
- * `pullHandles` returns nothing for a `'callback'` session.
10502
- */
10503
- frameSink: _enum(["callback", "shm"]).default("callback"),
10504
- /**
10505
- * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
10506
- * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
10507
- * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
10508
- * stream-broker's `streamingDebug` gate — off by default so production logs
10509
- * stay quiet and the emit path pays zero per-frame cost when disabled.
10510
- */
10511
- debug: boolean().optional()
10512
- });
10513
10366
  /**
10514
10367
  * Per-component log CHANNELS — the gate a hot path consults, and the registry
10515
10368
  * an addon declares its channels in.
@@ -12770,13 +12623,6 @@ var BrokerRtspClientSchema = object({
12770
12623
  lastRtpAt: number(),
12771
12624
  bytesSent: number()
12772
12625
  });
12773
- var BrokerDecodedClientSchema = object({
12774
- tag: string(),
12775
- subscribedAt: number(),
12776
- maxFps: number(),
12777
- framesDelivered: number(),
12778
- framesDropped: number()
12779
- });
12780
12626
  var BrokerAudioClientSchema = object({
12781
12627
  tag: string(),
12782
12628
  subscribedAt: number(),
@@ -12868,7 +12714,6 @@ var BrokerEncodedClientSchema = object({
12868
12714
  });
12869
12715
  var BrokerClientsSchema = object({
12870
12716
  rtsp: array(BrokerRtspClientSchema).readonly(),
12871
- decoded: array(BrokerDecodedClientSchema).readonly(),
12872
12717
  audio: array(BrokerAudioClientSchema).readonly(),
12873
12718
  encoded: array(BrokerEncodedClientSchema).readonly(),
12874
12719
  pipeClients: number(),
@@ -13237,11 +13082,7 @@ method(object({
13237
13082
  auth: "admin"
13238
13083
  }), method(object({ brokerId: string() }), BrokerClientsSchema), method(object({
13239
13084
  brokerId: string(),
13240
- channel: _enum([
13241
- "rtsp",
13242
- "decoded",
13243
- "audio"
13244
- ]),
13085
+ channel: _enum(["rtsp", "audio"]),
13245
13086
  handle: string()
13246
13087
  }), object({ killed: boolean() }), {
13247
13088
  kind: "mutation",
@@ -13276,10 +13117,7 @@ method(object({
13276
13117
  }), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
13277
13118
  subscriptionId: string(),
13278
13119
  maxCount: number().int().positive().default(8)
13279
- }), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(SubscribeFramesInputSchema, SubscribeFramesResultSchema, { kind: "mutation" }), method(object({
13280
- subscriptionId: string(),
13281
- maxCount: number().int().positive().default(4)
13282
- }), array(FrameHandleSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
13120
+ }), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
13283
13121
  brokerId: string(),
13284
13122
  seconds: number().min(0).max(30)
13285
13123
  }), _void(), {
@@ -14436,54 +14274,12 @@ id: union([string(), number()]) }), {
14436
14274
  kind: "mutation",
14437
14275
  auth: "admin"
14438
14276
  });
14439
- /**
14440
- * shm ring usage stats for a `frameSink: 'shm'` decoder session —
14441
- * exposed via `decoder.getShmStats` so downstream consumers can
14442
- * observe ring pressure (slot count, byte budget, hit/miss ratio).
14443
- */
14444
- var ShmRingStatsSchema = object({
14445
- sessionId: string(),
14446
- slotCount: number().int(),
14447
- slotByteLength: number().int(),
14448
- segmentBytes: number().int(),
14449
- budgetMb: number().int(),
14450
- framesWritten: number().int(),
14451
- getFrameHits: number().int(),
14452
- getFrameMisses: number().int()
14453
- });
14454
- method(object({ codec: string() }), boolean()), method(_void(), object({
14277
+ method(_void(), object({
14455
14278
  id: string(),
14456
14279
  name: string(),
14457
- isPullMode: boolean().optional(),
14458
- priority: number().optional(),
14459
14280
  hwaccel: string().optional(),
14460
14281
  probedBestHwaccel: string().optional()
14461
- })), method(DecoderSessionConfigSchema, object({
14462
- sessionId: string(),
14463
- nodeId: string()
14464
- })), method(object({ sessionId: string() }), _void()), method(object({
14465
- sessionId: string(),
14466
- packet: EncodedPacketSchema
14467
- }), _void()), method(object({
14468
- sessionId: string(),
14469
- url: string()
14470
- }), _void()), method(object({
14471
- sessionId: string(),
14472
- maxCount: number().default(1),
14473
- waitMs: number().optional()
14474
- }), array(DecodedFrameSchema)), method(object({
14475
- sessionId: string(),
14476
- maxCount: number().default(1),
14477
- waitMs: number().optional()
14478
- }), array(FrameHandleSchema)), method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()), method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()), method(object({
14479
- sessionId: string(),
14480
- config: DecoderSessionConfigSchema.partial()
14481
- }), _void()), method(object({ sessionId: string() }), DecoderStatsSchema), method(_void(), array(object({
14482
- sessionId: string(),
14483
- codec: string(),
14484
- outputFormat: string(),
14485
- createdAtMs: number()
14486
- })).readonly()), method(_void(), object({ backend: string() }), {
14282
+ })), method(_void(), object({ backend: string() }), {
14487
14283
  kind: "mutation",
14488
14284
  auth: "admin"
14489
14285
  });
@@ -21544,8 +21340,8 @@ Object.freeze(Object.fromEntries([{
21544
21340
  var CLUSTER_STEP_SETTING_FIELDS = [{
21545
21341
  stepId: "face-embedding",
21546
21342
  key: "minLandmarkFaceSize",
21547
- label: "Min face size for recognition (detection px)",
21548
- description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
21343
+ label: "Min face size to embed (detection-frame px)",
21344
+ description: "Faces smaller than this in the small frame the detector sees are not embedded, so they can never be recognised. Below it the face cannot be aligned reliably and a bad embedding matches everyone. Same value on every node. 0 = no limit.",
21549
21345
  type: "slider",
21550
21346
  min: 0,
21551
21347
  max: 64,
@@ -23597,21 +23393,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
23597
23393
  group: string().optional(),
23598
23394
  settings: record(string(), unknown()).optional()
23599
23395
  }));
23600
- var PipelineTemplateStepSchema = lazy(() => object({
23601
- addonId: string(),
23602
- enabled: boolean(),
23603
- modelId: string(),
23604
- children: array(PipelineTemplateStepSchema).readonly(),
23605
- settings: record(string(), unknown()).optional()
23606
- }));
23607
- var PipelineTemplateSchema$1 = object({
23608
- id: string(),
23609
- name: string(),
23610
- createdAt: string(),
23611
- updatedAt: string(),
23612
- engine: PipelineEngineChoiceSchema,
23613
- steps: array(PipelineTemplateStepSchema).readonly()
23614
- });
23615
23396
  var PipelineModelOptionSchema = object({
23616
23397
  id: string(),
23617
23398
  name: string(),
@@ -23823,15 +23604,7 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
23823
23604
  }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({
23824
23605
  steps: array(PipelineStepInputSchema),
23825
23606
  deviceKey: string().optional()
23826
- }), PipelineValidationResultSchema), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
23827
- name: string(),
23828
- steps: array(PipelineTemplateStepSchema).readonly(),
23829
- engine: PipelineEngineChoiceSchema
23830
- }), PipelineTemplateSchema$1, { kind: "mutation" }), method(object({
23831
- id: string(),
23832
- name: string().optional(),
23833
- steps: array(PipelineTemplateStepSchema).readonly().optional()
23834
- }), PipelineTemplateSchema$1, { kind: "mutation" }), method(object({ id: string() }), _void(), { kind: "mutation" }), method(_void(), InferenceCapabilitiesBridge), method(object({ addonId: string() }), ModelAvailabilityListBridge), method(object({
23607
+ }), PipelineValidationResultSchema), method(_void(), InferenceCapabilitiesBridge), method(object({ addonId: string() }), ModelAvailabilityListBridge), method(object({
23835
23608
  addonId: string(),
23836
23609
  modelId: string(),
23837
23610
  format: ModelFormatSchema$2
@@ -25241,14 +25014,15 @@ var RunnerLocalLoadSchema = object({
25241
25014
  * Per-inference-device live load (multi-device C4). One entry per deviceKey
25242
25015
  * this runner currently has attached cameras on, so the orchestrator's second
25243
25016
  * `balance()` pass (over a node's devices) weights on real per-pool session
25244
- * counts. Empty on single-device / pre-multi-device runners. `queueDepthTotal`
25245
- * per device is 0 until the pool backlog gets a public accessor (follow-up).
25017
+ * counts. Empty on single-device / pre-multi-device runners. There is no
25018
+ * per-device queue depth: the inference pool's backlog has no accessor, and
25019
+ * a field that always read 0 was removed rather than shown as a measurement
25020
+ * (D762).
25246
25021
  */
25247
25022
  devices: array(object({
25248
25023
  deviceKey: string(),
25249
25024
  backend: string(),
25250
- attachedCameras: number(),
25251
- queueDepthTotal: number()
25025
+ attachedCameras: number()
25252
25026
  })).default([]),
25253
25027
  /** Hardware capability flags reported by this node. */
25254
25028
  hardware: object({
@@ -25462,14 +25236,6 @@ var CameraPipelineConfigSchema = object({
25462
25236
  settings: record(string(), unknown()).readonly().optional()
25463
25237
  }).nullable().optional()
25464
25238
  });
25465
- var PipelineTemplateSchema = object({
25466
- id: string(),
25467
- name: string(),
25468
- description: string().optional(),
25469
- config: CameraPipelineConfigSchema,
25470
- createdAt: string(),
25471
- updatedAt: string()
25472
- });
25473
25239
  var DeviceStepConfigSchema = object({
25474
25240
  modelId: string().optional(),
25475
25241
  settings: record(string(), unknown()).optional(),
@@ -25579,7 +25345,10 @@ var PipelineAssignmentSchema = object({
25579
25345
  });
25580
25346
  /**
25581
25347
  * Per-agent load summary surfaced to the load balancer + dashboards.
25582
- * Aggregated from each runner's `getLocalLoad` cap call.
25348
+ * Aggregated from each runner's `getLocalLoad` cap call. In the
25349
+ * `getAgentLoad` report, a single-device node's default-pool sessions (no
25350
+ * `deviceKey`) are credited to its one eligible device in `load.devices` —
25351
+ * the default pool IS that device (D745); the balancer's census stays raw.
25583
25352
  */
25584
25353
  var AgentLoadSummarySchema = object({
25585
25354
  nodeId: string(),
@@ -25668,26 +25437,27 @@ var CameraBrokerStatusSchema = object({
25668
25437
  width: number(),
25669
25438
  height: number(),
25670
25439
  subscribers: number(),
25671
- inFps: number(),
25672
- outFps: number()
25440
+ /**
25441
+ * Encoded packets/s the broker receives from the camera. null when the
25442
+ * broker's stats read failed — unknown, never 0 (a 0 here reads as a stall).
25443
+ * There is no output rate: decoding runs in the session decode workers, not
25444
+ * in the broker, so the broker's own decoded-frame rate is 0 on every live
25445
+ * camera and is not served (D758).
25446
+ */
25447
+ inFps: number().nullable()
25673
25448
  })).readonly(),
25674
25449
  webrtcSessions: number(),
25675
25450
  rtspRestream: boolean()
25676
25451
  });
25677
- /** Shared-memory ring statistics within the decoder block. */
25678
- var CameraDecoderShmSchema = object({
25679
- framesWritten: number(),
25680
- getFrameHits: number(),
25681
- getFrameMisses: number(),
25682
- budgetMb: number()
25683
- });
25684
- /** Decoder block — null when the decoder stage is unreachable or inactive. */
25685
- var CameraDecoderStatusSchema = object({
25686
- nodeId: string(),
25687
- formats: array(string()).readonly(),
25688
- sessionCount: number(),
25689
- shm: CameraDecoderShmSchema
25690
- });
25452
+ /**
25453
+ * Decoder block — WHERE decode runs (co-located with the detection node by
25454
+ * construction); null when the camera has no detection node. It carries no
25455
+ * frame counter: the shared-memory plane whose counters it used to serve is
25456
+ * gone, and those fields read 0 forever. Whether frames actually flow is read
25457
+ * from `detection.actualFps` (frames delivered to and processed by the
25458
+ * detector), never from a counter nothing feeds.
25459
+ */
25460
+ var CameraDecoderStatusSchema = object({ nodeId: string() });
25691
25461
  /** Motion block — null when motion detection is not active for this device. */
25692
25462
  var CameraMotionStatusSchema = object({
25693
25463
  enabled: boolean(),
@@ -26140,25 +25910,7 @@ method(object({
26140
25910
  }), method(_void(), DiskReconcileJobSchema, {
26141
25911
  kind: "mutation",
26142
25912
  auth: "admin"
26143
- }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
26144
- name: string(),
26145
- description: string().optional(),
26146
- config: CameraPipelineConfigSchema
26147
- }), PipelineTemplateSchema, {
26148
- kind: "mutation",
26149
- auth: "admin"
26150
- }), method(object({
26151
- id: string(),
26152
- name: string().optional(),
26153
- description: string().optional(),
26154
- config: CameraPipelineConfigSchema.optional()
26155
- }), PipelineTemplateSchema, {
26156
- kind: "mutation",
26157
- auth: "admin"
26158
- }), method(object({ id: string() }), object({ success: literal(true) }), {
26159
- kind: "mutation",
26160
- auth: "admin"
26161
- });
25913
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" });
26162
25914
  /**
26163
25915
  * Cap → event-kind mapping for the SENSOR / CONTROL cap families — the table
26164
25916
  * `pipeline-analytics.listEventKinds` uses to turn a linked device's bound
@@ -28378,8 +28130,6 @@ var WebrtcStreamChoiceSchema = object({
28378
28130
  ]).nullable(),
28379
28131
  /** Input FPS from the camera (null for adaptive). */
28380
28132
  inputFps: number().nullable(),
28381
- /** Decoded FPS (null for adaptive). */
28382
- decodeFps: number().nullable(),
28383
28133
  /** Bitrate in kbps (null for adaptive). */
28384
28134
  bitrateKbps: number().nullable()
28385
28135
  });
@@ -38514,90 +38264,18 @@ Object.freeze({
38514
38264
  addonId: null,
38515
38265
  access: "create"
38516
38266
  },
38517
- "decoder.createSession": {
38518
- capName: "decoder",
38519
- capScope: "system",
38520
- addonId: null,
38521
- access: "view"
38522
- },
38523
- "decoder.destroySession": {
38524
- capName: "decoder",
38525
- capScope: "system",
38526
- addonId: null,
38527
- access: "delete"
38528
- },
38529
- "decoder.getFrame": {
38530
- capName: "decoder",
38531
- capScope: "system",
38532
- addonId: null,
38533
- access: "view"
38534
- },
38535
38267
  "decoder.getInfo": {
38536
38268
  capName: "decoder",
38537
38269
  capScope: "system",
38538
38270
  addonId: null,
38539
38271
  access: "view"
38540
38272
  },
38541
- "decoder.getShmStats": {
38542
- capName: "decoder",
38543
- capScope: "system",
38544
- addonId: null,
38545
- access: "view"
38546
- },
38547
- "decoder.getStats": {
38548
- capName: "decoder",
38549
- capScope: "system",
38550
- addonId: null,
38551
- access: "view"
38552
- },
38553
- "decoder.listActiveSessions": {
38554
- capName: "decoder",
38555
- capScope: "system",
38556
- addonId: null,
38557
- access: "view"
38558
- },
38559
- "decoder.openStream": {
38560
- capName: "decoder",
38561
- capScope: "system",
38562
- addonId: null,
38563
- access: "view"
38564
- },
38565
- "decoder.pullFrames": {
38566
- capName: "decoder",
38567
- capScope: "system",
38568
- addonId: null,
38569
- access: "view"
38570
- },
38571
- "decoder.pullHandles": {
38572
- capName: "decoder",
38573
- capScope: "system",
38574
- addonId: null,
38575
- access: "view"
38576
- },
38577
- "decoder.pushPacket": {
38578
- capName: "decoder",
38579
- capScope: "system",
38580
- addonId: null,
38581
- access: "view"
38582
- },
38583
38273
  "decoder.reprobeHwaccel": {
38584
38274
  capName: "decoder",
38585
38275
  capScope: "system",
38586
38276
  addonId: null,
38587
38277
  access: "create"
38588
38278
  },
38589
- "decoder.supportsCodec": {
38590
- capName: "decoder",
38591
- capScope: "system",
38592
- addonId: null,
38593
- access: "view"
38594
- },
38595
- "decoder.updateConfig": {
38596
- capName: "decoder",
38597
- capScope: "system",
38598
- addonId: null,
38599
- access: "view"
38600
- },
38601
38279
  "detectionPipeline.applyDeviceSettingsPatch": {
38602
38280
  capName: "detection-pipeline",
38603
38281
  capScope: "device",
@@ -41688,12 +41366,6 @@ Object.freeze({
41688
41366
  addonId: null,
41689
41367
  access: "delete"
41690
41368
  },
41691
- "pipelineExecutor.deleteTemplate": {
41692
- capName: "pipeline-executor",
41693
- capScope: "system",
41694
- addonId: null,
41695
- access: "delete"
41696
- },
41697
41369
  "pipelineExecutor.downloadModel": {
41698
41370
  capName: "pipeline-executor",
41699
41371
  capScope: "system",
@@ -41832,12 +41504,6 @@ Object.freeze({
41832
41504
  addonId: null,
41833
41505
  access: "view"
41834
41506
  },
41835
- "pipelineExecutor.listTemplates": {
41836
- capName: "pipeline-executor",
41837
- capScope: "system",
41838
- addonId: null,
41839
- access: "view"
41840
- },
41841
41507
  "pipelineExecutor.rearmInferenceDevice": {
41842
41508
  capName: "pipeline-executor",
41843
41509
  capScope: "system",
@@ -41862,12 +41528,6 @@ Object.freeze({
41862
41528
  addonId: null,
41863
41529
  access: "create"
41864
41530
  },
41865
- "pipelineExecutor.saveTemplate": {
41866
- capName: "pipeline-executor",
41867
- capScope: "system",
41868
- addonId: null,
41869
- access: "create"
41870
- },
41871
41531
  "pipelineExecutor.setVideoPipelineSteps": {
41872
41532
  capName: "pipeline-executor",
41873
41533
  capScope: "system",
@@ -41886,12 +41546,6 @@ Object.freeze({
41886
41546
  addonId: null,
41887
41547
  access: "create"
41888
41548
  },
41889
- "pipelineExecutor.updateTemplate": {
41890
- capName: "pipeline-executor",
41891
- capScope: "system",
41892
- addonId: null,
41893
- access: "create"
41894
- },
41895
41549
  "pipelineExecutor.validatePipeline": {
41896
41550
  capName: "pipeline-executor",
41897
41551
  capScope: "system",
@@ -41922,12 +41576,6 @@ Object.freeze({
41922
41576
  addonId: null,
41923
41577
  access: "create"
41924
41578
  },
41925
- "pipelineOrchestrator.deleteTemplate": {
41926
- capName: "pipeline-orchestrator",
41927
- capScope: "system",
41928
- addonId: null,
41929
- access: "delete"
41930
- },
41931
41579
  "pipelineOrchestrator.getAgentLoad": {
41932
41580
  capName: "pipeline-orchestrator",
41933
41581
  capScope: "system",
@@ -42078,12 +41726,6 @@ Object.freeze({
42078
41726
  addonId: null,
42079
41727
  access: "view"
42080
41728
  },
42081
- "pipelineOrchestrator.listTemplates": {
42082
- capName: "pipeline-orchestrator",
42083
- capScope: "system",
42084
- addonId: null,
42085
- access: "view"
42086
- },
42087
41729
  "pipelineOrchestrator.pauseForStorageMigration": {
42088
41730
  capName: "pipeline-orchestrator",
42089
41731
  capScope: "system",
@@ -42138,12 +41780,6 @@ Object.freeze({
42138
41780
  addonId: null,
42139
41781
  access: "create"
42140
41782
  },
42141
- "pipelineOrchestrator.saveTemplate": {
42142
- capName: "pipeline-orchestrator",
42143
- capScope: "system",
42144
- addonId: null,
42145
- access: "create"
42146
- },
42147
41783
  "pipelineOrchestrator.setAgentCapabilities": {
42148
41784
  capName: "pipeline-orchestrator",
42149
41785
  capScope: "system",
@@ -42252,12 +41888,6 @@ Object.freeze({
42252
41888
  addonId: null,
42253
41889
  access: "create"
42254
41890
  },
42255
- "pipelineOrchestrator.updateTemplate": {
42256
- capName: "pipeline-orchestrator",
42257
- capScope: "system",
42258
- addonId: null,
42259
- access: "create"
42260
- },
42261
41891
  "pipelineRunner.attachCamera": {
42262
41892
  capName: "pipeline-runner",
42263
41893
  capScope: "system",
@@ -43692,12 +43322,6 @@ Object.freeze({
43692
43322
  addonId: null,
43693
43323
  access: "view"
43694
43324
  },
43695
- "streamBroker.pullFrameHandles": {
43696
- capName: "stream-broker",
43697
- capScope: "system",
43698
- addonId: null,
43699
- access: "view"
43700
- },
43701
43325
  "streamBroker.regenerateRtspToken": {
43702
43326
  capName: "stream-broker",
43703
43327
  capScope: "system",
@@ -43764,12 +43388,6 @@ Object.freeze({
43764
43388
  addonId: null,
43765
43389
  access: "create"
43766
43390
  },
43767
- "streamBroker.subscribeFrames": {
43768
- capName: "stream-broker",
43769
- capScope: "system",
43770
- addonId: null,
43771
- access: "create"
43772
- },
43773
43391
  "streamBroker.unassignProfile": {
43774
43392
  capName: "stream-broker",
43775
43393
  capScope: "system",
@@ -43782,12 +43400,6 @@ Object.freeze({
43782
43400
  addonId: null,
43783
43401
  access: "create"
43784
43402
  },
43785
- "streamBroker.unsubscribeFrames": {
43786
- capName: "stream-broker",
43787
- capScope: "system",
43788
- addonId: null,
43789
- access: "create"
43790
- },
43791
43403
  "streamCatalog.getCatalog": {
43792
43404
  capName: "stream-catalog",
43793
43405
  capScope: "device",
@@ -46301,11 +45913,6 @@ Object.freeze({
46301
45913
  form: "single",
46302
45914
  optional: false
46303
45915
  }],
46304
- "decoder.createSession": [{
46305
- name: "deviceId",
46306
- form: "single",
46307
- optional: true
46308
- }],
46309
45916
  "detectionPipeline.applyDeviceSettingsPatch": [{
46310
45917
  name: "deviceId",
46311
45918
  form: "single",
@@ -49817,6 +49424,9 @@ function parseNonNegativeInt(s) {
49817
49424
  /**
49818
49425
  * Shared Alexa `Alexa/ErrorResponse` envelope builder.
49819
49426
  *
49427
+ * Certification checks the shape: `correlationToken` (set here) and
49428
+ * `endpoint` (set by `withErrorEndpoint` in the router) are both required.
49429
+ *
49820
49430
  * Lives in its own module so every handler (and the directive router)
49821
49431
  * can emit its own namespace-appropriate error without circular imports.
49822
49432
  */
@@ -49825,8 +49435,10 @@ object({ event: object({
49825
49435
  namespace: literal("Alexa"),
49826
49436
  name: literal("ErrorResponse"),
49827
49437
  messageId: string(),
49438
+ correlationToken: string().optional(),
49828
49439
  payloadVersion: literal("3")
49829
49440
  }),
49441
+ endpoint: object({ endpointId: string() }).optional(),
49830
49442
  payload: object({
49831
49443
  type: string(),
49832
49444
  message: string()
@@ -49838,6 +49450,7 @@ function errorEnvelope(header, type, message) {
49838
49450
  namespace: "Alexa",
49839
49451
  name: "ErrorResponse",
49840
49452
  messageId: header.messageId,
49453
+ ...header.correlationToken !== void 0 ? { correlationToken: header.correlationToken } : {},
49841
49454
  payloadVersion: "3"
49842
49455
  },
49843
49456
  payload: {
@@ -50198,18 +49811,43 @@ async function handleSessionDisconnected(header, endpointRaw, payloadRaw, deps,
50198
49811
  },
50199
49812
  meta: { endpointId: endpoint.endpointId }
50200
49813
  });
49814
+ return sessionEvent("SessionDisconnected", header, endpoint.endpointId, parsed.data.payload.sessionId);
49815
+ }
49816
+ /**
49817
+ * The response to `SessionConnected` / `SessionDisconnected` is the event of
49818
+ * the same name with the session id (alexa-rtcsessioncontroller) — not a
49819
+ * generic `Alexa.Response`.
49820
+ */
49821
+ function sessionEvent(name, header, endpointId, sessionId) {
50201
49822
  return { event: {
50202
49823
  header: {
50203
- namespace: "Alexa",
50204
- name: "Response",
49824
+ namespace: "Alexa.RTCSessionController",
49825
+ name,
50205
49826
  messageId: header.messageId,
50206
49827
  correlationToken: header.correlationToken,
50207
49828
  payloadVersion: "3"
50208
49829
  },
50209
- endpoint: { endpointId: endpoint.endpointId },
50210
- payload: {}
49830
+ endpoint: { endpointId },
49831
+ payload: { sessionId }
50211
49832
  } };
50212
49833
  }
49834
+ /**
49835
+ * `SessionConnected` — Alexa reports the media path is up. Nothing to do
49836
+ * hub-side (the session already exists from the offer); acknowledge it. Amazon
49837
+ * notes it is not always sent, so nothing may depend on it.
49838
+ */
49839
+ function handleSessionConnected(header, endpointRaw, payloadRaw, deps) {
49840
+ const parsed = SessionDisconnectedDirectiveSchema.safeParse({
49841
+ endpoint: endpointRaw,
49842
+ payload: payloadRaw ?? {}
49843
+ });
49844
+ if (!parsed.success) return errorEnvelope(header, "INVALID_DIRECTIVE", `payload validation failed: ${parsed.error.message}`);
49845
+ deps.logger.info("alexa rtc-session: SessionConnected", {
49846
+ tags: { sessionId: parsed.data.payload.sessionId },
49847
+ meta: { endpointId: parsed.data.endpoint.endpointId }
49848
+ });
49849
+ return sessionEvent("SessionConnected", header, parsed.data.endpoint.endpointId, parsed.data.payload.sessionId);
49850
+ }
50213
49851
  //#endregion
50214
49852
  //#region src/broker-client.ts
50215
49853
  /**
@@ -50577,6 +50215,27 @@ var PTZ_TILT_MODE_CONTROLLER = ptzDirectionalModeController("ptz.tilt", "Tilt",
50577
50215
  label: "Down"
50578
50216
  }
50579
50217
  ]);
50218
+ /** Amazon (alexa-modecontroller): "You must include a minimum of two mode
50219
+ * objects." One preset made the camera's whole AddOrUpdateReport fail
50220
+ * schema validation, and Discover dropped the camera. */
50221
+ var MIN_MODE_CONTROLLER_MODES = 2;
50222
+ /**
50223
+ * Preset names as distinct mode values. Two presets with the same name (the
50224
+ * camera allows it: `Stanza`, `Stanza`; `CamStack Home` ×3) produced two
50225
+ * identical modes, and Alexa rejected the capability — "Invalid capability
50226
+ * received." — and with it every endpoint of the camera. First wins,
50227
+ * case-insensitively: the SetMode handler resolves a value to the first
50228
+ * preset of that name, which is the one kept.
50229
+ */
50230
+ function distinctPresetNames(names) {
50231
+ const seen = /* @__PURE__ */ new Set();
50232
+ return names.filter((name) => {
50233
+ const key = name.toLowerCase();
50234
+ if (seen.has(key)) return false;
50235
+ seen.add(key);
50236
+ return true;
50237
+ });
50238
+ }
50580
50239
  function ptzPresetModeController(presetNames) {
50581
50240
  return {
50582
50241
  type: "AlexaInterface",
@@ -50621,7 +50280,8 @@ function buildParentCameraEndpoint(input) {
50621
50280
  if (input.isDoorbell) capabilities.push(DOORBELL_EVENT_SOURCE);
50622
50281
  if (input.hasMotion) capabilities.push(MOTION_SENSOR);
50623
50282
  if (input.hasBattery) capabilities.push(BATTERY_LEVEL_SENSOR);
50624
- if (input.ptzPresetNames.length > 0) capabilities.push(ptzPresetModeController(input.ptzPresetNames));
50283
+ const presetNames = distinctPresetNames(input.ptzPresetNames);
50284
+ if (presetNames.length >= MIN_MODE_CONTROLLER_MODES) capabilities.push(ptzPresetModeController(presetNames));
50625
50285
  if (input.hasPtz) {
50626
50286
  capabilities.push(PTZ_PAN_MODE_CONTROLLER);
50627
50287
  capabilities.push(PTZ_TILT_MODE_CONTROLLER);
@@ -50760,6 +50420,12 @@ function buildAlarmPanelEndpoint(input) {
50760
50420
  };
50761
50421
  }
50762
50422
  //#endregion
50423
+ //#region src/handlers/ptz-preset-recallable.ts
50424
+ var RECALLABLE_FUNCTIONS = new Set(["home"]);
50425
+ function isAlexaRecallablePreset(preset) {
50426
+ return preset.fn === null || RECALLABLE_FUNCTIONS.has(preset.fn);
50427
+ }
50428
+ //#endregion
50763
50429
  //#region src/handlers/discovery.ts
50764
50430
  var discoveryPayloadSchema = DiscoveryDirectiveSchema.shape.payload;
50765
50431
  /** Devices built at once in `handleDiscovery` — latency-bound tRPC reads, so
@@ -50947,7 +50613,7 @@ async function loadBoundDeviceInfo(device, api) {
50947
50613
  };
50948
50614
  }
50949
50615
  async function fetchPtzPresetNames(deviceId, api) {
50950
- return (await api.ptz.getPresets.query({ deviceId })).map((p) => p.name).filter((n) => n.length > 0);
50616
+ return (await api.ptz.getPresets.query({ deviceId })).filter(isAlexaRecallablePreset).map((p) => p.name).filter((n) => n.length > 0);
50951
50617
  }
50952
50618
  function classifyChildKind(deviceType, role) {
50953
50619
  if (role === "floodlight" || role === "spotlight" || role === "nightvision") return "light";
@@ -51347,8 +51013,8 @@ async function handleSetMode(header, endpointRaw, payloadRaw, deps, _callerIdent
51347
51013
  if (!mode.startsWith(PTZ_PRESET_MODE_PREFIX)) return errorEnvelope(header, "INVALID_DIRECTIVE", `mode '${mode}' is not a ptz.preset mode`);
51348
51014
  const presetName = mode.slice(11);
51349
51015
  if (presetName.length === 0) return errorEnvelope(header, "INVALID_DIRECTIVE", "preset name missing after prefix");
51350
- const preset = (await api.ptz.getPresets.query({ deviceId: parsedId.deviceId })).find((p) => p.name === presetName);
51351
- if (!preset) return errorEnvelope(header, "INVALID_DIRECTIVE", `preset '${presetName}' not found on device ${parsedId.deviceId}`);
51016
+ const preset = (await api.ptz.getPresets.query({ deviceId: parsedId.deviceId })).find((p) => p.name === presetName && isAlexaRecallablePreset(p));
51017
+ if (!preset) return errorEnvelope(header, "INVALID_DIRECTIVE", `preset '${presetName}' is not a recallable preset on device ${parsedId.deviceId}`);
51352
51018
  await api.ptz.goToPreset.mutate({
51353
51019
  deviceId: parsedId.deviceId,
51354
51020
  presetId: preset.id
@@ -51822,7 +51488,37 @@ function assertDeviceScope(scopes) {
51822
51488
  function requireDeviceScope(scopes, access) {
51823
51489
  if (!scopesAllowDeviceCap(scopes, access)) throw new Error(`directive token not authorized for device ${access}`);
51824
51490
  }
51491
+ /**
51492
+ * Give an `Alexa.ErrorResponse` the directive's endpoint when it lacks one.
51493
+ * Amazon requires `endpoint` on every ErrorResponse to an endpoint directive
51494
+ * (alexa-errorresponse); the handlers build their errors from the header
51495
+ * alone, so the router — which holds the endpoint for every path — adds it.
51496
+ */
51497
+ function withErrorEndpoint(body, endpoint) {
51498
+ const endpointId = typeof endpoint === "object" && endpoint !== null && typeof endpoint.endpointId === "string" ? endpoint.endpointId : null;
51499
+ if (endpointId === null || typeof body !== "object" || body === null) return body;
51500
+ const event = body.event;
51501
+ if (typeof event !== "object" || event === null) return body;
51502
+ const ev = event;
51503
+ if (ev.header?.namespace !== "Alexa" || ev.header?.name !== "ErrorResponse") return body;
51504
+ if (ev.endpoint !== void 0) return body;
51505
+ return {
51506
+ ...body,
51507
+ event: {
51508
+ ...ev,
51509
+ endpoint: { endpointId }
51510
+ }
51511
+ };
51512
+ }
51825
51513
  async function dispatchDirective(rawBody, deps, callerIdentity) {
51514
+ const result = await dispatchParsedDirective(rawBody, deps, callerIdentity);
51515
+ const endpoint = rawBody?.directive?.endpoint;
51516
+ return {
51517
+ status: result.status,
51518
+ body: withErrorEndpoint(result.body, endpoint)
51519
+ };
51520
+ }
51521
+ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
51826
51522
  const parsed = DirectiveRequestSchema.safeParse(rawBody);
51827
51523
  if (!parsed.success) return {
51828
51524
  status: 400,
@@ -51844,6 +51540,7 @@ async function dispatchDirective(rawBody, deps, callerIdentity) {
51844
51540
  "Alexa.Discovery/Discover": "view",
51845
51541
  "Alexa.CameraStreamController/InitializeCameraStreams": "view",
51846
51542
  "Alexa.RTCSessionController/InitiateSessionWithOffer": "view",
51543
+ "Alexa.RTCSessionController/SessionConnected": "view",
51847
51544
  "Alexa.RTCSessionController/SessionDisconnected": "view",
51848
51545
  "Alexa.PowerController/TurnOn": "create",
51849
51546
  "Alexa.PowerController/TurnOff": "create",
@@ -51878,6 +51575,10 @@ async function dispatchDirective(rawBody, deps, callerIdentity) {
51878
51575
  status: 200,
51879
51576
  body: await handleInitiateSessionWithOffer(header, endpoint, payload, deps.rtcSession, callerIdentity)
51880
51577
  };
51578
+ if (key === "Alexa.RTCSessionController/SessionConnected") return {
51579
+ status: 200,
51580
+ body: handleSessionConnected(header, endpoint, payload, deps.rtcSession)
51581
+ };
51881
51582
  if (key === "Alexa.RTCSessionController/SessionDisconnected") return {
51882
51583
  status: 200,
51883
51584
  body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
@@ -52384,10 +52085,12 @@ var DEFAULT_MOTION_CLEAR_HOLD_MS = 5e3;
52384
52085
  var MotionClearHold = class {
52385
52086
  entries = /* @__PURE__ */ new Map();
52386
52087
  holdMs;
52088
+ minDetectedMs;
52387
52089
  setTimer;
52388
52090
  clearTimer;
52389
52091
  constructor(options) {
52390
52092
  this.holdMs = options.holdMs;
52093
+ this.minDetectedMs = options.minDetectedMs ?? 3e4;
52391
52094
  this.setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
52392
52095
  this.clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
52393
52096
  }
@@ -52405,13 +52108,15 @@ var MotionClearHold = class {
52405
52108
  if (entry.timer !== null) this.clearTimer(entry.timer);
52406
52109
  this.entries.set(key, {
52407
52110
  state: "detected",
52408
- timer: null
52111
+ timer: null,
52112
+ detectedAt: entry.detectedAt
52409
52113
  });
52410
52114
  return false;
52411
52115
  }
52412
52116
  this.entries.set(key, {
52413
52117
  state: "detected",
52414
- timer: null
52118
+ timer: null,
52119
+ detectedAt: Date.now()
52415
52120
  });
52416
52121
  return true;
52417
52122
  }
@@ -52432,16 +52137,21 @@ var MotionClearHold = class {
52432
52137
  onCleared(key, release) {
52433
52138
  const entry = this.entries.get(key);
52434
52139
  if (entry !== void 0 && entry.state !== "detected") return;
52140
+ const detectedAt = entry?.detectedAt ?? null;
52141
+ const sinceDetected = detectedAt === null ? 0 : Date.now() - detectedAt;
52142
+ const delay = Math.max(this.holdMs, this.minDetectedMs - sinceDetected);
52435
52143
  const timer = this.setTimer(() => {
52436
52144
  this.entries.set(key, {
52437
52145
  state: "idle",
52438
- timer: null
52146
+ timer: null,
52147
+ detectedAt: null
52439
52148
  });
52440
52149
  release();
52441
- }, this.holdMs);
52150
+ }, delay);
52442
52151
  this.entries.set(key, {
52443
52152
  state: "holding",
52444
- timer
52153
+ timer,
52154
+ detectedAt
52445
52155
  });
52446
52156
  }
52447
52157
  /** What Alexa currently believes for this key — for logs and assertions. */
@@ -52495,6 +52205,9 @@ var REFRESH_PROACTIVE_WINDOW_MS = 300 * 1e3;
52495
52205
  var REFRESH_SCAN_INTERVAL_MS = 60 * 1e3;
52496
52206
  /** After a failed legacy-grant migration, wait this long before trying again. */
52497
52207
  var MIGRATION_RETRY_MS = 300 * 1e3;
52208
+ /** The interface a camera endpoint must declare for a connectivity
52209
+ * ChangeReport to mean anything to the Alexa cloud. */
52210
+ var ENDPOINT_HEALTH_INTERFACE = "Alexa.EndpointHealth";
52498
52211
  /** The interface an endpoint must advertise for a `DoorbellPress` to survive
52499
52212
  * the Alexa cloud. */
52500
52213
  var DOORBELL_INTERFACE = "Alexa.DoorbellEventSource";
@@ -52526,6 +52239,10 @@ var LwaEventGateway = class {
52526
52239
  unsubscribe = null;
52527
52240
  unsubscribeMotion = null;
52528
52241
  unsubscribeBattery = null;
52242
+ unsubscribeConnectivity = [];
52243
+ /** Last connectivity reported per `userId|endpointId` — a camera that
52244
+ * re-announces the same state must not become a ChangeReport stream. */
52245
+ lastConnectivity = /* @__PURE__ */ new Map();
52529
52246
  refreshTimer = null;
52530
52247
  lastSentAt = /* @__PURE__ */ new Map();
52531
52248
  /**
@@ -52574,6 +52291,11 @@ var LwaEventGateway = class {
52574
52291
  this.deps.logger.warn("alexa lwa-gateway: battery handler crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
52575
52292
  });
52576
52293
  });
52294
+ this.unsubscribeConnectivity = [[EventCategory.DeviceOnline, "OK"], [EventCategory.DeviceOffline, "UNREACHABLE"]].map(([category, connectivity]) => this.deps.ctx.eventBus.subscribe({ category }, (event) => {
52295
+ this.onConnectivityChanged(event.data, connectivity).catch((err) => {
52296
+ this.deps.logger.warn("alexa lwa-gateway: connectivity handler crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
52297
+ });
52298
+ }));
52577
52299
  this.refreshTimer = setInterval(() => {
52578
52300
  this.refreshExpiringTokens().catch((err) => {
52579
52301
  this.deps.logger.warn("alexa lwa-gateway: token refresh scan crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -52588,6 +52310,9 @@ var LwaEventGateway = class {
52588
52310
  this.unsubscribeMotion = null;
52589
52311
  if (this.unsubscribeBattery) this.unsubscribeBattery();
52590
52312
  this.unsubscribeBattery = null;
52313
+ for (const off of this.unsubscribeConnectivity) off();
52314
+ this.unsubscribeConnectivity = [];
52315
+ this.lastConnectivity.clear();
52591
52316
  if (this.refreshTimer) clearInterval(this.refreshTimer);
52592
52317
  this.refreshTimer = null;
52593
52318
  this.lastSentAt.clear();
@@ -52792,6 +52517,33 @@ var LwaEventGateway = class {
52792
52517
  }
52793
52518
  }
52794
52519
  /**
52520
+ * A camera went offline or came back. Amazon requires the `connectivity`
52521
+ * ChangeReport within 3 s of the change (alexa-endpointhealth). Deduped per
52522
+ * account and endpoint, and skipped for an endpoint Alexa is known to hold
52523
+ * WITHOUT `Alexa.EndpointHealth` — the cloud would drop it (D76).
52524
+ */
52525
+ async onConnectivityChanged(data, connectivity) {
52526
+ if (typeof data?.deviceId !== "number") return;
52527
+ if (!this.deps.listExposed().some((e) => e.deviceId === data.deviceId)) return;
52528
+ const signature = this.deps.getExportedSignature(data.deviceId);
52529
+ if (signature !== null && !signature.includes(ENDPOINT_HEALTH_INTERFACE)) return;
52530
+ const linked = this.deps.listLinkedAccounts();
52531
+ if (linked.length === 0) return;
52532
+ if (!this.hasProactiveCredentials()) return;
52533
+ const endpointId = String(data.deviceId);
52534
+ for (const account of linked) {
52535
+ const key = `${account.userId}|${endpointId}`;
52536
+ if (this.lastConnectivity.get(key) === connectivity) continue;
52537
+ this.lastConnectivity.set(key, connectivity);
52538
+ await this.postEventBody(account.userId, endpointId, (accessToken) => buildConnectivityChangeReportPayload({
52539
+ endpointId,
52540
+ connectivity,
52541
+ timestamp: Date.now(),
52542
+ accessToken
52543
+ }), `connectivity ${connectivity}`);
52544
+ }
52545
+ }
52546
+ /**
52795
52547
  * Send a one-shot `DeleteReport` for arbitrary endpoint ids per linked
52796
52548
  * account. Used to clean up stale entries Alexa still shows from prior
52797
52549
  * syncs (different addon version, different account, or an aborted
@@ -53112,6 +52864,36 @@ function buildBatteryChangeReportPayload(opts) {
53112
52864
  context: { properties: [] }
53113
52865
  };
53114
52866
  }
52867
+ function buildConnectivityChangeReportPayload(opts) {
52868
+ return {
52869
+ event: {
52870
+ header: {
52871
+ namespace: "Alexa",
52872
+ name: "ChangeReport",
52873
+ messageId: randomUUID(),
52874
+ payloadVersion: "3"
52875
+ },
52876
+ endpoint: {
52877
+ scope: {
52878
+ type: "BearerToken",
52879
+ token: opts.accessToken
52880
+ },
52881
+ endpointId: opts.endpointId
52882
+ },
52883
+ payload: { change: {
52884
+ cause: { type: "PERIODIC_POLL" },
52885
+ properties: [{
52886
+ namespace: "Alexa.EndpointHealth",
52887
+ name: "connectivity",
52888
+ value: { value: opts.connectivity },
52889
+ timeOfSample: new Date(opts.timestamp).toISOString(),
52890
+ uncertaintyInMilliseconds: 0
52891
+ }]
52892
+ } }
52893
+ },
52894
+ context: { properties: [] }
52895
+ };
52896
+ }
53115
52897
  function buildSmartVisionObjectDetectionEvent(opts) {
53116
52898
  const timeOfSample = new Date(opts.timestamp).toISOString();
53117
52899
  return { event: {
@@ -53280,7 +53062,7 @@ function buildAlexaOauthIntegration(publicHubUrl) {
53280
53062
  * client id and SECRET, on the theory that a per-account refresh token was the
53281
53063
  * real gate. The package is public on npm, so that secret was published with
53282
53064
  * every version and has to be rotated. Since D753 the secret lives only in the
53283
- * skill's Lambda (AWS Secrets Manager); the hub gets short-lived Event Gateway
53065
+ * skill's Lambda (SSM Parameter Store SecureString, D761); the hub gets short-lived Event Gateway
53284
53066
  * tokens from the Lambda's broker (`broker-client.ts`) with a per-link
53285
53067
  * credential. `scripts/check-no-client-secrets.ts` keeps it that way.
53286
53068
  *
@@ -54311,7 +54093,7 @@ var ExportAlexaAddon = class extends BaseAddon {
54311
54093
  content: [
54312
54094
  "Public skill (default): enable the \"CamStack\" skill in the Alexa app and link your account — no Amazon developer setup is needed. A single published skill + hosted Lambda route to this hub using its public HTTPS URL (baked into every JWT).",
54313
54095
  "The skill's Lambda keeps the Amazon credentials: this hub never holds an LWA client secret. It receives short-lived event tokens from the Lambda's token broker with a credential that is unique to this link.",
54314
- "Custom skill: switch \"Alexa skill\" below to Custom, enter your skill id, and deploy the skill Lambda from this addon's lambda-template in your own AWS account (your LWA credentials go into AWS Secrets Manager there, not here).",
54096
+ "Custom skill: switch \"Alexa skill\" below to Custom, enter your skill id, and deploy the skill Lambda from this addon's lambda-template in your own AWS account (your LWA credentials go into SSM Parameter Store there, not here).",
54315
54097
  "Either way this hub must be reachable from the public internet (Cloudflare Tunnel, Tailscale Funnel, …). The \"External address\" picker selects which detected endpoint the Lambda calls back on — press \"Refresh external addresses\" after setting up a new tunnel."
54316
54098
  ].join("\n")
54317
54099
  },
@@ -54378,7 +54160,7 @@ var ExportAlexaAddon = class extends BaseAddon {
54378
54160
  key: "__custom-skill-info",
54379
54161
  label: "Your skill Lambda",
54380
54162
  variant: "info",
54381
- content: "Proactive events (doorbell, motion, battery, device-list updates) are authorised by YOUR skill Lambda: it keeps the \"Send Alexa Events\" LWA client id/secret in AWS Secrets Manager and hands this hub short-lived tokens. Nothing to paste here — see the custom-skill setup guide."
54163
+ content: "Proactive events (doorbell, motion, battery, device-list updates) are authorised by YOUR skill Lambda: it keeps the \"Send Alexa Events\" LWA client id/secret in SSM Parameter Store and hands this hub short-lived tokens. Nothing to paste here — see the custom-skill setup guide."
54382
54164
  }] : [{
54383
54165
  type: "info",
54384
54166
  key: "__public-skill-info",