@endora-commerce/mod-admin-roles 0.100.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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/admin/index.d.ts +47 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +27 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +20 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +13 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +64 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +74 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +20 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +29 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/admin-role.entity.d.ts +20 -0
- package/dist/backend/entities/admin-role.entity.d.ts.map +1 -0
- package/dist/backend/entities/admin-role.entity.js +65 -0
- package/dist/backend/entities/admin-role.entity.js.map +1 -0
- package/dist/backend/index.d.ts +96 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +114 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/permission-inventory.d.ts +117 -0
- package/dist/backend/permission-inventory.d.ts.map +1 -0
- package/dist/backend/permission-inventory.js +585 -0
- package/dist/backend/permission-inventory.js.map +1 -0
- package/dist/backend/services/admin-role-ports.d.ts +39 -0
- package/dist/backend/services/admin-role-ports.d.ts.map +1 -0
- package/dist/backend/services/admin-role-ports.js +72 -0
- package/dist/backend/services/admin-role-ports.js.map +1 -0
- package/dist/backend/services/admin-role-service.d.ts +77 -0
- package/dist/backend/services/admin-role-service.d.ts.map +1 -0
- package/dist/backend/services/admin-role-service.js +219 -0
- package/dist/backend/services/admin-role-service.js.map +1 -0
- package/dist/backend/services/permission-catalogue.service.d.ts +130 -0
- package/dist/backend/services/permission-catalogue.service.d.ts.map +1 -0
- package/dist/backend/services/permission-catalogue.service.js +175 -0
- package/dist/backend/services/permission-catalogue.service.js.map +1 -0
- package/dist/backend/services/permission-service.d.ts +24 -0
- package/dist/backend/services/permission-service.d.ts.map +1 -0
- package/dist/backend/services/permission-service.js +40 -0
- package/dist/backend/services/permission-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +192 -0
- package/dist/manifest.js.map +1 -0
- package/docs/admin_roles.md +38 -0
- package/i18n/en.json +10 -0
- package/i18n/pl.json +10 -0
- package/package.json +73 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { PERMISSION_CATALOGUE, } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Merges the core `PERMISSION_CATALOGUE` with the `permissions` arrays of every
|
|
4
|
+
* registered module manifest. Powers `GET /admin/permissions` and role upsert
|
|
5
|
+
* validation (feature 026).
|
|
6
|
+
*
|
|
7
|
+
* **There is no memo, and that is the design** (issue #213). `listAssignable()`
|
|
8
|
+
* cached its result into a field that only an explicit `invalidate()` cleared,
|
|
9
|
+
* and the two things that had to call it — a Redis pub/sub listener in each
|
|
10
|
+
* composition root — were a duplicated wiring step rather than a property of the
|
|
11
|
+
* service. That is the shape of issue #33 (an admin "clear cache" action that
|
|
12
|
+
* missed the per-process LRU) and issue #45 (an invalidation that depended on
|
|
13
|
+
* subscriber registration order) for the third time. Now that the value tracks a
|
|
14
|
+
* Setting an operator flips from the Admin UI at runtime, a per-process memo can
|
|
15
|
+
* only be wrong between the flip and whatever drops it.
|
|
16
|
+
*
|
|
17
|
+
* Nothing is lost by dropping it: every input is already in memory — the
|
|
18
|
+
* manifests are fixed at composition and the presence predicate reads the
|
|
19
|
+
* registry cache, which does its own refreshing — so a call is a map build and a
|
|
20
|
+
* sort over a few hundred rows, on two admin-only paths. There is no ordering
|
|
21
|
+
* trap to test because there is no invalidation to order.
|
|
22
|
+
*/
|
|
23
|
+
export class PermissionCatalogueService {
|
|
24
|
+
options;
|
|
25
|
+
isModulePresent;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.isModulePresent = options.isModulePresent ?? (() => true);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The grantable set: what `/admin-roles` renders and what an operator may
|
|
32
|
+
* newly grant. Presence-filtered, so a module that is off contributes nothing
|
|
33
|
+
* (Constitution XVII item 5).
|
|
34
|
+
*/
|
|
35
|
+
listAssignable() {
|
|
36
|
+
return this.#merge()
|
|
37
|
+
.filter((row) => [...row.owners].some((owner) => this.isModulePresent(owner)))
|
|
38
|
+
.map((row) => row.entry);
|
|
39
|
+
}
|
|
40
|
+
listAssignableCodes() {
|
|
41
|
+
return this.listAssignable().map((e) => e.code);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every code the platform knows, present or not — the **vocabulary**, not the
|
|
45
|
+
* grantable set.
|
|
46
|
+
*
|
|
47
|
+
* Role upsert validates against this rather than against `listAssignable()`,
|
|
48
|
+
* and the difference is what keeps "off is non-destructive and reversible"
|
|
49
|
+
* true. An "Editor" role holds `blog.read`; the operator switches `blog` off
|
|
50
|
+
* and then renames the role. Validating the submitted list against the
|
|
51
|
+
* grantable set would answer 400 `Unknown permission(s): blog.read` — the
|
|
52
|
+
* operator either loses the role or silently drops a grant that must come back
|
|
53
|
+
* when `blog` does. A code of an absent module grants access to nothing anyway:
|
|
54
|
+
* the routes behind it answer 503 `MODULE_DISABLED` at their own seam.
|
|
55
|
+
*
|
|
56
|
+
* This is what the role editor already relies on: `AdminRolesPage` seeds its
|
|
57
|
+
* selection from the **role's** codes and renders a checkbox only for the ones
|
|
58
|
+
* the catalogue offers, so an absent module's grant is carried through the
|
|
59
|
+
* round trip untouched and simply has no checkbox to un-tick while the module
|
|
60
|
+
* is off. Narrowing this method would turn that into a silent revocation.
|
|
61
|
+
*/
|
|
62
|
+
listKnownCodes() {
|
|
63
|
+
return this.#merge().map((row) => row.entry.code);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every code the platform knows, mapped to the modules whose presence keeps
|
|
67
|
+
* it grantable — {@link MergedRow.owners}, published.
|
|
68
|
+
*
|
|
69
|
+
* D-173's sweep reads it, and it is a method rather than a derivation of its
|
|
70
|
+
* own because the whole question is *"does `/admin-roles` still offer this
|
|
71
|
+
* code when its owner is switched off?"* — a question only the merge this
|
|
72
|
+
* class performs can answer. A second implementation over
|
|
73
|
+
* `PERMISSION_CATALOGUE` plus the manifests would be right on the day it was
|
|
74
|
+
* written and would drift the first time the merge learns a rule: a shared
|
|
75
|
+
* code with two owners, or a catalogue row a manifest re-declares, is exactly
|
|
76
|
+
* where the two would disagree, and exactly the shape the sweep exists for.
|
|
77
|
+
*/
|
|
78
|
+
listOwnersByCode() {
|
|
79
|
+
return new Map(this.#merge().map((row) => [row.entry.code, row.owners]));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The codes `moduleId` contributes, whether or not it is present.
|
|
83
|
+
*
|
|
84
|
+
* Exists for the off-state harness: proving a module's codes leave the
|
|
85
|
+
* grantable set needs to know which codes are its, and deriving that from the
|
|
86
|
+
* `module` display field would silently measure nothing for `_lifecycle`.
|
|
87
|
+
*/
|
|
88
|
+
listOwnedCodes(moduleId) {
|
|
89
|
+
return this.#merge()
|
|
90
|
+
.filter((row) => row.owners.has(moduleId))
|
|
91
|
+
.map((row) => row.entry.code);
|
|
92
|
+
}
|
|
93
|
+
#merge() {
|
|
94
|
+
const byCode = new Map();
|
|
95
|
+
// Core first: a core row wins the label and the display module on a code
|
|
96
|
+
// collision, which is the long-standing behaviour and what the dedupe test
|
|
97
|
+
// pins. Its `module` is a real module id — `audit_log:read` says so in its
|
|
98
|
+
// own comment (feature 072, T017).
|
|
99
|
+
for (const row of PERMISSION_CATALOGUE) {
|
|
100
|
+
byCode.set(row.code, {
|
|
101
|
+
code: row.code,
|
|
102
|
+
module: row.module,
|
|
103
|
+
label: row.label,
|
|
104
|
+
owners: new Set([row.module]),
|
|
105
|
+
requires: new Set(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const { manifest } of this.options.registryEntries) {
|
|
109
|
+
if (!manifest.permissions?.length)
|
|
110
|
+
continue;
|
|
111
|
+
for (const decl of manifest.permissions) {
|
|
112
|
+
const existing = byCode.get(decl.code);
|
|
113
|
+
if (existing) {
|
|
114
|
+
// A second declarer of an already-known code contributes an owner and
|
|
115
|
+
// its requirements — the row keeps the first label it was given.
|
|
116
|
+
//
|
|
117
|
+
// `requires` is the one field a later declarer adds to rather than
|
|
118
|
+
// loses (D-175). A shared code opens more than one surface and each
|
|
119
|
+
// has its own needs, so the union is the only answer that is right for
|
|
120
|
+
// every one of them; it is advisory, so advising a code an operator
|
|
121
|
+
// already holds costs nothing while dropping one leaves a screen
|
|
122
|
+
// half-working with no explanation.
|
|
123
|
+
existing.owners.add(manifest.id);
|
|
124
|
+
for (const code of decl.requires ?? [])
|
|
125
|
+
existing.requires.add(code);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
byCode.set(decl.code, {
|
|
129
|
+
code: decl.code,
|
|
130
|
+
module: decl.module ?? manifest.id,
|
|
131
|
+
label: decl.label,
|
|
132
|
+
owners: new Set([manifest.id]),
|
|
133
|
+
requires: new Set(decl.requires ?? []),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...byCode.values()]
|
|
138
|
+
.map((row) => ({
|
|
139
|
+
entry: {
|
|
140
|
+
code: row.code,
|
|
141
|
+
module: row.module,
|
|
142
|
+
label: row.label,
|
|
143
|
+
owners: [...row.owners].sort(),
|
|
144
|
+
...(row.requires.size > 0 ? { requires: [...row.requires].sort() } : {}),
|
|
145
|
+
},
|
|
146
|
+
owners: row.owners,
|
|
147
|
+
}))
|
|
148
|
+
.sort((a, b) => a.entry.module.localeCompare(b.entry.module) ||
|
|
149
|
+
a.entry.code.localeCompare(b.entry.code));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Every declared requirement, as `code -> the codes a role holding it needs`
|
|
153
|
+
* (D-175, feature 080 T057).
|
|
154
|
+
*
|
|
155
|
+
* The **vocabulary's** view, not the grantable set's: the advisory has to
|
|
156
|
+
* survive its target's owner being switched off, or it would go quiet at
|
|
157
|
+
* exactly the moment an operator most needs to know why a screen came back
|
|
158
|
+
* incomplete. Empty requirement sets are omitted, so `size` is the number of
|
|
159
|
+
* codes that declare one.
|
|
160
|
+
*/
|
|
161
|
+
listRequirementsByCode() {
|
|
162
|
+
return new Map(this.#merge()
|
|
163
|
+
.filter((row) => (row.entry.requires?.length ?? 0) > 0)
|
|
164
|
+
.map((row) => [row.entry.code, row.entry.requires]));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Test / CI helper — build assignable codes without wiring composition. */
|
|
168
|
+
export function listAssignablePermissionCodes(registryEntries, presentModuleIds) {
|
|
169
|
+
const present = presentModuleIds === undefined ? null : new Set(presentModuleIds);
|
|
170
|
+
return new PermissionCatalogueService({
|
|
171
|
+
registryEntries,
|
|
172
|
+
...(present ? { isModulePresent: (id) => present.has(id) } : {}),
|
|
173
|
+
}).listAssignableCodes();
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=permission-catalogue.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-catalogue.service.js","sourceRoot":"","sources":["../../../src/backend/services/permission-catalogue.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AdminUserReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* PermissionService (T187). Looks up an AdminUser's Role and checks if a
|
|
5
|
+
* specific permission string is present in the Role's permissions array.
|
|
6
|
+
*
|
|
7
|
+
* The wildcard `*` in a Role's permissions grants every permission — used for
|
|
8
|
+
* the bootstrap "platform admin" Role.
|
|
9
|
+
*
|
|
10
|
+
* The admin identity comes from `adminUserReadPort` (feature 075, Phase C)
|
|
11
|
+
* rather than from `em.findOne(AdminUser, …)`: the role assignment is a column
|
|
12
|
+
* on a table `admin_users` owns, and this module owns only what the role says.
|
|
13
|
+
* `activeOnly` is the `deletedAt: null` half of the query it replaced; the
|
|
14
|
+
* `status` test below is the other half and stays here, because "may this admin
|
|
15
|
+
* act" is this module's question.
|
|
16
|
+
*/
|
|
17
|
+
export declare class PermissionService {
|
|
18
|
+
private readonly emFactory;
|
|
19
|
+
private readonly adminUsers;
|
|
20
|
+
constructor(emFactory: () => EntityManager, adminUsers: AdminUserReadPort);
|
|
21
|
+
hasPermission(adminUserId: string, permission: string): Promise<boolean>;
|
|
22
|
+
listPermissions(adminUserId: string): Promise<string[]>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=permission-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/permission-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBADV,SAAS,EAAE,MAAM,aAAa,EAC9B,UAAU,EAAE,iBAAiB;IAG1C,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQxE,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAM9D"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AdminRole } from '../entities/admin-role.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* PermissionService (T187). Looks up an AdminUser's Role and checks if a
|
|
4
|
+
* specific permission string is present in the Role's permissions array.
|
|
5
|
+
*
|
|
6
|
+
* The wildcard `*` in a Role's permissions grants every permission — used for
|
|
7
|
+
* the bootstrap "platform admin" Role.
|
|
8
|
+
*
|
|
9
|
+
* The admin identity comes from `adminUserReadPort` (feature 075, Phase C)
|
|
10
|
+
* rather than from `em.findOne(AdminUser, …)`: the role assignment is a column
|
|
11
|
+
* on a table `admin_users` owns, and this module owns only what the role says.
|
|
12
|
+
* `activeOnly` is the `deletedAt: null` half of the query it replaced; the
|
|
13
|
+
* `status` test below is the other half and stays here, because "may this admin
|
|
14
|
+
* act" is this module's question.
|
|
15
|
+
*/
|
|
16
|
+
export class PermissionService {
|
|
17
|
+
emFactory;
|
|
18
|
+
adminUsers;
|
|
19
|
+
constructor(emFactory, adminUsers) {
|
|
20
|
+
this.emFactory = emFactory;
|
|
21
|
+
this.adminUsers = adminUsers;
|
|
22
|
+
}
|
|
23
|
+
async hasPermission(adminUserId, permission) {
|
|
24
|
+
const admin = await this.adminUsers.findById(adminUserId, { activeOnly: true });
|
|
25
|
+
if (!admin || admin.status !== 'active' || !admin.adminRoleId)
|
|
26
|
+
return false;
|
|
27
|
+
const role = await this.emFactory().findOne(AdminRole, { id: admin.adminRoleId });
|
|
28
|
+
if (!role)
|
|
29
|
+
return false;
|
|
30
|
+
return role.permissions.includes('*') || role.permissions.includes(permission);
|
|
31
|
+
}
|
|
32
|
+
async listPermissions(adminUserId) {
|
|
33
|
+
const admin = await this.adminUsers.findById(adminUserId, { activeOnly: true });
|
|
34
|
+
if (!admin?.adminRoleId)
|
|
35
|
+
return [];
|
|
36
|
+
const role = await this.emFactory().findOne(AdminRole, { id: admin.adminRoleId });
|
|
37
|
+
return role?.permissions ?? [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=permission-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-service.js","sourceRoot":"","sources":["../../../src/backend/services/permission-service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,iBAAiB;IAET;IACA;IAFnB,YACmB,SAA8B,EAC9B,UAA6B;QAD7B,cAAS,GAAT,SAAS,CAAqB;QAC9B,eAAU,GAAV,UAAU,CAAmB;IAC7C,CAAC;IAEJ,KAAK,CAAC,aAAa,CAAC,WAAmB,EAAE,UAAkB;QACzD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,WAAW;YAAE,OAAO,KAAK,CAAC;QAC5E,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAClF,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QACxB,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,WAAmB;QACvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,KAAK,EAAE,WAAW;YAAE,OAAO,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAClF,OAAO,IAAI,EAAE,WAAW,IAAI,EAAE,CAAC;IACjC,CAAC;CACF"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Roles module — manifest backfill (Module Lifecycle, feature 018).
|
|
3
|
+
*
|
|
4
|
+
* Predates the lifecycle system; this manifest is the static record
|
|
5
|
+
* required so the module participates in the registry. No install /
|
|
6
|
+
* uninstall hook today — the module's schema is owned by earlier
|
|
7
|
+
* platform-wide migrations.
|
|
8
|
+
*/
|
|
9
|
+
export declare const manifest: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
dependencies: string[];
|
|
14
|
+
description?: string | undefined;
|
|
15
|
+
acknowledgedDependencies?: {
|
|
16
|
+
moduleId: string;
|
|
17
|
+
port: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}[] | undefined;
|
|
20
|
+
nonBindingDependencies?: {
|
|
21
|
+
moduleId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
kind: "contributes-to" | "degrades-without" | "refuses-without";
|
|
24
|
+
reason: string;
|
|
25
|
+
whenAbsent?: string | undefined;
|
|
26
|
+
}[] | undefined;
|
|
27
|
+
activation?: {
|
|
28
|
+
settingCode: string;
|
|
29
|
+
default: boolean;
|
|
30
|
+
} | {
|
|
31
|
+
nonDeactivatable: true;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
settings?: {
|
|
35
|
+
moduleCode: string;
|
|
36
|
+
groups: {
|
|
37
|
+
code: string;
|
|
38
|
+
name: string;
|
|
39
|
+
salesChannelCodes?: string[] | undefined;
|
|
40
|
+
isSystemProtected?: boolean | undefined;
|
|
41
|
+
}[];
|
|
42
|
+
settings: {
|
|
43
|
+
code: string;
|
|
44
|
+
name: string;
|
|
45
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
46
|
+
defaultValue: unknown;
|
|
47
|
+
description?: string | undefined;
|
|
48
|
+
groupCode?: string | undefined;
|
|
49
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
50
|
+
salesChannelCodes?: string[] | undefined;
|
|
51
|
+
enumOptions?: string[] | undefined;
|
|
52
|
+
configurationType?: string | undefined;
|
|
53
|
+
hidden?: boolean | undefined;
|
|
54
|
+
}[];
|
|
55
|
+
} | undefined;
|
|
56
|
+
i18n?: {
|
|
57
|
+
bundlesDir: string;
|
|
58
|
+
} | undefined;
|
|
59
|
+
docs?: false | {
|
|
60
|
+
dir: string;
|
|
61
|
+
} | undefined;
|
|
62
|
+
demo?: false | {
|
|
63
|
+
summary: string;
|
|
64
|
+
seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
|
|
65
|
+
reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
|
|
66
|
+
after?: readonly string[] | undefined;
|
|
67
|
+
package?: string | undefined;
|
|
68
|
+
} | undefined;
|
|
69
|
+
actions?: {
|
|
70
|
+
id: string;
|
|
71
|
+
labelKey: string;
|
|
72
|
+
icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
|
|
73
|
+
targetRoute: string;
|
|
74
|
+
keywords: string[];
|
|
75
|
+
weight: number;
|
|
76
|
+
descriptionKey?: string | undefined;
|
|
77
|
+
requiredPermission?: string | undefined;
|
|
78
|
+
}[] | undefined;
|
|
79
|
+
permissions?: {
|
|
80
|
+
code: string;
|
|
81
|
+
label: string;
|
|
82
|
+
module?: string | undefined;
|
|
83
|
+
description?: string | undefined;
|
|
84
|
+
requires?: string[] | undefined;
|
|
85
|
+
}[] | undefined;
|
|
86
|
+
transactionalEmails?: {
|
|
87
|
+
code: string;
|
|
88
|
+
name: string;
|
|
89
|
+
variables: {
|
|
90
|
+
key: string;
|
|
91
|
+
label: string;
|
|
92
|
+
sampleValue?: string | undefined;
|
|
93
|
+
description?: string | undefined;
|
|
94
|
+
}[];
|
|
95
|
+
description?: string | undefined;
|
|
96
|
+
group?: string | undefined;
|
|
97
|
+
}[] | undefined;
|
|
98
|
+
capabilities?: string[] | undefined;
|
|
99
|
+
exclusiveCapabilities?: {
|
|
100
|
+
key: string;
|
|
101
|
+
errorCode: string;
|
|
102
|
+
}[] | undefined;
|
|
103
|
+
errorCodes?: {
|
|
104
|
+
code: string;
|
|
105
|
+
tokens?: string[] | undefined;
|
|
106
|
+
}[] | undefined;
|
|
107
|
+
blocks?: {
|
|
108
|
+
name: string;
|
|
109
|
+
labelKey: string;
|
|
110
|
+
category: string;
|
|
111
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
112
|
+
fields: Record<string, {
|
|
113
|
+
type: "number" | "object" | "array" | "uuid" | "text" | "textarea" | "select" | "radio" | "external" | "richtext";
|
|
114
|
+
label?: string | undefined;
|
|
115
|
+
required?: boolean | undefined;
|
|
116
|
+
options?: {
|
|
117
|
+
label: string;
|
|
118
|
+
value: string | number;
|
|
119
|
+
}[] | undefined;
|
|
120
|
+
refKind?: string | undefined;
|
|
121
|
+
}>;
|
|
122
|
+
descriptionKey?: string | undefined;
|
|
123
|
+
defaultProps?: Record<string, unknown> | undefined;
|
|
124
|
+
responsiveFields?: string[] | undefined;
|
|
125
|
+
previewIcon?: string | undefined;
|
|
126
|
+
weight?: number | undefined;
|
|
127
|
+
}[] | undefined;
|
|
128
|
+
blockCategories?: {
|
|
129
|
+
key: string;
|
|
130
|
+
titleKey: string;
|
|
131
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
132
|
+
weight?: number | undefined;
|
|
133
|
+
visible?: boolean | undefined;
|
|
134
|
+
}[] | undefined;
|
|
135
|
+
env?: {
|
|
136
|
+
name: string;
|
|
137
|
+
describes: {
|
|
138
|
+
en: string;
|
|
139
|
+
pl: string;
|
|
140
|
+
};
|
|
141
|
+
requirement: {
|
|
142
|
+
kind: "required";
|
|
143
|
+
} | {
|
|
144
|
+
kind: "requiredWhen";
|
|
145
|
+
input: string;
|
|
146
|
+
equals: string;
|
|
147
|
+
} | {
|
|
148
|
+
kind: "optional";
|
|
149
|
+
without: {
|
|
150
|
+
en: string;
|
|
151
|
+
pl: string;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
secret: boolean;
|
|
155
|
+
generable: boolean;
|
|
156
|
+
owner: {
|
|
157
|
+
kind: "platform";
|
|
158
|
+
} | {
|
|
159
|
+
kind: "application";
|
|
160
|
+
application: "admin" | "backend" | "storefront";
|
|
161
|
+
} | {
|
|
162
|
+
kind: "module";
|
|
163
|
+
moduleId: string;
|
|
164
|
+
};
|
|
165
|
+
consumers: ("admin" | "backend" | "storefront")[];
|
|
166
|
+
addressOf: "admin" | "backend" | "storefront" | null;
|
|
167
|
+
}[] | undefined;
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AA4BA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgKnB,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { defineModuleManifest } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* The demo data this module owns (feature 113, T222 — contract §1.3).
|
|
4
|
+
*
|
|
5
|
+
* A typed `const` rather than an inline object literal: declared inline the
|
|
6
|
+
* parameter infers from the schema and is `never`, so the author loses
|
|
7
|
+
* `context.ctx: ModuleContext`.
|
|
8
|
+
*
|
|
9
|
+
* Both bodies are reached by a **relative `await import()`** (§1.4), in
|
|
10
|
+
* `cliCommands.run`'s shape and for `cliCommands`' reason: a manifest is loaded
|
|
11
|
+
* by every process that composes the platform and by the check scripts that
|
|
12
|
+
* import the generated index, so a demo body imported at the top of this file
|
|
13
|
+
* would be a service graph pulled into all of them. It needs no `exports`
|
|
14
|
+
* subpath and no `files` entry (§1.5).
|
|
15
|
+
*
|
|
16
|
+
* **Which administrator holds which role is not here.** An `admin_users` row
|
|
17
|
+
* carrying an `admin_roles` id is two modules' rows in one statement, so the
|
|
18
|
+
* assignment is a composition step (§5.1) and belongs to whoever owns the
|
|
19
|
+
* instance.
|
|
20
|
+
*/
|
|
21
|
+
const demo = {
|
|
22
|
+
summary: 'The platform-administrator and sales-representative roles the demo signs in with.',
|
|
23
|
+
seed: async (context) => (await import('./backend/demo/seed.js')).seedDemo(context),
|
|
24
|
+
reset: async (context) => (await import('./backend/demo/reset.js')).resetDemo(context),
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Admin Roles module — manifest backfill (Module Lifecycle, feature 018).
|
|
28
|
+
*
|
|
29
|
+
* Predates the lifecycle system; this manifest is the static record
|
|
30
|
+
* required so the module participates in the registry. No install /
|
|
31
|
+
* uninstall hook today — the module's schema is owned by earlier
|
|
32
|
+
* platform-wide migrations.
|
|
33
|
+
*/
|
|
34
|
+
export const manifest = defineModuleManifest({
|
|
35
|
+
id: 'admin_roles',
|
|
36
|
+
name: 'Admin Roles',
|
|
37
|
+
description: 'Admin RBAC — roles, permissions, and policy enforcement for admin sessions.',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
dependencies: [],
|
|
40
|
+
/**
|
|
41
|
+
* Feature 075, Phase C — the two reads of the `AdminUser` row became
|
|
42
|
+
* `adminUserReadPort`, and `admin_users` declares this module, so declaring
|
|
43
|
+
* it back in `dependencies` closes a cycle that `module-graph.test.ts` and the
|
|
44
|
+
* composer generator both refuse. The mutual shape
|
|
45
|
+
* `acknowledgedDependencies` exists for, and the same one `auth` records
|
|
46
|
+
* against `customer_accounts`.
|
|
47
|
+
*
|
|
48
|
+
* With `admin_users` absent the seam fails closed: the port is gated, so the
|
|
49
|
+
* permission check refuses rather than answering from an admin row nobody
|
|
50
|
+
* read. That is the same answer every `requireAdmin` route on such a
|
|
51
|
+
* deployment gives, and it is why the edge is safe to withhold from
|
|
52
|
+
* `dependencies` — not a claim about which flips the orchestrator refuses,
|
|
53
|
+
* which is derived from the manifests on every run and copied here by nobody
|
|
54
|
+
* (D-100).
|
|
55
|
+
*/
|
|
56
|
+
acknowledgedDependencies: [
|
|
57
|
+
{
|
|
58
|
+
moduleId: 'admin_users',
|
|
59
|
+
port: 'adminUserReadPort',
|
|
60
|
+
reason: 'Answering "may this admin do this?" starts from the admin row — its role assignment ' +
|
|
61
|
+
'and whether it is still live — and that row belongs to admin_users, which declares ' +
|
|
62
|
+
'this module for the catalogue it reads back. Declaring it here closes a cycle. With ' +
|
|
63
|
+
'admin_users absent the seam fails closed: the port is gated, so the permission check ' +
|
|
64
|
+
'refuses rather than answering from an admin row nobody read.',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
// Feature 072/073 (Constitution XVII) — `admin_roles` answers "may this admin
|
|
68
|
+
// do this?" for every guarded route in the platform. Switched off, the
|
|
69
|
+
// question has no answer and `requireAdmin` has nothing to check against, so
|
|
70
|
+
// the correct behaviour would be to refuse every admin request — which is not
|
|
71
|
+
// a deployment anyone wants and not a state the orchestrator will produce.
|
|
72
|
+
demo,
|
|
73
|
+
activation: {
|
|
74
|
+
nonDeactivatable: true,
|
|
75
|
+
reason: 'Answers the permission check behind every guarded admin route; switched off, no admin ' +
|
|
76
|
+
'request could be authorised at all.',
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* This module's first i18n bundle — D-129's remaining sweep, MR 6.
|
|
80
|
+
*
|
|
81
|
+
* It exists because a **declaring** module that contributes no bundle file is
|
|
82
|
+
* `check:error-translations` exit 2 for the whole tree rather than a finding
|
|
83
|
+
* (`specs/090-module-owned-error-codes/contracts/error-translation-population.md`
|
|
84
|
+
* §2.1, §2.3; `d129-sweep.md` §3.4). The bundle lives at the **package
|
|
85
|
+
* root**, not under `dist`: `manifest-locations.ts` resolves a packaged
|
|
86
|
+
* module's `manifestPath` to its `package.json`, so `dirname` is the package
|
|
87
|
+
* directory. The file is a **flat** `{"a.b.c": "text"}` map, because a nested
|
|
88
|
+
* object fails `TranslationBundleEntriesSchema` and the boot reconciler logs
|
|
89
|
+
* and skips it — silently.
|
|
90
|
+
*/
|
|
91
|
+
i18n: { bundlesDir: 'i18n' },
|
|
92
|
+
docs: { dir: 'docs' },
|
|
93
|
+
/**
|
|
94
|
+
* This module's first command-palette action — feature 091, Phase 4, batch
|
|
95
|
+
* four — and it points at a route **`admin_users` declares**.
|
|
96
|
+
*
|
|
97
|
+
* That is the same split `src/admin/index.ts` makes for the sidebar entry and
|
|
98
|
+
* it is made for the same reason: the roles editor is served by
|
|
99
|
+
* `GET /api/v1/admin/admin-roles` in `admin_users`, so that module owns the
|
|
100
|
+
* route, while the advertisement of the roles capability is this module's.
|
|
101
|
+
* `check:action-route-permissions` resolves a `targetRoute` by path and not
|
|
102
|
+
* by owner, so it holds this declaration to the gate `/admin-roles` really
|
|
103
|
+
* enforces — `admin_users:manage`, which is why the code below is not an
|
|
104
|
+
* `admin_roles:*` one. This module registers no admin route and declares no
|
|
105
|
+
* permission of its own.
|
|
106
|
+
*
|
|
107
|
+
* It pays one of the fifteen entries `specs/deferred-defects.md` still holds
|
|
108
|
+
* under *"Sixteen modules with an admin screen declare no command-palette
|
|
109
|
+
* action"*, and the two labels live in this module's own bundle above.
|
|
110
|
+
*/
|
|
111
|
+
actions: [
|
|
112
|
+
{
|
|
113
|
+
id: 'open-admin-roles',
|
|
114
|
+
labelKey: 'actions.openAdminRoles.label',
|
|
115
|
+
descriptionKey: 'actions.openAdminRoles.description',
|
|
116
|
+
icon: 'ShieldCheck',
|
|
117
|
+
targetRoute: '/admin-roles',
|
|
118
|
+
requiredPermission: 'admin_users:manage',
|
|
119
|
+
keywords: ['roles', 'role', 'rbac', 'permissions', 'uprawnienia', 'access'],
|
|
120
|
+
weight: 300,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
/**
|
|
124
|
+
* The three `ADMIN_ROLE_*` codes — D-129's remaining sweep, **Tier C**
|
|
125
|
+
* (`specs/090-module-owned-error-codes/d129-sweep.md` §5.2, Appendix A; MR 6).
|
|
126
|
+
*
|
|
127
|
+
* They were declared by `_i18n` until this merge request, not because anybody
|
|
128
|
+
* judged them the platform's but because the deleted prefix chain had no rule
|
|
129
|
+
* for them and its last line was `return 'core'`. **D-121 T1 puts all three
|
|
130
|
+
* here**: the noun is an admin role, this module's own `admin_roles` row, and
|
|
131
|
+
* every one of the four raise sites is in this module's
|
|
132
|
+
* `services/admin-role-service.ts`. Noun and thrower agree, which is why the
|
|
133
|
+
* sweep classes this batch by its *content* rather than by its judgement —
|
|
134
|
+
* Tier C is the two modules where prose moves.
|
|
135
|
+
*
|
|
136
|
+
* **`ADMIN_ROLE_IN_USE` is the sweep's only multi-token sentence**, and the
|
|
137
|
+
* one code in it where a wrong key is a wrong sentence rather than a missing
|
|
138
|
+
* one. `tokens` is derived from the raise sites and not from the bundle
|
|
139
|
+
* (runbook §5): both raises pass a `details.code` — `assigned` when live
|
|
140
|
+
* admin accounts still hold the role, `assigned_to_deleted` when only
|
|
141
|
+
* soft-deleted ones do — measured by balanced-paren extraction of each call's
|
|
142
|
+
* own argument list, resolving `ERROR_CODES.<CODE>` and the bare literal
|
|
143
|
+
* alike. The two populations are counted apart because the remedy differs,
|
|
144
|
+
* which is issue #168; `test/unit/admin_roles/role-in-use-sentence.test.ts`
|
|
145
|
+
* renders a real refusal against these keys.
|
|
146
|
+
*
|
|
147
|
+
* **The base key `errors.ADMIN_ROLE_IN_USE` is unreachable, and it is carried
|
|
148
|
+
* anyway.** `localizeErrorEnvelope` composes `` `${target.key}.${token}` ``
|
|
149
|
+
* whenever `details.code` is present and has **no** fall-back to the base key
|
|
150
|
+
* (`packages/platform/src/http/error-envelope.ts`), so with both raise sites
|
|
151
|
+
* tokened nothing in the tree renders that sentence. Deleting it is not free,
|
|
152
|
+
* and the measurement is the reason it is still here: P1 asks its question at
|
|
153
|
+
* `errors.<CODE>` and at no other key, so the bundle without the base pair
|
|
154
|
+
* reports `ADMIN_ROLE_IN_USE → admin_roles (missing in: en, pl)` and exits 1
|
|
155
|
+
* — for the best-translated code in the sweep. The two ways out are both
|
|
156
|
+
* worse than one unreachable string. `UNTRANSLATED_ERROR_CODES` would be a
|
|
157
|
+
* false entry: its own header defines an entry as a code with **no sentence
|
|
158
|
+
* anywhere**, this code has four, and nothing could ever drain it. Widening
|
|
159
|
+
* P1 to accept "every declared token answered" is the real repair, and it is
|
|
160
|
+
* a change to a tree-wide instrument made by the merge request that benefits
|
|
161
|
+
* from it — reported for the defect register instead, with the second half of
|
|
162
|
+
* the same finding: three places in this repository document a fall-back to
|
|
163
|
+
* `errors.<CODE>` that the envelope does not implement
|
|
164
|
+
* (`packages/contracts/src/modules.ts`'s `errorCodeTokenRe` note,
|
|
165
|
+
* `check-error-translations.ts`' `ERROR_KEY` note, and the test above).
|
|
166
|
+
*
|
|
167
|
+
* **The other two arrive with prose.** Both carried a placeholder in `_i18n`'s
|
|
168
|
+
* bundle — `"Admin Role Code Taken."` / `"Błąd: admin role code taken."` and
|
|
169
|
+
* the same for `ADMIN_ROLE_PROTECTED` — which D-186 §2 deletes rather than
|
|
170
|
+
* carries, because in this module's own bundle it would read as this module's
|
|
171
|
+
* answer. §5.4 keeps writing the prose available and is not a re-opening of
|
|
172
|
+
* that ruling, and both are the case for taking it: the reader is an operator
|
|
173
|
+
* on `/admin-roles`, saving a role or pressing Delete on a row, and each
|
|
174
|
+
* refusal has one remedy to offer.
|
|
175
|
+
*
|
|
176
|
+
* `ADMIN_ROLE_PROTECTED` needed one more thing first. Its raise interpolates
|
|
177
|
+
* the role's own code into the English message and passed no `details`, so a
|
|
178
|
+
* translated sentence with no placeholder would have rendered cleanly and
|
|
179
|
+
* lost the value — the shape !1181 measured at 85 codes tree-wide. The raise
|
|
180
|
+
* now carries `details: { role: role.code }` and both sentences name it as
|
|
181
|
+
* `{role}`. The member is `role` and not `code` deliberately: `details.code`
|
|
182
|
+
* is the refusal **token**, so the obvious spelling would have sent the
|
|
183
|
+
* envelope looking for `errors.ADMIN_ROLE_PROTECTED.<role code>` and rendered
|
|
184
|
+
* nothing at all.
|
|
185
|
+
*/
|
|
186
|
+
errorCodes: [
|
|
187
|
+
{ code: 'ADMIN_ROLE_CODE_TAKEN' },
|
|
188
|
+
{ code: 'ADMIN_ROLE_IN_USE', tokens: ['assigned', 'assigned_to_deleted'] },
|
|
189
|
+
{ code: 'ADMIN_ROLE_PROTECTED' },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAA2B,MAAM,4BAA4B,CAAC;AAG3F;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,IAAI,GAAsC;IAC9C,OAAO,EAAE,mFAAmF;IAC5F,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnF,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC;CACvF,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,aAAa;IACjB,IAAI,EAAE,aAAa;IACnB,WAAW,EACT,6EAA6E;IAC/E,OAAO,EAAE,OAAO;IAChB,YAAY,EAAE,EAAE;IAChB;;;;;;;;;;;;;;;OAeG;IACH,wBAAwB,EAAE;QACxB;YACE,QAAQ,EAAE,aAAa;YACvB,IAAI,EAAE,mBAAmB;YACzB,MAAM,EACJ,sFAAsF;gBACtF,qFAAqF;gBACrF,sFAAsF;gBACtF,uFAAuF;gBACvF,8DAA8D;SACjE;KACF;IACD,8EAA8E;IAC9E,uEAAuE;IACvE,6EAA6E;IAC7E,8EAA8E;IAC9E,2EAA2E;IAC3E,IAAI;IACJ,UAAU,EAAE;QACV,gBAAgB,EAAE,IAAI;QACtB,MAAM,EACJ,wFAAwF;YACxF,qCAAqC;KACxC;IACD;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,EAAE;QACP;YACE,EAAE,EAAE,kBAAkB;YACtB,QAAQ,EAAE,8BAA8B;YACxC,cAAc,EAAE,oCAAoC;YACpD,IAAI,EAAE,aAAa;YACnB,WAAW,EAAE,cAAc;YAC3B,kBAAkB,EAAE,oBAAoB;YACxC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,CAAC;YAC3E,MAAM,EAAE,GAAG;SACZ;KACF;IACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8DG;IACH,UAAU,EAAE;QACV,EAAE,IAAI,EAAE,uBAAuB,EAAE;QACjC,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,CAAC,UAAU,EAAE,qBAAqB,CAAC,EAAE;QAC1E,EAAE,IAAI,EAAE,sBAAsB,EAAE;KACjC;CACF,CAAC,CAAC"}
|