@7365admin1/core 3.66.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 +81 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.js +81 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +342 -272
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/module-access.test.mjs +27 -0
- package/test/module-gate-kill-switch.test.mjs +48 -0
- package/test/notification-access.test.mjs +35 -6
- package/test/socket-session.test.mjs +119 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
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
|
+
|
|
56
|
+
## 3.67.0
|
|
57
|
+
|
|
58
|
+
### Minor Changes
|
|
59
|
+
|
|
60
|
+
- 7b9ea2f: Notifications: stop sending alerts for modules a client was never given
|
|
61
|
+
|
|
62
|
+
`NOTIFY_ACCESS_FILTER` now defaults to `on` instead of `log`, so the module-based
|
|
63
|
+
drop is real: a recipient no longer gets a notification when every membership they
|
|
64
|
+
hold at the site denies every module the notification's category is about.
|
|
65
|
+
|
|
66
|
+
Only the module drop acts. The role-based check is still computed and logged and
|
|
67
|
+
never drops anybody, because most apps do not enforce their role gates yet and a
|
|
68
|
+
role-based drop would hide alerts for screens people can still open.
|
|
69
|
+
|
|
70
|
+
Unchanged: residents, anyone with no membership at the site, Seven365 staff, sends
|
|
71
|
+
with no site, and categories the catalogue does not govern are all exempt; any
|
|
72
|
+
error sends to everyone; and `MODULE_LIST_GATE=off` still disables it entirely.
|
|
73
|
+
`NOTIFY_ACCESS_FILTER=log` or `off` overrides this default with no deploy.
|
|
74
|
+
|
|
75
|
+
This is inert until a client's module list is enforced — measured read-only on
|
|
76
|
+
staging at the time of the flip, no organisation (0 of 199) and no site (0 of 256)
|
|
77
|
+
had a stored list and there were no acknowledged-save markers, so nothing is
|
|
78
|
+
dropped for anybody until a Super Admin saves a module list with the acknowledged
|
|
79
|
+
preview.
|
|
80
|
+
|
|
3
81
|
## 3.66.0
|
|
4
82
|
|
|
5
83
|
### Minor Changes
|
|
@@ -62,7 +140,9 @@
|
|
|
62
140
|
the keys in `MODULE_GATE_ENFORCE_KEYS`. Work stamped (`at`) before the
|
|
63
141
|
acknowledged save is accepted.
|
|
64
142
|
- `MODULE_LIST_GATE` (code default `on`) switches all of the above off at
|
|
65
|
-
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.
|
|
66
146
|
|
|
67
147
|
### Patch Changes
|
|
68
148
|
|
package/dist/index.d.ts
CHANGED
|
@@ -14185,13 +14185,29 @@ declare function isPlatformOwner(userId?: string | ObjectId | null): Promise<boo
|
|
|
14185
14185
|
* with no site (marketplace, broadcasts); a category not in the catalogue or
|
|
14186
14186
|
* with no permission resources (resident-only and marketplace ones).
|
|
14187
14187
|
*
|
|
14188
|
-
* `NOTIFY_ACCESS_FILTER`: `
|
|
14189
|
-
*
|
|
14190
|
-
*
|
|
14188
|
+
* `NOTIFY_ACCESS_FILTER`: `on` (code default) DROPS; `log` counts, logs and
|
|
14189
|
+
* sends to everyone; `off` reads nothing. `MODULE_LIST_GATE=off` is the same as
|
|
14190
|
+
* `off`. Any error sends to everyone.
|
|
14191
|
+
*
|
|
14192
|
+
* The default was `log` from the day this shipped until the owner turned it on
|
|
14193
|
+
* (2026-09-13), having accepted that some staff will notice alerts they used to
|
|
14194
|
+
* get stopping. Only the MODULE drop became real; the role check above stays
|
|
14195
|
+
* logged only, because most apps still do not enforce their role gates.
|
|
14196
|
+
*
|
|
14197
|
+
* Turning it on changed nothing on its own: the filter returns early unless the
|
|
14198
|
+
* site or its organisation has an ENFORCED module list, and on the day of the
|
|
14199
|
+
* flip no organisation and no site had one (measured read-only on staging: 0 of
|
|
14200
|
+
* 199 organisations, 0 of 256 sites, 0 acknowledged-save markers). It takes
|
|
14201
|
+
* effect for a client the first time a Super Admin saves that client's module
|
|
14202
|
+
* list with the acknowledged preview.
|
|
14203
|
+
*
|
|
14204
|
+
* The env var still wins over this default, so a lead can put it back to `log`
|
|
14205
|
+
* or `off` without waiting for a deploy.
|
|
14191
14206
|
*
|
|
14192
14207
|
* Both senders call this: core's `notification.service.ts send()` here, and
|
|
14193
14208
|
* API-core's own `send()` (a separate class; memory
|
|
14194
|
-
* `two-notification-senders-not-one`), which
|
|
14209
|
+
* `two-notification-senders-not-one`), which imports this very function — so
|
|
14210
|
+
* this default governs both, and neither can drift from the other.
|
|
14195
14211
|
*/
|
|
14196
14212
|
type TNotifyAccessMode = "log" | "on" | "off";
|
|
14197
14213
|
declare const NOTIFY_ACCESS_FILTER_DEFAULT: TNotifyAccessMode;
|
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
|
|
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(
|
|
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 =
|
|
557
|
-
if (typeof console !== "undefined" &&
|
|
558
|
-
var method =
|
|
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"] =
|
|
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;
|
|
@@ -16549,7 +16551,7 @@ async function siteModulePreview(site, org, proposed, data = moduleGateData()) {
|
|
|
16549
16551
|
}
|
|
16550
16552
|
|
|
16551
16553
|
// src/utils/notification-access.util.ts
|
|
16552
|
-
var NOTIFY_ACCESS_FILTER_DEFAULT = "
|
|
16554
|
+
var NOTIFY_ACCESS_FILTER_DEFAULT = "on";
|
|
16553
16555
|
function notifyAccessMode(env2 = process.env) {
|
|
16554
16556
|
return gateMode(env2.NOTIFY_ACCESS_FILTER, ["log", "on", "off"], NOTIFY_ACCESS_FILTER_DEFAULT);
|
|
16555
16557
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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;
|