@7365admin1/core 3.42.2 → 3.43.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,328 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.43.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f8750e6: Camera functions for Virtual Patrol, proxied server-side.
8
+
9
+ Adds authenticated camera endpoints so the apps can use a site camera without ever
10
+ holding its credential. Dahua devices authenticate with digest and the cleartext
11
+ password on every request (HTTP API V3.37 §3.4) and the specification offers no
12
+ session or usable token, so the only safe shape is a server-side proxy.
13
+
14
+ - `useCameraViewController` — snapshot, snapshot-to-file for evidence, and
15
+ per-recorder health.
16
+ - A camera record does not hold a device address: `site.cameras.host` is the CCTV
17
+ relay's page URL and its numeric last path segment is the stream channel. A
18
+ camera is therefore resolved as **recorder (from `CAMERA_RTSP_DEVICES`, keyed by
19
+ relay authority) + channel (from the stored host)**. The address and credential
20
+ live in deployment configuration only — never in the database, never in a client.
21
+ - Pictures come off the video stream (one `ffmpeg` frame per snapshot, substream
22
+ for a tile, main stream for an evidence capture). The recorder in use exposes
23
+ RTSP only; its HTTP CGI ports are not reachable, so `snapshot.cgi`, firmware
24
+ (§4.6.14), device clock (§4.6.2) and PTZ (§8.1.5) cannot be used against it.
25
+ Firmware and clock are reported as unavailable with a reason, and PTZ refuses.
26
+ `GET /site-cameras/capabilities` reports `ptz: false` whatever
27
+ `CAMERA_PTZ_ENABLED` says, so a client hides the control rather than drawing a
28
+ pad whose every press fails.
29
+ - A camera whose relay has no configured recorder, or whose address carries no
30
+ channel, is refused up-front with a reason rather than failing as a blank picture.
31
+ - At most `CAMERA_MAX_CONCURRENT_GRABS` (4) pictures are taken at once across the
32
+ process, so the endpoint cannot fill the API host with ffmpeg or overpull the
33
+ recorder; over that, the caller is told the cameras are busy.
34
+ - Access is decided by the caller's site/org membership and their role's
35
+ permissions, never by the camera id in the URL.
36
+ - ANPR units are refused: ANPR belongs to visitor and vehicle management, not to
37
+ Virtual Patrol or CCTV.
38
+ - Camera host, username, password and the RTSP URL are never returned and never
39
+ logged; ffmpeg's stderr is discarded because it echoes the credential.
40
+ - No retry on a camera call: the device locks an account for 1800 s after 3
41
+ failures in 30 s (§4.7.x).
42
+
43
+ Requires `ffmpeg` on the API host (or `CAMERA_FFMPEG_PATH`); where it is absent the
44
+ snapshot endpoints answer with "Video tooling is not available on this server."
45
+
46
+ - b06ae8f: Camera integration layer: a capability descriptor, a transport registry, and the
47
+ Dahua device HTTP adapter (written, tested, switched off).
48
+
49
+ Every camera in the wall and status responses now carries a `capabilities`
50
+ descriptor: `liveVideo`, `stillFrame`, `digitalZoom`, `ptz`, `presets`,
51
+ `playback`, `events`, `audio`, `deviceInfo`, each `supported` / `unsupported` /
52
+ `unknown` with a machine-readable reason code and one sentence. A client renders
53
+ itself from it without knowing anything about relays, recorders or networks.
54
+ `unknown` is distinct on purpose: "we have not been allowed to ask" is not the
55
+ same fact as "this camera cannot".
56
+
57
+ Capabilities resolve through a transport registry instead of the service picking
58
+ a path. `RELAY_PLAYER` is the stored player-page URL rendered in a WebView (the
59
+ live product's picture path, live video only, no control channel). `RTSP_FRAME`
60
+ is one ffmpeg still off the recorder. `DEVICE_HTTP` is the camera's own CGI API.
61
+ Registration order is preference order, so one camera serves live video over the
62
+ relay and evidence stills over RTSP at the same time, and adding a transport
63
+ later needs no service change.
64
+
65
+ `DEVICE_HTTP` covers device info/version, snapshot, PTZ (continuous, absolute,
66
+ stop), presets (list and recall), recorded-file query with playback URL
67
+ construction, and event subscription - digest auth only, values percent-encoded,
68
+ 403 treated as bad credentials and 401 as an ordinary challenge. It contacts
69
+ nothing today: the recorder in this estate answers on RTSP only. Enabling it is
70
+ configuration, not a code change.
71
+
72
+ A shared, persistent authentication failure budget is mandatory on that path. The
73
+ device locks an account for 1800 s after 3 failed logins in 30 s, so failures are
74
+ counted per device in the cache, the budget stops at 2, and once spent no request
75
+ leaves the process. Nothing retries anywhere.
76
+
77
+ Mutating operations are structurally unreachable while disabled: the adapter is
78
+ `null` without `CAMERA_DEVICE_HTTP_ENABLED`, and its `control` object is `null`
79
+ without `CAMERA_DEVICE_CONTROL_ENABLED`. Movement codes are an allow-list;
80
+ `SetPreset`, `ClearPreset`, tours, patterns, `configManager` writes and reboot are
81
+ absent.
82
+
83
+ Capability probing is read-only, cached, budget-aware, never throws, and happens
84
+ only on a deliberate single-camera status request - never as a side effect of a
85
+ list.
86
+
87
+ `host` is now returned for `type: "ip"` cameras on the wall response. It is the
88
+ video relay's player-page URL, not a device address and not a credential, and
89
+ `GET /site-cameras` already returns it to every authenticated caller. `anpr`
90
+ records, whose `host` is a real device endpoint, are excluded by the allow-list
91
+ itself.
92
+
93
+ New configuration, all optional and all off by default: `CAMERA_DEVICE_HTTP`
94
+ (JSON keyed by relay authority, credential referenced by environment variable
95
+ name), `CAMERA_DEVICE_HTTP_ENABLED`, `CAMERA_DEVICE_CONTROL_ENABLED`,
96
+ `CAMERA_DEVICE_HTTP_TIMEOUT_MS` (8000),
97
+ `CAMERA_DEVICE_HTTP_PROBE_TTL_SECONDS` (900). See
98
+ `docs/camera-integration-config.md`.
99
+
100
+ Existing responses keep every field they had, with the same wording, so current
101
+ consumers are unaffected.
102
+
103
+ - e6e1c4c: CCTV monitoring wall: a site-scoped camera list and a batched health sweep.
104
+
105
+ Two additions to the camera proxy so a supervisor can watch a whole site rather
106
+ than one checkpoint at a time.
107
+
108
+ `getSiteWall` returns the cameras of one site from `site.cameras` - the
109
+ collection the real cameras are in - together with the polling intervals and
110
+ fan-out caps the client should use. No device is contacted, so opening a wall is
111
+ one database read. Entitlement is by site membership, not by the id in the URL,
112
+ and a caller outside the site gets the same answer as one asking for a site that
113
+ does not exist.
114
+
115
+ `getSiteHealth` probes the cameras currently on screen with at most
116
+ `CAMERA_WALL_MAX_CONCURRENT_PROBES` in flight. Reachability is a **per-recorder**
117
+ answer, cached per recorder: twelve of this estate's cameras are twelve channels
118
+ on one device, so a full wall is one connect rather than one per tile. It never
119
+ throws for a camera that is down; unreachable is the answer, and a camera whose
120
+ relay has no configured recorder is refused with a reason instead.
121
+
122
+ Firmware version and device clock are reported as unavailable with a reason: they
123
+ are HTTP CGI facts and the recorder in use exposes RTSP only.
124
+
125
+ Snapshots now record when a camera last returned a picture, so a tile can say
126
+ "last frame 40 minutes ago" instead of "never".
127
+
128
+ New, all optional and conservative by default: `CAMERA_WALL_POLL_MS` (5000),
129
+ `CAMERA_SINGLE_POLL_MS` (2000), `CAMERA_WALL_MAX_TILES` (9),
130
+ `CAMERA_WALL_MAX_CONCURRENT_PROBES` (4), `CAMERA_HEALTH_CACHE_SECONDS` (20).
131
+
132
+ - dbc9026: Camera setup from the recorder and channel, and a read-only test before saving.
133
+
134
+ A CCTV camera record stores a video-relay player-page URL. Nothing printed on a
135
+ camera or shown in a recorder's own screens tells anybody what that URL is, so
136
+ adding a camera meant asking whoever runs the video service for one address per
137
+ camera and pasting it in unverified.
138
+
139
+ `POST /site-cameras` and `PATCH /site-cameras/id/:id` now also accept
140
+ `recorderHost`, `recorderPort` and `channel`. The server reads the existing
141
+ `CAMERA_RTSP_DEVICES` map backwards - relay authority to recorder becomes
142
+ recorder to relay authority - and derives the same `https://<relay>/<channel>`
143
+ address it has always stored. There is deliberately no second map and no new
144
+ stored field: the recorder fields are consumed by the controller and never reach
145
+ the model, so `site.cameras` keeps exactly the shape it has today.
146
+
147
+ Additive and ordered so: a request that sends `host` behaves exactly as it
148
+ always has, so every camera configured that way and anyone holding a ready-made
149
+ address is unaffected. A recorder with no configured entry is refused with a
150
+ sentence a property manager can act on rather than a code.
151
+
152
+ New: `POST /site-cameras/site/:siteId/test` - does this address actually show a
153
+ picture? Site-scoped because the camera does not exist yet, and it requires
154
+ `site-settings:manage-cctv-camera`. The probe is read only: one TCP connect with
155
+ no credential, then at most one video frame off the stream. There is no mutating
156
+ call in the path.
157
+
158
+ It is rationed before it can reach a device - one test per camera per 15 s, four
159
+ per recorder per 5 minutes - and it consults the shared authentication failure
160
+ budget first, so pressing the button cannot walk a recorder into its own
161
+ 3-failures-in-30-seconds, 1800-second lockout. It answers with one of five plain
162
+ sentences and never an address, port, credential or device message.
163
+
164
+ `ffmpeg`'s stderr is now read for one fact - was the handshake rejected - and
165
+ discarded chunk by chunk without being accumulated, logged or returned, so "the
166
+ recorder refused the credential" can be told apart from "the channel is not
167
+ sending video". Snapshot failures gain that distinction too.
168
+
169
+ `authorizeSite` takes an optional permission list; existing callers are unchanged
170
+ and still require the camera view permissions.
171
+
172
+ - 6238e88: Authorize creating, editing and deleting a site camera.
173
+
174
+ `POST /site-cameras`, `PATCH /site-cameras/id/:id` and `DELETE
175
+ /site-cameras/id/:id` carried `requireAuth` and nothing else. There was no
176
+ permission check and no site scoping, so any signed-in account in any
177
+ organisation could add a camera to a site it has no relationship with, rewrite
178
+ another customer's camera address, or hard-delete one - the delete is a real
179
+ `deleteOne`, not a soft delete, and it reported success whether or not it matched
180
+ anything.
181
+
182
+ All three now go through `authorizeSite`, the same rule the read paths already
183
+ use: the caller must be a member of the camera's site, and their role must hold
184
+ the permission the Settings panel that draws the button is drawn on -
185
+ `site-settings:manage-cctv-camera` for a CCTV camera, `site-settings:manage-anpr-camera`
186
+ for an ANPR one. One endpoint serves both panels, so the permission follows the
187
+ camera's own `type` rather than being one string for both; asking only for the
188
+ CCTV string would have refused a role provisioned to manage ANPR alone.
189
+
190
+ For `PATCH` and `DELETE` the site and the type are read off the stored camera,
191
+ never off the request - `schemaUpdateSiteCamera` carries neither, and a caller
192
+ must not be able to supply either. A camera the caller may not reach is reported
193
+ exactly as a camera that does not exist, so the difference cannot be used to
194
+ enumerate camera ids.
195
+
196
+ Measured on staging before shipping: of the 107 users entitled to a site that has
197
+ a CCTV camera, 12 pass this gate and every one of them passes on the `*`
198
+ wildcard. No role that would now be refused holds any `site-settings:` string at
199
+ all, so no account that can open the CCTV or ANPR panel today is refused by this
200
+ change.
201
+
202
+ No response shape, status code or field changed for a permitted caller.
203
+
204
+ ### Patch Changes
205
+
206
+ - aea01a0: Camera entitlement: an engagement grants a site only to an org-level membership, and permissions resolve against the granting role
207
+
208
+ Two follow-ups to the camera entitlement change:
209
+
210
+ - the engagement branch of `cameraGrant` ignored `members.siteId`, so a member
211
+ pinned to one site could reach every site their organisation is engaged at.
212
+ It now requires an org-level membership, matching the owning-org branch above
213
+ it: a membership pinned to a site never grants a different site.
214
+ - `getUserPermissions` accepts the granting membership's `role`, so a caller
215
+ holding several memberships in one organisation is no longer resolved by
216
+ whichever member document Mongo returns first.
217
+
218
+ - e890a82: Let camera authorization understand engagements, and resolve it against the
219
+ right organisation.
220
+
221
+ Three defects in the same decision, fixed together because they are the same
222
+ question: "may this person reach this site, and on whose behalf?"
223
+
224
+ **1. An engagement was not an entitlement.** The product routes a service
225
+ provider - a security agency, a property management agency - to a customer's
226
+ site through an ACTIVE `customer.sites` record (`{org: provider, siteOrg: owner,
227
+ site}`). 227 of those are active on staging, 53 of them cross-organisation. It
228
+ is the same list the web apps' own site switcher is drawn from
229
+ (`useCustomerSite().getAll()`). No camera authorization code read it, so a guard
230
+ whose agency is contracted at a site was refused the cameras of the site the
231
+ switcher had just offered them. Measured on staging: **11 people reach a site
232
+ holding an active IP camera by this route and no other, 7 of them on wildcard
233
+ `owner` / `Super Admin` roles** - and across all 200 sites the figure is 125
234
+ people, 43 of them on wildcard roles. Nobody loses access; the change is
235
+ additive.
236
+
237
+ **2. `site.org` does not exist.** `authorizeSite` and `authorizeCamera`
238
+ projected `{org: 1}` and tested `site?.org`. All 200 site documents carry
239
+ `orgId`; not one carries `org`. So the org-level branch of the rule - the branch
240
+ for the 90 membership rows (56 people) that carry no `siteId` - could never
241
+ match. It reads `orgId` now, with `org` kept as a fallback. This WIDENS access,
242
+ so it was measured before it was enabled: on staging it admits **0** additional
243
+ people to a site holding a camera, and 14 across all 200 sites.
244
+
245
+ **3. Permissions resolved against an arbitrary organisation.**
246
+ `getUserPermissions({user, org: memberships[0]?.org})` took whichever membership
247
+ Mongo returned first, which for the 22 people who belong to more than one
248
+ organisation is a coin toss - it could read the permissions of an organisation
249
+ that has nothing to do with the site being asked about. It now resolves against
250
+ the membership that actually granted access.
251
+
252
+ Also splits `entitleSite` (may this person reach this site) out of
253
+ `authorizeSite` (that, plus the permission this particular use needs), so a
254
+ plain read of a site's own camera list can be scoped without also being gated on
255
+ a permission that would strand people who are legitimately at the site.
256
+
257
+ No response field changed. No route changed. No device is contacted by any of
258
+ this.
259
+
260
+ - c205571: Scope `GET /site-cameras` to the caller's own site.
261
+
262
+ The site was an OPTIONAL query parameter and nothing on the handler looked at
263
+ the session beyond `requireAuth`. So any signed-in account, in any organisation,
264
+ could page the whole estate's cameras - on staging that is all 15 active IP
265
+ cameras across 3 owner organisations, each record carrying its `host` and its
266
+ `username`. It is the plainest cross-tenant read left in the camera module.
267
+
268
+ `site` is now required, and the caller is authorised against it with
269
+ `entitleSite`: membership of the site, of its owning organisation, or an ACTIVE
270
+ `customer.sites` engagement with it. "Not yours" and "does not exist" answer
271
+ identically, so site ids cannot be enumerated from the difference.
272
+
273
+ **Every caller in the estate already sends `site`** - checked across every
274
+ repository, not assumed:
275
+
276
+ | caller | sends |
277
+ | --------------------------------------------------------------- | ----------------------------- |
278
+ | `iservice365-mobile-app-security` `pages/cctv/index.vue` (live) | `site`, `type: ip`, `page` |
279
+ | `iservice365-mobile-app-security` `useAnprEnabled` (live) | `site`, `type: anpr` |
280
+ | `iservice365-layer-common` `CameraMain.vue` (Site Settings) | `site`, `type`, `page` |
281
+ | `iservice365-layer-common` `CameraWall.vue` | `site`, `type: ip`, `page` |
282
+ | `iservice365-web-app-security` `VirtualPatrolRoutesForm.vue` | `site`, `type: ip`, `page: 1` |
283
+ | `isecure365-mobile-app` `site.service.ts` `getAnprCameras` | `site`, `type: anpr` |
284
+
285
+ So no running client breaks.
286
+
287
+ Deliberately NOT gated on a camera permission. This is the same list the site's
288
+ own Settings panel and the guard's app have always read; on staging only 25 of
289
+ the 118 people entitled to a site with a camera hold a camera-view permission,
290
+ so a permission gate here would strand people who are legitimately at the site.
291
+ Restricting the read to the caller's own sites is the fix; who among them may
292
+ look is a separate decision that needs its own measurement.
293
+
294
+ The repository's 15-minute cache key already carries the site, so a cached page
295
+ can only be served to callers authorised for that same site. No caller id was
296
+ added to the key - it would make the cache per-person for an answer that is
297
+ identical per site.
298
+
299
+ ## 3.42.3
300
+
301
+ ### Patch Changes
302
+
303
+ - 854dd11: Editing a building no longer creates duplicate levels or orphaned units
304
+
305
+ Updating a block inserted a brand-new level document for every level name in the
306
+ payload, without checking whether that name was already a level on the block. A
307
+ client that posted the block's existing level names back — which the block
308
+ editor's own full-levels payload does — got a second "Level 1" document, a
309
+ second entry in `building.levels`, and units still pointing at the first one.
310
+
311
+ The submitted names are now reconciled against the block's existing ACTIVE
312
+ levels first. A name that already matches an existing level is left alone and
313
+ keeps its `_id`, so nothing that references it is orphaned; only genuinely new
314
+ names are inserted.
315
+
316
+ Matching is case- and whitespace-insensitive, using the same normalisation the
317
+ import path already uses, so "Level 1", "level 1" and "Level&nbsp;&nbsp;1" are
318
+ one level. Only the match key is normalised — the stored name keeps its
319
+ original spelling, so two genuinely different levels are never merged.
320
+
321
+ Removals are deliberately not inferred from this payload; deleting a level keeps
322
+ its own guarded path.
323
+
324
+ Worth a QA pass on building edit — this changes what a block update writes.
325
+
3
326
  ## 3.42.2
4
327
 
5
328
  ### Patch Changes