@7365admin1/core 3.64.5-staging.304 → 3.64.5-staging.306

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,26 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Module gate: the kill switch now stops the 409, and the check stops reading when there is nothing to decide
6
+
7
+ Two follow-ups from the module-gate server review, both behaviour-neutral for
8
+ anybody who is not using the kill switch.
9
+
10
+ **`MODULE_LIST_GATE=off` now means off.** Every other part of the gate checked the
11
+ master switch; `mustAcknowledge` did not. So with the gate switched off, a save that
12
+ narrowed a client's module list was still refused with a 409 "acknowledge the impact
13
+ first" — the one lever a lead would reach for in an emergency, not working for the
14
+ one path that blocks a save from going through. Both the organisation and the site
15
+ save paths go through that function, so both are fixed. With the switch at its
16
+ default the 409 behaves exactly as before, and that is asserted.
17
+
18
+ **`moduleDeniedForCaller` no longer reads memberships when no list is enforced.** It
19
+ runs on every governed request and read the caller's memberships before finding out
20
+ whether there was anything to decide — and `members.user` has no index to use. It now
21
+ returns early unless the site or its organisation has an enforced list, which is what
22
+ the notification filter already did. No verdict changes: a client that really has an
23
+ enforced list is still measured exactly as before, and that is asserted too.
24
+
25
+ Also corrects the 3.65.0 changelog, which claimed `MODULE_LIST_GATE` switched all of
26
+ the gate off at once. It did not, for the reason above.
@@ -0,0 +1,31 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ POST /api/people: count cross-client person creation (gate ships switched off)
6
+
7
+ The signed-in branch of `POST /api/people` checked only that somebody was signed
8
+ in. The site and organisation came straight from the request body, and where that
9
+ body carries an e-mail, a password and a resident/tenant type, the service goes on
10
+ to create a **user account** and an **active membership** as well as the person row.
11
+
12
+ `site.people` is the collection every site-reach decision on the platform reads its
13
+ verdict from, so a row written there is not an ordinary record — it manufactures
14
+ reach into another client's estate.
15
+
16
+ This adds the site-reach check the camera, HID and people-list paths already use,
17
+ behind `PEOPLE_CREATE_REACH_MODE`, and **ships it in `log` mode**: it records what
18
+ it would refuse and refuses nothing. `enforce` refuses; `off` does not look. Any
19
+ unexpected error allows the request — this must never be why a real registration
20
+ fails.
21
+
22
+ It ships off because of RP-5. A person row carries no `createdBy`, so who created
23
+ the existing rows cannot be established retrospectively and the true refusal count
24
+ is only knowable from live traffic. Measured read-only on staging: 372
25
+ `site.people` rows, of which exactly **1** is self-signup shaped (`platform:
26
+ "mobile"`, `status: "pending"`) and 371 were written by a signed-in caller, across
27
+ 3 of 256 sites.
28
+
29
+ Pre-login self-signup is untouched in every mode: a request with no session takes
30
+ the other branch of `add` before this runs, and that route is already constrained
31
+ by its own allow-list schema with `status` and `platform` pinned server-side.
package/dist/index.js CHANGED
@@ -15855,6 +15855,8 @@ function deniedForMembership({
15855
15855
  return { denied, since: [orgTier.since, siteTier.since].sort().pop() ?? "" };
15856
15856
  }
15857
15857
  function mustAcknowledge(preview, acknowledged) {
15858
+ if (!moduleListGateOn())
15859
+ return false;
15858
15860
  if (acknowledged === true || !preview.newlyDenied.length)
15859
15861
  return false;
15860
15862
  return !preview.impact || preview.impact.memberCount > 0;
@@ -62476,6 +62478,21 @@ var import_joi72 = __toESM(require("joi"));
62476
62478
  var import_node_server_utils148 = require("@7365admin1/node-server-utils");
62477
62479
  var import_node_server_utils149 = require("@7365admin1/node-server-utils");
62478
62480
 
62481
+ // src/utils/people-create-gate.util.ts
62482
+ var PEOPLE_CREATE_REACH_MODE_DEFAULT = "log";
62483
+ function peopleCreateReachMode(env2 = process.env) {
62484
+ return gateMode(
62485
+ env2.PEOPLE_CREATE_REACH_MODE,
62486
+ ["log", "enforce", "off"],
62487
+ PEOPLE_CREATE_REACH_MODE_DEFAULT
62488
+ );
62489
+ }
62490
+ function peopleCreateVerdict(params) {
62491
+ if (params.mode === "off" || params.reachable)
62492
+ return { refuse: false, record: false };
62493
+ return { refuse: params.mode === "enforce", record: true };
62494
+ }
62495
+
62479
62496
  // src/utils/self-signup-files.util.ts
62480
62497
  function dropUnsentFiles(body) {
62481
62498
  if (!Array.isArray(body?.files))
@@ -62585,6 +62602,41 @@ function usePersonController() {
62585
62602
  next(new import_node_server_utils148.BadRequestError(messages));
62586
62603
  return;
62587
62604
  }
62605
+ try {
62606
+ const mode = peopleCreateReachMode();
62607
+ if (mode !== "off") {
62608
+ const actor = await resolveInviteActor(callerId(req));
62609
+ let reachable = true;
62610
+ if (!staffMayBypass(actor)) {
62611
+ const site = payload?.site ? String(payload.site) : "";
62612
+ const org = payload?.org ? String(payload.org) : "";
62613
+ if (site) {
62614
+ reachable = await useCameraViewService().entitleSite({ siteId: site, userId: actor.id }).then(
62615
+ () => true,
62616
+ () => false
62617
+ );
62618
+ } else if (org) {
62619
+ reachable = actor.orgIds.includes(org);
62620
+ }
62621
+ }
62622
+ const verdict = peopleCreateVerdict({ mode, reachable });
62623
+ if (verdict.record) {
62624
+ import_node_server_utils148.logger.log({
62625
+ level: "info",
62626
+ message: `[people-create] ${verdict.refuse ? "refused" : "would-refuse"} caller=${actor.id} site=${payload?.site ?? ""} org=${payload?.org ?? ""} mode=${mode}`
62627
+ });
62628
+ }
62629
+ if (verdict.refuse) {
62630
+ next(new import_node_server_utils149.UnauthorizedError("Not authorized."));
62631
+ return;
62632
+ }
62633
+ }
62634
+ } catch (gateError) {
62635
+ import_node_server_utils148.logger.log({
62636
+ level: "warn",
62637
+ message: `[people-create] reach check failed open: ${gateError?.message}`
62638
+ });
62639
+ }
62588
62640
  try {
62589
62641
  const data = await _add(payload);
62590
62642
  res.status(201).json({ message: data });
@@ -96883,6 +96935,8 @@ async function moduleDeniedForCaller({
96883
96935
  if (!user || !siteId)
96884
96936
  return ALLOW;
96885
96937
  const siteDoc = typeof site === "string" ? await data.site(siteId).catch(() => null) : site ?? null;
96938
+ if (!await siteHasEnforcedList(siteDoc, data))
96939
+ return ALLOW;
96886
96940
  const gate = await gateAtSite(await data.membershipsOf([user]), siteDoc, siteId, data);
96887
96941
  if (gate.exempt)
96888
96942
  return ALLOW;