@camstack/system 1.2.58 → 1.2.59

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 (54) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  15. package/dist/builtins/doorbell/binding-mirror.d.ts +65 -0
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +56 -11
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +248 -26
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +248 -26
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +2 -2
  36. package/dist/builtins/snapshot/index.mjs +2 -2
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +94 -0
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +142 -2
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +142 -2
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-UrXyJ9Zm.mjs → dist-BqJJWCS8.mjs} +369 -24
  49. package/dist/{dist-Chl2MvMd.js → dist-CV6XTApk.js} +380 -23
  50. package/dist/index.js +187 -39
  51. package/dist/index.mjs +187 -39
  52. package/dist/kernel/addon-installer.d.ts +55 -7
  53. package/dist/kernel/addon-manifest.d.ts +36 -16
  54. package/package.json +1 -1
@@ -2299,9 +2299,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
2299
2299
  /**
2300
2300
  * Build the tRPC request options that pin a single capability call to `nodeId`.
2301
2301
  * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
2302
+ *
2303
+ * ## The id is normalised here, and it has to be
2304
+ *
2305
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
2306
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
2307
+ * compares a pin against real node ids, so such a pin matches nothing and the
2308
+ * call fails with `no provider registered for cap "…"`. The local-first
2309
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
2310
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
2311
+ * the thing you reach for when you specifically need THIS node — silently
2312
+ * addressed a node that does not exist.
2313
+ *
2314
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
2315
+ * own node to read the host's hardware-decode backend. It never once answered,
2316
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
2317
+ * while D67's whole premise was that the decoder addon is the authority on
2318
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
2319
+ * hardware", which was false.
2320
+ *
2321
+ * Normalising in the ONE constructor fixes every caller at once, which is why
2322
+ * it is here and not at the call sites.
2302
2323
  */
2303
2324
  function nodePin(nodeId) {
2304
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
2325
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
2326
+ }
2327
+ /**
2328
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
2329
+ * is idempotent, so passing an already-clean id costs nothing.
2330
+ */
2331
+ function toNodeId(idOrRunnerId) {
2332
+ const head = idOrRunnerId.split("/")[0];
2333
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
2305
2334
  }
2306
2335
  /**
2307
2336
  * Generic types for capability definitions.
@@ -3164,7 +3193,7 @@ z.object({
3164
3193
  * ## This file adds no state
3165
3194
  *
3166
3195
  * Every switch here is a VIEW onto an authority that already existed
3167
- * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
3196
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
3168
3197
  * group is that there is exactly one place each function is turned off, and
3169
3198
  * the group routes to it:
3170
3199
  *
@@ -3175,6 +3204,40 @@ z.object({
3175
3204
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
3176
3205
  * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
3177
3206
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
3207
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
3208
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
3209
+ *
3210
+ * ## The two switches whose authority is not on this server
3211
+ *
3212
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
3213
+ * in "the group stores nothing" — it is the purest form of it: the camera
3214
+ * holds the fact, every read is a read-through, and there is no server-side
3215
+ * copy that could drift. Their availability therefore cannot come from
3216
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
3217
+ * is filtered out there); it comes from the cap's own camera-probed
3218
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
3219
+ * THIS camera rather than for the device type
3220
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
3221
+ *
3222
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
3223
+ *
3224
+ * Every other switch means *this camera's function is doing its job*, so
3225
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
3226
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
3227
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
3228
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
3229
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
3230
+ * worse than either surface not having one.
3231
+ *
3232
+ * Two consequences follow and both are load-bearing:
3233
+ *
3234
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
3235
+ * exactly this row. With the polarity above, every camera that has NOT drawn
3236
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
3237
+ * normal, healthy state of most cameras rendered as an operator disablement.
3238
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
3239
+ * unconditionally by both clients, so for this row it has to read correctly
3240
+ * whichever way the switch is sitting.
3178
3241
  *
3179
3242
  * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
3180
3243
  * migrated the legacy `audioEnabled` / `pipelineEnabled` /
@@ -3193,14 +3256,17 @@ z.object({
3193
3256
  * `CameraStatus.switchedOff`.
3194
3257
  */
3195
3258
  /**
3196
- * The five functions the operator named (2026-08-05). Deliberately NOT one id
3197
- * per pipeline step: face recognition and plate/LPR are per-step toggles on
3259
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
3260
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
3261
+ * recognition and plate/LPR are per-step toggles on
3198
3262
  * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
3199
- * editor, not in a five-button safety group.
3263
+ * editor, not in a safety group.
3200
3264
  */
3201
3265
  var CameraSwitchIdSchema = z.enum([
3202
3266
  "stream-broker",
3203
3267
  "object-detection",
3268
+ "privacy-mask",
3269
+ "device-audio",
3204
3270
  "audio-analysis",
3205
3271
  "recording",
3206
3272
  "notifications"
@@ -3218,14 +3284,26 @@ var CameraSwitchAuthoritySchema = z.discriminatedUnion("kind", [
3218
3284
  capName: z.string()
3219
3285
  }),
3220
3286
  z.object({ kind: z.literal("recording-config") }),
3221
- z.object({ kind: z.literal("notification-mute") })
3287
+ z.object({ kind: z.literal("notification-mute") }),
3288
+ z.object({
3289
+ kind: z.literal("camera-audio"),
3290
+ capName: z.string()
3291
+ }),
3292
+ z.object({
3293
+ kind: z.literal("camera-mask"),
3294
+ capName: z.string()
3295
+ })
3222
3296
  ]);
3223
3297
  /**
3224
3298
  * Why a switch is not offered for this camera. Rendered instead of the
3225
3299
  * control, never as a dead control — an absent function and a broken one must
3226
3300
  * not look the same.
3227
3301
  */
3228
- var CameraSwitchUnavailableReasonSchema = z.enum(["no-provider", "source-unreachable"]);
3302
+ var CameraSwitchUnavailableReasonSchema = z.enum([
3303
+ "no-provider",
3304
+ "source-unreachable",
3305
+ "not-configured"
3306
+ ]);
3229
3307
  /**
3230
3308
  * One switch, resolved for one camera.
3231
3309
  *
@@ -5589,6 +5667,26 @@ var EgressTranscodeRequestSchema = z.object({
5589
5667
  "h264_mp4toannexb",
5590
5668
  "hevc_mp4toannexb"
5591
5669
  ]).optional(),
5670
+ /**
5671
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
5672
+ * consumer to dial the returned url. The broker picks the id and returns it
5673
+ * as `camStreamId` — a caller-supplied one would be circular, since the
5674
+ * sharing key is computed FROM this request.
5675
+ *
5676
+ * The url is still returned and still the contract for a transcode pinned to
5677
+ * another node. But dialling it locally costs an RTSP round trip that changes
5678
+ * the transport underneath the consumer: a dialled stream is an RTP source,
5679
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
5680
+ * repacketizer branch. The push branch — the one the derived mechanism has
5681
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
5682
+ * arriving, key frame arriving, black screen, on a chain healthy at every
5683
+ * other point.
5684
+ *
5685
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
5686
+ * gives priority to `hub-in-process` so a local call never leaves the node.
5687
+ * This is that rule for media.
5688
+ */
5689
+ publishLocally: z.boolean().optional(),
5592
5690
  pixelFormat: z.enum(["yuv420p", "nv12"]).optional(),
5593
5691
  /**
5594
5692
  * Operator/consumer override for decode hardware. ABSENT is the normal case
@@ -5633,7 +5731,13 @@ var EgressTranscodeSchema = z.object({
5633
5731
  * Returned rather than assumed: a consumer that asked for hardware and got
5634
5732
  * software needs to be able to see that without reading the broker's logs.
5635
5733
  */
5636
- decodeHwAccel: z.string().nullable()
5734
+ decodeHwAccel: z.string().nullable(),
5735
+ /**
5736
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
5737
+ * `url`, and the session takes the push/deframe transport rather than the
5738
+ * RTP-passthrough one. `null` means the consumer must dial.
5739
+ */
5740
+ camStreamId: z.string().nullable()
5637
5741
  });
5638
5742
  var streamBrokerCapability = {
5639
5743
  name: "stream-broker",
@@ -16100,9 +16204,15 @@ var snapshotCapability = {
16100
16204
  * Bypass the cache freshness check and fetch directly from the
16101
16205
  * native (or stream-broker fallback). Triggered by the UI's
16102
16206
  * "refresh" button so an operator can force a fresh frame
16103
- * even when the cache is well within `snapshotMaxAgeMs`.
16104
- * On battery cams this WILL wake the camera — accept the
16105
- * cost only when the user explicitly asks for it.
16207
+ * even when the cache is well within the device's
16208
+ * `snapshotMaxAgeS` window.
16209
+ *
16210
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
16211
+ * battery camera it is the one thing that walks past the wrapper's
16212
+ * sleep gate and wakes the camera, so a background caller — a poller,
16213
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
16214
+ * gets the cached frame, which on a sleeping battery camera is the
16215
+ * correct answer: stale but honest beats woken.
16106
16216
  */
16107
16217
  force: z.boolean().optional()
16108
16218
  }), SnapshotImageSchema.nullable()),
@@ -22557,12 +22667,30 @@ var pressureSensorCapability = {
22557
22667
  runtimeState: PressureSensorStatusSchema
22558
22668
  };
22559
22669
  /**
22560
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
22561
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
22562
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
22563
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
22564
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
22565
- * normalized 0..1 (top-left origin).
22670
+ * PRIVACY what the camera deliberately does not capture. Two planes:
22671
+ *
22672
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
22673
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
22674
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
22675
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
22676
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
22677
+ * normalized 0..1 (top-left origin).
22678
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
22679
+ * camera encoding an audio track at all, so EVERY consumer — live view,
22680
+ * recording, the audio analyzer, an export — sees silent video. There is
22681
+ * no server-side copy of this fact; the camera is the store and every read
22682
+ * is a read-through, which is why a switch over it cannot drift
22683
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
22684
+ *
22685
+ * Both belong here for one reason: they are the two things an operator turns
22686
+ * off when the answer to "what is this camera allowed to record" changes, and
22687
+ * both are applied ON the device, before anything leaves it.
22688
+ *
22689
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
22690
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
22691
+ * by one provider — and it was removed when this landed. A second writer onto
22692
+ * one device register is the shape of every knob this repo has shipped that
22693
+ * disagreed with the one the reader read.
22566
22694
  */
22567
22695
  /** A privacy-mask region's geometry — rectangle or free polygon. */
22568
22696
  var PrivacyMaskShapeSchema = z.discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -22574,21 +22702,45 @@ var PrivacyMaskRegionSchema = z.object({
22574
22702
  enabled: z.boolean(),
22575
22703
  shape: PrivacyMaskShapeSchema
22576
22704
  });
22577
- /** Current on-camera privacy-mask state — master enable + zones. */
22705
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
22578
22706
  var PrivacyMaskStatusSchema = z.object({
22579
22707
  enabled: z.boolean(),
22580
22708
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
22581
22709
  regions: z.array(PrivacyMaskRegionSchema),
22710
+ /**
22711
+ * Is the camera capturing sound right now? Read from the camera, never from
22712
+ * a server-side mirror.
22713
+ *
22714
+ * `null` means "no answer" — either this camera exposes no controllable
22715
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
22716
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
22717
+ * "the microphone is off" and "we could not ask" look identical to an
22718
+ * operator only until one of them is wrong.
22719
+ *
22720
+ * On a camera whose profiles carry the flag independently (Reolink writes
22721
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
22722
+ * privacy is only satisfied when every one of them is silent.
22723
+ */
22724
+ audioEnabled: z.boolean().nullable(),
22582
22725
  lastFetchedAt: z.number()
22583
22726
  });
22584
- /** Per-camera availability. */
22727
+ /** Per-camera availability. Probed, never assumed from the model name. */
22585
22728
  var PrivacyMaskOptionsSchema = z.object({
22586
22729
  /** Maximum number of supported zones. */
22587
22730
  maxRegions: z.number(),
22588
22731
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
22589
22732
  supportedShapes: z.array(MaskShapeKindSchema),
22590
22733
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
22591
- polygonVertices: MaskPolygonVerticesSchema.optional()
22734
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
22735
+ /**
22736
+ * Does this camera expose a microphone switch we can actually write?
22737
+ *
22738
+ * Camera-probed: `true` only when the firmware answered with an audio flag
22739
+ * we know how to patch. A camera that never answered is `false` — a control
22740
+ * the operator can press that changes nothing is worse than no control, and
22741
+ * the switch group renders "not available" instead.
22742
+ */
22743
+ supportsAudioMute: z.boolean()
22592
22744
  });
22593
22745
  /** Partial change — every field optional. */
22594
22746
  var PrivacyMaskPatchSchema = z.object({
@@ -22615,6 +22767,27 @@ var privacyMaskCapability = {
22615
22767
  }), z.void(), {
22616
22768
  kind: "mutation",
22617
22769
  auth: "admin"
22770
+ }),
22771
+ /**
22772
+ * Turn the camera's microphone on or off, at the camera.
22773
+ *
22774
+ * Deliberately its OWN mutation rather than a field on
22775
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
22776
+ * mask master switch", and overloading it would make one boolean mean two
22777
+ * unrelated things on the same call. It is also the only method here whose
22778
+ * write leaves the device in a state a later `getStatus` reads back
22779
+ * verbatim, which is what makes it safe as a switch authority.
22780
+ *
22781
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
22782
+ * this rather than silently accept it — a write nothing applies is exactly
22783
+ * what the switch group exists to remove.
22784
+ */
22785
+ setAudioEnabled: method(z.object({
22786
+ deviceId: z.number(),
22787
+ enabled: z.boolean()
22788
+ }), z.void(), {
22789
+ kind: "mutation",
22790
+ auth: "admin"
22618
22791
  })
22619
22792
  },
22620
22793
  status: {
@@ -22999,6 +23172,21 @@ var LocateSegmentResultSchema = z.discriminatedUnion("kind", [z.object({
22999
23172
  })]);
23000
23173
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
23001
23174
  var ReadSegmentBytesResultSchema = z.object({ data: z.instanceof(Uint8Array) });
23175
+ /**
23176
+ * One GOP of a finalized segment, cut by byte range through the segment's own
23177
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
23178
+ * the single `moof`+`mdat` covering the requested instant — standalone-
23179
+ * demuxable, never the whole file. When the segment's index cannot be parsed
23180
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
23181
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
23182
+ */
23183
+ var ReadGopBytesResultSchema = z.object({
23184
+ data: z.instanceof(Uint8Array),
23185
+ /** Absolute epoch ms of the returned fragment's first sample. */
23186
+ gopStartMs: z.number(),
23187
+ /** Media ms the returned fragment covers. */
23188
+ gopDurMs: z.number()
23189
+ });
23002
23190
  var recordingCapability = {
23003
23191
  name: "recording",
23004
23192
  scope: "system",
@@ -23066,6 +23254,18 @@ var recordingCapability = {
23066
23254
  kind: "query",
23067
23255
  auth: "admin"
23068
23256
  }),
23257
+ /** Read the single GOP of segment `startMs` covering `epochMs`, by mfra
23258
+ * byte range — the scrub-granular read (D31 letter on the feeder path).
23259
+ * See {@link ReadGopBytesResultSchema} for the degradation contract. */
23260
+ readGopBytes: method(z.object({
23261
+ deviceId: z.number(),
23262
+ profile: z.string(),
23263
+ startMs: z.number(),
23264
+ epochMs: z.number()
23265
+ }), ReadGopBytesResultSchema, {
23266
+ kind: "query",
23267
+ auth: "admin"
23268
+ }),
23069
23269
  setDeviceConfig: method(z.object({
23070
23270
  deviceId: z.number(),
23071
23271
  config: RecordingConfigSchema
@@ -23709,6 +23909,16 @@ var StreamProfileConfigSchema = z.object({
23709
23909
  "baseline"
23710
23910
  ]).optional(),
23711
23911
  gop: z.number().optional(),
23912
+ /**
23913
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
23914
+ *
23915
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
23916
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
23917
+ * writes every profile at once so "audio off" means silent everywhere. A
23918
+ * per-profile writer beside it would let a camera be half-muted and would be
23919
+ * a second knob onto one device register — the failure D62 exists to
23920
+ * prevent. Absent when the firmware does not report the flag.
23921
+ */
23712
23922
  audio: z.boolean().optional()
23713
23923
  });
23714
23924
  var StreamParamsStatusSchema = z.object({
@@ -23749,7 +23959,13 @@ var StreamParamsOptionsSchema = z.object({
23749
23959
  ext: StreamProfileOptionsSchema.optional()
23750
23960
  });
23751
23961
  /** A partial change to one profile — every field optional; a provider
23752
- * ignores fields it doesn't support. */
23962
+ * ignores fields it doesn't support.
23963
+ *
23964
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
23965
+ * reachable from no form and honoured by exactly one provider, while the
23966
+ * camera's microphone is a whole-device fact. It now has one writer,
23967
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
23968
+ * `privacy-mask.cap.ts`. */
23753
23969
  var StreamProfilePatchSchema = z.object({
23754
23970
  width: z.number().optional(),
23755
23971
  height: z.number().optional(),
@@ -23762,8 +23978,7 @@ var StreamProfilePatchSchema = z.object({
23762
23978
  "main",
23763
23979
  "baseline"
23764
23980
  ]).optional(),
23765
- gop: z.number().optional(),
23766
- audio: z.boolean().optional()
23981
+ gop: z.number().optional()
23767
23982
  });
23768
23983
  var streamParamsCapability = {
23769
23984
  name: "stream-params",
@@ -29368,6 +29583,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
29368
29583
  addonId: null,
29369
29584
  access: "view"
29370
29585
  },
29586
+ "privacyMask.setAudioEnabled": {
29587
+ capName: "privacy-mask",
29588
+ capScope: "device",
29589
+ addonId: null,
29590
+ access: "create"
29591
+ },
29371
29592
  "privacyMask.setMask": {
29372
29593
  capName: "privacy-mask",
29373
29594
  capScope: "device",
@@ -29536,6 +29757,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
29536
29757
  addonId: null,
29537
29758
  access: "create"
29538
29759
  },
29760
+ "recording.readGopBytes": {
29761
+ capName: "recording",
29762
+ capScope: "system",
29763
+ addonId: null,
29764
+ access: "view"
29765
+ },
29539
29766
  "recording.readSegmentBytes": {
29540
29767
  capName: "recording",
29541
29768
  capScope: "system",
@@ -30933,6 +31160,42 @@ TimelapseRuleInputSchema.extend({
30933
31160
  updatedAt: z.number()
30934
31161
  });
30935
31162
  /**
31163
+ * The addon id as the REGISTRY, the wire and the durable stores spell it.
31164
+ *
31165
+ * `AddonContext.id` does not agree with itself across the two context
31166
+ * factories:
31167
+ *
31168
+ * - a FORKED addon gets the bare manifest id
31169
+ * (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
31170
+ * - an addon co-located in hub-main gets it PREFIXED
31171
+ * (`server/backend/src/core/addon/addon-registry.service.ts` →
31172
+ * ``id: `addon:${addonId}` ``).
31173
+ *
31174
+ * Everything an addon might compare `ctx.id` AGAINST carries the bare form:
31175
+ * `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
31176
+ * `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
31177
+ *
31178
+ * So `entry.providerAddonId === this.ctx.id` is silently, permanently false
31179
+ * for a builtin — and only for a builtin, which is why it survives a green
31180
+ * suite whose fake supplies the bare id. That is exactly how camera 615's
31181
+ * virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
31182
+ * binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
31183
+ *
31184
+ * Route every comparison between `ctx.id` and a registry/store addon id
31185
+ * through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
31186
+ * enforces it in the processes where the prefix exists.
31187
+ */
31188
+ /** The prefix the hub-main context factory prepends to the manifest id. */
31189
+ var ADDON_ID_PREFIX = "addon:";
31190
+ /** The manifest id, whichever spelling of `ctx.id` you were handed. */
31191
+ function bareAddonId(id) {
31192
+ return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
31193
+ }
31194
+ /** True when both ids name the same addon, prefixed or not. */
31195
+ function isSameAddonId(a, b) {
31196
+ return bareAddonId(a) === bareAddonId(b);
31197
+ }
31198
+ /**
30936
31199
  * Decode base64 little-endian Float32 back into a vector.
30937
31200
  *
30938
31201
  * Throws on a byte length that is not a multiple of 4 — a truncated vector
@@ -30975,6 +31238,88 @@ z.object({
30975
31238
  square: false
30976
31239
  }).paddingRatio;
30977
31240
  /**
31241
+ * WHICH delivered frames the decode worker retains a native copy of.
31242
+ *
31243
+ * - `all` — every frame the worker delivered to the runner. The shipped
31244
+ * behaviour, and the only correct one if something can ask for a crop of a
31245
+ * frame the runner never sent to inference.
31246
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
31247
+ * A native-crop request always names a `frameId` that rode an inference
31248
+ * result, so that is the only set a request can name. How much it drops is
31249
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
31250
+ * this cluster, not the ~80% the design sketch assumed, because the governor
31251
+ * was not throttling as hard as the sketch supposed. Read
31252
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
31253
+ * of you rather than quoting a number from here. The newest delivered frame is
31254
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
31255
+ * which covers the one-frame race between a mark and the supersede that
31256
+ * consumes it.
31257
+ */
31258
+ var NativeLeaseAdmissionSchema = z.enum(["all", "inferred"]);
31259
+ z.object({
31260
+ /**
31261
+ * How long a retained native frame is served before it counts as a miss.
31262
+ *
31263
+ * Must cover the FULL late-crop horizon: detection inference + the
31264
+ * cross-process inference-result hop to hub post-analysis + tracking + the
31265
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
31266
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
31267
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
31268
+ */
31269
+ ttlMs: z.number().int().min(250).max(1e4),
31270
+ /**
31271
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
31272
+ *
31273
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
31274
+ * which one is actually binding before reasoning from that. At the shipped
31275
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
31276
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
31277
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
31278
+ * change that admits fewer frames buys retention WINDOW at constant RAM
31279
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
31280
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
31281
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
31282
+ * to replace).
31283
+ */
31284
+ budgetMb: z.number().int().min(0).max(4096),
31285
+ /**
31286
+ * Demand window: eager per-frame native retention runs only within this many
31287
+ * ms of the last native-crop request (or of the dial starting).
31288
+ *
31289
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
31290
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
31291
+ * per second on a camera with zero crop demand), so leave it non-zero unless
31292
+ * you are reproducing that.
31293
+ */
31294
+ activityMs: z.number().int().min(0).max(12e4),
31295
+ /**
31296
+ * Which delivered frames are retained at all — see
31297
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
31298
+ * changes WHAT is kept rather than for how long, so it is also the only one
31299
+ * that can turn a crop that used to hit into a miss. The worker counts every
31300
+ * crop request naming a frame it did NOT see marked
31301
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
31302
+ * there is the signal that some caller names frames outside the inference set
31303
+ * and that this must go back to `all`.
31304
+ */
31305
+ admission: NativeLeaseAdmissionSchema
31306
+ });
31307
+ /**
31308
+ * The values in force when the operator has set nothing — byte-for-byte the
31309
+ * constants the decode worker shipped with as env-var defaults, so making these
31310
+ * settings changed no behaviour on the day it landed.
31311
+ */
31312
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
31313
+ ttlMs: 1200,
31314
+ budgetMb: 1024,
31315
+ activityMs: 15e3,
31316
+ admission: "inferred"
31317
+ };
31318
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
31319
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
31320
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
31321
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
31322
+ /**
30978
31323
  * Scores all applicable inference backends for the given hardware.
30979
31324
  *
30980
31325
  * Rules (hardware-driven only — no Python import probes):
@@ -31088,4 +31433,4 @@ function enumerateInferenceDevices(hw) {
31088
31433
  return out;
31089
31434
  }
31090
31435
  //#endregion
31091
- export { setByPath as $, enumerateItemArrayFields as A, nodePin as At, kebabToCamel as B, dataStoreProviderCapability as C, asString as Ct, deviceStatusCapability as D, expandCapMethods as Dt, deviceStateCapability as E, emitReadiness as Et, getByPath as F, scopeKey as Ft, looseSchema as G, localNetworkCapability as H, isArrayOutputSchema as I, sleep as It, objectInputDeclaresAddonId as J, metricsProviderCapability as K, isCollectionArrayMethod as L, EventCategory as Lt, evaluateLinkExpression as M, parseJsonUnknown as Mt, extractNestedAddonId as N, readinessKey as Nt, doorbellCapability as O, hydrateSchema as Ot, filesystemBrowseCapability as P, resolveCapMount as Pt, scoreRuntimes as Q, isObjectInput as R, coreBlocksCapability as S, asNumber as St, deviceManagerCapability as T, emitDownForOwnedCaps as Tt, logDestinationCapability as U, lifecycleJobSchema as V, logLevelAtMost as W, platformProbeCapability as X, parseStreamParamsFormPatch as Y, procedureAuthKey as Z, alertsCapability as _, DeviceType as _t, CAP_NAMES_WITH_STATUS as a, toExpressionValue as at, backupCapability as b, WELL_KNOWN_TAB_MAP as bt, METHOD_ACCESS_MAP as c, vectorDimFromBase64 as ct, ScopedTokenSchema as d, BaseAddon as dt, settingsStoreCapability as et, StorageLocationTypeSchema as f, DATAPLANE_SECRET_HEADER as ft, addonWidgetsCapability as g, DeviceRole as gt, addonSettingsCapability as h, DeviceFeature as ht, BatteryStatusSchema as i, streamQualityLabel as it, enumerateSchemaFields as j, parseJsonObject as jt, enumerateInferenceDevices as k, isDeviceConfigCap as kt, RUNTIME_DEFAULTS as l, vectorStoreCapability as lt, addonPagesCapability as m, DEVICE_STATUS_METHOD as mt, AlertSchema as n, storageCapability as nt, CoreBlockSchema as o, userManagementCapability as ot, UserRecordSchema as p, DEVICE_SETTINGS_CONTRIBUTION_METHODS as pt, normalizeUnit as q, ApiKeyRecordSchema as r, storageProviderCapability as rt, DeviceStatusSchema as s, validateExpressionSource as st, ALL_CAPABILITY_DEFINITIONS as t, snapshotCapability as tt, STREAM_PROFILE_META as u, errMsg as ut, applyTransform as v, ReadinessRegistry as vt, decodeVectorBase64 as w, createEvent as wt, buildStreamParamsConfigSchema as x, asJsonObject as xt, authProviderCapability as y, ReadinessTimeoutError as yt, isVoidInput as z };
31436
+ export { procedureAuthKey as $, enumerateInferenceDevices as A, hydrateSchema as At, isSameAddonId as B, coreBlocksCapability as C, asJsonObject as Ct, deviceStateCapability as D, emitDownForOwnedCaps as Dt, deviceManagerCapability as E, createEvent as Et, filesystemBrowseCapability as F, readinessKey as Ft, logDestinationCapability as G, kebabToCamel as H, getByPath as I, resolveCapMount as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, scopeKey as Lt, enumerateSchemaFields as M, nodePin as Mt, evaluateLinkExpression as N, parseJsonObject as Nt, deviceStatusCapability as O, emitReadiness as Ot, extractNestedAddonId as P, parseJsonUnknown as Pt, platformProbeCapability as Q, isCollectionArrayMethod as R, sleep as Rt, buildStreamParamsConfigSchema as S, WELL_KNOWN_TAB_MAP as St, decodeVectorBase64 as T, asString as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, objectInputDeclaresAddonId as X, normalizeUnit as Y, parseStreamParamsFormPatch as Z, alertsCapability as _, DeviceFeature as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, ReadinessRegistry as bt, METHOD_ACCESS_MAP as c, userManagementCapability as ct, ScopedTokenSchema as d, vectorStoreCapability as dt, scoreRuntimes as et, StorageLocationTypeSchema as f, errMsg as ft, addonWidgetsCapability as g, DEVICE_STATUS_METHOD as gt, addonSettingsCapability as h, DEVICE_SETTINGS_CONTRIBUTION_METHODS as ht, BatteryStatusSchema as i, storageCapability as it, enumerateItemArrayFields as j, isDeviceConfigCap as jt, doorbellCapability as k, expandCapMethods as kt, RUNTIME_DEFAULTS as l, validateExpressionSource as lt, addonPagesCapability as m, DATAPLANE_SECRET_HEADER as mt, AlertSchema as n, settingsStoreCapability as nt, CoreBlockSchema as o, streamQualityLabel as ot, UserRecordSchema as p, BaseAddon as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, DeviceStatusSchema as s, toExpressionValue as st, ALL_CAPABILITY_DEFINITIONS as t, setByPath as tt, STREAM_PROFILE_META as u, vectorDimFromBase64 as ut, applyTransform as v, DeviceRole as vt, dataStoreProviderCapability as w, asNumber as wt, bareAddonId as x, ReadinessTimeoutError as xt, authProviderCapability as y, DeviceType as yt, isObjectInput as z, EventCategory as zt };