@7365admin1/core 3.84.0 → 3.86.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.84.0",
4
+ "version": "3.86.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -13,7 +13,7 @@
13
13
  "build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
14
14
  "release": "yarn run build && changeset publish",
15
15
  "lint": "tsc",
16
- "test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
16
+ "test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/png-encoder.util.ts src/services/demo.adapter.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
17
17
  "test:camera": "yarn test",
18
18
  "test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
19
19
  },
@@ -0,0 +1,164 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { createDemoAdapter, DEMO_CAPABILITIES } from "./.build/services/demo.adapter.mjs";
5
+ import { robotTier, commandSupported } from "./.build/utils/robot-adapter.util.mjs";
6
+ import { encodeGreyscalePng } from "./.build/utils/png-encoder.util.mjs";
7
+ import { wallsFromOccupancyGrid } from "./.build/utils/building-map.util.mjs";
8
+
9
+ /* --------------------------------------------------------------------------
10
+ * The demo brand has two jobs, and these guard both.
11
+ *
12
+ * It makes the work visible: until a vendor supplies credentials, every robot
13
+ * screen in the platform is empty and no button can be pressed.
14
+ *
15
+ * And it proves the vendor-neutral design. It is a second brand written against
16
+ * the same contract as Ounitech, declaring DIFFERENT capabilities. If the
17
+ * screens behave differently for the two without being told which is which,
18
+ * the platform really is brand-agnostic.
19
+ * ----------------------------------------------------------------------- */
20
+
21
+ const at = (ms) => createDemoAdapter({ now: () => ms });
22
+
23
+ test("the demo brand reaches Full Control, where Ounitech reaches none", () => {
24
+ // The contrast IS the demonstration: same contract, different capabilities,
25
+ // and the screens draw different controls without being told to.
26
+ assert.equal(robotTier(DEMO_CAPABILITIES), "full-control");
27
+
28
+ const ounitech = { readLocation: true, sendToDock: true, startProcess: true };
29
+ assert.equal(robotTier(ounitech), "no-interface");
30
+
31
+ // So the demo offers pause; Ounitech does not.
32
+ assert.equal(commandSupported(DEMO_CAPABILITIES, "pause"), true);
33
+ assert.equal(commandSupported(ounitech, "pause"), false);
34
+ });
35
+
36
+ test("it lists robots across more than one floor", () => {
37
+ return at(0).listRobots().then((robots) => {
38
+ assert.ok(robots.length >= 3);
39
+ assert.ok(new Set(robots.map((r) => r.floorNo)).size >= 2, "more than one floor");
40
+ });
41
+ });
42
+
43
+ test("every demo robot says it is a demo, in its own name", async () => {
44
+ // A simulated robot appearing in a real estate's list without saying so would
45
+ // be worse than no demo at all.
46
+ const robots = await at(0).listRobots();
47
+ for (const r of robots) {
48
+ assert.match(r.name, /demo/i, r.name);
49
+ assert.match(r.robotSn, /^DEMO-/, r.robotSn);
50
+ }
51
+ assert.match(at(0).displayName, /demo/i);
52
+ });
53
+
54
+ test("it reports a KNOWN floor and a real position — unlike Ounitech", async () => {
55
+ // Which is what lets the 3D layout actually draw the robot.
56
+ const [robot] = await at(0).listRobots();
57
+ assert.equal(robot.floorKnown, true);
58
+ assert.equal(typeof robot.floorNo, "number");
59
+ assert.ok(robot.position);
60
+ assert.equal(typeof robot.position.x, "number");
61
+ assert.equal(typeof robot.position.theta, "number");
62
+ });
63
+
64
+ test("the robots move as the clock advances", async () => {
65
+ const [early] = await at(0).listRobots();
66
+ const [later] = await at(30_000).listRobots();
67
+ assert.notDeepEqual(early.position, later.position, "a demo robot that never moves is a picture");
68
+ });
69
+
70
+ test("battery changes over time and stays in range", async () => {
71
+ const seen = new Set();
72
+ for (const ms of [0, 600_000, 1_800_000, 3_000_000]) {
73
+ const [robot] = await at(ms).listRobots();
74
+ assert.ok(robot.batteryPercent >= 0 && robot.batteryPercent <= 100, String(robot.batteryPercent));
75
+ seen.add(robot.batteryPercent);
76
+ }
77
+ assert.ok(seen.size > 1, "the battery never changed");
78
+ });
79
+
80
+ test("a command changes what the robot reports, and then wears off", async () => {
81
+ const adapter = at(1_000_000);
82
+ const before = await adapter.getRobot("DEMO-0001");
83
+
84
+ await adapter.sendCommand("DEMO-0001", { kind: "full" });
85
+ const during = await at(1_000_000).getRobot("DEMO-0001");
86
+ assert.equal(during.mode, "WORKING", `was ${before.mode}`);
87
+
88
+ // It returns to idle by itself, so the demo needs no resetting between
89
+ // viewings.
90
+ const after = await at(1_000_000 + 11 * 60_000).getRobot("DEMO-0001");
91
+ assert.notEqual(after.mode, "WORKING");
92
+
93
+ await adapter.sendCommand("DEMO-0001", { kind: "cancel" });
94
+ });
95
+
96
+ test("commanding a robot that does not exist is refused by name", async () => {
97
+ await assert.rejects(
98
+ () => at(0).sendCommand("NOT-A-ROBOT", { kind: "full" }),
99
+ /No demo robot with serial NOT-A-ROBOT/,
100
+ );
101
+ });
102
+
103
+ test("an unknown serial returns null rather than a stranger's robot", async () => {
104
+ assert.equal(await at(0).getRobot("DEMO-9999"), null);
105
+ });
106
+
107
+ /* --------------------------------------------------------- the floor plans */
108
+
109
+ test("the plans carry the scale and areas the map screens need", async () => {
110
+ const maps = await at(0).getMaps();
111
+ assert.ok(maps.length >= 2, "more than one level");
112
+ for (const m of maps) {
113
+ // 1px = 5cm, traffic_editor's default and Ounitech's own resolution.
114
+ assert.equal(m.resolution, 0.05);
115
+ assert.ok(m.areaList.length >= 1);
116
+ assert.ok(m.backgroundFileKey);
117
+ }
118
+ });
119
+
120
+ test("the generated plan is a real PNG a browser will accept", async () => {
121
+ const files = await at(0).getMapImages("DEMO-0001", ["demo/L1.png"]);
122
+ const url = files[0].url;
123
+ assert.match(url, /^data:image\/png;base64,/);
124
+
125
+ // The 8-byte PNG signature, so this is decodable rather than merely base64.
126
+ const bytes = Buffer.from(url.split(",")[1], "base64");
127
+ assert.deepEqual([...bytes.subarray(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
128
+ assert.equal(bytes.subarray(12, 16).toString("ascii"), "IHDR");
129
+ assert.equal(bytes.subarray(bytes.length - 8, bytes.length - 4).toString("ascii"), "IEND");
130
+ });
131
+
132
+ test("a data URI is used so the canvas is not tainted", async () => {
133
+ // The layout is built by reading these pixels BACK. A cross-origin image
134
+ // would throw on getImageData, which looks like a broken floor plan.
135
+ const files = await at(0).getMapImages("DEMO-0001", ["demo/L1.png", "demo/L2.png"]);
136
+ assert.equal(files.length, 2);
137
+ for (const f of files) assert.ok(f.url.startsWith("data:"));
138
+ });
139
+
140
+ test("the two levels are genuinely different plans", async () => {
141
+ const [l1, l2] = await at(0).getMapImages("DEMO-0001", ["demo/L1.png", "demo/L2.png"]);
142
+ assert.notEqual(l1.url, l2.url, "both floors rendered the same picture");
143
+ });
144
+
145
+ test("THE POINT: the generated plan yields a 3D layout", () => {
146
+ // End to end, with no vendor and no network: a plan becomes walls.
147
+ const width = 60, height = 40;
148
+ const px = new Uint8Array(width * height).fill(255);
149
+ for (let x = 0; x < width; x++) { px[x] = 0; px[(height - 1) * width + x] = 0; }
150
+ for (let y = 0; y < height; y++) { px[y * width] = 0; px[y * width + width - 1] = 0; }
151
+
152
+ const png = encodeGreyscalePng(px, width, height);
153
+ assert.ok(png.length > 0);
154
+
155
+ const { walls } = wallsFromOccupancyGrid({ data: px, width, height }, { minRunPx: 5 });
156
+ assert.equal(walls.length, 4, "a closed room is four walls");
157
+ });
158
+
159
+ /* -------------------------------------------------------- the PNG encoder */
160
+
161
+ test("the encoder refuses an impossible image rather than emitting a broken one", () => {
162
+ assert.throws(() => encodeGreyscalePng(new Uint8Array(4), 0, 4), /positive size/);
163
+ assert.throws(() => encodeGreyscalePng(new Uint8Array(2), 10, 10), /one byte per pixel/);
164
+ });
@@ -3,12 +3,13 @@ import { test } from "node:test";
3
3
 
4
4
  import {
5
5
  ROBOT_CAPABILITIES,
6
- clearRobotAdapters,
6
+ adapterForRobot,
7
+ clearRobotBrands,
7
8
  commandSupported,
8
9
  missingForTier,
9
- registerRobotAdapter,
10
+ registerRobotBrand,
10
11
  registeredRobotBrands,
11
- robotAdapterFor,
12
+ robotBrandFor,
12
13
  robotTier,
13
14
  supports,
14
15
  uncontrollableFleetWarning,
@@ -136,44 +137,94 @@ test("an unknown command kind is refused, not defaulted", () => {
136
137
 
137
138
  /* --------------------------------------------------------------- registry */
138
139
 
139
- test("an adapter is found by brand, and never by fallback", () => {
140
- clearRobotAdapters();
141
- const a = { brand: "brand-a", displayName: "Brand A", capabilities: ALL, listRobots: async () => [], getRobot: async () => null };
142
- registerRobotAdapter(a);
140
+ const brand = (over = {}) => ({
141
+ brand: "brand-a",
142
+ displayName: "Brand A",
143
+ capabilities: ALL,
144
+ create: () => ({ brand: "brand-a", displayName: "Brand A", capabilities: ALL,
145
+ listRobots: async () => [], getRobot: async () => null }),
146
+ ...over,
147
+ });
148
+
149
+ const robot = (over = {}) => ({
150
+ name: "Cleaner 02", brand: "brand-a", appKey: "k", appToken: "s", ...over,
151
+ });
143
152
 
144
- assert.equal(robotAdapterFor("brand-a"), a);
145
- // Falling back to the only registered adapter would send one brand's robot a
153
+ test("a brand is found by name, and never by fallback", () => {
154
+ clearRobotBrands();
155
+ registerRobotBrand(brand());
156
+
157
+ assert.equal(robotBrandFor("brand-a")?.brand, "brand-a");
158
+ // Falling back to the only registered brand would send one brand's robot a
146
159
  // command in a protocol it does not speak.
147
- assert.equal(robotAdapterFor("brand-b"), null);
148
- assert.equal(robotAdapterFor(""), null);
149
- assert.equal(robotAdapterFor(null), null);
150
- assert.equal(robotAdapterFor(undefined), null);
151
- assert.equal(robotAdapterFor(42), null);
160
+ assert.equal(robotBrandFor("brand-b"), null);
161
+ for (const bad of ["", null, undefined, 42]) {
162
+ assert.equal(robotBrandFor(bad), null, String(bad));
163
+ }
152
164
  });
153
165
 
154
- test("an adapter without a brand is refused at registration", () => {
155
- clearRobotAdapters();
156
- assert.throws(() => registerRobotAdapter({ displayName: "x" }), /must name its brand/);
157
- assert.throws(() => registerRobotAdapter(null), /must name its brand/);
166
+ test("a brand without a name or a factory is refused at registration", () => {
167
+ clearRobotBrands();
168
+ assert.throws(() => registerRobotBrand({ displayName: "x" }), /must name itself/);
169
+ assert.throws(() => registerRobotBrand(null), /must name itself/);
170
+ // A brand registered with no way to build an adapter would fail later, at the
171
+ // moment somebody tried to use a robot - not here, where it is fixable.
172
+ assert.throws(
173
+ () => registerRobotBrand({ brand: "x", displayName: "X", capabilities: {} }),
174
+ /no way to build an adapter/,
175
+ );
158
176
  });
159
177
 
160
- test("the brand list reports each brand's tier, for a settings picker", () => {
161
- clearRobotAdapters();
162
- registerRobotAdapter({ brand: "good", displayName: "Good", capabilities: ALL, listRobots: async () => [], getRobot: async () => null });
163
- registerRobotAdapter({ brand: "poor", displayName: "Poor", capabilities: caps("readLocation"), listRobots: async () => [], getRobot: async () => null });
178
+ test("an adapter is built per robot, from THAT robot's credentials", () => {
179
+ clearRobotBrands();
180
+ let seen = null;
181
+ registerRobotBrand(brand({ create: (c) => { seen = c; return { brand: "brand-a",
182
+ displayName: "Brand A", capabilities: ALL, listRobots: async () => [],
183
+ getRobot: async () => null }; } }));
164
184
 
165
- assert.deepEqual(registeredRobotBrands().sort((x, y) => x.brand.localeCompare(y.brand)), [
166
- { brand: "good", displayName: "Good", tier: "full-control" },
167
- { brand: "poor", displayName: "Poor", tier: "no-interface" },
168
- ]);
185
+ adapterForRobot(robot({ appKey: "estate-1-id", appToken: "estate-1-secret" }));
186
+ assert.deepEqual(seen, { appid: "estate-1-id", secret: "estate-1-secret" });
187
+
188
+ // One estate's credentials must never sign another estate's request, which is
189
+ // why the adapter is constructed per robot rather than per brand.
190
+ adapterForRobot(robot({ appKey: "estate-2-id", appToken: "estate-2-secret" }));
191
+ assert.deepEqual(seen, { appid: "estate-2-id", secret: "estate-2-secret" });
169
192
  });
170
193
 
171
- test("re-registering a brand replaces it rather than duplicating", () => {
172
- clearRobotAdapters();
173
- const base = { brand: "x", displayName: "One", capabilities: {}, listRobots: async () => [], getRobot: async () => null };
174
- registerRobotAdapter(base);
175
- registerRobotAdapter({ ...base, displayName: "Two" });
194
+ test("an unknown brand is refused BY NAME, not with a null dereference", () => {
195
+ clearRobotBrands();
196
+ registerRobotBrand(brand());
197
+ assert.throws(() => adapterForRobot(robot({ brand: "unregistered" })),
198
+ /No robot adapter is registered for the brand "unregistered"/);
199
+ assert.throws(() => adapterForRobot(robot({ brand: "" })),
200
+ /does not say which brand it is/);
201
+ });
176
202
 
203
+ test("a robot with no credentials is refused before any call is attempted", () => {
204
+ clearRobotBrands();
205
+ registerRobotBrand(brand());
206
+ assert.throws(() => adapterForRobot(robot({ appKey: "" })), /no cloud credentials/);
207
+ assert.throws(() => adapterForRobot(robot({ appToken: " " })), /no cloud credentials/);
208
+ });
209
+
210
+ test("the brand list reports each brand's tier, for a settings picker", () => {
211
+ clearRobotBrands();
212
+ registerRobotBrand(brand({ brand: "good", displayName: "Good", capabilities: ALL }));
213
+ registerRobotBrand(brand({ brand: "poor", displayName: "Poor", capabilities: caps("readLocation") }));
214
+
215
+ assert.deepEqual(
216
+ registeredRobotBrands().sort((x, y) => x.brand.localeCompare(y.brand)),
217
+ [
218
+ { brand: "good", displayName: "Good", tier: "full-control" },
219
+ { brand: "poor", displayName: "Poor", tier: "no-interface" },
220
+ ],
221
+ );
222
+ });
223
+
224
+ test("re-registering a brand replaces it rather than duplicating", () => {
225
+ clearRobotBrands();
226
+ registerRobotBrand(brand({ displayName: "One" }));
227
+ registerRobotBrand(brand({ displayName: "Two" }));
177
228
  assert.equal(registeredRobotBrands().length, 1);
178
- assert.equal(robotAdapterFor("x").displayName, "Two");
229
+ assert.equal(robotBrandFor("brand-a").displayName, "Two");
179
230
  });