@7365admin1/core 3.32.2-staging.68 → 3.32.2-staging.69

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Camera entitlement: an engagement grants a site only to an org-level membership, and permissions resolve against the granting role
6
+
7
+ Two follow-ups to the camera entitlement change:
8
+
9
+ - the engagement branch of `cameraGrant` ignored `members.siteId`, so a member
10
+ pinned to one site could reach every site their organisation is engaged at.
11
+ It now requires an org-level membership, matching the owning-org branch above
12
+ it: a membership pinned to a site never grants a different site.
13
+ - `getUserPermissions` accepts the granting membership's `role`, so a caller
14
+ holding several memberships in one organisation is no longer resolved by
15
+ whichever member document Mongo returns first.
package/dist/index.d.ts CHANGED
@@ -2658,6 +2658,12 @@ declare function hasAnyPermission(permissions: unknown, allowed: Array<string>):
2658
2658
  type CameraMembership = {
2659
2659
  siteId?: unknown;
2660
2660
  org?: unknown;
2661
+ /**
2662
+ * The role this membership carries. Returned with the grant so permissions
2663
+ * are read off the role that actually granted access — see
2664
+ * `permissionsForGrant` in the service.
2665
+ */
2666
+ role?: unknown;
2661
2667
  };
2662
2668
  /**
2663
2669
  * WHICH membership lets this caller reach this site, or `null` for none.
@@ -2669,12 +2675,35 @@ type CameraMembership = {
2669
2675
  * 2. **A membership at the site's OWNING organisation, with no site on it** —
2670
2676
  * an org-wide role. 90 rows, 56 people, are recorded this way.
2671
2677
  * 3. **An engagement** — an ACTIVE `customer.sites` row saying one of the
2672
- * caller's organisations is contracted to serve this site. This is how the
2673
- * product routes a security agency to a property manager's site, and it is
2674
- * the list the web apps' own site switcher is built from
2675
- * (`useCustomerSite().getAll()` in the Security app's layout). Nothing in the
2676
- * camera path read it before, so a guard whose agency is engaged at a site
2677
- * was refused a camera the switcher had just offered them.
2678
+ * caller's organisations is contracted to serve this site, AND a membership
2679
+ * that is not pinned to some other site. This is how the product routes a
2680
+ * security agency to a property manager's site, and it is the list the web
2681
+ * apps' own site switcher is built from (`useCustomerSite().getAll()` in the
2682
+ * Security app's layout). Nothing in the camera path read it before, so a
2683
+ * guard whose agency is engaged at a site was refused a camera the switcher
2684
+ * had just offered them.
2685
+ *
2686
+ * ## Why the engagement branch requires an ORG-LEVEL membership
2687
+ *
2688
+ * **Product decision, owner, 2026-08-10, settled:** an agency engaged at several
2689
+ * sites gives its staff access to **the site each person is assigned to, and no
2690
+ * other**. It matches how the rest of the product is described — modules are set
2691
+ * up per site.
2692
+ *
2693
+ * So `!idOf(membership.siteId)` is required here exactly as branch 2 requires
2694
+ * it. Before, this branch matched on organisation alone, which made a person
2695
+ * pinned to site X reach every site their agency serves — while somebody pinned
2696
+ * to site X of the site's OWNING organisation could not reach its other sites.
2697
+ * The two branches now say the same thing: **a membership pinned to a site never
2698
+ * grants a different site**, and a membership with no site is an org-wide role
2699
+ * within whichever organisation holds it.
2700
+ *
2701
+ * Measured on staging before the change: this removes 68 (user, camera-site)
2702
+ * grants, all of them cross-organisation engagements held by a membership pinned
2703
+ * elsewhere; every one of those 68 belongs to somebody whose organisation is
2704
+ * engaged at the site they ARE pinned to, so nobody is left without their own
2705
+ * site. 0 grants through branch 2 are affected — **estate-side org-level access
2706
+ * is a different path and is untouched.**
2678
2707
  *
2679
2708
  * The membership is RETURNED, not just a yes/no, because the caller's
2680
2709
  * permissions must then be resolved against the organisation that actually
package/dist/index.js CHANGED
@@ -27401,6 +27401,7 @@ var import_mongodb49 = require("mongodb");
27401
27401
  async function getUserPermissions(params) {
27402
27402
  const user = params.user?.toString();
27403
27403
  const org = params.org?.toString();
27404
+ const grantingRole = params.role?.toString();
27404
27405
  if (!user || !import_mongodb49.ObjectId.isValid(user)) {
27405
27406
  return [];
27406
27407
  }
@@ -27408,19 +27409,23 @@ async function getUserPermissions(params) {
27408
27409
  if (!db) {
27409
27410
  return [];
27410
27411
  }
27411
- const memberQuery = {
27412
- user: new import_mongodb49.ObjectId(user),
27413
- status: { $ne: "deleted" }
27414
- };
27415
- if (org && import_mongodb49.ObjectId.isValid(org)) {
27416
- memberQuery.org = new import_mongodb49.ObjectId(org);
27417
- }
27418
- const member = await db.collection("members").findOne(memberQuery);
27419
- if (!member?.role || !import_mongodb49.ObjectId.isValid(member.role)) {
27420
- return [];
27412
+ let roleId = grantingRole;
27413
+ if (!roleId || !import_mongodb49.ObjectId.isValid(roleId)) {
27414
+ const memberQuery = {
27415
+ user: new import_mongodb49.ObjectId(user),
27416
+ status: { $ne: "deleted" }
27417
+ };
27418
+ if (org && import_mongodb49.ObjectId.isValid(org)) {
27419
+ memberQuery.org = new import_mongodb49.ObjectId(org);
27420
+ }
27421
+ const member = await db.collection("members").findOne(memberQuery);
27422
+ if (!member?.role || !import_mongodb49.ObjectId.isValid(member.role)) {
27423
+ return [];
27424
+ }
27425
+ roleId = member.role.toString();
27421
27426
  }
27422
27427
  const role = await db.collection("roles").findOne({
27423
- _id: new import_mongodb49.ObjectId(member.role),
27428
+ _id: new import_mongodb49.ObjectId(roleId),
27424
27429
  status: "active"
27425
27430
  });
27426
27431
  if (!role || !Array.isArray(role.permissions)) {
@@ -32349,7 +32354,7 @@ function cameraGrant(params) {
32349
32354
  if (atOrg)
32350
32355
  return atOrg;
32351
32356
  const engaged = params.engagedOrgs?.size ? params.memberships.find(
32352
- (membership) => params.engagedOrgs.has(idOf(membership.org))
32357
+ (membership) => !idOf(membership.siteId) && params.engagedOrgs.has(idOf(membership.org))
32353
32358
  ) : void 0;
32354
32359
  return engaged ?? null;
32355
32360
  }
@@ -33691,7 +33696,8 @@ function useCameraViewService() {
33691
33696
  async function permissionsForGrant(userId, grant, memberships) {
33692
33697
  return getUserPermissions({
33693
33698
  user: userId,
33694
- org: grant?.org ?? memberships[0]?.org
33699
+ org: grant?.org ?? memberships[0]?.org,
33700
+ role: grant?.role
33695
33701
  });
33696
33702
  }
33697
33703
  async function authorizeCamera(params) {