rhombus-node-mcp 0.1.55 → 0.1.57

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.
Files changed (108) hide show
  1. package/dist/api/access-control-tool-api.js +231 -32
  2. package/dist/api/alarm-monitoring-tool-api.js +50 -1
  3. package/dist/api/automated-prompts-tool-api.js +9 -17
  4. package/dist/api/camera-policy-tool-api.js +122 -0
  5. package/dist/api/camera-tool-api.js +3 -5
  6. package/dist/api/camera-uptime-tool-api.js +62 -25
  7. package/dist/api/clips-tool-api.js +45 -9
  8. package/dist/api/create-tool-api.js +55 -7
  9. package/dist/api/door-tool-api.js +74 -13
  10. package/dist/api/entity-lookup-tool-api.js +8 -1
  11. package/dist/api/events-tool-api.js +79 -6
  12. package/dist/api/faces-tool-api.js +112 -7
  13. package/dist/api/get-accessible-apps.js +68 -6
  14. package/dist/api/get-entity-tool-api.js +12 -12
  15. package/dist/api/get-org-information-tool-api.js +2 -2
  16. package/dist/api/guest-management-tool-api.js +75 -1
  17. package/dist/api/location-tool-api.js +120 -28
  18. package/dist/api/lpr-tool-api.js +71 -16
  19. package/dist/api/policy-alerts-tool-api.js +7 -3
  20. package/dist/api/report-tool-api.js +4 -1
  21. package/dist/api/rules-tool-api.js +6 -16
  22. package/dist/api/schedule-tool-api.js +107 -0
  23. package/dist/api/search-tool-api.js +5 -13
  24. package/dist/api/update-tool-api.js +96 -1
  25. package/dist/api/user-tool-api.js +58 -13
  26. package/dist/createServer.js +18 -6
  27. package/dist/filtering-utils.js +421 -29
  28. package/dist/index.js +1 -1
  29. package/dist/network/network.js +122 -10
  30. package/dist/network/org-reference-cache.js +74 -0
  31. package/dist/telemetry/tracingProxy.js +4 -0
  32. package/dist/tools/entity-lookup-tool.js +8 -4
  33. package/dist/tools/get-entity-tool.js +46 -21
  34. package/dist/tools/get-org-information-tool.js +36 -3
  35. package/dist/tools/time-conversion-tool.js +65 -37
  36. package/dist/tools/time-tool.js +18 -4
  37. package/dist/tools/user-tool.js +101 -1
  38. package/dist/tools-console/access-anomaly-tool.js +4 -19
  39. package/dist/tools-console/access-control-tool.js +330 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +73 -1
  41. package/dist/tools-console/analytics-tool.js +4 -2
  42. package/dist/tools-console/automated-prompts-tool.js +2 -0
  43. package/dist/tools-console/badge-timeline-tool.js +4 -21
  44. package/dist/tools-console/camera-policy-tool.js +252 -0
  45. package/dist/tools-console/camera-tool.js +113 -89
  46. package/dist/tools-console/camera-uptime-tool.js +2 -0
  47. package/dist/tools-console/clips-tool.js +23 -31
  48. package/dist/tools-console/count-tool.js +19 -6
  49. package/dist/tools-console/create-camera-policy-tool.js +304 -122
  50. package/dist/tools-console/door-schedule-exception-tool.js +4 -33
  51. package/dist/tools-console/door-tool.js +131 -1
  52. package/dist/tools-console/elements-access-anomaly-tool.js +4 -19
  53. package/dist/tools-console/elements-badge-timeline-tool.js +4 -21
  54. package/dist/tools-console/elements-lost-badge-tool.js +4 -17
  55. package/dist/tools-console/elements-tool.js +4 -29
  56. package/dist/tools-console/events-tool.js +24 -101
  57. package/dist/tools-console/faces-tool.js +255 -27
  58. package/dist/tools-console/guest-management-tool.js +128 -1
  59. package/dist/tools-console/location-tool.js +161 -35
  60. package/dist/tools-console/lost-badge-tool.js +4 -17
  61. package/dist/tools-console/lpr-tool.js +90 -22
  62. package/dist/tools-console/netbox-access-anomaly-tool.js +4 -19
  63. package/dist/tools-console/netbox-badge-timeline-tool.js +4 -21
  64. package/dist/tools-console/netbox-lost-badge-tool.js +4 -17
  65. package/dist/tools-console/netbox-tool.js +4 -29
  66. package/dist/tools-console/onguard-tool.js +4 -30
  67. package/dist/tools-console/person-tracking-tool.js +4 -2
  68. package/dist/tools-console/policy-alerts-tool.js +6 -28
  69. package/dist/tools-console/reboot-cameras-tool.js +23 -3
  70. package/dist/tools-console/report-tool.js +12 -40
  71. package/dist/tools-console/rules-tool.js +2 -0
  72. package/dist/tools-console/schedule-tool.js +190 -0
  73. package/dist/tools-console/search-tool.js +2 -0
  74. package/dist/tools-console/update-tool.js +300 -168
  75. package/dist/tools-console/user-access-trail-tool.js +2 -0
  76. package/dist/tools-console/user-audit-tool.js +2 -0
  77. package/dist/tools-console/video-walls-tool.js +94 -20
  78. package/dist/types/access-anomaly-tool-types.js +22 -2
  79. package/dist/types/access-control-tool-types.js +116 -1
  80. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  81. package/dist/types/badge-timeline-tool-types.js +18 -3
  82. package/dist/types/camera-policy-tool-types.js +104 -0
  83. package/dist/types/camera-tool-types.js +80 -1
  84. package/dist/types/clips-tool-types.js +27 -3
  85. package/dist/types/create-camera-policy-tool-types.js +275 -26
  86. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  87. package/dist/types/door-tool-types.js +36 -4
  88. package/dist/types/entity-lookup-tool-types.js +6 -1
  89. package/dist/types/events-tools-types.js +24 -16
  90. package/dist/types/faces-tools-types.js +106 -3
  91. package/dist/types/get-entity-tool-types.js +76 -12
  92. package/dist/types/guest-management-tool-types.js +103 -1
  93. package/dist/types/location-tool-types.js +100 -14
  94. package/dist/types/lost-badge-tool-types.js +17 -2
  95. package/dist/types/lpr-tool-types.js +47 -8
  96. package/dist/types/onguard-tool-types.js +21 -1
  97. package/dist/types/policy-alerts-tool-types.js +1 -1
  98. package/dist/types/report-tool-types.js +29 -1
  99. package/dist/types/schedule-tool-types.js +370 -0
  100. package/dist/types/schema.js +479 -9
  101. package/dist/types/time-conversion-tool-types.js +3 -4
  102. package/dist/types/update-tool-types.js +40 -18
  103. package/dist/types/user-tool-types.js +44 -1
  104. package/dist/types/video-walls-tool-types.js +30 -2
  105. package/dist/types/zod-schemas.js +5128 -996
  106. package/dist/util.js +62 -3
  107. package/dist/utils/entity-name-match.js +11 -0
  108. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { apiWarning, postApi, throwIfApiError } from "../network/network.js";
2
2
  import { formatTimestamp } from "../util.js";
3
3
  import { removeNulls } from "../utils/remove-nulls.js";
4
4
  export async function getFaceEvents(args, timeZone, requestModifiers, sessionId) {
@@ -78,8 +78,7 @@ export async function searchSimilarFaces(faceEventUuid, timeZone, requestModifie
78
78
  modifiers: requestModifiers,
79
79
  sessionId,
80
80
  });
81
- if (res.error)
82
- throw new Error(JSON.stringify(res));
81
+ throwIfApiError(res);
83
82
  return (res.faceEvents || []).map((event) => ({
84
83
  uuid: event.uuid ?? undefined,
85
84
  deviceUuid: event.deviceUuid ?? undefined,
@@ -98,8 +97,7 @@ export async function getFaceMatchmakers(requestModifiers, sessionId) {
98
97
  modifiers: requestModifiers,
99
98
  sessionId,
100
99
  });
101
- if (res.error)
102
- throw new Error(JSON.stringify(res));
100
+ throwIfApiError(res);
103
101
  return (res.faceMatchmakers || []).map((m) => ({
104
102
  uuid: m.uuid ?? undefined,
105
103
  personUuid: m.personUuid ?? undefined,
@@ -122,8 +120,7 @@ export async function getFaceEventsByPerson(personUuid, timeZone, requestModifie
122
120
  modifiers: requestModifiers,
123
121
  sessionId,
124
122
  });
125
- if (res.error)
126
- throw new Error(JSON.stringify(res));
123
+ throwIfApiError(res);
127
124
  const faceEvents = (res.faceEvents || []).map((event) => ({
128
125
  uuid: event.uuid ?? undefined,
129
126
  personUuid: event.personUuid ?? undefined,
@@ -137,3 +134,111 @@ export async function getFaceEventsByPerson(personUuid, timeZone, requestModifie
137
134
  lastEvaluatedKey: res.lastEvaluatedKey,
138
135
  };
139
136
  }
137
+ // ---------------------------------------------------------------------------
138
+ // Person write paths
139
+ // ---------------------------------------------------------------------------
140
+ export async function findPerson(personUuid, requestModifiers, sessionId) {
141
+ const res = await postApi({
142
+ route: "/faceRecognition/person/findPeopleByOrg",
143
+ body: {},
144
+ modifiers: requestModifiers,
145
+ sessionId,
146
+ });
147
+ throwIfApiError(res);
148
+ return res.people?.find(person => person?.uuid === personUuid) ?? undefined;
149
+ }
150
+ export async function createPerson(name, requestModifiers, sessionId) {
151
+ const res = await postApi({
152
+ route: "/faceRecognition/person/createPerson",
153
+ body: { name },
154
+ modifiers: requestModifiers,
155
+ sessionId,
156
+ });
157
+ throwIfApiError(res);
158
+ return {
159
+ success: true,
160
+ uuid: res.person?.uuid ?? undefined,
161
+ warningMsg: apiWarning(res),
162
+ };
163
+ }
164
+ /**
165
+ * Selective person update.
166
+ *
167
+ * api2 marks which fields were deliberately set via `updatedSetMethodMap`
168
+ * rather than inferring it from presence — so a field omitted from that map is
169
+ * left alone, and a field IN the map with no value is cleared. Only the fields
170
+ * the caller actually changed are flagged.
171
+ *
172
+ * The keys are SETTER names (`setName`, `setEmail`), NOT field names. The spec
173
+ * declares the map as an open `{string: boolean}`, so nothing enforces this.
174
+ * Sending `{name: true}` returns `error: false` and bumps the record's
175
+ * `updatedOn` while leaving the name unchanged — a silent no-op that reports
176
+ * success. Verified against api2.itg on 2026-08-05; pinned by
177
+ * `test/api/faces-tool-api.test.ts`.
178
+ */
179
+ export async function updatePerson(personUuid, changes, requestModifiers, sessionId) {
180
+ const updatedSetMethodMap = {};
181
+ if (changes.name !== undefined)
182
+ updatedSetMethodMap.setName = true;
183
+ if (changes.email !== undefined)
184
+ updatedSetMethodMap.setEmail = true;
185
+ const res = await postApi({
186
+ route: "/faceRecognition/person/updatePerson",
187
+ body: {
188
+ personSelectiveUpdate: {
189
+ uuid: personUuid,
190
+ name: changes.name,
191
+ email: changes.email,
192
+ updatedSetMethodMap,
193
+ },
194
+ },
195
+ modifiers: requestModifiers,
196
+ sessionId,
197
+ });
198
+ throwIfApiError(res);
199
+ return { success: true, uuid: personUuid, warningMsg: apiWarning(res) };
200
+ }
201
+ export async function deletePerson(personUuid, requestModifiers, sessionId) {
202
+ const res = await postApi({
203
+ route: "/faceRecognition/person/deletePerson",
204
+ body: {
205
+ personUuid,
206
+ },
207
+ modifiers: requestModifiers,
208
+ sessionId,
209
+ });
210
+ throwIfApiError(res);
211
+ return { success: true, uuid: personUuid, warningMsg: apiWarning(res) };
212
+ }
213
+ export async function changePersonLabel(personUuid, label, action, requestModifiers, sessionId) {
214
+ const res = await postApi({
215
+ route: action === "add"
216
+ ? "/faceRecognition/person/addPersonLabel"
217
+ : "/faceRecognition/person/removePersonLabel",
218
+ body: {
219
+ personUuid,
220
+ label,
221
+ },
222
+ modifiers: requestModifiers,
223
+ sessionId,
224
+ });
225
+ throwIfApiError(res);
226
+ return { success: true, uuid: personUuid, label, warningMsg: apiWarning(res) };
227
+ }
228
+ /**
229
+ * Delete one enrolled face image. Keyed on `faceId`, NOT on a person uuid — a
230
+ * person can have several matchmakers, and deleting one only removes that
231
+ * enrolled image, leaving the person and their other faces intact.
232
+ */
233
+ export async function deleteFaceMatchmaker(faceId, requestModifiers, sessionId) {
234
+ const res = await postApi({
235
+ route: "/faceRecognition/matchmaker/deleteFaceMatchmaker",
236
+ body: {
237
+ faceId,
238
+ },
239
+ modifiers: requestModifiers,
240
+ sessionId,
241
+ });
242
+ throwIfApiError(res);
243
+ return { success: true, faceId, warningMsg: apiWarning(res) };
244
+ }
@@ -1,5 +1,11 @@
1
1
  import { logger } from "../logger.js";
2
2
  import { postApi } from "../network/network.js";
3
+ import { RhombusAppEnum } from "../types/schema.js";
4
+ /**
5
+ * `enduser.id` for a support session whose payload carries no `supportSession`
6
+ * block (webservice older than the additive `sessionType`/`supportSession` fields).
7
+ */
8
+ export const SUPPORT_SESSION_MARKER = "support-session";
3
9
  /**
4
10
  * Single cached `getCurrentUser` fetch per session. Both `resolveAccessibleApps`
5
11
  * and `resolveSessionIdentity` read from this cache, so identity for tracing
@@ -7,6 +13,38 @@ import { postApi } from "../network/network.js";
7
13
  * the session; failures are NOT cached so transient errors don't poison it.
8
14
  */
9
15
  const cache = new Map();
16
+ function optionalString(value) {
17
+ return typeof value === "string" && value.length > 0 ? value : undefined;
18
+ }
19
+ /**
20
+ * `sessionType` and `supportSession` are ADDITIVE fields on
21
+ * GetCurrentUserWSResponse (webservice, SUPPORT_AUTHORITY_MIND C.4). They are
22
+ * not in the generated client type until the openapi is regenerated, so they
23
+ * are read off the raw payload and tolerated when absent.
24
+ */
25
+ function readSupportFields(res) {
26
+ if (!res || typeof res !== "object")
27
+ return {};
28
+ const raw = res;
29
+ const sessionType = optionalString(raw.sessionType);
30
+ const ss = raw.supportSession;
31
+ if (!ss || typeof ss !== "object")
32
+ return { sessionType };
33
+ const s = ss;
34
+ return {
35
+ sessionType,
36
+ supportSession: {
37
+ authorityUuid: optionalString(s.authorityUuid),
38
+ name: optionalString(s.name),
39
+ email: optionalString(s.email),
40
+ orgUuid: optionalString(s.orgUuid),
41
+ },
42
+ };
43
+ }
44
+ function describeIdentity(identity) {
45
+ const who = [identity.name, identity.email].filter(Boolean).join(" ");
46
+ return `${who ? `${who} ` : ""}(${identity.userId ?? "no id"})`;
47
+ }
10
48
  async function fetchSession(sessionId) {
11
49
  const cached = cache.get(sessionId);
12
50
  if (cached !== undefined)
@@ -22,11 +60,31 @@ async function fetchSession(sessionId) {
22
60
  return null;
23
61
  }
24
62
  const user = res.user;
25
- const apps = (user?.accessibleRhombusApps ?? []).filter((a) => a !== null && a !== undefined);
63
+ const { sessionType, supportSession } = readSupportFields(res);
64
+ // A successful getCurrentUser with no `user` is a support-authority session
65
+ // (the webservice never populates `user` for a SupportUserPrincipal). Such a
66
+ // session acts inside the customer's console, so it gets exactly the
67
+ // CONSOLE tool set — never the permissive console+partner union that an
68
+ // empty accessibleRhombusApps list used to fall into.
69
+ if (sessionType === "SUPPORT" || user === null || user === undefined) {
70
+ const identity = {
71
+ userId: supportSession?.authorityUuid ?? SUPPORT_SESSION_MARKER,
72
+ orgUuid: supportSession?.orgUuid,
73
+ email: supportSession?.email,
74
+ name: supportSession?.name,
75
+ sessionType: "SUPPORT",
76
+ };
77
+ const entry = { apps: [RhombusAppEnum.CONSOLE], identity };
78
+ cache.set(sessionId, entry);
79
+ logger.info(`resolveAccessibleApps: session ${sessionId} -> support session ${describeIdentity(identity)}; tool set=CONSOLE`);
80
+ return entry;
81
+ }
82
+ const apps = (user.accessibleRhombusApps ?? []).filter((a) => a !== null && a !== undefined);
26
83
  const identity = {
27
- userId: user?.uuid ?? user?.rhombusUserUuid ?? undefined,
28
- orgUuid: user?.orgUuid ?? undefined,
29
- email: user?.email ?? undefined,
84
+ userId: user.uuid ?? user.rhombusUserUuid ?? undefined,
85
+ orgUuid: user.orgUuid ?? undefined,
86
+ email: user.email ?? undefined,
87
+ sessionType: "USER",
30
88
  };
31
89
  const entry = { apps, identity };
32
90
  cache.set(sessionId, entry);
@@ -40,6 +98,7 @@ async function fetchSession(sessionId) {
40
98
  }
41
99
  /**
42
100
  * Fetches `user.accessibleRhombusApps` from getCurrentUser for the given session.
101
+ * Support-authority sessions resolve to exactly `[CONSOLE]`.
43
102
  * Returns null on error or missing session; callers should fall back to a
44
103
  * permissive default.
45
104
  */
@@ -49,8 +108,11 @@ export async function resolveAccessibleApps(sessionId) {
49
108
  return (await fetchSession(sessionId))?.apps ?? null;
50
109
  }
51
110
  /**
52
- * Fetches identity (user uuid, org uuid, email) from getCurrentUser for the
53
- * given session, reusing the same cached response as `resolveAccessibleApps`.
111
+ * Fetches identity (user uuid, org uuid, email, session type) from
112
+ * getCurrentUser for the given session, reusing the same cached response as
113
+ * `resolveAccessibleApps`. For support sessions the identity comes from
114
+ * `supportSession` (authorityUuid / name / email) when the webservice sends it,
115
+ * else `userId` is the fixed {@link SUPPORT_SESSION_MARKER}.
54
116
  * Returns null on error or missing session.
55
117
  */
56
118
  export async function resolveSessionIdentity(sessionId) {
@@ -1,9 +1,9 @@
1
1
  import { logger } from "../logger.js";
2
- import { postApi } from "../network/network.js";
2
+ import { cachedPostApi } from "../network/org-reference-cache.js";
3
3
  import { formatTimestamp } from "../util.js";
4
4
  import { tempFunc } from "../utils/temp.js";
5
5
  export async function getCameraList(requestModifiers, sessionId) {
6
- const body = await postApi({
6
+ const body = await cachedPostApi({
7
7
  route: "/camera/getMinimalCameraStateList",
8
8
  body: {},
9
9
  modifiers: requestModifiers,
@@ -14,7 +14,7 @@ export async function getCameraList(requestModifiers, sessionId) {
14
14
  };
15
15
  }
16
16
  export async function getDoorbellCameras(requestModifiers, sessionId) {
17
- const body = await postApi({
17
+ const body = await cachedPostApi({
18
18
  route: "/doorbellcamera/getMinimalStateList",
19
19
  body: {},
20
20
  modifiers: requestModifiers,
@@ -25,7 +25,7 @@ export async function getDoorbellCameras(requestModifiers, sessionId) {
25
25
  };
26
26
  }
27
27
  export async function getBadgeReaders(requestModifiers, sessionId) {
28
- return await postApi({
28
+ return await cachedPostApi({
29
29
  route: "/badgereader/getMinimalStateList",
30
30
  body: {},
31
31
  modifiers: requestModifiers,
@@ -37,7 +37,7 @@ export async function getBadgeReaders(requestModifiers, sessionId) {
37
37
  });
38
38
  }
39
39
  export async function getAccessControlledDoors(requestModifiers, sessionId) {
40
- return await postApi({
40
+ return await cachedPostApi({
41
41
  route: "/component/findAccessControlledDoors",
42
42
  body: {},
43
43
  modifiers: requestModifiers,
@@ -60,7 +60,7 @@ export async function getAccessControlledDoors(requestModifiers, sessionId) {
60
60
  });
61
61
  }
62
62
  export async function getAudioGateways(timeZone, requestModifiers, sessionId) {
63
- return await postApi({
63
+ return await cachedPostApi({
64
64
  route: "/audiogateway/getMinimalAudioGatewayStateList",
65
65
  body: {},
66
66
  modifiers: requestModifiers,
@@ -79,7 +79,7 @@ export async function getAudioGateways(timeZone, requestModifiers, sessionId) {
79
79
  });
80
80
  }
81
81
  export async function getDoorSensors(requestModifiers, sessionId) {
82
- return await postApi({
82
+ return await cachedPostApi({
83
83
  route: "/door/getMinimalDoorStateList",
84
84
  body: {},
85
85
  modifiers: requestModifiers,
@@ -91,7 +91,7 @@ export async function getDoorSensors(requestModifiers, sessionId) {
91
91
  });
92
92
  }
93
93
  export async function getEnvironmentalSensors(timeZone, tempUnit, requestModifiers, sessionId) {
94
- return await postApi({
94
+ return await cachedPostApi({
95
95
  route: "/climate/getMinimalClimateStateList",
96
96
  body: {},
97
97
  modifiers: requestModifiers,
@@ -115,7 +115,7 @@ export async function getEnvironmentalSensors(timeZone, tempUnit, requestModifie
115
115
  });
116
116
  }
117
117
  export async function getMotionSensors(requestModifiers, sessionId) {
118
- return await postApi({
118
+ return await cachedPostApi({
119
119
  route: "/occupancy/getMinimalOccupancySensorStateList",
120
120
  body: {},
121
121
  modifiers: requestModifiers,
@@ -127,7 +127,7 @@ export async function getMotionSensors(requestModifiers, sessionId) {
127
127
  });
128
128
  }
129
129
  export async function getButtons(timeZone, requestModifiers, sessionId) {
130
- return await postApi({
130
+ return await cachedPostApi({
131
131
  route: "/button/getMinimalButtonStateList",
132
132
  body: {},
133
133
  modifiers: requestModifiers,
@@ -146,7 +146,7 @@ export async function getButtons(timeZone, requestModifiers, sessionId) {
146
146
  });
147
147
  }
148
148
  export async function getKeypads(requestModifiers, sessionId) {
149
- return await postApi({
149
+ return await cachedPostApi({
150
150
  route: "/keypad/getKeypadsForOrg",
151
151
  body: {},
152
152
  modifiers: requestModifiers,
@@ -158,7 +158,7 @@ export async function getKeypads(requestModifiers, sessionId) {
158
158
  });
159
159
  }
160
160
  export async function getEnvironmentalGateways(timeZone, requestModifiers, sessionId) {
161
- return await postApi({
161
+ return await cachedPostApi({
162
162
  route: "/climate/getMinimalEnvironmentalGatewayStates",
163
163
  body: {},
164
164
  modifiers: requestModifiers,
@@ -1,6 +1,6 @@
1
- import { postApi } from "../network/network.js";
1
+ import { cachedPostApi } from "../network/org-reference-cache.js";
2
2
  export async function getOrg(requestModifiers, sessionId) {
3
- return await postApi({
3
+ return await cachedPostApi({
4
4
  route: "/org/getOrgV2",
5
5
  body: {},
6
6
  modifiers: requestModifiers,
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { apiWarning, postApi, throwIfApiError } from "../network/network.js";
2
2
  export async function getAllGuestsByOrg(requestModifiers, sessionId) {
3
3
  const res = await postApi({
4
4
  route: "/guestmanagement/getAllGuestsByOrg",
@@ -73,3 +73,77 @@ export async function getActivitiesForLocation(locationUuid, startTimeMs, endTim
73
73
  timestampMs: a.timestampMs ?? undefined,
74
74
  })) ?? []);
75
75
  }
76
+ // ---------------------------------------------------------------------------
77
+ // Guest write paths
78
+ // ---------------------------------------------------------------------------
79
+ export async function createGuest(guest, requestModifiers, sessionId) {
80
+ const res = await postApi({
81
+ route: "/guestmanagement/createGuest",
82
+ body: { guest },
83
+ modifiers: requestModifiers,
84
+ sessionId,
85
+ });
86
+ throwIfApiError(res);
87
+ return { success: true, email: guest.email, warningMsg: apiWarning(res) };
88
+ }
89
+ /** Guests are addressed by EMAIL, not a uuid — there is no delete-by-uuid. */
90
+ export async function deleteGuest(email, requestModifiers, sessionId) {
91
+ const res = await postApi({
92
+ route: "/guestmanagement/deleteGuest",
93
+ body: { email },
94
+ modifiers: requestModifiers,
95
+ sessionId,
96
+ });
97
+ throwIfApiError(res);
98
+ return { success: true, email, warningMsg: apiWarning(res) };
99
+ }
100
+ export async function getGuestPasses(range, requestModifiers, sessionId) {
101
+ const res = await postApi({
102
+ route: "/accesscontrol/guestpass/findGuestPasses",
103
+ body: {
104
+ startAfterMs: range.startAfterMs,
105
+ startBeforeMs: range.startBeforeMs,
106
+ },
107
+ modifiers: requestModifiers,
108
+ sessionId,
109
+ });
110
+ throwIfApiError(res);
111
+ return {
112
+ guestPasses: res.guestPasses?.flatMap(pass => pass
113
+ ? [
114
+ {
115
+ uuid: pass.uuid ?? undefined,
116
+ locationUuid: pass.locationUuid ?? undefined,
117
+ lifecycle: pass.lifecycle ?? undefined,
118
+ note: pass.note ?? undefined,
119
+ passStartTimeMs: pass.passStartTimeMs ?? undefined,
120
+ principalType: pass.principalType ?? undefined,
121
+ },
122
+ ]
123
+ : []) ?? [],
124
+ lastEvaluatedKey: res.lastEvaluatedKey ?? undefined,
125
+ };
126
+ }
127
+ /**
128
+ * Guest pass state changes.
129
+ *
130
+ * They are not interchangeable: suspend is reversible via unsuspend, expire ends
131
+ * the pass but keeps its record and history, and delete removes the record
132
+ * outright.
133
+ */
134
+ export async function changeGuestPassState(guestPassUuid, action, requestModifiers, sessionId) {
135
+ const routes = {
136
+ expire: "/accesscontrol/guestpass/expireGuestPass",
137
+ suspend: "/accesscontrol/guestpass/suspendGuestPass",
138
+ unsuspend: "/accesscontrol/guestpass/unsuspendGuestPass",
139
+ delete: "/accesscontrol/guestpass/deleteGuestPass",
140
+ };
141
+ const res = await postApi({
142
+ route: routes[action],
143
+ body: { guestPassUuid },
144
+ modifiers: requestModifiers,
145
+ sessionId,
146
+ });
147
+ throwIfApiError(res);
148
+ return { success: true, uuid: guestPassUuid, action, warningMsg: apiWarning(res) };
149
+ }
@@ -1,18 +1,43 @@
1
- import { postApi } from "../network/network.js";
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
+ import { cachedPostApi, clearOrgReferenceCache } from "../network/org-reference-cache.js";
3
+ function mapLocation(raw) {
4
+ const addressParts = [raw.address1, raw.address2, raw.postalCode, raw.countryCode]
5
+ .map(part => part?.trim())
6
+ .filter((part) => !!part);
7
+ return {
8
+ uuid: raw.uuid ?? undefined,
9
+ name: raw.name ?? undefined,
10
+ address1: raw.address1 ?? undefined,
11
+ address2: raw.address2 ?? undefined,
12
+ postalCode: raw.postalCode ?? undefined,
13
+ countryCode: raw.countryCode ?? undefined,
14
+ // Pre-joined so callers never have to reassemble it (and so the model
15
+ // doesn't invent a format).
16
+ address: addressParts.length > 0 ? addressParts.join(", ") : undefined,
17
+ timezone: raw.tz ?? undefined,
18
+ labels: raw.labels?.filter((label) => !!label) ?? [],
19
+ latitude: raw.latitude ?? undefined,
20
+ longitude: raw.longitude ?? undefined,
21
+ policyUuid: raw.policyUuid ?? undefined,
22
+ };
23
+ }
2
24
  export async function getLocations(requestModifiers, sessionId) {
3
- const res = await postApi({
25
+ const res = await cachedPostApi({
4
26
  route: "/location/getLocationsV2",
5
27
  body: {},
6
28
  modifiers: requestModifiers,
7
29
  sessionId,
8
30
  });
31
+ throwIfApiError(res);
9
32
  return {
10
- locations: (res.locations || []).map((loc) => ({
11
- uuid: loc.uuid ?? undefined,
12
- name: loc.name ?? undefined,
13
- })),
33
+ locations: res.locations?.flatMap(raw => (raw ? [mapLocation(raw)] : [])) ?? [],
34
+ warningMsg: res.warningMsg ?? undefined,
14
35
  };
15
36
  }
37
+ export async function findLocation(locationUuid, requestModifiers, sessionId) {
38
+ const { locations } = await getLocations(requestModifiers, sessionId);
39
+ return locations.find(location => location.uuid === locationUuid);
40
+ }
16
41
  export async function createLocation(name, address, requestModifiers, sessionId) {
17
42
  const res = await postApi({
18
43
  route: "/location/createLocation",
@@ -20,37 +45,104 @@ export async function createLocation(name, address, requestModifiers, sessionId)
20
45
  modifiers: requestModifiers,
21
46
  sessionId,
22
47
  });
23
- if (res.error)
24
- throw new Error(JSON.stringify(res));
25
- return { uuid: res.locationUuid ?? res.uuid ?? undefined, success: true };
26
- }
27
- export async function updateLocation(locationUuid, name, address, requestModifiers, sessionId) {
28
- const body = { locationUuid };
29
- if (name)
30
- body.name = name;
31
- if (address)
32
- body.address = address;
48
+ throwIfApiError(res);
49
+ // The org-wide location list is cached for up to 60s; without this, a read
50
+ // straight after the write still reports the old list.
51
+ clearOrgReferenceCache();
52
+ return {
53
+ uuid: res.locationUuid ?? res.uuid ?? undefined,
54
+ success: true,
55
+ warningMsg: res.warningMsg ?? undefined,
56
+ };
57
+ }
58
+ /**
59
+ * `selectiveUpdateLocation` leaves out-of-body fields alone, unlike
60
+ * `updateLocation` which replaces the record — so a rename can't blank the
61
+ * address, and setting a timezone can't blank the labels.
62
+ */
63
+ export async function updateLocation(update, requestModifiers, sessionId) {
33
64
  const res = await postApi({
34
- route: "/location/updateLocation",
35
- body,
65
+ route: "/location/selectiveUpdateLocation",
66
+ body: update,
36
67
  modifiers: requestModifiers,
37
68
  sessionId,
38
69
  });
39
- if (res.error)
40
- throw new Error(JSON.stringify(res));
41
- return { success: true };
70
+ throwIfApiError(res);
71
+ clearOrgReferenceCache();
72
+ return {
73
+ updated: { success: true, locationUuid: update.locationUuid },
74
+ warningMsg: res.warningMsg ?? undefined,
75
+ };
42
76
  }
43
- export async function getLocationLabels(requestModifiers, sessionId) {
77
+ export async function deleteLocation(locationUuid, requestModifiers, sessionId) {
44
78
  const res = await postApi({
79
+ route: "/location/deleteLocation",
80
+ body: { uuid: locationUuid },
81
+ modifiers: requestModifiers,
82
+ sessionId,
83
+ });
84
+ throwIfApiError(res);
85
+ clearOrgReferenceCache();
86
+ return {
87
+ deleted: { success: true, locationUuid },
88
+ warningMsg: res.warningMsg ?? undefined,
89
+ };
90
+ }
91
+ export async function getLocationLabels(requestModifiers, sessionId) {
92
+ const res = await cachedPostApi({
45
93
  route: "/location/getLocationLabelsForOrg",
46
94
  body: {},
47
95
  modifiers: requestModifiers,
48
96
  sessionId,
49
97
  });
50
- if (res.error)
51
- throw new Error(JSON.stringify(res));
52
- return (res.locationLabels || []).map((label) => ({
53
- uuid: label.uuid ?? undefined,
54
- name: label.name ?? undefined,
55
- }));
98
+ throwIfApiError(res);
99
+ // api2 returns location labels as bare strings on some orgs and objects on
100
+ // others; normalise both to {name}.
101
+ const raw = res.locationLabels ?? [];
102
+ const labels = [];
103
+ for (const label of raw) {
104
+ if (typeof label === "string") {
105
+ if (label.trim())
106
+ labels.push({ name: label.trim() });
107
+ }
108
+ else if (label && typeof label === "object") {
109
+ const record = label;
110
+ labels.push({ uuid: record.uuid ?? undefined, name: record.name ?? undefined });
111
+ }
112
+ }
113
+ return labels;
114
+ }
115
+ export async function addLocationLabel(locationUuid, label, requestModifiers, sessionId) {
116
+ const res = await postApi({
117
+ route: "/location/addLocationLabel",
118
+ body: {
119
+ locationIdentifier: locationUuid,
120
+ label,
121
+ },
122
+ modifiers: requestModifiers,
123
+ sessionId,
124
+ });
125
+ throwIfApiError(res);
126
+ clearOrgReferenceCache();
127
+ return {
128
+ labelChanged: { success: true, locationUuid, label },
129
+ warningMsg: res.warningMsg ?? undefined,
130
+ };
131
+ }
132
+ export async function removeLocationLabel(locationUuid, label, requestModifiers, sessionId) {
133
+ const res = await postApi({
134
+ route: "/location/removeLocationLabel",
135
+ body: {
136
+ locationIdentifier: locationUuid,
137
+ label,
138
+ },
139
+ modifiers: requestModifiers,
140
+ sessionId,
141
+ });
142
+ throwIfApiError(res);
143
+ clearOrgReferenceCache();
144
+ return {
145
+ labelChanged: { success: true, locationUuid, label },
146
+ warningMsg: res.warningMsg ?? undefined,
147
+ };
56
148
  }