@7365admin1/core 3.53.8 → 3.53.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.53.8",
4
+ "version": "3.53.9",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -13,7 +13,6 @@ import {
13
13
  hasAnyPermission,
14
14
  isCameraEntitled,
15
15
  cameraGrant,
16
- orgSiteScope,
17
16
  parseDeviceTime,
18
17
  parseSoftwareVersion,
19
18
  ptzEndpoint,
@@ -938,93 +937,10 @@ test("a write is scoped to the caller's own sites, not just to any signed-in use
938
937
  });
939
938
 
940
939
  /*
941
- * `orgSiteScope` which sites of ONE organisation a caller may be OFFERED.
942
- *
943
- * The listing half of `cameraGrant`. `GET /api/customer-sites?org=` authorised
944
- * at organisation level and then returned the organisation's whole estate, so a
945
- * user invited to a single site saw every site in the client in the switcher.
940
+ * The OFFER half of `cameraGrant` is `entitledSiteScope`, and it now answers
941
+ * `GET /api/customer-sites?org=` directly (`entitledSites`). Its own tests are
942
+ * `picker-site-scope.test.mjs` and `building-estate-site-scope.test.mjs`.
946
943
  */
947
- const SCOPE_ORG = "aaaaaaaaaaaaaaaaaaaaaaaa";
948
- const SCOPE_OTHER_ORG = "bbbbbbbbbbbbbbbbbbbbbbbb";
949
- const SCOPE_SITE_A = "cccccccccccccccccccccccc";
950
- const SCOPE_SITE_B = "dddddddddddddddddddddddd";
951
-
952
- test("orgSiteScope: an org-wide membership keeps the whole list", () => {
953
- // The half a careless narrowing breaks. An org owner/admin holds a membership
954
- // with no site, and must still see every site in their organisation.
955
- assert.equal(
956
- orgSiteScope({ memberships: [{ org: SCOPE_ORG, siteId: "" }], org: SCOPE_ORG }),
957
- null,
958
- );
959
- assert.equal(
960
- orgSiteScope({ memberships: [{ org: SCOPE_ORG }], org: SCOPE_ORG }),
961
- null,
962
- );
963
- // One org-wide membership wins even alongside pinned ones.
964
- assert.equal(
965
- orgSiteScope({
966
- memberships: [
967
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A },
968
- { org: SCOPE_ORG, siteId: "" },
969
- ],
970
- org: SCOPE_ORG,
971
- }),
972
- null,
973
- );
974
- });
975
-
976
- test("orgSiteScope: a site-pinned membership is offered that site alone", () => {
977
- assert.deepEqual(
978
- orgSiteScope({ memberships: [{ org: SCOPE_ORG, siteId: SCOPE_SITE_A }], org: SCOPE_ORG }),
979
- [SCOPE_SITE_A],
980
- );
981
- // Two pins in the same organisation are both offered, de-duplicated.
982
- assert.deepEqual(
983
- orgSiteScope({
984
- memberships: [
985
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A },
986
- { org: SCOPE_ORG, siteId: SCOPE_SITE_B },
987
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A },
988
- ],
989
- org: SCOPE_ORG,
990
- }),
991
- [SCOPE_SITE_A, SCOPE_SITE_B],
992
- );
993
- });
994
-
995
- test("orgSiteScope: another organisation's memberships do not widen this one", () => {
996
- // The reported defect in one line: pinned to one site here, org-wide
997
- // somewhere else. The org-wide row must not unlock this organisation.
998
- assert.deepEqual(
999
- orgSiteScope({
1000
- memberships: [
1001
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A },
1002
- { org: SCOPE_OTHER_ORG, siteId: "" },
1003
- ],
1004
- org: SCOPE_ORG,
1005
- }),
1006
- [SCOPE_SITE_A],
1007
- );
1008
- // No membership in this organisation at all: fail closed, not "show all".
1009
- assert.deepEqual(
1010
- orgSiteScope({ memberships: [{ org: SCOPE_OTHER_ORG, siteId: "" }], org: SCOPE_ORG }),
1011
- [],
1012
- );
1013
- });
1014
-
1015
- test("orgSiteScope: ObjectId and hex-string ids are the same id", () => {
1016
- assert.deepEqual(
1017
- orgSiteScope({
1018
- memberships: [{ org: { toString: () => SCOPE_ORG }, siteId: { toString: () => SCOPE_SITE_A } }],
1019
- org: SCOPE_ORG,
1020
- }),
1021
- [SCOPE_SITE_A],
1022
- );
1023
- });
1024
-
1025
- test("orgSiteScope: no organisation asked about means no restriction", () => {
1026
- assert.equal(orgSiteScope({ memberships: [{ org: SCOPE_ORG, siteId: SCOPE_SITE_A }] }), null);
1027
- });
1028
944
 
1029
945
  /**
1030
946
  * The owner's rule of 2026-09-02: "Org-level roles see all sites."
@@ -1034,80 +950,10 @@ test("orgSiteScope: no organisation asked about means no restriction", () => {
1034
950
  * (`Org Owner` in most cases) but whose membership row also happens to name a
1035
951
  * site. These three tests are the three shapes the rule has to get right.
1036
952
  */
1037
- test("orgSiteScope: a site-LEVEL role pinned to a site sees only that site", () => {
1038
- assert.deepEqual(
1039
- orgSiteScope({
1040
- memberships: [
1041
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A, orgLevelRole: false },
1042
- ],
1043
- org: SCOPE_ORG,
1044
- }),
1045
- [SCOPE_SITE_A],
1046
- );
1047
- });
1048
-
1049
- test("orgSiteScope: an ORG-level role carrying a siteId still sees every site", () => {
1050
- assert.equal(
1051
- orgSiteScope({
1052
- memberships: [
1053
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A, orgLevelRole: true },
1054
- ],
1055
- org: SCOPE_ORG,
1056
- }),
1057
- null,
1058
- );
1059
-
1060
- // and it widens the whole organisation, not just its own row
1061
- assert.equal(
1062
- orgSiteScope({
1063
- memberships: [
1064
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A, orgLevelRole: false },
1065
- { org: SCOPE_ORG, siteId: SCOPE_SITE_B, orgLevelRole: true },
1066
- ],
1067
- org: SCOPE_ORG,
1068
- }),
1069
- null,
1070
- );
1071
- });
1072
-
1073
- test("orgSiteScope: an ORG-level role with no siteId sees every site", () => {
1074
- assert.equal(
1075
- orgSiteScope({
1076
- memberships: [{ org: SCOPE_ORG, siteId: "", orgLevelRole: true }],
1077
- org: SCOPE_ORG,
1078
- }),
1079
- null,
1080
- );
1081
- });
1082
-
1083
- test("orgSiteScope: an org-level role in ANOTHER organisation does not widen this one", () => {
1084
- assert.deepEqual(
1085
- orgSiteScope({
1086
- memberships: [
1087
- { org: SCOPE_ORG, siteId: SCOPE_SITE_A, orgLevelRole: false },
1088
- { org: SCOPE_OTHER_ORG, siteId: "", orgLevelRole: true },
1089
- ],
1090
- org: SCOPE_ORG,
1091
- }),
1092
- [SCOPE_SITE_A],
1093
- );
1094
- });
1095
-
1096
- test("orgSiteScope: an unresolved role fails closed — it does not widen", () => {
1097
- // `orgLevelRole` absent is how a missing or soft-deleted role arrives.
1098
- assert.deepEqual(
1099
- orgSiteScope({
1100
- memberships: [{ org: SCOPE_ORG, siteId: SCOPE_SITE_A }],
1101
- org: SCOPE_ORG,
1102
- }),
1103
- [SCOPE_SITE_A],
1104
- );
1105
- });
1106
-
1107
953
  /**
1108
954
  * The camera side of the site-scope rule, reconciled with the LIST side.
1109
955
  *
1110
- * `orgSiteScope` (below) was changed on 2026-09-02 to the owner's rule: an
956
+ * The site LIST was changed on 2026-09-02 to the owner's rule: an
1111
957
  * ORGANISATION-LEVEL role sees every site of its organisation even when its
1112
958
  * membership row also names a `siteId`. `cameraGrant` kept the older, stricter
1113
959
  * reading — `!siteId` only — so the switcher offered those people sites the
@@ -0,0 +1,181 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ cameraGrant,
6
+ entitledSiteScope,
7
+ } from "./.build/utils/camera-view.util.mjs";
8
+
9
+ /**
10
+ * The site PICKER must offer exactly the sites the camera check will serve.
11
+ *
12
+ * `GET /api/customer-sites?org=<agency>` fills the site switcher in the guard
13
+ * app, the MA apps and the seven vertical web apps. Every screen then calls with
14
+ * the chosen site id — `getAnprCameras` -> `GET /api/site-cameras` -> core's
15
+ * `entitleSite`. When the two disagree, the switcher is a menu of things that do
16
+ * not work: the user picks a site and reads "Site not found.".
17
+ *
18
+ * Owner's ruling, 2026-09-06: **a guard reaches only the sites they are
19
+ * personally assigned to, not every site their agency is engaged at.** So the
20
+ * PICKER is the half that was wrong, and the entitlement rule is unchanged.
21
+ *
22
+ * The controller therefore now asks the SAME helper the camera check asks:
23
+ * `entitledSites` -> `entitledSiteScope`, which is `cameraGrant`'s three
24
+ * branches expressed as a list. These tests pin that agreement, in both
25
+ * directions, and pin that an ORG-level role is not narrowed with the guards.
26
+ *
27
+ * Measured read-only on staging 2026-09-06: 384 offered pairs -> 342; the 42
28
+ * removed are exactly the 42 `entitleSite` already refuses; 26 of 189 users see
29
+ * a shorter list; ZERO users gain a site.
30
+ */
31
+
32
+ const AGENCY = "aaaaaaaaaaaaaaaaaaaaaaaa";
33
+ const CLIENT = "bbbbbbbbbbbbbbbbbbbbbbbb";
34
+ const SITE_A = "cccccccccccccccccccccccc";
35
+ const SITE_B = "dddddddddddddddddddddddd";
36
+
37
+ /** The engagement rows `?org=AGENCY` returns: the agency serves both sites. */
38
+ const ENGAGED = { [AGENCY]: [SITE_A, SITE_B] };
39
+ /** Both sites belong to the CLIENT, not to the agency. */
40
+ const OWNED = { [CLIENT]: [SITE_A, SITE_B] };
41
+
42
+ /**
43
+ * The rule the controller used BEFORE this change (`orgSiteScope`), transcribed
44
+ * so the defect is reproducible from the test file alone rather than only from
45
+ * the staging measurement. It is the control every test below is measured
46
+ * against: each `notDeepEqual`/`ok` pair here fails if the two rules are made to
47
+ * agree by accident, and each assertion on `entitledSiteScope` is one the
48
+ * pre-fix rule does not satisfy.
49
+ */
50
+ function preFixOrgSiteScope({ memberships, org }) {
51
+ if (!org) return null;
52
+ const inOrg = memberships.filter((m) => String(m.org ?? "") === org);
53
+ if (inOrg.some((m) => !String(m.siteId ?? "") || m.orgLevelRole === true)) {
54
+ return null; // "no restriction" — the whole engagement list
55
+ }
56
+ return inOrg.map((m) => String(m.siteId ?? "")).filter(Boolean);
57
+ }
58
+
59
+ /** What the picker offers for `?org=AGENCY` under a given scope list. */
60
+ function offered(scope) {
61
+ return scope === null
62
+ ? ENGAGED[AGENCY]
63
+ : ENGAGED[AGENCY].filter((site) => scope.includes(site));
64
+ }
65
+
66
+ /** Does the camera check serve this site to this caller? */
67
+ function grants(site, memberships) {
68
+ return Boolean(
69
+ cameraGrant({
70
+ cameraSite: site,
71
+ cameraOrg: CLIENT,
72
+ memberships,
73
+ engagedOrgs: new Set(
74
+ Object.entries(ENGAGED)
75
+ .filter(([, sites]) => sites.includes(site))
76
+ .map(([org]) => org),
77
+ ),
78
+ }),
79
+ );
80
+ }
81
+
82
+ test("a guard assigned to one site is offered that site, and its cameras load", () => {
83
+ // The shape the owner ruled on: an ORG-LEVEL role at the agency whose
84
+ // membership row names site A. 26 of 189 staging users are this shape.
85
+ const memberships = [
86
+ { org: AGENCY, siteId: SITE_A, role: "guard-role", orgLevelRole: true },
87
+ ];
88
+
89
+ const scope = entitledSiteScope({
90
+ memberships,
91
+ sitesByOrg: OWNED,
92
+ engagedSitesByOrg: ENGAGED,
93
+ });
94
+
95
+ assert.deepEqual(offered(scope), [SITE_A]);
96
+ assert.equal(grants(SITE_A, memberships), true);
97
+ });
98
+
99
+ test("an agency site the guard is NOT assigned to is neither offered nor reachable", () => {
100
+ const memberships = [
101
+ { org: AGENCY, siteId: SITE_A, role: "guard-role", orgLevelRole: true },
102
+ ];
103
+
104
+ const scope = entitledSiteScope({
105
+ memberships,
106
+ sitesByOrg: OWNED,
107
+ engagedSitesByOrg: ENGAGED,
108
+ });
109
+
110
+ assert.equal(offered(scope).includes(SITE_B), false);
111
+ assert.equal(grants(SITE_B, memberships), false);
112
+
113
+ // CONTROL — this is exactly what the pre-fix rule did, and why site B showed
114
+ // up in the switcher and then answered "Site not found.".
115
+ const before = offered(preFixOrgSiteScope({ memberships, org: AGENCY }));
116
+ assert.equal(before.includes(SITE_B), true);
117
+ assert.equal(grants(SITE_B, memberships), false);
118
+ });
119
+
120
+ test("the offer and the camera check agree on every site, both ways", () => {
121
+ // The property the whole change exists for: offered == granted, with no
122
+ // site offered that is refused and no site refused that is offered.
123
+ const shapes = [
124
+ // a guard pinned to a site, org-level role — the defect's shape
125
+ [{ org: AGENCY, siteId: SITE_A, role: "r", orgLevelRole: true }],
126
+ // a guard pinned to a site, site-level role
127
+ [{ org: AGENCY, siteId: SITE_A, role: "r" }],
128
+ // agency staff with NO site — org-wide, reaches every engaged site
129
+ [{ org: AGENCY, siteId: "", role: "r" }],
130
+ // a member of nothing relevant
131
+ [{ org: CLIENT, siteId: SITE_A, role: "r" }],
132
+ ];
133
+
134
+ for (const memberships of shapes) {
135
+ const scope = entitledSiteScope({
136
+ memberships,
137
+ sitesByOrg: OWNED,
138
+ engagedSitesByOrg: ENGAGED,
139
+ });
140
+ for (const site of ENGAGED[AGENCY]) {
141
+ assert.equal(
142
+ offered(scope).includes(site),
143
+ grants(site, memberships),
144
+ `offer and camera check disagree on ${site} for ${JSON.stringify(memberships)}`,
145
+ );
146
+ }
147
+ }
148
+
149
+ // CONTROL — the pre-fix rule disagrees on the first shape. If this ever
150
+ // stops failing, the transcription above has drifted and the test is void.
151
+ const [defect] = shapes;
152
+ const before = offered(preFixOrgSiteScope({ memberships: defect, org: AGENCY }));
153
+ assert.ok(
154
+ ENGAGED[AGENCY].some(
155
+ (site) => before.includes(site) !== grants(site, defect),
156
+ ),
157
+ "the pre-fix rule must disagree — otherwise there was nothing to fix",
158
+ );
159
+ });
160
+
161
+ test("an ORG-level role at the site's OWNER still reaches its whole estate", () => {
162
+ // Do not narrow admins while narrowing guards. An organisation-level role at
163
+ // the CLIENT keeps every site the client owns, pinned row or not.
164
+ const memberships = [
165
+ { org: CLIENT, siteId: SITE_A, role: "org-owner", orgLevelRole: true },
166
+ ];
167
+
168
+ const scope = entitledSiteScope({
169
+ memberships,
170
+ sitesByOrg: OWNED,
171
+ engagedSitesByOrg: ENGAGED,
172
+ });
173
+
174
+ assert.deepEqual([...scope].sort(), [SITE_A, SITE_B].sort());
175
+ assert.equal(grants(SITE_A, memberships), true);
176
+ assert.equal(grants(SITE_B, memberships), true);
177
+
178
+ // ...and a site-less membership at the owner is the same answer.
179
+ const siteless = [{ org: CLIENT, siteId: "", role: "org-owner" }];
180
+ assert.equal(grants(SITE_B, siteless), true);
181
+ });