hazo_auth 3.0.4 → 4.0.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.
Files changed (61) hide show
  1. package/README.md +146 -0
  2. package/SETUP_CHECKLIST.md +369 -0
  3. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
  4. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +2 -2
  5. package/dist/components/layouts/rbac_test/index.d.ts +15 -0
  6. package/dist/components/layouts/rbac_test/index.d.ts.map +1 -0
  7. package/dist/components/layouts/rbac_test/index.js +378 -0
  8. package/dist/components/layouts/shared/components/password_field.js +1 -1
  9. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  10. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  11. package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
  12. package/dist/components/layouts/user_management/components/roles_matrix.d.ts +2 -3
  13. package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
  14. package/dist/components/layouts/user_management/components/roles_matrix.js +133 -8
  15. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +12 -0
  16. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -0
  17. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +291 -0
  18. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +13 -0
  19. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -0
  20. package/dist/components/layouts/user_management/components/scope_labels_tab.js +158 -0
  21. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +11 -0
  22. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -0
  23. package/dist/components/layouts/user_management/components/user_scopes_tab.js +267 -0
  24. package/dist/components/layouts/user_management/index.d.ts +9 -2
  25. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  26. package/dist/components/layouts/user_management/index.js +22 -6
  27. package/dist/components/ui/select.d.ts +14 -0
  28. package/dist/components/ui/select.d.ts.map +1 -0
  29. package/dist/components/ui/select.js +59 -0
  30. package/dist/components/ui/tree-view.d.ts +108 -0
  31. package/dist/components/ui/tree-view.d.ts.map +1 -0
  32. package/dist/components/ui/tree-view.js +194 -0
  33. package/dist/lib/auth/auth_types.d.ts +45 -0
  34. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  35. package/dist/lib/auth/auth_types.js +13 -0
  36. package/dist/lib/auth/hazo_get_auth.server.d.ts +4 -2
  37. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  38. package/dist/lib/auth/hazo_get_auth.server.js +107 -3
  39. package/dist/lib/auth/scope_cache.d.ts +92 -0
  40. package/dist/lib/auth/scope_cache.d.ts.map +1 -0
  41. package/dist/lib/auth/scope_cache.js +171 -0
  42. package/dist/lib/scope_hierarchy_config.server.d.ts +39 -0
  43. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -0
  44. package/dist/lib/scope_hierarchy_config.server.js +96 -0
  45. package/dist/lib/services/email_service.d.ts.map +1 -1
  46. package/dist/lib/services/email_service.js +7 -2
  47. package/dist/lib/services/profile_picture_service.d.ts +1 -7
  48. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  49. package/dist/lib/services/profile_picture_service.js +77 -32
  50. package/dist/lib/services/registration_service.js +1 -1
  51. package/dist/lib/services/scope_labels_service.d.ts +48 -0
  52. package/dist/lib/services/scope_labels_service.d.ts.map +1 -0
  53. package/dist/lib/services/scope_labels_service.js +277 -0
  54. package/dist/lib/services/scope_service.d.ts +114 -0
  55. package/dist/lib/services/scope_service.d.ts.map +1 -0
  56. package/dist/lib/services/scope_service.js +582 -0
  57. package/dist/lib/services/user_scope_service.d.ts +74 -0
  58. package/dist/lib/services/user_scope_service.d.ts.map +1 -0
  59. package/dist/lib/services/user_scope_service.js +415 -0
  60. package/hazo_auth_config.example.ini +1 -1
  61. package/package.json +3 -1
@@ -0,0 +1,92 @@
1
+ import type { ScopeLevel } from "../services/scope_service";
2
+ /**
3
+ * User scope assignment record
4
+ */
5
+ export type UserScopeEntry = {
6
+ scope_type: ScopeLevel;
7
+ scope_id: string;
8
+ scope_seq: string;
9
+ };
10
+ /**
11
+ * Cache entry structure for user scopes
12
+ */
13
+ type ScopeCacheEntry = {
14
+ user_id: string;
15
+ scopes: UserScopeEntry[];
16
+ timestamp: number;
17
+ cache_version: number;
18
+ };
19
+ /**
20
+ * LRU cache implementation for user scope lookups
21
+ * Uses Map to maintain insertion order for LRU eviction
22
+ */
23
+ declare class ScopeCache {
24
+ private cache;
25
+ private max_size;
26
+ private ttl_ms;
27
+ private scope_version_map;
28
+ constructor(max_size: number, ttl_minutes: number);
29
+ /**
30
+ * Gets a cache entry for a user's scopes
31
+ * Returns undefined if not found or expired
32
+ * @param user_id - User ID to look up
33
+ * @returns Cache entry or undefined
34
+ */
35
+ get(user_id: string): ScopeCacheEntry | undefined;
36
+ /**
37
+ * Sets a cache entry for a user's scopes
38
+ * Evicts least recently used entries if cache is full
39
+ * @param user_id - User ID
40
+ * @param scopes - User's scope assignments
41
+ */
42
+ set(user_id: string, scopes: UserScopeEntry[]): void;
43
+ /**
44
+ * Invalidates cache for a specific user
45
+ * @param user_id - User ID to invalidate
46
+ */
47
+ invalidate_user(user_id: string): void;
48
+ /**
49
+ * Invalidates cache for all users with a specific scope
50
+ * Uses cache version to determine if invalidation is needed
51
+ * @param scope_type - Scope level
52
+ * @param scope_id - Scope ID to invalidate
53
+ */
54
+ invalidate_by_scope(scope_type: ScopeLevel, scope_id: string): void;
55
+ /**
56
+ * Invalidates cache for all users with any scope of a specific level
57
+ * @param scope_type - Scope level to invalidate
58
+ */
59
+ invalidate_by_scope_level(scope_type: ScopeLevel): void;
60
+ /**
61
+ * Invalidates all cache entries
62
+ */
63
+ invalidate_all(): void;
64
+ /**
65
+ * Gets the maximum cache version for a set of scopes
66
+ * Used to determine if cache entry is stale
67
+ * @param scopes - Array of scope entries
68
+ * @returns Maximum version number
69
+ */
70
+ private get_max_scope_version;
71
+ /**
72
+ * Gets cache statistics
73
+ * @returns Object with cache size and max size
74
+ */
75
+ get_stats(): {
76
+ size: number;
77
+ max_size: number;
78
+ };
79
+ }
80
+ /**
81
+ * Gets or creates the global scope cache instance
82
+ * @param max_size - Maximum cache size (default: 5000)
83
+ * @param ttl_minutes - TTL in minutes (default: 15)
84
+ * @returns Scope cache instance
85
+ */
86
+ export declare function get_scope_cache(max_size?: number, ttl_minutes?: number): ScopeCache;
87
+ /**
88
+ * Resets the global scope cache instance (useful for testing)
89
+ */
90
+ export declare function reset_scope_cache(): void;
91
+ export {};
92
+ //# sourceMappingURL=scope_cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope_cache.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/scope_cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAI5D;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,KAAK,eAAe,GAAG;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,cAAM,UAAU;IACd,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,iBAAiB,CAAsB;gBAEnC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAOjD;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IA8BjD;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI;IAwBpD;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAItC;;;;;OAKG;IACH,mBAAmB,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAsBnE;;;OAGG;IACH,yBAAyB,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAevD;;OAEG;IACH,cAAc,IAAI,IAAI;IAKtB;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAe7B;;;OAGG;IACH,SAAS,IAAI;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB;CAMF;AAMD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,GAAE,MAAa,EACvB,WAAW,GAAE,MAAW,GACvB,UAAU,CAKZ;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * LRU cache implementation for user scope lookups
3
+ * Uses Map to maintain insertion order for LRU eviction
4
+ */
5
+ class ScopeCache {
6
+ constructor(max_size, ttl_minutes) {
7
+ this.cache = new Map();
8
+ this.max_size = max_size;
9
+ this.ttl_ms = ttl_minutes * 60 * 1000;
10
+ this.scope_version_map = new Map();
11
+ }
12
+ /**
13
+ * Gets a cache entry for a user's scopes
14
+ * Returns undefined if not found or expired
15
+ * @param user_id - User ID to look up
16
+ * @returns Cache entry or undefined
17
+ */
18
+ get(user_id) {
19
+ const entry = this.cache.get(user_id);
20
+ if (!entry) {
21
+ return undefined;
22
+ }
23
+ const now = Date.now();
24
+ const age = now - entry.timestamp;
25
+ // Check if entry is expired (TTL)
26
+ if (age > this.ttl_ms) {
27
+ this.cache.delete(user_id);
28
+ return undefined;
29
+ }
30
+ // Check if any of user's scopes have been invalidated
31
+ const max_scope_version = this.get_max_scope_version(entry.scopes);
32
+ if (max_scope_version > entry.cache_version) {
33
+ this.cache.delete(user_id);
34
+ return undefined;
35
+ }
36
+ // Move to end (most recently used)
37
+ this.cache.delete(user_id);
38
+ this.cache.set(user_id, entry);
39
+ return entry;
40
+ }
41
+ /**
42
+ * Sets a cache entry for a user's scopes
43
+ * Evicts least recently used entries if cache is full
44
+ * @param user_id - User ID
45
+ * @param scopes - User's scope assignments
46
+ */
47
+ set(user_id, scopes) {
48
+ // Evict LRU entries if cache is full
49
+ while (this.cache.size >= this.max_size) {
50
+ const first_key = this.cache.keys().next().value;
51
+ if (first_key) {
52
+ this.cache.delete(first_key);
53
+ }
54
+ else {
55
+ break;
56
+ }
57
+ }
58
+ // Get current cache version for user's scopes
59
+ const cache_version = this.get_max_scope_version(scopes);
60
+ const entry = {
61
+ user_id,
62
+ scopes,
63
+ timestamp: Date.now(),
64
+ cache_version,
65
+ };
66
+ this.cache.set(user_id, entry);
67
+ }
68
+ /**
69
+ * Invalidates cache for a specific user
70
+ * @param user_id - User ID to invalidate
71
+ */
72
+ invalidate_user(user_id) {
73
+ this.cache.delete(user_id);
74
+ }
75
+ /**
76
+ * Invalidates cache for all users with a specific scope
77
+ * Uses cache version to determine if invalidation is needed
78
+ * @param scope_type - Scope level
79
+ * @param scope_id - Scope ID to invalidate
80
+ */
81
+ invalidate_by_scope(scope_type, scope_id) {
82
+ const scope_key = `${scope_type}:${scope_id}`;
83
+ const current_version = this.scope_version_map.get(scope_key) || 0;
84
+ this.scope_version_map.set(scope_key, current_version + 1);
85
+ // Remove entries where cache version is older than scope version
86
+ const entries_to_remove = [];
87
+ for (const [user_id, entry] of this.cache.entries()) {
88
+ // Check if user has this scope
89
+ const has_scope = entry.scopes.some((s) => s.scope_type === scope_type && s.scope_id === scope_id);
90
+ if (has_scope) {
91
+ entries_to_remove.push(user_id);
92
+ }
93
+ }
94
+ for (const user_id of entries_to_remove) {
95
+ this.cache.delete(user_id);
96
+ }
97
+ }
98
+ /**
99
+ * Invalidates cache for all users with any scope of a specific level
100
+ * @param scope_type - Scope level to invalidate
101
+ */
102
+ invalidate_by_scope_level(scope_type) {
103
+ const entries_to_remove = [];
104
+ for (const [user_id, entry] of this.cache.entries()) {
105
+ // Check if user has any scope of this level
106
+ const has_level = entry.scopes.some((s) => s.scope_type === scope_type);
107
+ if (has_level) {
108
+ entries_to_remove.push(user_id);
109
+ }
110
+ }
111
+ for (const user_id of entries_to_remove) {
112
+ this.cache.delete(user_id);
113
+ }
114
+ }
115
+ /**
116
+ * Invalidates all cache entries
117
+ */
118
+ invalidate_all() {
119
+ this.cache.clear();
120
+ this.scope_version_map.clear();
121
+ }
122
+ /**
123
+ * Gets the maximum cache version for a set of scopes
124
+ * Used to determine if cache entry is stale
125
+ * @param scopes - Array of scope entries
126
+ * @returns Maximum version number
127
+ */
128
+ get_max_scope_version(scopes) {
129
+ if (scopes.length === 0) {
130
+ return 0;
131
+ }
132
+ let max_version = 0;
133
+ for (const scope of scopes) {
134
+ const scope_key = `${scope.scope_type}:${scope.scope_id}`;
135
+ const version = this.scope_version_map.get(scope_key) || 0;
136
+ max_version = Math.max(max_version, version);
137
+ }
138
+ return max_version;
139
+ }
140
+ /**
141
+ * Gets cache statistics
142
+ * @returns Object with cache size and max size
143
+ */
144
+ get_stats() {
145
+ return {
146
+ size: this.cache.size,
147
+ max_size: this.max_size,
148
+ };
149
+ }
150
+ }
151
+ // section: singleton
152
+ // Global scope cache instance (initialized with defaults, will be configured on first use)
153
+ let scope_cache_instance = null;
154
+ /**
155
+ * Gets or creates the global scope cache instance
156
+ * @param max_size - Maximum cache size (default: 5000)
157
+ * @param ttl_minutes - TTL in minutes (default: 15)
158
+ * @returns Scope cache instance
159
+ */
160
+ export function get_scope_cache(max_size = 5000, ttl_minutes = 15) {
161
+ if (!scope_cache_instance) {
162
+ scope_cache_instance = new ScopeCache(max_size, ttl_minutes);
163
+ }
164
+ return scope_cache_instance;
165
+ }
166
+ /**
167
+ * Resets the global scope cache instance (useful for testing)
168
+ */
169
+ export function reset_scope_cache() {
170
+ scope_cache_instance = null;
171
+ }
@@ -0,0 +1,39 @@
1
+ import type { ScopeLevel } from "./services/scope_service";
2
+ /**
3
+ * Scope hierarchy configuration options for HRBAC
4
+ */
5
+ export type ScopeHierarchyConfig = {
6
+ /** Whether HRBAC is enabled (default: false) */
7
+ enable_hrbac: boolean;
8
+ /** Default organization for single-tenant apps (optional) */
9
+ default_org: string;
10
+ /** Cache TTL in minutes for scope lookups (default: 15) */
11
+ scope_cache_ttl_minutes: number;
12
+ /** Maximum entries in scope cache (default: 5000) */
13
+ scope_cache_max_entries: number;
14
+ /** Which scope levels are active/enabled */
15
+ active_levels: ScopeLevel[];
16
+ /** Default labels for each scope level */
17
+ default_labels: Record<ScopeLevel, string>;
18
+ };
19
+ /**
20
+ * Reads HRBAC scope hierarchy configuration from hazo_auth_config.ini file
21
+ * Falls back to defaults if config file is not found or section is missing
22
+ * @returns Scope hierarchy configuration options
23
+ */
24
+ export declare function get_scope_hierarchy_config(): ScopeHierarchyConfig;
25
+ /**
26
+ * Checks if HRBAC is enabled in the configuration
27
+ * Convenience function for quick checks
28
+ */
29
+ export declare function is_hrbac_enabled(): boolean;
30
+ /**
31
+ * Gets the default organization from config
32
+ * Returns empty string if not configured (multi-tenant mode)
33
+ */
34
+ export declare function get_default_org(): string;
35
+ /**
36
+ * Gets the default label for a scope level
37
+ */
38
+ export declare function get_default_label(level: ScopeLevel): string;
39
+ //# sourceMappingURL=scope_hierarchy_config.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope_hierarchy_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/scope_hierarchy_config.server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAK3D;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,gDAAgD;IAChD,YAAY,EAAE,OAAO,CAAC;IACtB,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,uBAAuB,EAAE,MAAM,CAAC;IAChC,qDAAqD;IACrD,uBAAuB,EAAE,MAAM,CAAC;IAChC,4CAA4C;IAC5C,aAAa,EAAE,UAAU,EAAE,CAAC;IAC5B,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;CAC5C,CAAC;AA0DF;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,oBAAoB,CAkCjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAG3D"}
@@ -0,0 +1,96 @@
1
+ // file_description: server-only helper to read HRBAC scope hierarchy configuration from hazo_auth_config.ini
2
+ // section: imports
3
+ import { get_config_value, get_config_number, get_config_boolean, } from "./config/config_loader.server";
4
+ import { SCOPE_LEVELS } from "./services/scope_service";
5
+ // section: constants
6
+ const SECTION_NAME = "hazo_auth__scope_hierarchy";
7
+ const DEFAULT_LABELS = {
8
+ hazo_scopes_l1: "Level 1",
9
+ hazo_scopes_l2: "Level 2",
10
+ hazo_scopes_l3: "Level 3",
11
+ hazo_scopes_l4: "Level 4",
12
+ hazo_scopes_l5: "Level 5",
13
+ hazo_scopes_l6: "Level 6",
14
+ hazo_scopes_l7: "Level 7",
15
+ };
16
+ // section: helpers
17
+ /**
18
+ * Parses the active_levels config value into an array of ScopeLevel
19
+ * If not configured, returns all levels
20
+ */
21
+ function parse_active_levels(config_value) {
22
+ if (!config_value || config_value.trim().length === 0) {
23
+ return [...SCOPE_LEVELS]; // All levels active by default
24
+ }
25
+ const levels = config_value.split(",").map((s) => s.trim());
26
+ const valid_levels = [];
27
+ for (const level of levels) {
28
+ if (SCOPE_LEVELS.includes(level)) {
29
+ valid_levels.push(level);
30
+ }
31
+ }
32
+ return valid_levels.length > 0 ? valid_levels : [...SCOPE_LEVELS];
33
+ }
34
+ /**
35
+ * Reads default labels from config, falling back to defaults
36
+ */
37
+ function get_default_labels() {
38
+ const labels = Object.assign({}, DEFAULT_LABELS);
39
+ for (let i = 1; i <= 7; i++) {
40
+ const level = `hazo_scopes_l${i}`;
41
+ const config_key = `default_label_l${i}`;
42
+ const config_value = get_config_value(SECTION_NAME, config_key, "");
43
+ if (config_value && config_value.trim().length > 0) {
44
+ labels[level] = config_value.trim();
45
+ }
46
+ }
47
+ return labels;
48
+ }
49
+ /**
50
+ * Reads HRBAC scope hierarchy configuration from hazo_auth_config.ini file
51
+ * Falls back to defaults if config file is not found or section is missing
52
+ * @returns Scope hierarchy configuration options
53
+ */
54
+ export function get_scope_hierarchy_config() {
55
+ // Core HRBAC enablement
56
+ const enable_hrbac = get_config_boolean(SECTION_NAME, "enable_hrbac", false);
57
+ // Default organization for single-tenant apps
58
+ const default_org = get_config_value(SECTION_NAME, "default_org", "");
59
+ // Cache settings
60
+ const scope_cache_ttl_minutes = get_config_number(SECTION_NAME, "scope_cache_ttl_minutes", 15);
61
+ const scope_cache_max_entries = get_config_number(SECTION_NAME, "scope_cache_max_entries", 5000);
62
+ // Active levels
63
+ const active_levels_str = get_config_value(SECTION_NAME, "active_levels", "");
64
+ const active_levels = parse_active_levels(active_levels_str);
65
+ // Default labels
66
+ const default_labels = get_default_labels();
67
+ return {
68
+ enable_hrbac,
69
+ default_org,
70
+ scope_cache_ttl_minutes,
71
+ scope_cache_max_entries,
72
+ active_levels,
73
+ default_labels,
74
+ };
75
+ }
76
+ /**
77
+ * Checks if HRBAC is enabled in the configuration
78
+ * Convenience function for quick checks
79
+ */
80
+ export function is_hrbac_enabled() {
81
+ return get_config_boolean(SECTION_NAME, "enable_hrbac", false);
82
+ }
83
+ /**
84
+ * Gets the default organization from config
85
+ * Returns empty string if not configured (multi-tenant mode)
86
+ */
87
+ export function get_default_org() {
88
+ return get_config_value(SECTION_NAME, "default_org", "");
89
+ }
90
+ /**
91
+ * Gets the default label for a scope level
92
+ */
93
+ export function get_default_label(level) {
94
+ const config_key = `default_label_l${level.charAt(level.length - 1)}`;
95
+ return get_config_value(SECTION_NAME, config_key, DEFAULT_LABELS[level]);
96
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"email_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/email_service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,aAAa,CAAC;AAGnE,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;AAE9F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC,CAAC;AAaF;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAEpE;AA0YD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwErG;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,iBAAiB,EAChC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD/C"}
1
+ {"version":3,"file":"email_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/email_service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,aAAa,CAAC;AAGnE,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;AAE9F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC,CAAC;AAmBF;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAEpE;AA0YD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwErG;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,iBAAiB,EAChC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD/C"}
@@ -6,7 +6,12 @@ import { create_app_logger } from "../app_logger";
6
6
  import { read_config_section } from "../config/config_loader.server";
7
7
  // section: constants
8
8
  const DEFAULT_EMAIL_FROM = "noreply@hazo_auth.local";
9
- const DEFAULT_EMAIL_TEMPLATE_DIR = path.resolve(process.cwd(), "email_templates");
9
+ /**
10
+ * Gets the default email template directory (lazy-evaluated to avoid Edge Runtime issues)
11
+ */
12
+ function get_default_email_template_dir() {
13
+ return path.resolve(process.cwd(), "email_templates");
14
+ }
10
15
  // section: singleton
11
16
  /**
12
17
  * Singleton instance for hazo_notify emailer configuration
@@ -66,7 +71,7 @@ function get_email_template_directory() {
66
71
  ? template_dir
67
72
  : path.resolve(process.cwd(), template_dir);
68
73
  }
69
- return DEFAULT_EMAIL_TEMPLATE_DIR;
74
+ return get_default_email_template_dir();
70
75
  }
71
76
  /**
72
77
  * Gets email from address from config
@@ -55,13 +55,7 @@ export declare function get_library_source(): "project" | "node_modules" | null;
55
55
  * Clears the library path cache (useful for testing or after copying files)
56
56
  */
57
57
  export declare function clear_library_cache(): void;
58
- /**
59
- * Gets default profile picture based on configuration priority
60
- * @param user_email - User's email address
61
- * @param user_name - User's name (optional)
62
- * @returns Default profile picture URL and source, or null if no default available
63
- */
64
- export declare function get_default_profile_picture(user_email: string, user_name?: string): DefaultProfilePictureResult | null;
58
+ export declare function get_default_profile_picture(user_email: string, user_name?: string): Promise<DefaultProfilePictureResult | null>;
65
59
  /**
66
60
  * Updates user profile picture in database
67
61
  * @param adapter - The hazo_connect adapter instance
@@ -1 +1 @@
1
- {"version":3,"file":"profile_picture_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/profile_picture_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASvD,OAAO,EAAuB,KAAK,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAGnG,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE1D,MAAM,MAAM,2BAA2B,GAAG;IACxC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,cAAc,CAAC;CACpC,CAAC;AA2DF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAyBjD;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,MAAU,EAChB,SAAS,GAAE,MAAW,GACrB,mBAAmB,CA6FrB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAI7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcxF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,GAAG,cAAc,GAAG,IAAI,CAGtE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,2BAA2B,GAAG,IAAI,CA2DpC;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,MAAM,EAC3B,cAAc,EAAE,oBAAoB,GACnC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsB/C"}
1
+ {"version":3,"file":"profile_picture_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/profile_picture_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASvD,OAAO,EAAuB,KAAK,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAGnG,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE1D,MAAM,MAAM,2BAA2B,GAAG;IACxC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,cAAc,CAAC;CACpC,CAAC;AA2DF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAyBjD;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,MAAU,EAChB,SAAS,GAAE,MAAW,GACrB,mBAAmB,CA6FrB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAI7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcxF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,GAAG,cAAc,GAAG,IAAI,CAGtE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AA4DD,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CA+D7C;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,MAAM,EAC3B,cAAc,EAAE,oBAAoB,GACnC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsB/C"}
@@ -238,7 +238,50 @@ export function clear_library_cache() {
238
238
  * @param user_name - User's name (optional)
239
239
  * @returns Default profile picture URL and source, or null if no default available
240
240
  */
241
- export function get_default_profile_picture(user_email, user_name) {
241
+ /**
242
+ * Checks if a Gravatar exists for the given email by making a HEAD request
243
+ * @param email - User email address
244
+ * @returns Promise<boolean> - true if Gravatar exists (status 200), false otherwise (404 or error)
245
+ */
246
+ async function check_gravatar_exists(email) {
247
+ try {
248
+ const uiSizes = get_ui_sizes_config();
249
+ const gravatar_url = get_gravatar_url(email, uiSizes.gravatar_size);
250
+ // Make HEAD request to check if image exists without downloading it
251
+ const response = await fetch(gravatar_url, {
252
+ method: 'HEAD',
253
+ // Add timeout to prevent hanging
254
+ signal: AbortSignal.timeout(5000) // 5 second timeout
255
+ });
256
+ // Gravatar returns 200 if user has an image, 404 if using default/mystery-person
257
+ return response.ok;
258
+ }
259
+ catch (error) {
260
+ // If fetch fails (network error, timeout, etc.), assume no Gravatar
261
+ return false;
262
+ }
263
+ }
264
+ /**
265
+ * Gets a random image from a random category in the library
266
+ * @returns string | null - Random library photo URL or null if no photos available
267
+ */
268
+ function get_random_library_photo() {
269
+ const categories = get_library_categories();
270
+ if (categories.length === 0) {
271
+ return null;
272
+ }
273
+ // Pick a random category
274
+ const random_category = categories[Math.floor(Math.random() * categories.length)];
275
+ // Get photos from that category
276
+ const photos = get_library_photos(random_category);
277
+ if (photos.length === 0) {
278
+ return null;
279
+ }
280
+ // Pick a random photo from that category
281
+ const random_photo = photos[Math.floor(Math.random() * photos.length)];
282
+ return random_photo;
283
+ }
284
+ export async function get_default_profile_picture(user_email, user_name) {
242
285
  const config = get_profile_picture_config();
243
286
  if (!config.user_photo_default) {
244
287
  return null;
@@ -246,25 +289,25 @@ export function get_default_profile_picture(user_email, user_name) {
246
289
  const uiSizes = get_ui_sizes_config();
247
290
  // Try priority 1
248
291
  if (config.user_photo_default_priority1 === "gravatar") {
249
- const gravatar_url = get_gravatar_url(user_email, uiSizes.gravatar_size);
250
- // Note: We can't check if Gravatar actually exists without making a request
251
- // For now, we'll always return Gravatar URL and let the browser handle 404
252
- return {
253
- profile_picture_url: gravatar_url,
254
- profile_source: "gravatar",
255
- };
292
+ // Check if Gravatar actually exists for this email
293
+ const gravatar_exists = await check_gravatar_exists(user_email);
294
+ if (gravatar_exists) {
295
+ const gravatar_url = get_gravatar_url(user_email, uiSizes.gravatar_size);
296
+ return {
297
+ profile_picture_url: gravatar_url,
298
+ profile_source: "gravatar",
299
+ };
300
+ }
301
+ // If Gravatar doesn't exist, fall through to priority 2
256
302
  }
257
303
  else if (config.user_photo_default_priority1 === "library") {
258
- const categories = get_library_categories();
259
- if (categories.length > 0) {
260
- // Use first category, first photo as default
261
- const photos = get_library_photos(categories[0]);
262
- if (photos.length > 0) {
263
- return {
264
- profile_picture_url: photos[0],
265
- profile_source: "library",
266
- };
267
- }
304
+ // Use random library photo instead of first photo
305
+ const random_photo = get_random_library_photo();
306
+ if (random_photo) {
307
+ return {
308
+ profile_picture_url: random_photo,
309
+ profile_source: "library",
310
+ };
268
311
  }
269
312
  }
270
313
  // Try priority 2 if priority 1 didn't work (only if priority2 is different from priority1)
@@ -272,22 +315,24 @@ export function get_default_profile_picture(user_email, user_name) {
272
315
  const priority2 = config.user_photo_default_priority2;
273
316
  if (priority2 && priority2 !== priority1) {
274
317
  if (priority2 === "gravatar") {
275
- const gravatar_url = get_gravatar_url(user_email, uiSizes.gravatar_size);
276
- return {
277
- profile_picture_url: gravatar_url,
278
- profile_source: "gravatar",
279
- };
318
+ // Check if Gravatar actually exists for this email
319
+ const gravatar_exists = await check_gravatar_exists(user_email);
320
+ if (gravatar_exists) {
321
+ const gravatar_url = get_gravatar_url(user_email, uiSizes.gravatar_size);
322
+ return {
323
+ profile_picture_url: gravatar_url,
324
+ profile_source: "gravatar",
325
+ };
326
+ }
280
327
  }
281
328
  else if (priority2 === "library") {
282
- const categories = get_library_categories();
283
- if (categories.length > 0) {
284
- const photos = get_library_photos(categories[0]);
285
- if (photos.length > 0) {
286
- return {
287
- profile_picture_url: photos[0],
288
- profile_source: "library",
289
- };
290
- }
329
+ // Use random library photo instead of first photo
330
+ const random_photo = get_random_library_photo();
331
+ if (random_photo) {
332
+ return {
333
+ profile_picture_url: random_photo,
334
+ profile_source: "library",
335
+ };
291
336
  }
292
337
  }
293
338
  }
@@ -61,7 +61,7 @@ export async function register_user(adapter, data) {
61
61
  // Set default profile picture if enabled
62
62
  const profile_picture_config = get_profile_picture_config();
63
63
  if (profile_picture_config.user_photo_default) {
64
- const default_photo = get_default_profile_picture(email, name);
64
+ const default_photo = await get_default_profile_picture(email, name);
65
65
  if (default_photo) {
66
66
  insert_data.profile_picture_url = default_photo.profile_picture_url;
67
67
  // Map UI source value to database enum value
@@ -0,0 +1,48 @@
1
+ import type { HazoConnectAdapter } from "hazo_connect";
2
+ import type { ScopeLevel } from "./scope_service";
3
+ export type ScopeLabel = {
4
+ id: string;
5
+ org: string;
6
+ scope_type: ScopeLevel;
7
+ label: string;
8
+ created_at: string;
9
+ changed_at: string;
10
+ };
11
+ export type ScopeLabelResult = {
12
+ success: boolean;
13
+ label?: ScopeLabel;
14
+ labels?: ScopeLabel[];
15
+ error?: string;
16
+ };
17
+ export declare const DEFAULT_SCOPE_LABELS: Record<ScopeLevel, string>;
18
+ /**
19
+ * Gets all scope labels for an organization
20
+ */
21
+ export declare function get_scope_labels(adapter: HazoConnectAdapter, org: string): Promise<ScopeLabelResult>;
22
+ /**
23
+ * Gets all scope labels for an organization, with defaults filled in for missing levels
24
+ */
25
+ export declare function get_scope_labels_with_defaults(adapter: HazoConnectAdapter, org: string, custom_defaults?: Record<ScopeLevel, string>): Promise<ScopeLabelResult>;
26
+ /**
27
+ * Gets the label for a specific scope level
28
+ * Returns the custom label if set, otherwise returns the default
29
+ */
30
+ export declare function get_label_for_level(adapter: HazoConnectAdapter, org: string, scope_type: ScopeLevel, custom_default?: string): Promise<string>;
31
+ /**
32
+ * Creates or updates a scope label for an organization
33
+ * Uses upsert pattern - creates if not exists, updates if exists
34
+ */
35
+ export declare function upsert_scope_label(adapter: HazoConnectAdapter, org: string, scope_type: ScopeLevel, label: string): Promise<ScopeLabelResult>;
36
+ /**
37
+ * Batch upsert scope labels for an organization
38
+ * Useful for saving all labels at once from the UI
39
+ */
40
+ export declare function batch_upsert_scope_labels(adapter: HazoConnectAdapter, org: string, labels: Array<{
41
+ scope_type: ScopeLevel;
42
+ label: string;
43
+ }>): Promise<ScopeLabelResult>;
44
+ /**
45
+ * Deletes a scope label, reverting to default
46
+ */
47
+ export declare function delete_scope_label(adapter: HazoConnectAdapter, org: string, scope_type: ScopeLevel): Promise<ScopeLabelResult>;
48
+ //# sourceMappingURL=scope_labels_service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope_labels_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/scope_labels_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAKvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,UAAU,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAQ3D,CAAC;AAIF;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,gBAAgB,CAAC,CA4B3B;AAED;;GAEG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,EACX,eAAe,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,gBAAgB,CAAC,CAuE3B;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,KAAK,CAAC;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GACvD,OAAO,CAAC,gBAAgB,CAAC,CAwC3B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,gBAAgB,CAAC,CAwC3B"}