@7365admin1/core 3.67.0 → 3.68.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,58 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.68.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7e9298: Manpower live status: an authenticated namespace beside the open one
8
+
9
+ `/manpower` streams every client site's live staffing level — how many guards are
10
+ actually on shift against how many were promised — to anyone who connects, with no
11
+ credential of any kind.
12
+
13
+ This adds `/manpower-live`, carrying exactly the same `manpower-status-update`
14
+ events, which admits a socket only when the handshake presents a session id that
15
+ resolves to a live session. Every refusal is an identical `Unauthorized`, and a
16
+ session store that is down refuses rather than letting everyone in.
17
+
18
+ `/manpower` itself is deliberately unchanged: the Capacitor guard app installed on
19
+ phones today and the Nuxt security screen both connect to it with no credential and
20
+ would be cut off the moment it were tightened. Every update is emitted to both, so
21
+ the two feeds can never disagree, and both namespaces share one connection handler,
22
+ so a client on the authenticated feed still starts the poll and still gets an
23
+ immediate snapshot.
24
+
25
+ A live session is the whole check, deliberately. Scoping the feed to the caller's
26
+ own sites is not possible today: manpower lives in HRM Labs' vendor id space, none
27
+ of those site ids match ours, and no field links the two — a site check would empty
28
+ the screen for the one customer using it. That mapping is a schema change and
29
+ belongs to the leads.
30
+
31
+ ### Patch Changes
32
+
33
+ - 87c8679: Module gate: the kill switch now stops the 409, and the check stops reading when there is nothing to decide
34
+
35
+ Two follow-ups from the module-gate server review, both behaviour-neutral for
36
+ anybody who is not using the kill switch.
37
+
38
+ **`MODULE_LIST_GATE=off` now means off.** Every other part of the gate checked the
39
+ master switch; `mustAcknowledge` did not. So with the gate switched off, a save that
40
+ narrowed a client's module list was still refused with a 409 "acknowledge the impact
41
+ first" — the one lever a lead would reach for in an emergency, not working for the
42
+ one path that blocks a save from going through. Both the organisation and the site
43
+ save paths go through that function, so both are fixed. With the switch at its
44
+ default the 409 behaves exactly as before, and that is asserted.
45
+
46
+ **`moduleDeniedForCaller` no longer reads memberships when no list is enforced.** It
47
+ runs on every governed request and read the caller's memberships before finding out
48
+ whether there was anything to decide — and `members.user` has no index to use. It now
49
+ returns early unless the site or its organisation has an enforced list, which is what
50
+ the notification filter already did. No verdict changes: a client that really has an
51
+ enforced list is still measured exactly as before, and that is asserted too.
52
+
53
+ Also corrects the 3.65.0 changelog, which claimed `MODULE_LIST_GATE` switched all of
54
+ the gate off at once. It did not, for the reason above.
55
+
3
56
  ## 3.67.0
4
57
 
5
58
  ### Minor Changes
@@ -87,7 +140,9 @@
87
140
  the keys in `MODULE_GATE_ENFORCE_KEYS`. Work stamped (`at`) before the
88
141
  acknowledged save is accepted.
89
142
  - `MODULE_LIST_GATE` (code default `on`) switches all of the above off at
90
- once.
143
+ once. CORRECTION (3.68.0): it did not. `mustAcknowledge` never read it, so
144
+ with the gate off a narrowing save was still refused with a 409. Fixed in
145
+ 3.68.0; every other path did check it.
91
146
 
92
147
  ### Patch Changes
93
148
 
package/dist/index.js CHANGED
@@ -536,13 +536,13 @@ var require_logger = __commonJS({
536
536
  "use strict";
537
537
  exports.__esModule = true;
538
538
  var _utils = require_utils();
539
- var logger226 = {
539
+ var logger227 = {
540
540
  methodMap: ["debug", "info", "warn", "error"],
541
541
  level: "info",
542
542
  // Maps a given level value to the `methodMap` indexes above.
543
543
  lookupLevel: function lookupLevel(level) {
544
544
  if (typeof level === "string") {
545
- var levelMap = _utils.indexOf(logger226.methodMap, level.toLowerCase());
545
+ var levelMap = _utils.indexOf(logger227.methodMap, level.toLowerCase());
546
546
  if (levelMap >= 0) {
547
547
  level = levelMap;
548
548
  } else {
@@ -553,9 +553,9 @@ var require_logger = __commonJS({
553
553
  },
554
554
  // Can be overridden in the host environment
555
555
  log: function log(level) {
556
- level = logger226.lookupLevel(level);
557
- if (typeof console !== "undefined" && logger226.lookupLevel(logger226.level) <= level) {
558
- var method = logger226.methodMap[level];
556
+ level = logger227.lookupLevel(level);
557
+ if (typeof console !== "undefined" && logger227.lookupLevel(logger227.level) <= level) {
558
+ var method = logger227.methodMap[level];
559
559
  if (!console[method]) {
560
560
  method = "log";
561
561
  }
@@ -566,7 +566,7 @@ var require_logger = __commonJS({
566
566
  }
567
567
  }
568
568
  };
569
- exports["default"] = logger226;
569
+ exports["default"] = logger227;
570
570
  module2.exports = exports["default"];
571
571
  }
572
572
  });
@@ -15844,6 +15844,8 @@ function deniedForMembership({
15844
15844
  return { denied, since: [orgTier.since, siteTier.since].sort().pop() ?? "" };
15845
15845
  }
15846
15846
  function mustAcknowledge(preview, acknowledged) {
15847
+ if (!moduleListGateOn())
15848
+ return false;
15847
15849
  if (acknowledged === true || !preview.newlyDenied.length)
15848
15850
  return false;
15849
15851
  return !preview.impact || preview.impact.memberCount > 0;
@@ -85931,6 +85933,52 @@ var isWithinHour = (alertTime, timeNow) => {
85931
85933
  var import_node_server_utils264 = require("@7365admin1/node-server-utils");
85932
85934
  var import_mongodb162 = require("mongodb");
85933
85935
  var import_moment_timezone11 = __toESM(require("moment-timezone"));
85936
+
85937
+ // src/utils/socket-session.util.ts
85938
+ var MANPOWER_LIVE_NAMESPACE = "/manpower-live";
85939
+ function sidFromHandshake(handshake) {
85940
+ const cookie = String(handshake?.headers?.cookie ?? "");
85941
+ const fromCookie = /(?:^|;\s*)sid=([^;]*)/.exec(cookie);
85942
+ if (fromCookie) {
85943
+ const value = decodeURIComponent(fromCookie[1]).trim();
85944
+ if (value)
85945
+ return value;
85946
+ }
85947
+ for (const key of ["sid", "token"]) {
85948
+ const value = handshake?.auth?.[key];
85949
+ if (typeof value === "string" && value.trim())
85950
+ return value.trim();
85951
+ }
85952
+ const header = handshake?.headers?.authorization;
85953
+ if (typeof header === "string" && header.trim()) {
85954
+ const value = header.trim();
85955
+ return value.toLowerCase().startsWith("bearer ") ? value.slice("bearer ".length).trim() : value;
85956
+ }
85957
+ return "";
85958
+ }
85959
+ function userIdOfSession(session) {
85960
+ if (!session || typeof session !== "object")
85961
+ return "";
85962
+ const record = session;
85963
+ return String(record.user ?? record._id ?? "").trim();
85964
+ }
85965
+ async function authoriseSocketSession(handshake, readSession) {
85966
+ const sid = sidFromHandshake(handshake);
85967
+ if (!sid)
85968
+ throw new Error("Unauthorized");
85969
+ let session;
85970
+ try {
85971
+ session = await readSession(sid);
85972
+ } catch {
85973
+ throw new Error("Unauthorized");
85974
+ }
85975
+ const userId = userIdOfSession(session);
85976
+ if (!userId)
85977
+ throw new Error("Unauthorized");
85978
+ return userId;
85979
+ }
85980
+
85981
+ // src/events/manpower.event.ts
85934
85982
  async function manpowerEvents(io) {
85935
85983
  let intervalId = null;
85936
85984
  let activeConnections = 0;
@@ -85991,8 +86039,7 @@ async function manpowerEvents(io) {
85991
86039
  newStatus = "Over";
85992
86040
  else if (actual < expected)
85993
86041
  newStatus = "Under";
85994
- const namespace2 = `/manpower`;
85995
- io.of(namespace2).emit("manpower-status-update", {
86042
+ const update = {
85996
86043
  siteId: doc.siteId,
85997
86044
  siteName: doc.siteName,
85998
86045
  shift: shift.key,
@@ -86000,7 +86047,9 @@ async function manpowerEvents(io) {
86000
86047
  actual,
86001
86048
  expected,
86002
86049
  selectedDateSGT
86003
- });
86050
+ };
86051
+ io.of("/manpower").emit("manpower-status-update", update);
86052
+ io.of(MANPOWER_LIVE_NAMESPACE).emit("manpower-status-update", update);
86004
86053
  }
86005
86054
  }
86006
86055
  };
@@ -86020,7 +86069,7 @@ async function manpowerEvents(io) {
86020
86069
  console.log(" Interval stopped.");
86021
86070
  }
86022
86071
  };
86023
- io.of("/manpower").on("connection", (socket) => {
86072
+ const onConnection = (socket) => {
86024
86073
  activeConnections += 1;
86025
86074
  const date = socket.handshake.query.date;
86026
86075
  if (activeConnections === 1) {
@@ -86035,7 +86084,26 @@ async function manpowerEvents(io) {
86035
86084
  stopInterval();
86036
86085
  }
86037
86086
  });
86087
+ };
86088
+ io.of("/manpower").on("connection", onConnection);
86089
+ const live = io.of(MANPOWER_LIVE_NAMESPACE);
86090
+ live.use(async (socket, next) => {
86091
+ try {
86092
+ const { getCache } = (0, import_node_server_utils264.useCache)("sessions");
86093
+ socket.data.userId = await authoriseSocketSession(
86094
+ socket.handshake,
86095
+ (sid) => getCache(`sid:${sid}`)
86096
+ );
86097
+ next();
86098
+ } catch (error) {
86099
+ import_node_server_utils264.logger.log({
86100
+ level: "debug",
86101
+ message: `[manpower] refused a socket: ${error?.message}`
86102
+ });
86103
+ next(new Error("Unauthorized"));
86104
+ }
86038
86105
  });
86106
+ live.on("connection", onConnection);
86039
86107
  }
86040
86108
 
86041
86109
  // src/services/reddot-payment.service.ts
@@ -94810,6 +94878,8 @@ async function moduleDeniedForCaller({
94810
94878
  if (!user || !siteId)
94811
94879
  return ALLOW;
94812
94880
  const siteDoc = typeof site === "string" ? await data.site(siteId).catch(() => null) : site ?? null;
94881
+ if (!await siteHasEnforcedList(siteDoc, data))
94882
+ return ALLOW;
94813
94883
  const gate = await gateAtSite(await data.membershipsOf([user]), siteDoc, siteId, data);
94814
94884
  if (gate.exempt)
94815
94885
  return ALLOW;