@lbstack/accessx 0.1.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.
@@ -0,0 +1,11 @@
1
+ import * as React from "react";
2
+ import { PermissionType } from "./types";
3
+ export declare function createAccessUI(): {
4
+ useCan: (granted: PermissionType[], permission: PermissionType) => boolean;
5
+ Can: ({ permissions, permission, children, }: {
6
+ permissions: PermissionType[];
7
+ permission: PermissionType;
8
+ children: React.ReactNode;
9
+ }) => React.ReactNode;
10
+ canDo: (granted: PermissionType[], permission: PermissionType) => boolean;
11
+ };
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ export function createAccessUI() {
3
+ function useCan(granted, permission) {
4
+ return React.useMemo(() => canDo(granted, permission), [granted, permission]);
5
+ }
6
+ function Can({ permissions, permission, children, }) {
7
+ return canDo(permissions, permission) ? children : null;
8
+ }
9
+ // โœ… Function-level permission checker
10
+ function canDo(granted, permission) {
11
+ if (granted.includes("*")) {
12
+ console.warn(`Global '*' permission used: allowing all permissions!`);
13
+ return true;
14
+ }
15
+ if (granted.includes(permission))
16
+ return true;
17
+ const [resource] = permission.split(":");
18
+ if (granted.includes(`${resource}:*`))
19
+ return true;
20
+ return false;
21
+ }
22
+ return { useCan, Can, canDo };
23
+ }
@@ -0,0 +1,23 @@
1
+ import { Resource } from "./types";
2
+ export declare function createAccess<const R extends readonly string[], const A extends readonly string[], const Res extends readonly Resource[]>(config: {
3
+ roles: R;
4
+ actions: A;
5
+ resources: Res;
6
+ }): {
7
+ roles: R;
8
+ actions: A;
9
+ resources: Res;
10
+ permissions: {
11
+ key: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`;
12
+ resource: Resource;
13
+ action: string;
14
+ }[];
15
+ permissionKeys: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
16
+ allow: (role: R[number], permission: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[]) => void;
17
+ can: (granted: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], required: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) => boolean;
18
+ resolvePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
19
+ normalizePermissions: (raw: string[]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
20
+ assignPermissions: (role: R[number], perms: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[]) => void;
21
+ getRolePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
22
+ hasPermission: (granted: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], required: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) => boolean;
23
+ };
@@ -0,0 +1,54 @@
1
+ export function createAccess(config) {
2
+ const permissions = config.resources.flatMap((resource) => config.actions.map((action) => ({
3
+ key: `${resource.key}:${action}`,
4
+ resource,
5
+ action,
6
+ })));
7
+ const permissionKeys = permissions.map((p) => p.key);
8
+ const rolePermissions = new Map();
9
+ function assignPermissions(role, perms) {
10
+ const list = Array.isArray(perms) ? perms : [perms];
11
+ if (!rolePermissions.has(role))
12
+ rolePermissions.set(role, new Set());
13
+ list.forEach((p) => rolePermissions.get(role).add(p));
14
+ }
15
+ function getRolePermissions(role) {
16
+ return Array.from(rolePermissions.get(role) ?? []);
17
+ }
18
+ function allow(role, permission) {
19
+ assignPermissions(role, permission);
20
+ }
21
+ function resolvePermissions(role) {
22
+ return getRolePermissions(role);
23
+ }
24
+ function hasPermission(granted, required) {
25
+ if (granted.includes("*")) {
26
+ console.warn("Global '*' permission used: allowing all permissions!");
27
+ return true;
28
+ }
29
+ if (granted.includes(required))
30
+ return true;
31
+ const [resource] = required.split(":");
32
+ if (granted.includes(`${resource}:*`))
33
+ return true;
34
+ return false;
35
+ }
36
+ function normalizePermissions(raw) {
37
+ const valid = new Set(permissionKeys);
38
+ return raw.filter((p) => valid.has(p));
39
+ }
40
+ return {
41
+ roles: config.roles,
42
+ actions: config.actions,
43
+ resources: config.resources,
44
+ permissions,
45
+ permissionKeys,
46
+ allow,
47
+ can: hasPermission,
48
+ resolvePermissions,
49
+ normalizePermissions,
50
+ assignPermissions,
51
+ getRolePermissions,
52
+ hasPermission,
53
+ };
54
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./src/core/access-control";
2
+ export * from "./src/core/ui/access-control-ui";
3
+ export * from "./src/types";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./src/core/access-control";
2
+ export * from "./src/core/ui/access-control-ui";
3
+ export * from "./src/types";
@@ -0,0 +1,23 @@
1
+ import { ConditionFn, Resource } from "../types";
2
+ export declare function createAccess<const R extends readonly string[], const A extends readonly string[], const Res extends readonly Resource[]>(config: {
3
+ roles: R;
4
+ actions: A;
5
+ resources: Res;
6
+ }): {
7
+ roles: R;
8
+ actions: A;
9
+ resources: Res;
10
+ permissions: {
11
+ key: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`;
12
+ resource: Resource;
13
+ action: string;
14
+ }[];
15
+ permissionKeys: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
16
+ allow: (role: R[number], permission: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], condition?: ConditionFn) => void;
17
+ can: (role: R[number], permission: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`, context?: any) => boolean;
18
+ hasPermission: (granted: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], required: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`, context?: any) => boolean;
19
+ resolvePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
20
+ normalizePermissions: (raw: string[]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
21
+ assignPermissions: (role: R[number], perms: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], condition?: ConditionFn) => void;
22
+ getRolePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
23
+ };
@@ -0,0 +1,85 @@
1
+ export function createAccess(config) {
2
+ const permissions = config.resources.flatMap((resource) => config.actions.map((action) => ({
3
+ key: `${resource.key}:${action}`,
4
+ resource,
5
+ action,
6
+ })));
7
+ const permissionKeys = permissions.map((p) => p.key);
8
+ // Store perms with optional conditions
9
+ const rolePermissions = new Map();
10
+ function assignPermissions(role, perms, condition) {
11
+ const list = Array.isArray(perms) ? perms : [perms];
12
+ if (!rolePermissions.has(role))
13
+ rolePermissions.set(role, new Map());
14
+ const permsMap = rolePermissions.get(role);
15
+ list.forEach((p) => permsMap.set(p, condition || (() => true)));
16
+ }
17
+ function getRolePermissions(role) {
18
+ const permsMap = rolePermissions.get(role);
19
+ return permsMap ? Array.from(permsMap.keys()) : [];
20
+ }
21
+ function allow(role, permission, condition) {
22
+ assignPermissions(role, permission, condition);
23
+ }
24
+ function resolvePermissions(role) {
25
+ return getRolePermissions(role);
26
+ }
27
+ function hasPermission(granted, required, context) {
28
+ // This is a static check for local lists.
29
+ // For ABAC, we need the actual definition with conditions.
30
+ // If 'granted' is just string[], we can't check conditions unless we have the role definition.
31
+ if (granted.includes("*"))
32
+ return true;
33
+ if (granted.includes(required))
34
+ return true;
35
+ const [resource] = required.split(":");
36
+ if (granted.includes(`${resource}:*`))
37
+ return true;
38
+ return false;
39
+ }
40
+ // New: Role-aware permission check (supports ABAC)
41
+ function can(role, permission, context) {
42
+ const permsMap = rolePermissions.get(role);
43
+ if (!permsMap)
44
+ return false;
45
+ // 1. Global wildcard
46
+ if (permsMap.has("*")) {
47
+ const condition = permsMap.get("*");
48
+ if (condition(context))
49
+ return true;
50
+ }
51
+ // 2. Direct match
52
+ if (permsMap.has(permission)) {
53
+ const condition = permsMap.get(permission);
54
+ if (condition(context))
55
+ return true;
56
+ }
57
+ // 3. Resource wildcard
58
+ const [resource] = permission.split(":");
59
+ const resourceWildcard = `${resource}:*`;
60
+ if (permsMap.has(resourceWildcard)) {
61
+ const condition = permsMap.get(resourceWildcard);
62
+ if (condition(context))
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+ function normalizePermissions(raw) {
68
+ const valid = new Set(permissionKeys);
69
+ return raw.filter((p) => valid.has(p));
70
+ }
71
+ return {
72
+ roles: config.roles,
73
+ actions: config.actions,
74
+ resources: config.resources,
75
+ permissions,
76
+ permissionKeys,
77
+ allow,
78
+ can, // Role-based check (ABAC supported)
79
+ hasPermission, // Static list check (Legacy/Simple)
80
+ resolvePermissions,
81
+ normalizePermissions,
82
+ assignPermissions,
83
+ getRolePermissions,
84
+ };
85
+ }
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ import { PermissionType } from "../../types";
3
+ export declare function createAccessUI(): {
4
+ useCan: (granted: PermissionType[], permission: PermissionType, context?: any) => boolean;
5
+ Can: ({ permissions, permission, context, children, }: {
6
+ permissions: PermissionType[];
7
+ permission: PermissionType;
8
+ context?: any;
9
+ children: React.ReactNode;
10
+ }) => React.ReactNode;
11
+ canDo: (granted: PermissionType[], permission: PermissionType, context?: any) => boolean;
12
+ };
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ export function createAccessUI() {
3
+ function useCan(granted, permission, context) {
4
+ return React.useMemo(() => canDo(granted, permission, context), [granted, permission, context]);
5
+ }
6
+ function Can({ permissions, permission, context, children, }) {
7
+ return canDo(permissions, permission, context) ? children : null;
8
+ }
9
+ // โœ… Function-level permission checker
10
+ // Note: This is a static list check. If conditions are needed on frontend,
11
+ // the frontend list should contain the logic or be checked against a role-aware engine.
12
+ function canDo(granted, permission, context) {
13
+ if (granted.includes("*"))
14
+ return true;
15
+ if (granted.includes(permission))
16
+ return true;
17
+ const [resource] = permission.split(":");
18
+ if (granted.includes(`${resource}:*`))
19
+ return true;
20
+ return false;
21
+ }
22
+ return { useCan, Can, canDo };
23
+ }
@@ -0,0 +1,10 @@
1
+ export type RoleType = string;
2
+ export type ActionType = string;
3
+ export type ResourceKeyType = string;
4
+ export type PermissionType = `${ResourceKeyType}:${ActionType}` | `${ResourceKeyType}:*` | "*";
5
+ export type ConditionFn<TContext = any> = (context: TContext) => boolean;
6
+ export interface Resource {
7
+ name: string;
8
+ key: ResourceKeyType;
9
+ description?: string;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export type RoleType = string;
2
+ export type ActionType = string;
3
+ export type ResourceKeyType = string;
4
+ export type PermissionType = `${ResourceKeyType}:${ActionType}` | `${ResourceKeyType}:*` | "*";
5
+ export interface Resource {
6
+ name: string;
7
+ key: ResourceKeyType;
8
+ description?: string;
9
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@lbstack/accessx",
3
+ "version": "0.1.0",
4
+ "description": "A role & resource based access control system with end to end type safety.",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "jest",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "devDependencies": {
21
+ "@types/jest": "^29.5.11",
22
+ "@types/react": "^18.2.48",
23
+ "jest": "^29.7.0",
24
+ "ts-jest": "^29.1.1",
25
+ "typescript": "^5.3.3"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=16.8.0"
29
+ }
30
+ }
package/readme.md ADDED
@@ -0,0 +1,315 @@
1
+ # @lbstack/accessx
2
+
3
+ A **TypeScript-first RBAC permission engine** with **automatic permission generation**, designed to be the **single source of truth** for:
4
+
5
+ - Backend authorization
6
+ - Frontend UI access control
7
+ - Database permission storage
8
+ - Admin permission management
9
+
10
+ No manual permission strings.
11
+ No role leakage to frontend.
12
+ Full autocomplete everywhere.
13
+
14
+ ---
15
+
16
+ ## โœจ Key Features
17
+
18
+ - ๐Ÿ” **Automatic permission generation**
19
+ - ๐Ÿง  **Strong TypeScript inference & autocomplete**
20
+ - ๐Ÿ—๏ธ **Single initialization โ€“ use everywhere**
21
+ - ๐Ÿ–ฅ๏ธ Backend (Express / Nest / Hono)
22
+ - ๐ŸŽจ Frontend (React hooks & components)
23
+ - ๐Ÿ—„๏ธ Database-friendly permission keys
24
+ - ๐Ÿ“ฆ Package & framework agnostic
25
+
26
+ ---
27
+
28
+ ## ๐Ÿงฉ Core Concept
29
+
30
+ You define:
31
+
32
+ - **Roles**
33
+ - **Actions**
34
+ - **Resources (modules)**
35
+
36
+ The package automatically generates permissions in this format:
37
+
38
+ RESOURCE_KEY:ACTION
39
+
40
+ Example:
41
+
42
+ BLOGS:CREATE
43
+ BLOGS:READ
44
+ USER:DELETE
45
+
46
+ These permission keys are:
47
+
48
+ - Stored in DB
49
+ - Sent to frontend after login
50
+ - Used in UI & API checks
51
+ - Fully type-safe
52
+
53
+ ---
54
+
55
+ ## ๐Ÿ“ฆ Installation
56
+
57
+ ```bash
58
+ npm install @lbstack/accessx
59
+
60
+
61
+ or
62
+
63
+ pnpm add @lbstack/accessx
64
+
65
+ ๐Ÿš€ Initialization (Single Source of Truth)
66
+ import { createAccess } from "@lbstack/accessx";
67
+
68
+ export const access = createAccess({
69
+ roles: ["ADMIN", "EDITOR", "CUSTOMER"] as const,
70
+
71
+ actions: ["CREATE", "READ", "UPDATE", "DELETE"] as const,
72
+
73
+ resources: [
74
+ {
75
+ name: "Users",
76
+ key: "USER",
77
+ description: "User management",
78
+ },
79
+ {
80
+ name: "Blogs",
81
+ key: "BLOGS",
82
+ description: "Blog posts",
83
+ },
84
+ ] as const,
85
+ });
86
+
87
+
88
+ โš ๏ธ as const is required for TypeScript autocomplete.
89
+
90
+ ๐Ÿ”‘ Automatically Generated Permissions
91
+ access.permissionKeys
92
+
93
+ [
94
+ "USER:CREATE",
95
+ "USER:READ",
96
+ "USER:UPDATE",
97
+ "USER:DELETE",
98
+ "BLOGS:CREATE",
99
+ "BLOGS:READ",
100
+ "BLOGS:UPDATE",
101
+ "BLOGS:DELETE",
102
+ ]
103
+
104
+
105
+ No permission strings are written manually.
106
+
107
+ ๐Ÿ—„๏ธ Database Usage
108
+ Seed permissions table
109
+ await db.permissions.insertMany(access.permissions);
110
+
111
+
112
+ Each permission contains metadata:
113
+
114
+ {
115
+ key: "BLOGS:CREATE",
116
+ resource: { name, key, description },
117
+ action: "CREATE"
118
+ }
119
+
120
+ ๐Ÿ” Backend Usage
121
+ Assign permissions to roles
122
+ access.allow("ADMIN", "USER:DELETE");
123
+ access.allow("EDITOR", "BLOGS:CREATE");
124
+
125
+ Check permission (Service / Controller)
126
+ access.can(user.role, "BLOGS:UPDATE");
127
+
128
+ Normalize permissions from DB
129
+ const permissionsFromDb = ["BLOGS:READ", "USER:DELETE"];
130
+
131
+ const permissions = access.normalizePermissions(permissionsFromDb);
132
+
133
+
134
+ Ensures only valid generated permissions are used.
135
+
136
+ Login Response
137
+ res.json({
138
+ user,
139
+ permissions: access.resolvePermissions(user.role),
140
+ });
141
+
142
+ ๐ŸŽจ Frontend Usage (React)
143
+
144
+ Frontend never uses roles.
145
+ It only receives resolved permissions.
146
+
147
+ useCan Hook
148
+ const canEdit = access.useCan(
149
+ auth.permissions,
150
+ "BLOGS:UPDATE"
151
+ );
152
+
153
+ <button disabled={!canEdit}>Edit</button>
154
+
155
+ <Can /> Component
156
+ <access.Can
157
+ permissions={auth.permissions}
158
+ permission="USER:DELETE"
159
+ >
160
+ <DeleteUserButton />
161
+ </access.Can>
162
+
163
+ ๐Ÿง  Type Safety & Autocomplete
164
+
165
+ Invalid permission โ†’ โŒ TypeScript error
166
+
167
+ Invalid resource/action โ†’ โŒ TypeScript error
168
+
169
+ IDE auto-suggests valid permissions everywhere
170
+
171
+ // โŒ Invalid
172
+ "BLOGS:PUBLISH"
173
+
174
+ // โœ… Valid
175
+ "BLOGS:CREATE"
176
+
177
+ ๐Ÿ—๏ธ API Reference
178
+ Metadata
179
+ access.roles
180
+ access.actions
181
+ access.resources
182
+ access.permissions
183
+ access.permissionKeys
184
+
185
+ Backend
186
+ access.allow(role, permission)
187
+ access.can(role, permission)
188
+ access.resolvePermissions(role)
189
+ access.normalizePermissions(raw)
190
+
191
+ Frontend
192
+ access.useCan(permissions, permission)
193
+ <access.Can />
194
+
195
+ ๐Ÿ†š Comparison with Keycloak
196
+
197
+ This package is an application-level authorization engine, not a full IAM system.
198
+
199
+ Area @accessx/core Keycloak
200
+ Identity โŒ โœ…
201
+ RBAC Core โœ… โœ…
202
+ Type Safety โœ… โŒ
203
+ Frontend Hooks โœ… โŒ
204
+ Performance โœ… โŒ
205
+ Admin UI โŒ โœ…
206
+ Multi-App SSO โŒ โœ…
207
+ Runtime Policy Change โŒ โœ…
208
+ Enterprise Compliance โŒ โœ…
209
+ Ops Overhead Low High
210
+ Strengths
211
+
212
+ Excellent developer experience (TypeScript-first)
213
+
214
+ Frontend hooks and React components
215
+
216
+ High performance and low latency
217
+
218
+ Embedded, framework-agnostic, zero ops
219
+
220
+ Weaknesses Compared to Keycloak
221
+
222
+ No identity or login management
223
+
224
+ No multi-application SSO
225
+
226
+ No admin UI or delegation
227
+
228
+ Permissions mostly static (redeploy needed for new resources/actions)
229
+
230
+ No ABAC / advanced policy language
231
+
232
+ No enterprise audit / compliance tooling
233
+
234
+ Ideal Usage
235
+ Keycloak (Authentication + Identity)
236
+ โ†“
237
+ JWT / Claims
238
+ โ†“
239
+ @accessx/core (Authorization + UI permissions)
240
+
241
+
242
+ This is complementary, not a replacement for Keycloak.
243
+
244
+ ๐Ÿ† Why Use @accessx/core?
245
+
246
+ Zero manual permission creation
247
+
248
+ DB, backend & frontend always in sync
249
+
250
+ Enterprise-grade RBAC foundation for apps
251
+
252
+ Scales to ABAC, multi-role, multi-tenant systems
253
+
254
+ ## ๐Ÿง  ABAC (Attribute-Based Access Control)
255
+
256
+ You can define dynamic permissions based on context (e.g., user ID, ownership checking).
257
+
258
+ ### 1. Define with Conditions
259
+ ```typescript
260
+ access.allow("USER", "BLOG:UPDATE", (context: { user: any; post: any }) => {
261
+ return context.post.authorId === context.user.id;
262
+ });
263
+ ```
264
+
265
+ ### 2. Check with Context
266
+ The `can` method accepts an optional context object as the third argument.
267
+
268
+ ```typescript
269
+ const canEdit = access.can("USER", "BLOG:UPDATE", { user, post });
270
+ ```
271
+
272
+ ### 3. Frontend Usage
273
+ React components also support `context`.
274
+
275
+ ```tsx
276
+ <Can permissions={myPermissions} permission="BLOG:UPDATE" context={{ user, post }}>
277
+ <button>Edit Post</button>
278
+ </Can>
279
+ ```
280
+
281
+ ## ๐Ÿงช Testing
282
+
283
+ The package includes a comprehensive test suite using Jest.
284
+
285
+ ```bash
286
+ npm test
287
+ ```
288
+
289
+ ## ๐Ÿ›ฃ๏ธ Roadmap
290
+
291
+ Wildcard permissions (BLOGS:*)
292
+
293
+ Multi-role users
294
+
295
+ Attribute-based access control (ABAC)
296
+
297
+ Permission groups
298
+
299
+ JWT permission compression
300
+
301
+ CLI generator
302
+
303
+ ๐Ÿ“„ License
304
+
305
+ MIT
306
+
307
+ ๐Ÿ’ก Inspiration
308
+
309
+ Zanzibar (Google)
310
+
311
+ Auth0 / Keycloak permission models
312
+
313
+ CASL & OPA (simplified DX)
314
+
315
+ One definition. One truth. Everywhere. ๐Ÿ”โœจ