camstack 1.2.11 → 1.2.13

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.
@@ -14522,7 +14522,7 @@ function date4(params) {
14522
14522
  // ../../node_modules/zod/v4/classic/external.js
14523
14523
  config(en_default());
14524
14524
 
14525
- // ../types/dist/sleep-C3ceNSsf.mjs
14525
+ // ../types/dist/sleep-BYyv9gUM.mjs
14526
14526
  var WELL_KNOWN_TABS = [
14527
14527
  {
14528
14528
  id: "overview",
@@ -15504,18 +15504,6 @@ var ConvertResultSchema = external_exports.object({
15504
15504
  });
15505
15505
  var RecordingWeekdaySchema = external_exports.number().int().min(0).max(6);
15506
15506
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
15507
- var RecordingScheduleSchema = external_exports.discriminatedUnion("kind", [external_exports.object({ kind: external_exports.literal("always") }), external_exports.object({
15508
- kind: external_exports.literal("timeOfDay"),
15509
- start: external_exports.string().regex(HHMM),
15510
- end: external_exports.string().regex(HHMM),
15511
- /** Restrict to these weekdays; omit = every day. */
15512
- days: external_exports.array(RecordingWeekdaySchema).optional()
15513
- })]);
15514
- var RecordingModeSchema = external_exports.enum([
15515
- "continuous",
15516
- "onMotion",
15517
- "onAudioThreshold"
15518
- ]);
15519
15507
  var RecordingStorageModeSchema = external_exports.enum([
15520
15508
  "off",
15521
15509
  "events",
@@ -15536,18 +15524,6 @@ var RecordingBandSchema = external_exports.object({
15536
15524
  preBufferSec: external_exports.number().min(0).optional(),
15537
15525
  postBufferSec: external_exports.number().min(0).optional()
15538
15526
  });
15539
- var RecordingRuleSchema = external_exports.object({
15540
- schedule: RecordingScheduleSchema,
15541
- mode: RecordingModeSchema,
15542
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
15543
- preBufferSec: external_exports.number().min(0).default(0),
15544
- /** Keep recording until this many seconds after the last trigger. */
15545
- postBufferSec: external_exports.number().min(0).default(0),
15546
- /** Each new trigger restarts the post-buffer window. */
15547
- resetTimeoutOnNewEvent: external_exports.boolean().default(true),
15548
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
15549
- thresholdDbfs: external_exports.number().optional()
15550
- });
15551
15527
  var RecordingRetentionSchema = external_exports.object({
15552
15528
  maxAgeDays: external_exports.number().min(0).optional(),
15553
15529
  maxSizeGb: external_exports.number().min(0).optional()
@@ -15561,33 +15537,17 @@ var ScrubThumbnailPresetSchema = external_exports.enum([
15561
15537
  ]);
15562
15538
  var RecordingConfigSchema = external_exports.object({
15563
15539
  enabled: external_exports.boolean(),
15564
- /** Authoritative storage mode. Absent on legacy targets derived once via
15565
- * `migrateRulesToMode`, then persisted. */
15540
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
15541
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
15566
15542
  mode: RecordingStorageModeSchema.optional(),
15567
15543
  profiles: external_exports.array(CamProfileSchema).optional(),
15568
15544
  segmentSeconds: external_exports.number().int().positive().optional(),
15569
- /** Shared recording time-bands for `events` & `continuous` — record only when
15570
- * the wall-clock falls inside one of these windows. Omit or empty = always.
15571
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
15572
- schedules: external_exports.array(RecordingScheduleSchema).optional(),
15573
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
15574
- * normalized into `schedules` on read and never written going forward. (Not
15575
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
15576
- schedule: RecordingScheduleSchema.optional(),
15577
- /** `events`-mode only — which detectors trigger a recording. */
15578
- triggers: RecordingTriggersSchema.optional(),
15579
- /** `events`-mode only — seconds retained before / after a trigger. */
15580
- preBufferSec: external_exports.number().min(0).optional(),
15581
- postBufferSec: external_exports.number().min(0).optional(),
15582
- /** DEPRECATED authoring input; retained for migration/transition. */
15583
- rules: external_exports.array(RecordingRuleSchema).optional(),
15584
15545
  /**
15585
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
15586
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
15587
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
15588
- * derived into bands once via `migrateConfigToBands`.
15546
+ * AUTHORITATIVE mode-per-band recording model the single source of truth
15547
+ * the recorder's band engine consumes. An empty array = record nothing;
15548
+ * "off" is the absence of a covering band, never a band value.
15589
15549
  */
15590
- bands: external_exports.array(RecordingBandSchema).optional(),
15550
+ bands: external_exports.array(RecordingBandSchema).default([]),
15591
15551
  retention: RecordingRetentionSchema.optional(),
15592
15552
  /**
15593
15553
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -15595,14 +15555,22 @@ var RecordingConfigSchema = external_exports.object({
15595
15555
  * windows only — existing sheets are immutable, and each window's index
15596
15556
  * carries its own tile dims so mixed-preset history renders correctly.
15597
15557
  */
15598
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
15599
- });
15558
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
15559
+ /**
15560
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
15561
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
15562
+ * cache that eviction reclaims with the footage. Absent/false = no strips
15563
+ * are written and scrub reads exact keyframes at every velocity.
15564
+ */
15565
+ stripsEnabled: external_exports.boolean().optional()
15566
+ }).strict();
15600
15567
  var OpsLogDomainSchema = external_exports.enum(["recording", "events"]);
15601
15568
  var OpsLogOpSchema = external_exports.enum([
15602
15569
  "prune",
15603
15570
  "manual-delete",
15604
15571
  "rescan",
15605
- "retention-run"
15572
+ "retention-run",
15573
+ "relocate"
15606
15574
  ]);
15607
15575
  var OpsLogReasonSchema = external_exports.enum([
15608
15576
  "retention",
@@ -15637,6 +15605,45 @@ var OpsLogQueryInputSchema = external_exports.object({
15637
15605
  /** Max rows returned, newest-first. */
15638
15606
  limit: external_exports.number().int().min(1).max(1e3).optional()
15639
15607
  });
15608
+ var RelocateJobStateSchema = external_exports.enum([
15609
+ "running",
15610
+ "done",
15611
+ "failed",
15612
+ "cancelled"
15613
+ ]);
15614
+ var RelocateJobSchema = external_exports.object({
15615
+ jobId: external_exports.string(),
15616
+ state: RelocateJobStateSchema,
15617
+ /** Source location — for media relocation this is informational ('*': rows
15618
+ * move from wherever they are to the target). */
15619
+ fromLocationId: external_exports.string(),
15620
+ toLocationId: external_exports.string(),
15621
+ /** Scoped device, or null = every device. */
15622
+ deviceId: external_exports.number().nullable(),
15623
+ /** What the job moves (owner-addon specific: segments/strips or media). */
15624
+ entities: external_exports.array(external_exports.string()),
15625
+ filesMoved: external_exports.number().int(),
15626
+ bytesMoved: external_exports.number().int(),
15627
+ /** Total files discovered up front; null while (or when) unknown. */
15628
+ filesTotal: external_exports.number().int().nullable(),
15629
+ startedAt: external_exports.number(),
15630
+ finishedAt: external_exports.number().nullable(),
15631
+ error: external_exports.string().nullable()
15632
+ });
15633
+ var RelocateFootageInputSchema = external_exports.object({
15634
+ deviceId: external_exports.number().optional(),
15635
+ fromLocationId: external_exports.string(),
15636
+ toLocationId: external_exports.string(),
15637
+ entities: external_exports.array(external_exports.enum(["segments", "strips"])).optional(),
15638
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
15639
+ * never allowed to starve live writers. */
15640
+ throttleMbps: external_exports.number().min(1).max(1e3).optional()
15641
+ });
15642
+ var RelocateMediaInputSchema = external_exports.object({
15643
+ deviceId: external_exports.number().optional(),
15644
+ toLocationId: external_exports.string(),
15645
+ throttleMbps: external_exports.number().min(1).max(1e3).optional()
15646
+ });
15640
15647
  var StorageLocationTypeSchema = external_exports.string().regex(/^[a-z][a-zA-Z0-9-]*$/);
15641
15648
  var StorageLocationSchema = external_exports.object({
15642
15649
  id: external_exports.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -15654,6 +15661,13 @@ var StorageLocationSchema = external_exports.object({
15654
15661
  nodeId: external_exports.string().optional(),
15655
15662
  isDefault: external_exports.boolean().default(false),
15656
15663
  isSystem: external_exports.boolean().default(false),
15664
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
15665
+ * for node-local locations it can reach) — never persisted, absent when the
15666
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
15667
+ capacity: external_exports.object({
15668
+ totalBytes: external_exports.number(),
15669
+ availableBytes: external_exports.number()
15670
+ }).nullable().optional(),
15657
15671
  createdAt: external_exports.number(),
15658
15672
  updatedAt: external_exports.number()
15659
15673
  });
@@ -16921,6 +16935,8 @@ var BATTERY_DEVICE_PROFILE = {
16921
16935
  "streamBroker.preBufferEnabled": false
16922
16936
  }
16923
16937
  };
16938
+ var NC_BASE_CONDITION_KEYS = ["devices", "classes"];
16939
+ var BASE_CONDITION_SET = new Set(NC_BASE_CONDITION_KEYS);
16924
16940
  var MaskPointSchema = external_exports.object({
16925
16941
  x: external_exports.number(),
16926
16942
  y: external_exports.number()
@@ -17004,6 +17020,11 @@ var NcOccupancyConditionSchema = external_exports.object({
17004
17020
  count: external_exports.number().int().min(0).default(1),
17005
17021
  sustainSeconds: external_exports.number().int().min(0).max(3600).default(15)
17006
17022
  });
17023
+ var NcCrossingSchema = external_exports.enum([
17024
+ "enter",
17025
+ "exit",
17026
+ "any"
17027
+ ]);
17007
17028
  var NcZoneConditionSchema = external_exports.object({
17008
17029
  ids: external_exports.array(external_exports.string().min(1)).min(1),
17009
17030
  /** Quantifier over `ids` — at least one / every one visited. */
@@ -17023,6 +17044,13 @@ var NcConditionsSchema = external_exports.object({
17023
17044
  /** Veto zones — any hit fails the rule. */
17024
17045
  zonesExclude: external_exports.array(external_exports.string().min(1)).optional(),
17025
17046
  /**
17047
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
17048
+ * and a closed track carries none, so a `track-end` rule asking for one
17049
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
17050
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
17051
+ */
17052
+ crossing: NcCrossingSchema.optional(),
17053
+ /**
17026
17054
  * Exact (case-insensitive) match on the record's collapsed `label`
17027
17055
  * (identity name / plate text / subclass).
17028
17056
  */
@@ -17143,16 +17171,62 @@ var NcRuleTargetSchema = external_exports.object({
17143
17171
  */
17144
17172
  params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
17145
17173
  });
17146
- var NcMediaPolicySchema = external_exports.object({ attach: external_exports.enum([
17147
- "best",
17148
- "best-matching",
17149
- "keyFrame",
17150
- "none"
17151
- ]).default("best") });
17174
+ var NcMediaFrameSchema = external_exports.enum([
17175
+ "cropped",
17176
+ "full",
17177
+ "boxed"
17178
+ ]);
17179
+ var NcMediaPolicySchema = external_exports.object({
17180
+ attach: external_exports.enum([
17181
+ "best",
17182
+ "best-matching",
17183
+ "keyFrame",
17184
+ "none"
17185
+ ]).default("best"),
17186
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
17187
+ frame: NcMediaFrameSchema.optional(),
17188
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
17189
+ * "show me the ZONE", not the whole scene or the subject crop. */
17190
+ zoneCrop: external_exports.boolean().optional(),
17191
+ /**
17192
+ * Also attach a short GIF cut from the stream broker's clip ring around the
17193
+ * event — NOT from the recording, so the camera does not have to be
17194
+ * recording, and the window sits AROUND the moment instead of a segment
17195
+ * behind it. Fail-closed: a window the ring does not cover contributes no
17196
+ * gif, never a failed notification.
17197
+ */
17198
+ gif: external_exports.boolean().optional(),
17199
+ /**
17200
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
17201
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
17202
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
17203
+ * the degrade engine, which drops the video and keeps the still.
17204
+ */
17205
+ clip: external_exports.boolean().optional(),
17206
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
17207
+ clipPreRollSec: external_exports.number().int().min(0).max(30).optional(),
17208
+ clipPostRollSec: external_exports.number().int().min(0).max(30).optional(),
17209
+ /**
17210
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
17211
+ * assigned profile: a notification is watched on a phone, so the 4K
17212
+ * rendition would burn CPU to produce a file the client downscales anyway.
17213
+ * A profile that is not assigned falls back to the cheapest, and the render
17214
+ * reports which one actually ran.
17215
+ */
17216
+ profile: CamProfileSchema.optional()
17217
+ });
17218
+ var NcThrottleGranularitySchema = external_exports.enum(["shared", "per-class"]);
17152
17219
  var NcThrottleSchema = external_exports.object({
17153
17220
  cooldownSec: external_exports.number().int().min(0).max(86400).default(60),
17154
17221
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
17155
- scope: external_exports.enum(["rule", "rule-device"]).default("rule-device")
17222
+ scope: external_exports.enum(["rule", "rule-device"]).default("rule-device"),
17223
+ /**
17224
+ * Class granularity of the cooldown key. Optional rather than defaulted:
17225
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
17226
+ * rule authored before this field simply carries none — and the engine
17227
+ * reads absent as `shared`, the pre-existing behaviour.
17228
+ */
17229
+ granularity: NcThrottleGranularitySchema.optional()
17156
17230
  });
17157
17231
  var NcRuleInputSchema = external_exports.object({
17158
17232
  name: external_exports.string().min(1).max(200),
@@ -17160,7 +17234,19 @@ var NcRuleInputSchema = external_exports.object({
17160
17234
  delivery: NcDeliverySchema,
17161
17235
  conditions: NcConditionsSchema.default({}),
17162
17236
  schedule: NcScheduleSchema.optional(),
17163
- targets: external_exports.array(NcRuleTargetSchema).min(1),
17237
+ /** May be empty when `targetUsers` addresses at least one user — the
17238
+ * "at least one addressee" invariant is enforced by the provider, because
17239
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
17240
+ targets: external_exports.array(NcRuleTargetSchema),
17241
+ /**
17242
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
17243
+ * time each user fans out to the personal targets they own
17244
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
17245
+ * firing camera — a user is never notified about a device they cannot open.
17246
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
17247
+ * targets.
17248
+ */
17249
+ targetUsers: external_exports.array(external_exports.string()).optional(),
17164
17250
  media: NcMediaPolicySchema.default({ attach: "best" }),
17165
17251
  throttle: NcThrottleSchema.default({
17166
17252
  cooldownSec: 60,
@@ -17237,6 +17323,7 @@ var NcConditionDescriptorSchema = external_exports.object({
17237
17323
  "schedule",
17238
17324
  "plateMatcher",
17239
17325
  "packagePhase",
17326
+ "crossingSelect",
17240
17327
  "polygonDraw",
17241
17328
  "occupancy"
17242
17329
  ]),
@@ -17349,7 +17436,20 @@ var notificationRulesCapability = {
17349
17436
  kind: "mutation",
17350
17437
  auth: "admin"
17351
17438
  }),
17352
- getConditionCatalog: method(external_exports.object({}), external_exports.object({ catalog: external_exports.array(NcConditionDescriptorSchema) })),
17439
+ /**
17440
+ * The machine-readable condition surface a rule editor renders from, plus
17441
+ * the picker VOCABULARY those conditions draw on.
17442
+ *
17443
+ * `taxonomy` is optional so an older provider that only returns `catalog`
17444
+ * still validates; a client that receives none keeps free-text inputs.
17445
+ * It was previously served ONLY on the `nc.getConditionCatalog` bridge
17446
+ * action, which is why the viewer had grouped pickers and the admin UI —
17447
+ * which reaches this cap — was stuck typing `person` from memory.
17448
+ */
17449
+ getConditionCatalog: method(external_exports.object({}), external_exports.object({
17450
+ catalog: external_exports.array(NcConditionDescriptorSchema),
17451
+ taxonomy: NcTaxonomySchema.optional()
17452
+ })),
17353
17453
  /**
17354
17454
  * Queryable delivery history — a read-only view over the durable outbox
17355
17455
  * (fired rule, subject summary, target, status, timestamps, error on a
@@ -18219,6 +18319,48 @@ var streamBrokerCapability = {
18219
18319
  kind: "mutation",
18220
18320
  auth: "admin"
18221
18321
  }),
18322
+ /**
18323
+ * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
18324
+ *
18325
+ * The pre-buffer is the only source that already holds the seconds BEFORE
18326
+ * an event (it is what it exists for — "when an addon needs the last N
18327
+ * seconds of video for clip creation"). Cutting from recorded segments
18328
+ * cannot: only finalized segments are addressable and the one covering the
18329
+ * moment is still being written, so a segment-sourced clip is either
18330
+ * delayed or shows the approach instead of the event (operator,
18331
+ * 2026-07-30). It also means a camera does not have to be RECORDING to
18332
+ * produce a notification clip.
18333
+ *
18334
+ * Returns muxed BYTES, never packets: the frame plane never crosses a
18335
+ * process boundary at frame rate (D9/D18) — this is one compressed,
18336
+ * on-demand payload per notification.
18337
+ *
18338
+ * Waits up to `postRollSec` for the tail to accumulate, so the window is
18339
+ * CENTRED on `aroundMs`.
18340
+ */
18341
+ renderPreBufferClip: method(external_exports.object({
18342
+ deviceId: external_exports.number(),
18343
+ /** Absent = the LOWEST assigned profile — a notification attachment is
18344
+ * watched on a phone, and the cheap rendition is the right default. */
18345
+ profile: CamProfileSchema.optional(),
18346
+ aroundMs: external_exports.number(),
18347
+ preRollSec: external_exports.number().min(0).max(20).default(3),
18348
+ postRollSec: external_exports.number().min(0).max(20).default(5),
18349
+ format: external_exports.enum(["gif", "mp4"]).default("gif"),
18350
+ maxWidth: external_exports.number().int().min(120).max(1920).default(480),
18351
+ /** GIF only — MP4 keeps the source cadence. */
18352
+ fps: external_exports.number().int().min(1).max(15).default(5)
18353
+ }), external_exports.object({
18354
+ base64: external_exports.string(),
18355
+ mime: external_exports.string(),
18356
+ bytes: external_exports.number().int(),
18357
+ /** The profile actually rendered (what the default resolved to). */
18358
+ profile: CamProfileSchema,
18359
+ durationMs: external_exports.number()
18360
+ }), {
18361
+ kind: "mutation",
18362
+ auth: "admin"
18363
+ }),
18222
18364
  listAllCameraStreams: method(external_exports.void(), external_exports.array(CameraStreamSchema).readonly()),
18223
18365
  listAllProfileSlots: method(external_exports.void(), external_exports.array(ProfileSlotSchema).readonly()),
18224
18366
  getBrokerStats: method(external_exports.object({ brokerId: external_exports.string() }), BrokerStatsSchema),
@@ -21137,6 +21279,18 @@ var motionCapability = {
21137
21279
  name: "motion",
21138
21280
  scope: "device",
21139
21281
  mode: "singleton",
21282
+ /**
21283
+ * Providers register per-device natives via `ctx.registerNativeCap`
21284
+ * (Hikvision/Reolink/Amcrest/Wyze/HA/Homematic/Alexa/Matter) — there is
21285
+ * NO system singleton provider. Without this flag `resolveCapMount`
21286
+ * derived `{ kind: 'singleton' }`, so `motion.getStatus`/`isDetected`
21287
+ * resolved via `registry.getSingleton('motion')` (always null) and every
21288
+ * call 412'd "provider not available" while bindings listed a live
21289
+ * `motion` native (2026-08-02). The flag routes the router through
21290
+ * `requireDeviceScoped` → `getProviderForDevice`, like `motion-trigger`,
21291
+ * `snapshot` and every other per-device native cap.
21292
+ */
21293
+ deviceNative: true,
21140
21294
  deviceTypes: [DeviceType.Camera, DeviceType.Sensor],
21141
21295
  methods: {
21142
21296
  /**
@@ -25430,7 +25584,29 @@ var deviceManagerCapability = {
25430
25584
  getDeviceStatusAggregate: method(external_exports.object({
25431
25585
  deviceId: external_exports.number(),
25432
25586
  caps: external_exports.array(external_exports.string()).readonly().optional()
25433
- }), external_exports.record(external_exports.string(), external_exports.unknown().nullable()))
25587
+ }), external_exports.record(external_exports.string(), external_exports.unknown().nullable())),
25588
+ /**
25589
+ * The same aggregate, for a LIST of devices in one round-trip.
25590
+ *
25591
+ * The viewer's camera list is the caller this exists for. Each card asked
25592
+ * for its own badges — battery here, recording mode there — so a ten-camera
25593
+ * install issued N separate procedures per reconcile, each its own message
25594
+ * on the one WebSocket that also carries the WebRTC signalling (the client
25595
+ * has no `httpBatchLink`, so there is no transport-level batching to lean
25596
+ * on: collapsing has to happen at the procedure level, which is here).
25597
+ *
25598
+ * Keyed by device id as a STRING — a JSON object cannot key by number, and
25599
+ * pretending otherwise is how a record survives the wire only in tests.
25600
+ * The per-device value is exactly what `getDeviceStatusAggregate` returns,
25601
+ * so a caller can move between the two without reshaping anything.
25602
+ *
25603
+ * `caps` matters more here than in the single call: a client that needs two
25604
+ * caps should not pay for every status-carrying cap × every device.
25605
+ */
25606
+ getDeviceStatusAggregateBatch: method(external_exports.object({
25607
+ deviceIds: external_exports.array(external_exports.number()).readonly(),
25608
+ caps: external_exports.array(external_exports.string()).readonly().optional()
25609
+ }), external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown().nullable())))
25434
25610
  }
25435
25611
  };
25436
25612
  var deviceStateCapability = {
@@ -26420,6 +26596,36 @@ var TargetKindSchema = external_exports.object({
26420
26596
  icon: external_exports.string(),
26421
26597
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
26422
26598
  addonId: external_exports.string(),
26599
+ /**
26600
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
26601
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
26602
+ * when the addon bundles no icon for that kind — the client then falls back
26603
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
26604
+ *
26605
+ * Root-relative on purpose: it resolves against whatever origin serves a web
26606
+ * client, and a native client joins it onto its own hub base.
26607
+ *
26608
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
26609
+ * field that survived only because the runtime cap-router forwards provider
26610
+ * output verbatim — so every consumer had to re-declare it by hand to stop
26611
+ * its own Zod parse from stripping it, and the whole arrangement would have
26612
+ * broken silently the moment output validation was tightened anywhere.
26613
+ */
26614
+ iconUrl: external_exports.string().optional(),
26615
+ /**
26616
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
26617
+ *
26618
+ * The server knows this and therefore says it, because the client cannot
26619
+ * safely guess: a React-Native client renders SVG and raster through two
26620
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
26621
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
26622
+ * placeholder glyph for every vector icon while the web build looked fine.
26623
+ *
26624
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
26625
+ * not been updated — a client that cannot determine the type should prefer
26626
+ * its raster path, which is the safe default for an unknown image.
26627
+ */
26628
+ iconMediaType: external_exports.string().optional(),
26423
26629
  configSchema: ConfigSchemaPassthrough,
26424
26630
  supportsDiscovery: external_exports.boolean(),
26425
26631
  caps: TargetKindCapsSchema
@@ -26754,6 +26960,14 @@ var MotionEventSchema = external_exports.object({
26754
26960
  mediaUrl: external_exports.string().optional()
26755
26961
  });
26756
26962
  var DetectionSourceSchema = external_exports.enum(["pipeline", "onboard"]);
26963
+ var ZoneCrossingDirectionSchema = external_exports.enum(["enter", "exit"]);
26964
+ var ZoneCrossingSchema = external_exports.object({
26965
+ direction: ZoneCrossingDirectionSchema,
26966
+ /** Admin zone id crossed. */
26967
+ zoneId: external_exports.string(),
26968
+ /** Zone display name at crossing time (falls back to the id). */
26969
+ zoneName: external_exports.string().optional()
26970
+ });
26757
26971
  var ObjectEventSchema = external_exports.object({
26758
26972
  ...BaseEventFields,
26759
26973
  kind: external_exports.literal("object"),
@@ -26780,6 +26994,12 @@ var ObjectEventSchema = external_exports.object({
26780
26994
  zones: external_exports.array(external_exports.string()).readonly().optional(),
26781
26995
  /** Omitted in slim projection. */
26782
26996
  state: TrackStateSchema.optional(),
26997
+ /**
26998
+ * The zone crossing this event IS, when it is one. Absent on every other
26999
+ * event kind (movement state, appearance, package) — see
27000
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
27001
+ */
27002
+ zoneCrossing: ZoneCrossingSchema.optional(),
26783
27003
  /** Detection-frame dimensions in pixels — let consumers normalize the
26784
27004
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
26785
27005
  frameWidth: external_exports.number().optional(),
@@ -27156,6 +27376,23 @@ var pipelineAnalyticsCapability = {
27156
27376
  }),
27157
27377
  /** The events ops-log rows (newest-first), optionally scoped to one camera.
27158
27378
  * Backed by a declared pipeline-analytics SQLite collection. */
27379
+ /**
27380
+ * Move event-media blobs onto `toLocationId` (entity-routing spec Phase
27381
+ * 4): per-row copy → stamp row.locationId → delete old blob. Rows already
27382
+ * at the target are skipped, so a re-run resumes. Single-flight.
27383
+ */
27384
+ relocateMedia: method(RelocateMediaInputSchema, external_exports.object({ jobId: external_exports.string() }), {
27385
+ kind: "mutation",
27386
+ auth: "admin"
27387
+ }),
27388
+ getMediaRelocateStatus: method(external_exports.object({}), external_exports.array(RelocateJobSchema).readonly(), {
27389
+ kind: "query",
27390
+ auth: "admin"
27391
+ }),
27392
+ cancelMediaRelocate: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
27393
+ kind: "mutation",
27394
+ auth: "admin"
27395
+ }),
27159
27396
  listOpsLog: method(OpsLogQueryInputSchema, external_exports.array(OpsLogEntrySchema).readonly(), {
27160
27397
  kind: "query",
27161
27398
  auth: "admin"
@@ -30142,6 +30379,12 @@ var ConnectionEndpointSchema = external_exports.object({
30142
30379
  priority: external_exports.number()
30143
30380
  });
30144
30381
  var GetConnectionEndpointsResultSchema = external_exports.object({ endpoints: external_exports.array(ConnectionEndpointSchema).readonly() });
30382
+ var NotificationEndpointSchema = external_exports.object({
30383
+ /** The operator's explicit choice, or null for AUTO. */
30384
+ baseUrl: external_exports.string().nullable(),
30385
+ /** What the ranking currently resolves to (null when nothing is reachable). */
30386
+ resolved: external_exports.string().nullable()
30387
+ });
30145
30388
  var AllowedAddressesSchema = external_exports.object({
30146
30389
  /**
30147
30390
  * Allowlist of interface addresses operators have explicitly opted
@@ -30194,6 +30437,26 @@ var localNetworkCapability = {
30194
30437
  scheme: external_exports.enum(["http", "https"]).optional()
30195
30438
  }), GetConnectionEndpointsResultSchema),
30196
30439
  /**
30440
+ * The endpoint OUTBOUND artifacts must be addressed by — notification
30441
+ * attachment links and deep links. `baseUrl: null` means AUTO: the first
30442
+ * public candidate (a configured origin, then a connected tunnel, then the
30443
+ * LAN), never loopback.
30444
+ *
30445
+ * Why an explicit marker: the automatic ranking cannot know which of
30446
+ * several reachable addresses the RECIPIENT can actually open. A link is
30447
+ * consumed on someone's phone, off the LAN, possibly hours later — and a
30448
+ * link nobody can open is worse than no link, because the notification
30449
+ * arrives advertising media that 404s.
30450
+ */
30451
+ getNotificationEndpoint: method(external_exports.void(), NotificationEndpointSchema),
30452
+ /**
30453
+ * Mark which endpoint notification artifacts use. `baseUrl: null` restores
30454
+ * AUTO. The value is stored verbatim (not an index into the candidate
30455
+ * list): interfaces come and go, and an index would silently re-point at a
30456
+ * different address after a reboot.
30457
+ */
30458
+ setNotificationEndpoint: method(external_exports.object({ baseUrl: external_exports.string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }),
30459
+ /**
30197
30460
  * Read the operator's allowlist. Empty = "auto" (no filter). Used
30198
30461
  * by the admin UI's address selector to seed its checkbox state.
30199
30462
  */
@@ -31172,7 +31435,12 @@ var RecordingDeviceUsageSchema = external_exports.object({
31172
31435
  var RecordingLocationUsageSchema = external_exports.object({
31173
31436
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
31174
31437
  locationId: external_exports.string().nullable(),
31175
- /** Bytes of recordings stored on this location. */
31438
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
31439
+ * is emitted per physical disk (2026-07-29): two locations on one root
31440
+ * previously rendered as two identical "disks" with a nonsensical used
31441
+ * split — hydrate attribution across aliases is arbitrary by nature. */
31442
+ locationIds: external_exports.array(external_exports.string()).optional(),
31443
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
31176
31444
  usedBytes: external_exports.number(),
31177
31445
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
31178
31446
  availableBytes: external_exports.number().nullable(),
@@ -31314,6 +31582,77 @@ var recordingCapability = {
31314
31582
  listOpsLog: method(OpsLogQueryInputSchema, external_exports.array(OpsLogEntrySchema).readonly(), {
31315
31583
  kind: "query",
31316
31584
  auth: "admin"
31585
+ }),
31586
+ /**
31587
+ * Move footage (segments and/or strips) from one recordings location to
31588
+ * another — the drain workflow's mover (entity-routing spec Phase 4).
31589
+ * Throttled copy → size-verify → delete source → index refresh; resumable
31590
+ * by construction (copy-if-absent). Single-flight: one job at a time.
31591
+ */
31592
+ /**
31593
+ * Render a short GIF from recorded footage around `aroundMs` (low profile,
31594
+ * palette-free ffmpeg gif). Synchronous — the window is a few seconds of
31595
+ * `low`, rendering in ~1-2s. Built for notification attachments: the
31596
+ * caller gets bytes, nothing persists. Fails when no footage covers the
31597
+ * window (camera not recording → caller skips the attachment).
31598
+ */
31599
+ renderGif: method(external_exports.object({
31600
+ deviceId: external_exports.number(),
31601
+ aroundMs: external_exports.number(),
31602
+ preRollSec: external_exports.number().min(0).max(30).default(2),
31603
+ postRollSec: external_exports.number().min(0).max(30).default(5),
31604
+ maxWidth: external_exports.number().int().min(120).max(1280).default(480),
31605
+ fps: external_exports.number().int().min(1).max(15).default(5)
31606
+ }), external_exports.object({
31607
+ gifBase64: external_exports.string(),
31608
+ fromMs: external_exports.number(),
31609
+ toMs: external_exports.number()
31610
+ }), {
31611
+ kind: "mutation",
31612
+ auth: "admin"
31613
+ }),
31614
+ /**
31615
+ * Render a short MP4 clip from recorded footage around `aroundMs` — the
31616
+ * "send a clip, not a gif" notification attachment. Same window, source
31617
+ * and fail-closed contract as {@link renderGif}: no footage covering the
31618
+ * window ⇒ it throws and the caller ships no attachment. Re-encoded (never
31619
+ * stream-copied) so a concat across segments with different encoder
31620
+ * parameters still plays. Silent by contract.
31621
+ *
31622
+ * The returned window is SEGMENT-GRANULAR: the source playlist is built
31623
+ * from whole segments, so the clip starts at or before `fromMs` and ends
31624
+ * at or after `toMs` — overshoot bounded by one segment per side (an 8 s
31625
+ * ask over 4 s segments measured 12 s). `fromMs`/`toMs` report the window
31626
+ * ASKED for, not the encoded duration.
31627
+ */
31628
+ renderClip: method(external_exports.object({
31629
+ deviceId: external_exports.number(),
31630
+ aroundMs: external_exports.number(),
31631
+ preRollSec: external_exports.number().min(0).max(30).default(3),
31632
+ postRollSec: external_exports.number().min(0).max(30).default(7),
31633
+ maxWidth: external_exports.number().int().min(160).max(1920).default(640)
31634
+ }), external_exports.object({
31635
+ clipBase64: external_exports.string(),
31636
+ mime: external_exports.string(),
31637
+ fromMs: external_exports.number(),
31638
+ toMs: external_exports.number(),
31639
+ bytes: external_exports.number().int()
31640
+ }), {
31641
+ kind: "mutation",
31642
+ auth: "admin"
31643
+ }),
31644
+ relocateFootage: method(RelocateFootageInputSchema, external_exports.object({ jobId: external_exports.string() }), {
31645
+ kind: "mutation",
31646
+ auth: "admin"
31647
+ }),
31648
+ /** Every known relocate job (in-RAM, newest-first). */
31649
+ getRelocateStatus: method(external_exports.object({}), external_exports.array(RelocateJobSchema).readonly(), {
31650
+ kind: "query",
31651
+ auth: "admin"
31652
+ }),
31653
+ cancelRelocate: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
31654
+ kind: "mutation",
31655
+ auth: "admin"
31317
31656
  })
31318
31657
  }
31319
31658
  };
@@ -32922,6 +33261,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
32922
33261
  addonId: null,
32923
33262
  access: "view"
32924
33263
  },
33264
+ "deviceManager.getDeviceStatusAggregateBatch": {
33265
+ capName: "device-manager",
33266
+ capScope: "system",
33267
+ addonId: null,
33268
+ access: "view"
33269
+ },
32925
33270
  "deviceManager.getLinkedDevices": {
32926
33271
  capName: "device-manager",
32927
33272
  capScope: "system",
@@ -33816,6 +34161,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33816
34161
  addonId: null,
33817
34162
  access: "view"
33818
34163
  },
34164
+ "localNetwork.getNotificationEndpoint": {
34165
+ capName: "local-network",
34166
+ capScope: "system",
34167
+ addonId: null,
34168
+ access: "view"
34169
+ },
33819
34170
  "localNetwork.getPreferred": {
33820
34171
  capName: "local-network",
33821
34172
  capScope: "system",
@@ -33840,6 +34191,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33840
34191
  addonId: null,
33841
34192
  access: "create"
33842
34193
  },
34194
+ "localNetwork.setNotificationEndpoint": {
34195
+ capName: "local-network",
34196
+ capScope: "system",
34197
+ addonId: null,
34198
+ access: "create"
34199
+ },
33843
34200
  "lockControl.lock": {
33844
34201
  capName: "lock-control",
33845
34202
  capScope: "device",
@@ -34482,6 +34839,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34482
34839
  addonId: null,
34483
34840
  access: "create"
34484
34841
  },
34842
+ "pipelineAnalytics.cancelMediaRelocate": {
34843
+ capName: "pipeline-analytics",
34844
+ capScope: "device",
34845
+ addonId: null,
34846
+ access: "create"
34847
+ },
34485
34848
  "pipelineAnalytics.clearTracks": {
34486
34849
  capName: "pipeline-analytics",
34487
34850
  capScope: "device",
@@ -34536,6 +34899,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34536
34899
  addonId: null,
34537
34900
  access: "view"
34538
34901
  },
34902
+ "pipelineAnalytics.getMediaRelocateStatus": {
34903
+ capName: "pipeline-analytics",
34904
+ capScope: "device",
34905
+ addonId: null,
34906
+ access: "view"
34907
+ },
34539
34908
  "pipelineAnalytics.getMotionEvents": {
34540
34909
  capName: "pipeline-analytics",
34541
34910
  capScope: "device",
@@ -34608,6 +34977,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34608
34977
  addonId: null,
34609
34978
  access: "create"
34610
34979
  },
34980
+ "pipelineAnalytics.relocateMedia": {
34981
+ capName: "pipeline-analytics",
34982
+ capScope: "device",
34983
+ addonId: null,
34984
+ access: "create"
34985
+ },
34611
34986
  "pipelineAnalytics.searchObjectEvents": {
34612
34987
  capName: "pipeline-analytics",
34613
34988
  capScope: "device",
@@ -35370,6 +35745,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
35370
35745
  addonId: null,
35371
35746
  access: "create"
35372
35747
  },
35748
+ "recording.cancelRelocate": {
35749
+ capName: "recording",
35750
+ capScope: "system",
35751
+ addonId: null,
35752
+ access: "create"
35753
+ },
35373
35754
  "recording.deleteFootprint": {
35374
35755
  capName: "recording",
35375
35756
  capScope: "system",
@@ -35400,6 +35781,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
35400
35781
  addonId: null,
35401
35782
  access: "view"
35402
35783
  },
35784
+ "recording.getRelocateStatus": {
35785
+ capName: "recording",
35786
+ capScope: "system",
35787
+ addonId: null,
35788
+ access: "view"
35789
+ },
35403
35790
  "recording.getStorageUsage": {
35404
35791
  capName: "recording",
35405
35792
  capScope: "system",
@@ -35430,6 +35817,24 @@ var METHOD_ACCESS_MAP = Object.freeze({
35430
35817
  addonId: null,
35431
35818
  access: "view"
35432
35819
  },
35820
+ "recording.relocateFootage": {
35821
+ capName: "recording",
35822
+ capScope: "system",
35823
+ addonId: null,
35824
+ access: "create"
35825
+ },
35826
+ "recording.renderClip": {
35827
+ capName: "recording",
35828
+ capScope: "system",
35829
+ addonId: null,
35830
+ access: "create"
35831
+ },
35832
+ "recording.renderGif": {
35833
+ capName: "recording",
35834
+ capScope: "system",
35835
+ addonId: null,
35836
+ access: "create"
35837
+ },
35433
35838
  "recording.rescanStorage": {
35434
35839
  capName: "recording",
35435
35840
  capScope: "system",
@@ -36024,6 +36429,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
36024
36429
  addonId: null,
36025
36430
  access: "create"
36026
36431
  },
36432
+ "streamBroker.renderPreBufferClip": {
36433
+ capName: "stream-broker",
36434
+ capScope: "system",
36435
+ addonId: null,
36436
+ access: "create"
36437
+ },
36027
36438
  "streamBroker.restartProfile": {
36028
36439
  capName: "stream-broker",
36029
36440
  capScope: "system",
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runDiscover
4
- } from "./chunk-TFRON3LI.js";
4
+ } from "./chunk-EA6763XQ.js";
5
5
  import "./chunk-K3NQKI34.js";
6
6
 
7
7
  // src/cli.ts
@@ -1079,7 +1079,7 @@ function isUnknown(_value) {
1079
1079
  return true;
1080
1080
  }
1081
1081
  async function resolveServerInteractive(presetNamespace) {
1082
- const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-GQWPB7TQ.js");
1082
+ const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-UMBUCPOL.js");
1083
1083
  if (presetNamespace) {
1084
1084
  const spinner4 = clack.spinner();
1085
1085
  spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
@@ -5,7 +5,7 @@ import {
5
5
  filterHubNodes,
6
6
  resolveHubFromDiscovered,
7
7
  runDiscover
8
- } from "./chunk-TFRON3LI.js";
8
+ } from "./chunk-EA6763XQ.js";
9
9
  import "./chunk-K3NQKI34.js";
10
10
  export {
11
11
  DEFAULT_HUB_HTTPS_PORT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",