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