@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/CHANGELOG.md +23 -0
- package/dist/index.d.ts +1 -57
- package/dist/index.js +1 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/camera-view.util.test.mjs +4 -158
- package/test/picker-site-scope.test.mjs +181 -0
package/package.json
CHANGED
|
@@ -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
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
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
|
-
*
|
|
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
|
+
});
|