@7365admin1/layer-common 4.0.3 → 4.0.4-staging.237

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,680 @@
1
+ /**
2
+ * The canonical permission vocabulary for the iService365 estate.
3
+ *
4
+ * WHY THIS FILE EXISTS
5
+ *
6
+ * `roles.permissions` is a flat array of `"resource:action"` strings and the API
7
+ * validates it as `Joi.array().items(Joi.string())` -- any string at all. The
8
+ * grantable vocabulary therefore lives in the front end, spread across this
9
+ * layer and eleven web apps, and nothing has ever compared the three sides. The
10
+ * result, measured on `origin/main`:
11
+ *
12
+ * - work orders are spelled FOUR ways in shipped code -- `work_orders`,
13
+ * `workOrder`, `work_order` and `work-orders` -- so a role granted
14
+ * "see all work orders" in one app's role editor is refused by another app's
15
+ * screen. `DashboardMain.vue` already unions three of them and says in its
16
+ * own comment that it cannot fix this from inside one component;
17
+ * - visitors are `visitor-mgmt` and `visitorManagement`;
18
+ * - `visitor:create`, `visitor:update` and `visitor:check-out` are enforced by
19
+ * the server on the HID door-reader endpoints and exist in no catalogue
20
+ * anywhere, so nobody can grant them;
21
+ * - `incident-reports:see-ncident-reports` is a live typo in the security
22
+ * mobile app -- a permanently false check that throws nothing.
23
+ *
24
+ * Every one of those was invisible until somebody noticed a blank screen.
25
+ *
26
+ * WHAT THIS FILE IS, AND IS NOT
27
+ *
28
+ * It is a NAMING source of truth: the canonical spelling of every permission the
29
+ * estate uses, typed, plus a map from each legacy spelling to its canonical
30
+ * form. Importing a permission from here makes a typo a compile error instead of
31
+ * a silently false check.
32
+ *
33
+ * It is NOT a grant list and it changes no gate. Nothing in this package is
34
+ * rewired by adding it; `useCleaningPermission()` is still the catalogue
35
+ * `hasPermission` looks actions up in, and every existing check still asks for
36
+ * exactly the string it asked for before. Adopting a canonical spelling in place
37
+ * of a legacy one is a per-app change with its own behaviour risk (a role
38
+ * holding the old string stops matching, a role holding the new one starts) and
39
+ * is deliberately NOT done here. See `LEGACY_PERMISSION_ALIASES`.
40
+ *
41
+ * HOW "CANONICAL" WAS DECIDED
42
+ *
43
+ * 1. If the server READS the string, the server's spelling wins -- that is
44
+ * what actually gates data. This settled `site-settings:*`,
45
+ * `virtual-patrol:*`, `facility-mgmt:update-facility` (all of which the UI
46
+ * already agreed with) and `service-provider-mgmt` (where the UI was split
47
+ * and `core/utils/notification-category.util.ts:185` broke the tie).
48
+ * 2. Where the server reads none, the spelling used by the MOST apps wins --
49
+ * counting an app once, whether it checks the string in its own code or
50
+ * through a composable in this layer.
51
+ * 3. Where rule 2 ties, the estate's dominant convention wins: kebab-case
52
+ * resource, `-mgmt` suffix, `see-all-<plural>` for list actions. That is
53
+ * what the clear majority of the 279 shipped strings already look like.
54
+ *
55
+ * One family could not be settled by those rules and is deliberately left
56
+ * unresolved -- see `UNRESOLVED_SERVER_SPELLINGS`.
57
+ *
58
+ * KEEPING IT HONEST
59
+ *
60
+ * `test/permission-vocabulary.test.mjs` fails when this file and reality drift
61
+ * apart. Read its header for exactly what it does and does not catch.
62
+ */
63
+
64
+ /**
65
+ * Canonical `resource -> actions`.
66
+ *
67
+ * Only canonical spellings appear here. A legacy spelling that some app still
68
+ * ships lives in `LEGACY_PERMISSION_ALIASES` and resolves to an entry below.
69
+ *
70
+ * Provenance: extracted from `iservice365-layer-common` plus all eleven
71
+ * `iservice365-web-app-*` repos at `origin/main`, structurally (every
72
+ * `hasPermission(...)` call site plus every permission-shaped literal), not by
73
+ * eyeballing.
74
+ */
75
+ export const PERMISSIONS = {
76
+ // ---- shared administration: every app that has a role editor ----
77
+ members: [
78
+ "view-members",
79
+ "assign-member-role",
80
+ "suspend-member",
81
+ "activate-member",
82
+ "delete-member",
83
+ ],
84
+ // `roles-and-permissions` is `-admin`'s spelling of the same five actions;
85
+ // eight apps say `roles`.
86
+ roles: [
87
+ "add-role",
88
+ "see-all-roles",
89
+ "see-role-details",
90
+ "update-role",
91
+ "delete-role",
92
+ ],
93
+ invitations: ["create-invitation", "view-invitations", "cancel-invitation"],
94
+
95
+ // ---- work orders: the four-way split this catalogue exists to end ----
96
+ // `work_orders` is canonical on rule 2: four apps (`-landscape`,
97
+ // `-mechanical-electrical`, `-pool-mgmt`, `-security`) against two for
98
+ // `workOrder` and one for `work_order`. The server gates none of them; its
99
+ // only read is the notification filter at
100
+ // `core/utils/notification-category.util.ts:97`, which already accepts all
101
+ // four spellings defensively.
102
+ work_orders: [
103
+ "create-work-order",
104
+ "see-all-work-orders",
105
+ "see-work-order-details",
106
+ "update-work-order",
107
+ "delete-work-order",
108
+ ],
109
+
110
+ feedbacks: [
111
+ "add-feedback",
112
+ "see-all-feedback",
113
+ "see-feedback-details",
114
+ "update-feedback",
115
+ "delete-feedback",
116
+ ],
117
+
118
+ // `bulletin-board` is canonical on rule 2: this layer's
119
+ // `useBulletinBoardPermission` is what `-hygiene`, `-landscape`,
120
+ // `-mechanical-electrical`, `-pest-control` and `-pool-mgmt` render (five
121
+ // apps), against two spelling it `bulletin-board-mgmt`.
122
+ "bulletin-board": [
123
+ "add-bulletin-board",
124
+ "see-all-bulletin-boards",
125
+ "see-bulletin-board-details",
126
+ "update-bulletin-board",
127
+ "delete-bulletin-board",
128
+ ],
129
+ // Videos are a separate resource, not a spelling of the board above.
130
+ "bulletin-videos-mgmt": [
131
+ "add-bulletin-video",
132
+ "see-all-bulletin-video",
133
+ "see-bulletin-video-details",
134
+ "update-bulletin-video",
135
+ "delete-bulletin-video",
136
+ ],
137
+
138
+ // ---- site settings: the server reads five of these, so it decides ----
139
+ "site-settings": [
140
+ "can-view-site-settings",
141
+ "can-manage-site-settings",
142
+ "manage-site-information",
143
+ "manage-cctv-camera",
144
+ "manage-anpr-camera",
145
+ "manage-delivery-companies",
146
+ "manage-resident-visitors",
147
+ "manage-work-order-settings",
148
+ "manage-billing-and-soa-configuration",
149
+ "manage-nfc-management",
150
+ "manage-nfc-patrol-settings",
151
+ "manage-entry-pass",
152
+ "manage-red-dot-settings",
153
+ ],
154
+
155
+ // ---- module apps (cleaning, landscape, M&E, pest, pool) ----
156
+ "area-mgmt": [
157
+ "see-all-areas",
158
+ "add-area",
159
+ "update-area",
160
+ "see-area-details",
161
+ "import-areas",
162
+ "delete-area",
163
+ ],
164
+ "unit-mgmt": [
165
+ "see-all-units",
166
+ "add-unit",
167
+ "update-unit",
168
+ "see-unit-details",
169
+ "import-units",
170
+ "delete-unit",
171
+ ],
172
+ "supply-mgmt": [
173
+ "see-all-supplies",
174
+ "add-supply",
175
+ "update-supply",
176
+ "delete-supply",
177
+ "add-stock",
178
+ "view-stock",
179
+ "checkout-item",
180
+ ],
181
+ "checkout-item-mgmt": [
182
+ "see-all-checkouts",
183
+ "create-checkout",
184
+ "update-checkout",
185
+ "approve-checkout",
186
+ "disapprove-checkout",
187
+ "view-checkout-details",
188
+ ],
189
+ "schedule-task-mgmt": [
190
+ "see-all-schedule-tasks",
191
+ "add-schedule-task",
192
+ "update-schedule-task",
193
+ "see-schedule-task-details",
194
+ "delete-schedule-task",
195
+ ],
196
+ "attendance-mgmt": [
197
+ "see-all-attendance",
198
+ "see-attendance-details",
199
+ "manage-attendance-settings",
200
+ ],
201
+ "my-attendance": ["see-own-attendance", "check-in-out"],
202
+ inventory: ["view-inventory"],
203
+ toilet_locations: ["create-toilet-location"],
204
+
205
+ // ---- per-trade schedules ----
206
+ // These four carry the same seven actions on purpose:
207
+ // `useCleaningSchedulePermission` here, `useSchedulePermission` in
208
+ // `-landscape`, and `DashboardMain` all match them by SUFFIX
209
+ // (`permission.endsWith(`-schedule-mgmt:${action}`)`), so the trade prefix is
210
+ // data, not vocabulary. A new trade needs no code change -- and, because the
211
+ // match is a suffix, no test in this package can see a new prefix until it is
212
+ // listed here. This list was built by grepping the four apps for the key, not
213
+ // from the literal sweep, which cannot see a resource that only ever exists as
214
+ // a catalogue object key.
215
+ // Written out four times rather than shared: `as const` needs a literal to
216
+ // keep each tuple's element types, which is what makes a typo a build error.
217
+ "cleaning-schedule-mgmt": [
218
+ "see-all-schedules",
219
+ "see-schedule-details",
220
+ "download-schedule",
221
+ "manage-schedule-tasks",
222
+ "generate-checklist",
223
+ "view-history",
224
+ "add-remarks",
225
+ ],
226
+ "landscape-schedule-mgmt": [
227
+ "see-all-schedules",
228
+ "see-schedule-details",
229
+ "download-schedule",
230
+ "manage-schedule-tasks",
231
+ "generate-checklist",
232
+ "view-history",
233
+ "add-remarks",
234
+ ],
235
+ "pest-schedule-mgmt": [
236
+ "see-all-schedules",
237
+ "see-schedule-details",
238
+ "download-schedule",
239
+ "manage-schedule-tasks",
240
+ "generate-checklist",
241
+ "view-history",
242
+ "add-remarks",
243
+ ],
244
+ "pool-schedule-mgmt": [
245
+ "see-all-schedules",
246
+ "see-schedule-details",
247
+ "download-schedule",
248
+ "manage-schedule-tasks",
249
+ "generate-checklist",
250
+ "view-history",
251
+ "add-remarks",
252
+ ],
253
+
254
+ // ---- property management ----
255
+ "people-mgmt": [
256
+ "see-all-people",
257
+ "add-people",
258
+ "update-people",
259
+ "see-people-details",
260
+ "see-people-nric",
261
+ "delete-people",
262
+ ],
263
+ "building-mgmt": [
264
+ "see-all-buildings",
265
+ "add-building",
266
+ "update-building",
267
+ "see-building-details",
268
+ "delete-building",
269
+ ],
270
+ // `-property-management`'s only spelling; no kebab counterpart ships anywhere,
271
+ // so rule 2 leaves it as-is rather than inventing one nobody uses.
272
+ buildingUnitManagement: [
273
+ "see-all-building-units",
274
+ "add-building-unit",
275
+ "update-building-unit",
276
+ "see-building-unit-details",
277
+ "delete-building-unit",
278
+ ],
279
+ "document-mgmt": [
280
+ "add-document",
281
+ "add-folder",
282
+ "update-document",
283
+ "delete-document",
284
+ ],
285
+ "emergency-contact": [
286
+ "see-all",
287
+ "see-details",
288
+ "add-emergency-contact",
289
+ "update-emergency-contact",
290
+ "delete-emergency-contact",
291
+ "add-emergency-contact-category",
292
+ "delete-emergency-contact-category",
293
+ ],
294
+ "event-mgmt": [
295
+ "see-all-event",
296
+ "add-event",
297
+ "update-event",
298
+ "see-event-details",
299
+ "delete-event",
300
+ ],
301
+ "facility-mgmt": [
302
+ "see-all-facility",
303
+ "add-facility",
304
+ "update-facility",
305
+ "see-facility-details",
306
+ "delete-facility",
307
+ ],
308
+ "facility-booking-mgmt": [
309
+ "see-all-facility-booking",
310
+ "add-facility-booking",
311
+ "update-facility-booking",
312
+ "see-facility-booking-details",
313
+ "delete-facility-booking",
314
+ ],
315
+ "entry-pass-mgmt": [
316
+ "view-entry-pass-settings",
317
+ "add-entry-pass-settings",
318
+ "update-entry-pass-settings",
319
+ ],
320
+ "online-form-configuration": [
321
+ "add-online-form-configuration",
322
+ "update-online-form-configuration",
323
+ "delete-online-form-configuration",
324
+ ],
325
+ "nfc-patrol": [
326
+ "see-all-nfc-patrol",
327
+ "add-nfc-patrol",
328
+ "update-nfc-patrol",
329
+ "delete-nfc-patrol",
330
+ ],
331
+ "billing-mgmt": [
332
+ "see-all-billing-summary",
333
+ "see-all-billing-items",
334
+ "see-billing-items-details",
335
+ "add-billing-items",
336
+ "update-billing-items",
337
+ "delete-billing-items",
338
+ "add-billing-configuration",
339
+ "update-billing-configuration",
340
+ "see-billing-configuration-details",
341
+ ],
342
+ "SOA-mgmt": ["see-all-soa", "see-soa-details", "add-soa", "download-soa"],
343
+ // `service-provider-mgmt` is canonical on rule 1: the server reads the
344
+ // resource at `core/utils/notification-category.util.ts:185`. The UI is split
345
+ // one-app-one-layer-composable, so without the server read this would have
346
+ // been a coin toss.
347
+ "service-provider-mgmt": [
348
+ "see-all-service-providers",
349
+ "add-service-provider",
350
+ "invite-service-provider",
351
+ ],
352
+ "access-card-mgmt": [
353
+ "see-all-card",
354
+ "see-all-qr-tagging",
355
+ "view",
356
+ "add-access-card",
357
+ "update-access-card",
358
+ "replace-access-card",
359
+ "delete-access-card",
360
+ "assign-access-card",
361
+ "manage-hid",
362
+ ],
363
+
364
+ // ---- security ----
365
+ "visitor-mgmt": [
366
+ "see-all-visitor",
367
+ "add-visitor",
368
+ "update-visitor",
369
+ "see-visitor-details",
370
+ "delete-visitor",
371
+ ],
372
+ "vehicle-mgmt": [
373
+ "see-all-vehicles",
374
+ "add-vehicle",
375
+ "update-vehicle",
376
+ "see-vehicle-details",
377
+ "approve-vehicle",
378
+ "delete-vehicle",
379
+ ],
380
+ "incident-reports": [
381
+ "see-incident-reports",
382
+ "see-incident-report-details",
383
+ "add-incident-report",
384
+ "update-incident-report",
385
+ "review-incident-report",
386
+ "delete-incident-report",
387
+ ],
388
+ // The server reads five of these (`core/utils/camera-view.util.ts:64,77`), so
389
+ // rule 1 fixes the whole family. Note `see-all-virtual-patrol-question` is
390
+ // singular and `see-all-virtual-patrols` has no `-route`/`-log` suffix: both
391
+ // are verbatim production spelling and must not be "corrected".
392
+ "virtual-patrol": [
393
+ "see-all-virtual-patrol-route",
394
+ "see-all-virtual-patrol-logs",
395
+ "see-all-virtual-patrol-question",
396
+ "see-all-virtual-patrols",
397
+ "see-virtual-patrol-route-details",
398
+ "see-virtual-patrol-log-details",
399
+ "see-virtual-patrol-question-details",
400
+ "add-virtual-patrol-route",
401
+ "add-virtual-patrol-log",
402
+ "add-virtual-patrol-question",
403
+ "update-virtual-patrol-route",
404
+ "update-virtual-patrol-log",
405
+ "update-virtual-patrol-question",
406
+ "start-virtual-patrol-route",
407
+ "delete-virtual-patrol-route",
408
+ "delete-virtual-patrol-log",
409
+ "delete-virtual-patrol-question",
410
+ ],
411
+ "daily-occurrence-books": [
412
+ "see-all-daily-occurrence",
413
+ "add-daily-occurrence",
414
+ "update-daily-occurrence",
415
+ "see-daily-occurrence-details",
416
+ "delete-daily-occurrence",
417
+ ],
418
+ "daily-occurrence-entries": [
419
+ "see-all-daily-occurrence-entries",
420
+ "add-daily-occurrence-entries",
421
+ "update-daily-occurrence-entries",
422
+ "see-daily-occurrence-entries-details",
423
+ "delete-daily-occurrence-entries",
424
+ ],
425
+ "manpower-monitoring": [
426
+ "see-all-manpower-monitoring-sites",
427
+ "see-manpower-monitoring-sites-detail",
428
+ "see-all-manpower-monitoring-attendance",
429
+ "download-manpower-monitoring-attendance",
430
+ "see-all-manpower-monitoring-settings",
431
+ "save-manpower-monitoring-general-settings",
432
+ "save-manpower-monitoring-designation-settings",
433
+ "delete-manpower-sites",
434
+ ],
435
+ "robot-mgmt": [
436
+ "see-all-robots",
437
+ "add-robot",
438
+ "update-robot",
439
+ "see-robot-details",
440
+ "delete-robot",
441
+ ],
442
+
443
+ // ---- organisation console (`-org`) ----
444
+ customers: [
445
+ "see-all-customers",
446
+ "add-customer",
447
+ "see-customer-details",
448
+ "delete-customer",
449
+ ],
450
+ sites: ["see-all-sites", "add-site", "see-site-details", "delete-site"],
451
+ subscriptions: ["view-subscription-details", "management-subscription"],
452
+ } as const;
453
+
454
+ /** Every canonical resource name. */
455
+ export type TPermissionResource = keyof typeof PERMISSIONS;
456
+
457
+ /**
458
+ * Every canonical `"resource:action"` string, as a union type.
459
+ *
460
+ * This is the typo guard: a misspelt permission is not assignable to
461
+ * `TPermissionString`, so it fails `yarn build` instead of quietly answering
462
+ * `false` forever the way `incident-reports:see-ncident-reports` does today.
463
+ */
464
+ export type TPermissionString = {
465
+ [R in TPermissionResource]: `${R}:${(typeof PERMISSIONS)[R][number]}`;
466
+ }[TPermissionResource];
467
+
468
+ /** The wildcard every gate in the estate short-circuits on. */
469
+ export const PERMISSION_WILDCARD = "*" as const;
470
+
471
+ /** Every canonical string, flattened. Frozen so a consumer cannot mutate it. */
472
+ export const PERMISSION_STRINGS: readonly TPermissionString[] = Object.freeze(
473
+ Object.entries(PERMISSIONS).flatMap(([resource, actions]) =>
474
+ (actions as readonly string[]).map((action) => `${resource}:${action}`),
475
+ ) as TPermissionString[],
476
+ );
477
+
478
+ /**
479
+ * Build a canonical permission string with the compiler checking both halves.
480
+ *
481
+ * `permission("work_orders", "see-all-work-orders")` compiles;
482
+ * `permission("work_orders", "see-all-work-order")` does not.
483
+ */
484
+ export function permission<
485
+ R extends TPermissionResource,
486
+ A extends (typeof PERMISSIONS)[R][number],
487
+ >(resource: R, action: A): `${R}:${A}` {
488
+ return `${resource}:${action}` as `${R}:${A}`;
489
+ }
490
+
491
+ /**
492
+ * Legacy spelling -> canonical spelling.
493
+ *
494
+ * Every key is a string SHIPPED TODAY by something that still runs, so a role
495
+ * out there may be holding it. Nothing here is renamed by this package; the map
496
+ * exists so a consumer can accept both spellings during a migration and so the
497
+ * drift test can tell "an old spelling we know about" from "a new string nobody
498
+ * catalogued".
499
+ *
500
+ * Each entry names the consumer that still needs it. An entry can be deleted
501
+ * once that consumer ships the canonical string AND the roles carrying the old
502
+ * one have been migrated -- deleting it earlier strands whoever holds the old
503
+ * string.
504
+ */
505
+ export const LEGACY_PERMISSION_ALIASES: Readonly<
506
+ Record<string, TPermissionString>
507
+ > = Object.freeze({
508
+ // --- work orders (four spellings, one resource) ---
509
+ // Needed by: `-property-management`, `-pest-control` (own catalogues) and
510
+ // `DashboardMain.vue`'s union in this package.
511
+ "workOrder:create-work-order": "work_orders:create-work-order",
512
+ "workOrder:see-all-work-orders": "work_orders:see-all-work-orders",
513
+ "workOrder:see-work-order-details": "work_orders:see-work-order-details",
514
+ "workOrder:update-work-order": "work_orders:update-work-order",
515
+ "workOrder:delete-work-order": "work_orders:delete-work-order",
516
+ // Needed by: `-hygiene`, and by `useCleaningPermission()` in this package,
517
+ // whose catalogue key is `work_order`.
518
+ "work_order:create-work-order": "work_orders:create-work-order",
519
+ "work_order:see-all-work-orders": "work_orders:see-all-work-orders",
520
+ "work_order:see-work-order-details": "work_orders:see-work-order-details",
521
+ "work_order:update-work-order": "work_orders:update-work-order",
522
+ "work_order:delete-work-order": "work_orders:delete-work-order",
523
+ // Needed by: the security mobile app (`isecure365-mobile-app`), and accepted
524
+ // defensively by `core/utils/notification-category.util.ts:97`. No web app
525
+ // ships this spelling.
526
+ "work-orders:create-work-order": "work_orders:create-work-order",
527
+ "work-orders:see-all-work-orders": "work_orders:see-all-work-orders",
528
+ "work-orders:update-work-order": "work_orders:update-work-order",
529
+ "work-orders:delete-work-order": "work_orders:delete-work-order",
530
+ // Needed by: `-security`, which says `add-` where every other app says
531
+ // `create-`.
532
+ "work_orders:add-work-order": "work_orders:create-work-order",
533
+ "work-orders:add-work-order": "work_orders:create-work-order",
534
+
535
+ // --- visitors ---
536
+ // Needed by: `-property-management`, and by `DashboardMain.vue`'s union here.
537
+ "visitorManagement:see-all-visitor": "visitor-mgmt:see-all-visitor",
538
+ "visitorManagement:add-visitor": "visitor-mgmt:add-visitor",
539
+ "visitorManagement:update-visitor": "visitor-mgmt:update-visitor",
540
+ "visitorManagement:see-visitor-details": "visitor-mgmt:see-visitor-details",
541
+ "visitorManagement:delete-visitor": "visitor-mgmt:delete-visitor",
542
+
543
+ // --- bulletin board ---
544
+ // Needed by: `-property-management` and `-security`, which both suffix the
545
+ // resource `-mgmt` while the five module apps do not.
546
+ "bulletin-board-mgmt:add-bulletin-board": "bulletin-board:add-bulletin-board",
547
+ "bulletin-board-mgmt:see-all-bulletin-boards":
548
+ "bulletin-board:see-all-bulletin-boards",
549
+ "bulletin-board-mgmt:see-bulletin-board-details":
550
+ "bulletin-board:see-bulletin-board-details",
551
+ "bulletin-board-mgmt:update-bulletin-board":
552
+ "bulletin-board:update-bulletin-board",
553
+ "bulletin-board-mgmt:delete-bulletin-board":
554
+ "bulletin-board:delete-bulletin-board",
555
+ // Needed by: `-property-management` and the security mobile app, which say
556
+ // "board" singular where everyone else says "boards".
557
+ "bulletin-board-mgmt:see-all-bulletin-board":
558
+ "bulletin-board:see-all-bulletin-boards",
559
+
560
+ // --- facility bookings ---
561
+ // Needed by: `DashboardMain.vue`'s module-mode widget map in this package,
562
+ // the only place that drops the `-mgmt`.
563
+ "facility-booking:see-all-facility-booking":
564
+ "facility-booking-mgmt:see-all-facility-booking",
565
+
566
+ // --- feedback ---
567
+ // Needed by: `DashboardMain.vue`'s union here (both of these), and by
568
+ // `-security`, which pluralises the action.
569
+ "feedback:see-all-feedback": "feedbacks:see-all-feedback",
570
+ "feedbacks:see-all-feedbacks": "feedbacks:see-all-feedback",
571
+
572
+ // --- incident reports ---
573
+ // Needed by: `DashboardMain.vue`'s union here. No app ships the `-all-` form.
574
+ "incident-reports:see-all-incident-reports":
575
+ "incident-reports:see-incident-reports",
576
+
577
+ // --- buildings ---
578
+ // Needed by: `-property-management`. `-security` already spells it kebab.
579
+ "buildingManagement:see-all-buildings": "building-mgmt:see-all-buildings",
580
+ "buildingManagement:add-building": "building-mgmt:add-building",
581
+ "buildingManagement:update-building": "building-mgmt:update-building",
582
+ "buildingManagement:see-building-details":
583
+ "building-mgmt:see-building-details",
584
+ "buildingManagement:delete-building": "building-mgmt:delete-building",
585
+
586
+ // --- roles ---
587
+ // Needed by: `-admin`, which is retired by redirect but still deployed.
588
+ "roles-and-permissions:add-role": "roles:add-role",
589
+ "roles-and-permissions:see-all-roles": "roles:see-all-roles",
590
+ "roles-and-permissions:see-role-details": "roles:see-role-details",
591
+ "roles-and-permissions:update-role": "roles:update-role",
592
+ "roles-and-permissions:delete-role": "roles:delete-role",
593
+
594
+ // --- service providers ---
595
+ // Needed by: `useServiceProviderPermission` in this package, which drops the
596
+ // `-mgmt` the server's notification filter expects.
597
+ "service-provider:see-all-service-providers":
598
+ "service-provider-mgmt:see-all-service-providers",
599
+ "service-provider:add-service-provider":
600
+ "service-provider-mgmt:add-service-provider",
601
+ "service-provider:invite-service-provider":
602
+ "service-provider-mgmt:invite-service-provider",
603
+ // Needed by: `-property-management`, which says `view-` where this layer says
604
+ // `see-all-`.
605
+ "service-provider-mgmt:view-service-providers":
606
+ "service-provider-mgmt:see-all-service-providers",
607
+ });
608
+
609
+ /**
610
+ * Strings the SERVER enforces that no UI checks and no role editor can grant.
611
+ *
612
+ * `core/src/utils/hid-access.util.ts:36-38` gates the HID Amico visitor
613
+ * endpoints on `visitor:create`, `visitor:update` and `visitor:check-out`. The
614
+ * whole estate spells visitor rights `visitor-mgmt:add-visitor`,
615
+ * `:update-visitor` and so on; a bare `visitor` resource appears in no catalogue
616
+ * in any repo, so no role can hold these and the branch is reachable only
617
+ * through `*` or through the `access-card-mgmt` strings the same list unions in.
618
+ *
619
+ * They are NOT aliased to `visitor-mgmt:*` here, because doing so would be a
620
+ * decision with a real consequence in both directions:
621
+ *
622
+ * - teaching the server to read `visitor-mgmt:add-visitor` would take a check
623
+ * that is dead today and make it LIVE for every role already granted visitor
624
+ * management -- widening who may write a credential to a door reader;
625
+ * - adding `visitor:create` to the role editors would create a second visitor
626
+ * vocabulary next to the one already in use.
627
+ *
628
+ * Which of the two is right is a backend-lead and owner call, not this file's.
629
+ * Recorded rather than guessed, and pinned by the drift test so the list cannot
630
+ * grow without somebody noticing.
631
+ *
632
+ * (The estate's previous example of this, `vehicleManagement:approve-vehicle`,
633
+ * was fixed in `core` before this catalogue was written: `vehicle.service.ts:133`
634
+ * on `origin/main` now reads `vehicle-mgmt:approve-vehicle`, which every
635
+ * catalogue can grant. Notes written before 2026-08-23 still describe the old
636
+ * spelling.)
637
+ */
638
+ export const SERVER_ONLY_UNGRANTABLE: readonly string[] = Object.freeze([
639
+ "visitor:create",
640
+ "visitor:update",
641
+ "visitor:check-out",
642
+ ]);
643
+
644
+ /**
645
+ * Canonical strings that are CHECKED -- by this package and by the server -- and
646
+ * that no role editor can GRANT.
647
+ *
648
+ * `NavigationDrawer.vue:172-180` accepts eight `access-card-mgmt` strings for
649
+ * the HID menu and `core/src/utils/hid-access.util.ts:19-32` enforces all nine;
650
+ * only six of them appear in any app's catalogue. The menu still opens, because
651
+ * the gate is an OR and the other six are grantable -- but a role built out of
652
+ * only these three is refused at both ends while looking correctly configured in
653
+ * the role editor. Frozen here so the list cannot grow without somebody noticing.
654
+ */
655
+ export const CHECKED_BUT_UNGRANTABLE: readonly TPermissionString[] =
656
+ Object.freeze([
657
+ "access-card-mgmt:view",
658
+ "access-card-mgmt:assign-access-card",
659
+ "access-card-mgmt:manage-hid",
660
+ ]);
661
+
662
+ const CANONICAL = new Set<string>(PERMISSION_STRINGS);
663
+
664
+ /**
665
+ * Resolve any shipped spelling to its canonical form.
666
+ *
667
+ * Returns `undefined` for a string this catalogue has never heard of -- which is
668
+ * the signal the drift test looks for.
669
+ */
670
+ export function canonicalPermission(
671
+ value: string,
672
+ ): TPermissionString | undefined {
673
+ if (CANONICAL.has(value)) return value as TPermissionString;
674
+ return LEGACY_PERMISSION_ALIASES[value];
675
+ }
676
+
677
+ /** Is this string one the catalogue knows, canonical or legacy? */
678
+ export function isKnownPermission(value: string): boolean {
679
+ return canonicalPermission(value) !== undefined;
680
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.0.3",
5
+ "version": "4.0.4-staging.237",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",