hazo_auth 9.0.1 → 10.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 (65) hide show
  1. package/README.md +39 -11
  2. package/SETUP_CHECKLIST.md +35 -16
  3. package/cli-src/cli/init_users.ts +40 -48
  4. package/cli-src/lib/auth/auth_types.ts +0 -2
  5. package/cli-src/lib/auth/hazo_get_auth.server.ts +31 -25
  6. package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +9 -13
  7. package/cli-src/lib/config/config_loader.server.ts +41 -3
  8. package/cli-src/lib/config/hazo_auth_core_config.ts +1 -1
  9. package/cli-src/lib/constants.ts +2 -0
  10. package/cli-src/lib/hazo_connect_setup.server.ts +20 -2
  11. package/cli-src/lib/profile_pic_menu_config.server.ts +4 -3
  12. package/cli-src/lib/schema/sqlite_schema.ts +0 -4
  13. package/cli-src/lib/scope_hierarchy_config.server.ts +1 -9
  14. package/cli-src/lib/services/invitation_service.ts +1 -1
  15. package/cli-src/lib/services/scope_service.ts +2 -76
  16. package/cli-src/lib/services/user_scope_service.ts +7 -61
  17. package/config/hazo_auth_config.example.ini +3 -1
  18. package/dist/cli/init_users.d.ts.map +1 -1
  19. package/dist/cli/init_users.js +42 -42
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +1 -1
  23. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
  24. package/dist/components/layouts/shared/components/profile_pic_menu.js +7 -1
  25. package/dist/components/ui/input-otp.d.ts +2 -2
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/lib/auth/auth_types.d.ts +0 -2
  30. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  31. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  32. package/dist/lib/auth/hazo_get_auth.server.js +27 -19
  33. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
  34. package/dist/lib/auth/hazo_get_tenant_auth.server.js +10 -10
  35. package/dist/lib/config/config_loader.server.d.ts.map +1 -1
  36. package/dist/lib/config/config_loader.server.js +38 -3
  37. package/dist/lib/config/hazo_auth_core_config.js +1 -1
  38. package/dist/lib/constants.d.ts +1 -0
  39. package/dist/lib/constants.d.ts.map +1 -1
  40. package/dist/lib/constants.js +1 -0
  41. package/dist/lib/hazo_connect_setup.server.d.ts +1 -0
  42. package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
  43. package/dist/lib/hazo_connect_setup.server.js +15 -2
  44. package/dist/lib/profile_pic_menu_config.server.d.ts +2 -1
  45. package/dist/lib/profile_pic_menu_config.server.d.ts.map +1 -1
  46. package/dist/lib/profile_pic_menu_config.server.js +1 -1
  47. package/dist/lib/schema/sqlite_schema.d.ts +1 -1
  48. package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
  49. package/dist/lib/schema/sqlite_schema.js +0 -4
  50. package/dist/lib/scope_hierarchy_config.server.d.ts +0 -2
  51. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -1
  52. package/dist/lib/scope_hierarchy_config.server.js +1 -3
  53. package/dist/lib/services/invitation_service.d.ts +1 -1
  54. package/dist/lib/services/invitation_service.js +1 -1
  55. package/dist/lib/services/scope_service.d.ts +1 -14
  56. package/dist/lib/services/scope_service.d.ts.map +1 -1
  57. package/dist/lib/services/scope_service.js +2 -67
  58. package/dist/lib/services/user_scope_service.d.ts +5 -12
  59. package/dist/lib/services/user_scope_service.d.ts.map +1 -1
  60. package/dist/lib/services/user_scope_service.js +8 -45
  61. package/dist/server/routes/invitations.d.ts +1 -1
  62. package/dist/server/routes/invitations.d.ts.map +1 -1
  63. package/dist/server/routes/invitations.js +12 -11
  64. package/dist/server/routes/user_management_users.d.ts +1 -1
  65. package/package.json +15 -15
package/README.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
4
4
 
5
+ ### What's New in v9.1.1 🔧
6
+
7
+ **Dev-server noise fixes for Next.js 16 + Turbopack**
8
+
9
+ - `next.config.mjs`: `hazo_core` and `hazo_config` added to `serverExternalPackages`. Turbopack was bundling hazo_config and breaking `ini.parse()` CJS interop, causing every config-section read to throw and producing `config_loader_read_section_failed` ×9 + `me_endpoint_error` per request.
10
+ - `config/hazo_auth_config.ini`: renamed `[log.overrides]` → `[log_overrides]`. The `ini` v4 library parses dots in section names as nesting separators, creating null-prototype objects that hazo_config cannot stringify. Dotted section names must be avoided.
11
+ - `src/lib/config/config_loader.server.ts`: `HazoConfig` instances are now memoized per resolved file path — one INI parse per server process instead of one per getter call.
12
+ - Companion fixes in hazo_core@1.0.3 (registerSingleton moved to module level, hazo_debug probe → lazy import) and hazo_config@2.1.9 (hazo_core/errors probe → lazy import, graceful skip of null-prototype nested objects in refresh()).
13
+
14
+ > **Action required if you use `[log.overrides]` in your app's config:** rename it to `[log_overrides]` (or any non-dotted name). This applies to any hazo_config-backed INI file, not just hazo_auth.
15
+
5
16
  ### What's New in v8.0.1 🔧
6
17
 
7
18
  **Auto-test and middleware bug fixes**
@@ -207,7 +218,7 @@ See [CHANGE_LOG.md](./CHANGE_LOG.md) for detailed migration guide, rationale, an
207
218
 
208
219
  ```bash
209
220
  # Install hazo_auth and required peer dependencies
210
- npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth
221
+ npm install hazo_auth hazo_core hazo_config hazo_connect hazo_logs next react react-dom next-auth
211
222
 
212
223
  # UI peer dependencies (required if using hazo_auth UI components)
213
224
  npm install hazo_ui lucide-react sonner next-themes @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-tooltip
@@ -231,7 +242,7 @@ The fastest way to get started is using the CLI commands:
231
242
 
232
243
  ```bash
233
244
  # 1. Install the package and peer dependencies
234
- npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth
245
+ npm install hazo_auth hazo_core hazo_config hazo_connect hazo_logs next react react-dom next-auth
235
246
 
236
247
  # 2. Initialize your project (directories, config, database, images)
237
248
  npx hazo_auth init
@@ -425,7 +436,7 @@ import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
425
436
 
426
437
  **Peer Dependencies (Required):**
427
438
  ```bash
428
- npm install hazo_config hazo_connect hazo_logs
439
+ npm install hazo_core hazo_config hazo_connect hazo_logs
429
440
  ```
430
441
 
431
442
  **UI Components:** All shadcn/ui components are bundled with hazo_auth. You do NOT need to install them separately.
@@ -731,9 +742,7 @@ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
731
742
  CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
732
743
 
733
744
  -- 7a. Reserved system scopes
734
- INSERT INTO hazo_scopes (id, parent_id, name, level)
735
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system')
736
- ON CONFLICT (id) DO NOTHING;
745
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
737
746
  INSERT INTO hazo_scopes (id, parent_id, name, level)
738
747
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
739
748
  ON CONFLICT (id) DO NOTHING;
@@ -889,8 +898,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
889
898
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
890
899
 
891
900
  -- Reserved system scopes
892
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
893
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
901
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
894
902
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
895
903
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
896
904
 
@@ -1732,7 +1740,6 @@ type TenantOrganization = {
1732
1740
  slug: string | null; // URL-friendly identifier
1733
1741
  level: string; // "Company", "Division", etc.
1734
1742
  role_id: string; // User's role in this scope
1735
- is_super_admin: boolean;
1736
1743
  branding?: {
1737
1744
  logo_url: string | null;
1738
1745
  primary_color: string | null;
@@ -2489,6 +2496,28 @@ import { ProfilePicMenu } from "hazo_auth/components/layouts/shared";
2489
2496
  />
2490
2497
  ```
2491
2498
 
2499
+ #### Menu item types
2500
+
2501
+ `custom_menu_items` accepts four `type` values:
2502
+
2503
+ | Type | Renders as | Fields used |
2504
+ |------|------------|-------------|
2505
+ | `info` | Read-only label/value row | `label`, `value` |
2506
+ | `link` | Navigation link | `label`, `href` |
2507
+ | `separator` | Divider | — |
2508
+ | `action` | Clickable item that fires a client-side callback | `label`, `onSelect` |
2509
+
2510
+ ```typescript
2511
+ <ProfilePicMenu
2512
+ variant="dropdown"
2513
+ custom_menu_items={[
2514
+ { type: "action", label: "Switch workspace", onSelect: () => openSwitcher(), order: 1, id: "switch" }
2515
+ ]}
2516
+ />
2517
+ ```
2518
+
2519
+ > **`action` items are React-only.** Because `onSelect` is a function, action items cannot be expressed via the INI `custom_menu_items` config (which only supports the serialisable `info` / `link` / `separator` types) — pass them through the `custom_menu_items` prop instead.
2520
+
2492
2521
  ### Configuration
2493
2522
 
2494
2523
  ```ini
@@ -2879,8 +2908,7 @@ Retrieves basic profile information for multiple users in a single batch call.
2879
2908
  **Location:** `src/lib/services/user_profiles_service.ts`
2880
2909
 
2881
2910
  ```typescript
2882
- import { hazo_get_user_profiles } from "hazo_auth/lib/services/user_profiles_service";
2883
- import { get_hazo_connect_instance } from "hazo_auth/server/hazo_connect_instance.server";
2911
+ import { hazo_get_user_profiles, get_hazo_connect_instance } from "hazo_auth/server-lib";
2884
2912
 
2885
2913
  export async function GET(request: NextRequest) {
2886
2914
  const adapter = get_hazo_connect_instance();
@@ -2,6 +2,35 @@
2
2
 
3
3
  This checklist provides step-by-step instructions for setting up the `hazo_auth` package in your Next.js project. AI assistants can follow this guide to ensure complete and correct setup.
4
4
 
5
+ ## v9.1.1 — Next.js 16 / Turbopack setup (new installs and migrations)
6
+
7
+ ### Add to `serverExternalPackages`
8
+
9
+ hazo_core and hazo_config use CJS dependencies (`ini`, optional peer probes) that Turbopack cannot bundle cleanly. Add them to the external packages list in `next.config.mjs` / `next.config.js`:
10
+
11
+ ```js
12
+ const nextConfig = {
13
+ serverExternalPackages: [
14
+ "hazo_notify", "argon2",
15
+ "hazo_core", "hazo_config", // required for Next 16 + Turbopack
16
+ ],
17
+ };
18
+ ```
19
+
20
+ ### Avoid dotted section names in INI config files
21
+
22
+ `ini` v4 parses dots in section headers as nesting separators (`[log.overrides]` becomes `parsed.log.overrides`). hazo_config uses flat sections — use underscores instead:
23
+
24
+ ```ini
25
+ # Bad: creates a nested null-prototype object that hazo_config cannot stringify
26
+ [log.overrides]
27
+
28
+ # Good:
29
+ [log_overrides]
30
+ ```
31
+
32
+ ---
33
+
5
34
  ## v8.0.0 Migration (from v7.x)
6
35
 
7
36
  ### Breaking changes
@@ -515,9 +544,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
515
544
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
516
545
 
517
546
  -- Reserved system scopes
518
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
519
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
520
-
547
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
521
548
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
522
549
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
523
550
 
@@ -704,10 +731,7 @@ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
704
731
  CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
705
732
 
706
733
  -- 7a. Reserved system scopes
707
- INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
708
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
709
- ON CONFLICT (id) DO NOTHING;
710
-
734
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
711
735
  INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
712
736
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
713
737
  ON CONFLICT (id) DO NOTHING;
@@ -868,8 +892,8 @@ GRANT USAGE ON TYPE hazo_enum_invitation_status TO anon, authenticated;
868
892
  - [ ] `hazo_invitations`
869
893
  - [ ] `hazo_user_relationships` (managed sub-profile parent/child links)
870
894
  - [ ] Reserved system scopes inserted:
871
- - [ ] `00000000-0000-0000-0000-000000000000` (Super Admin)
872
895
  - [ ] `00000000-0000-0000-0000-000000000001` (System / non-multi-tenancy default)
896
+ - [ ] Migration 020 run: Super Admin scope retired, `hazo_org_global_admin` permission granted to affected roles
873
897
  - [ ] `firm_admin` role inserted into `hazo_roles`
874
898
 
875
899
  ---
@@ -1739,10 +1763,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
1739
1763
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1740
1764
 
1741
1765
  -- 3. Create system scopes
1742
- INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1743
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
1744
- ON CONFLICT (id) DO NOTHING;
1745
-
1766
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
1746
1767
  INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1747
1768
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
1748
1769
  ON CONFLICT (id) DO NOTHING;
@@ -1832,9 +1853,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
1832
1853
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1833
1854
 
1834
1855
  -- 3. Create system scopes
1835
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1836
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
1837
-
1856
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
1838
1857
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1839
1858
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
1840
1859
 
@@ -1930,8 +1949,8 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1930
1949
  - [ ] `hazo_user_scopes` (user-scope-role assignments)
1931
1950
  - [ ] `hazo_invitations` (user invitation flow)
1932
1951
  - [ ] System scopes exist:
1933
- - [ ] Super Admin scope (00000000-0000-0000-0000-000000000000)
1934
1952
  - [ ] Default System scope (00000000-0000-0000-0000-000000000001)
1953
+ - [ ] Migration 020 run: Super Admin scope retired, `hazo_org_global_admin` permission granted
1935
1954
  - [ ] Grants applied (PostgreSQL)
1936
1955
  - [ ] HRBAC tabs visible in User Management
1937
1956
  - [ ] Scope test page works
@@ -5,7 +5,8 @@ import { createCrudService } from "hazo_connect/server";
5
5
  import { get_user_management_config } from "../lib/user_management_config.server.js";
6
6
  import { get_config_value } from "../lib/config/config_loader.server.js";
7
7
  import { create_app_logger } from "../lib/app_logger.js";
8
- import { SUPER_ADMIN_SCOPE_ID } from "../lib/services/scope_service.js";
8
+ import { DEFAULT_SYSTEM_SCOPE_ID } from "../lib/services/scope_service.js";
9
+ import { GLOBAL_ADMIN_PERMISSION } from "../lib/constants.js";
9
10
 
10
11
  // section: types
11
12
  type InitSummary = {
@@ -23,10 +24,6 @@ type InitSummary = {
23
24
  existing: number;
24
25
  };
25
26
  // v5.x: Removed user_role - roles are now assigned via hazo_user_scopes
26
- super_admin_scope: {
27
- inserted: boolean;
28
- existing: boolean;
29
- };
30
27
  user_scope: {
31
28
  inserted: boolean;
32
29
  existing: boolean;
@@ -77,23 +74,13 @@ function print_summary(summary: InitSummary): void {
77
74
 
78
75
  // v5.x: User-Role assignments are now handled via User-Scope assignments (see below)
79
76
 
80
- // Super admin scope summary
81
- console.log("Super Admin Scope:");
82
- if (summary.super_admin_scope.inserted) {
83
- console.log(` ✓ Inserted: Super Admin scope (ID: ${SUPER_ADMIN_SCOPE_ID})`);
84
- }
85
- if (summary.super_admin_scope.existing) {
86
- console.log(` ⊙ Already existed: Super Admin scope (ID: ${SUPER_ADMIN_SCOPE_ID})`);
87
- }
88
- console.log();
89
-
90
77
  // User scope summary
91
78
  console.log("User-Scope Assignment:");
92
79
  if (summary.user_scope.inserted) {
93
- console.log(` ✓ Inserted: User assigned to Super Admin scope`);
80
+ console.log(` ✓ Inserted: User assigned to default system scope`);
94
81
  }
95
82
  if (summary.user_scope.existing) {
96
- console.log(` ⊙ Already existed: User already in Super Admin scope`);
83
+ console.log(` ⊙ Already existed: User already in default system scope`);
97
84
  }
98
85
  console.log();
99
86
 
@@ -131,10 +118,6 @@ export async function handle_init_users(options: InitUsersOptions = {}): Promise
131
118
  existing: 0,
132
119
  },
133
120
  // v5.x: Removed user_role - roles are now assigned via hazo_user_scopes
134
- super_admin_scope: {
135
- inserted: false,
136
- existing: false,
137
- },
138
121
  user_scope: {
139
122
  inserted: false,
140
123
  existing: false,
@@ -155,7 +138,6 @@ export async function handle_init_users(options: InitUsersOptions = {}): Promise
155
138
  });
156
139
  const users_service = createCrudService(hazoConnect, "hazo_users");
157
140
  // v5.x: Removed hazo_user_roles - roles are now assigned via hazo_user_scopes
158
- const scopes_service = createCrudService(hazoConnect, "hazo_scopes");
159
141
  // hazo_user_scopes uses composite primary key (user_id, scope_id), no 'id' column
160
142
  const user_scopes_service = createCrudService(hazoConnect, "hazo_user_scopes", {
161
143
  primaryKeys: ["user_id", "scope_id"],
@@ -317,54 +299,63 @@ export async function handle_init_users(options: InitUsersOptions = {}): Promise
317
299
  console.log(`✓ Found user: ${super_user_email} (ID: ${user_id})`);
318
300
  console.log();
319
301
 
320
- // v5.x: Step 7 removed - role assignment now happens via hazo_user_scopes (see step 9)
321
-
322
- // 8. Ensure super admin scope exists
323
- const existing_scopes = await scopes_service.findBy({ id: SUPER_ADMIN_SCOPE_ID });
324
-
325
- if (Array.isArray(existing_scopes) && existing_scopes.length > 0) {
326
- summary.super_admin_scope.existing = true;
327
- console.log(`✓ Super Admin scope already exists (ID: ${SUPER_ADMIN_SCOPE_ID})`);
328
- } else {
329
- await scopes_service.insert({
330
- id: SUPER_ADMIN_SCOPE_ID,
331
- parent_id: null,
332
- name: "Super Admin",
333
- level: "system",
302
+ // 7. Ensure hazo_org_global_admin is in the permission catalog
303
+ const global_admin_perms = await permissions_service.findBy({
304
+ permission_name: GLOBAL_ADMIN_PERMISSION,
305
+ });
306
+ if (!Array.isArray(global_admin_perms) || global_admin_perms.length === 0) {
307
+ await permissions_service.insert({
308
+ permission_name: GLOBAL_ADMIN_PERMISSION,
309
+ description: "Global admin access to all scopes and operations",
334
310
  created_at: now,
335
311
  changed_at: now,
336
312
  });
337
- summary.super_admin_scope.inserted = true;
338
- console.log(`✓ Created Super Admin scope (ID: ${SUPER_ADMIN_SCOPE_ID})`);
313
+ console.log(`✓ Created permission: ${GLOBAL_ADMIN_PERMISSION}`);
314
+ } else {
315
+ console.log(`✓ Permission already exists: ${GLOBAL_ADMIN_PERMISSION}`);
339
316
  }
317
+ console.log();
340
318
 
319
+ // 9. Ensure hazo_org_global_admin is assigned to the super user role
320
+ // (The role already has all configured permissions; this ensures the global admin perm is included)
321
+ const perm_row = await permissions_service.findBy({ permission_name: GLOBAL_ADMIN_PERMISSION });
322
+ const perm_id = Array.isArray(perm_row) && perm_row.length > 0 ? (perm_row[0].id as string) : null;
323
+ if (perm_id && role_id) {
324
+ const existing_rp = await role_permissions_service.findBy({ role_id, permission_id: perm_id });
325
+ if (!Array.isArray(existing_rp) || existing_rp.length === 0) {
326
+ await role_permissions_service.insert({ role_id, permission_id: perm_id });
327
+ console.log(`✓ Assigned ${GLOBAL_ADMIN_PERMISSION} to super user role`);
328
+ } else {
329
+ console.log(`✓ Super user role already has ${GLOBAL_ADMIN_PERMISSION}`);
330
+ }
331
+ }
341
332
  console.log();
342
333
 
343
- // 9. Assign user to super admin scope
334
+ // 10. Assign user to DEFAULT_SYSTEM_SCOPE_ID (global access comes from the permission, not the scope)
344
335
  const existing_user_scopes = await user_scopes_service.findBy({
345
336
  user_id,
346
- scope_id: SUPER_ADMIN_SCOPE_ID,
337
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
347
338
  });
348
339
 
349
340
  if (Array.isArray(existing_user_scopes) && existing_user_scopes.length > 0) {
350
341
  summary.user_scope.existing = true;
351
- console.log(`✓ User already assigned to Super Admin scope`);
342
+ console.log(`✓ User already assigned to default system scope`);
352
343
  } else {
353
344
  await user_scopes_service.insert({
354
345
  user_id,
355
- scope_id: SUPER_ADMIN_SCOPE_ID,
356
- root_scope_id: SUPER_ADMIN_SCOPE_ID,
346
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
347
+ root_scope_id: DEFAULT_SYSTEM_SCOPE_ID,
357
348
  role_id,
358
349
  created_at: now,
359
350
  changed_at: now,
360
351
  });
361
352
  summary.user_scope.inserted = true;
362
- console.log(`✓ Assigned user to Super Admin scope`);
353
+ console.log(`✓ Assigned user to default system scope`);
363
354
  }
364
355
 
365
356
  console.log();
366
357
 
367
- // 10. Print summary
358
+ // 11. Print summary
368
359
  print_summary(summary);
369
360
 
370
361
  logger.info("init_users_completed", {
@@ -402,15 +393,16 @@ export function show_init_users_help(): void {
402
393
  console.log(`
403
394
  hazo_auth init-users
404
395
 
405
- Initialize users, roles, permissions, and super admin scope from configuration.
396
+ Initialize users, roles, and permissions from configuration.
406
397
 
407
398
  This command reads from hazo_auth_config.ini and:
408
399
  1. Creates permissions from [hazo_auth__user_management] application_permission_list_defaults
409
400
  2. Creates a 'default_super_user_role' role
410
401
  3. Assigns all permissions to the super user role
411
402
  4. Finds user by email (from --email parameter or config)
412
- 5. Creates the Super Admin scope (${SUPER_ADMIN_SCOPE_ID})
413
- 6. Assigns the user to the Super Admin scope with the super user role
403
+ 5. Ensures the '${GLOBAL_ADMIN_PERMISSION}' permission exists and is assigned to the super user role
404
+ 6. Assigns the user to the default system scope (${DEFAULT_SYSTEM_SCOPE_ID})
405
+ Global admin access is granted via the '${GLOBAL_ADMIN_PERMISSION}' permission, not by scope
414
406
  (v5.x: Roles are assigned per-scope via hazo_user_scopes table)
415
407
 
416
408
  Options:
@@ -24,7 +24,6 @@ export type HazoAuthUser = {
24
24
  export type ScopeAccessInfo = {
25
25
  scope_id: string;
26
26
  scope_name?: string;
27
- is_super_admin?: boolean;
28
27
  };
29
28
 
30
29
  /**
@@ -135,7 +134,6 @@ export type TenantOrganization = {
135
134
  slug: string | null;
136
135
  level: string;
137
136
  role_id: string;
138
- is_super_admin: boolean;
139
137
  branding?: {
140
138
  logo_url: string | null;
141
139
  primary_color: string | null;
@@ -30,6 +30,7 @@ import {
30
30
  } from "../services/user_scope_service.js";
31
31
  import { get_cookie_name, BASE_COOKIE_NAMES } from "../cookies_config.server.js";
32
32
  import { get_app_permission_descriptions } from "../app_permissions_config.server.js";
33
+ import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
33
34
 
34
35
  // section: helpers
35
36
 
@@ -383,7 +384,6 @@ async function check_scope_access_internal(
383
384
  scope_access_via: {
384
385
  scope_id: result.access_via.scope_id,
385
386
  scope_name: result.access_via.scope_name,
386
- is_super_admin: result.is_super_admin,
387
387
  },
388
388
  user_scopes,
389
389
  };
@@ -587,31 +587,37 @@ export async function hazo_get_auth(
587
587
  const hrbac_enabled = is_hrbac_enabled();
588
588
 
589
589
  if (hrbac_enabled && options?.scope_id) {
590
- const scope_result = await check_scope_access_internal(
591
- user.id,
592
- options.scope_id,
593
- );
594
-
595
- scope_ok = scope_result.scope_ok;
596
- scope_access_via = scope_result.scope_access_via;
597
-
598
- // Log scope denial if permission logging is enabled
599
- if (!scope_ok && config.log_permission_denials) {
600
- const client_ip = get_client_ip(request);
601
- logger.warn("auth_utility_scope_access_denied", {
602
- filename: get_filename(),
603
- line_number: get_line_number(),
604
- user_id: user.id,
605
- scope_id: options.scope_id,
606
- user_scopes: scope_result.user_scopes,
607
- ip: client_ip,
608
- correlation_id: getCorrelationId(),
609
- });
610
- }
590
+ // Global admin permission grants access to all scopes
591
+ const has_global_admin = permissions.includes(GLOBAL_ADMIN_PERMISSION);
592
+ if (has_global_admin) {
593
+ scope_ok = true;
594
+ scope_access_via = { scope_id: options.scope_id };
595
+ } else {
596
+ const scope_result = await check_scope_access_internal(
597
+ user.id,
598
+ options.scope_id,
599
+ );
600
+ scope_ok = scope_result.scope_ok;
601
+ scope_access_via = scope_result.scope_access_via;
602
+
603
+ // Log scope denial if permission logging is enabled
604
+ if (!scope_ok && config.log_permission_denials) {
605
+ const client_ip = get_client_ip(request);
606
+ logger.warn("auth_utility_scope_access_denied", {
607
+ filename: get_filename(),
608
+ line_number: get_line_number(),
609
+ user_id: user.id,
610
+ scope_id: options.scope_id,
611
+ user_scopes: scope_result.user_scopes,
612
+ ip: client_ip,
613
+ correlation_id: getCorrelationId(),
614
+ });
615
+ }
611
616
 
612
- // Throw error if strict mode and scope access denied
613
- if (!scope_ok && options.strict) {
614
- throw new ScopeAccessError(options.scope_id, scope_result.user_scopes);
617
+ // Throw error if strict mode and scope access denied
618
+ if (!scope_ok && options.strict) {
619
+ throw new ScopeAccessError(options.scope_id, scope_result.user_scopes);
620
+ }
615
621
  }
616
622
  }
617
623
 
@@ -7,6 +7,7 @@ import { NextRequest } from "next/server";
7
7
  import { hazo_get_auth } from "./hazo_get_auth.server.js";
8
8
  import { get_auth_cache } from "./auth_cache.js";
9
9
  import { get_scope_by_id } from "../services/scope_service.js";
10
+ import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
10
11
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
11
12
  import { get_cookie_name } from "../cookies_config.server.js";
12
13
  import { get_auth_utility_config } from "../auth_utility_config.server.js";
@@ -62,14 +63,12 @@ export function extract_scope_id_from_request(
62
63
  }
63
64
 
64
65
  /**
65
- * Builds TenantOrganization from scope details and access info
66
+ * Builds TenantOrganization from scope details
66
67
  * @param scope_details - Full scope details from cache
67
- * @param is_super_admin - Whether user is accessing as super admin
68
68
  * @returns TenantOrganization object
69
69
  */
70
70
  function build_tenant_organization(
71
71
  scope_details: ScopeDetails,
72
- is_super_admin: boolean,
73
72
  ): TenantOrganization {
74
73
  return {
75
74
  id: scope_details.id,
@@ -77,7 +76,6 @@ function build_tenant_organization(
77
76
  slug: scope_details.slug,
78
77
  level: scope_details.level,
79
78
  role_id: scope_details.role_id,
80
- is_super_admin,
81
79
  branding:
82
80
  scope_details.logo_url || scope_details.primary_color
83
81
  ? {
@@ -159,18 +157,17 @@ export async function hazo_get_tenant_auth(
159
157
  let organization: TenantOrganization | null = null;
160
158
 
161
159
  if (scope_id && auth_result.scope_ok && auth_result.scope_access_via) {
162
- // Find the scope in user's scopes that matches the access_via scope
160
+ // Try to find the scope in user's cached scope assignments first.
161
+ // For global admins the scope may not be in their cache (they can access any scope),
162
+ // in which case we fall through to the permission-based fetch below.
163
163
  const access_scope = user_scopes.find(
164
164
  (s) => s.id === auth_result.scope_access_via?.scope_id,
165
165
  );
166
166
 
167
167
  if (access_scope) {
168
- organization = build_tenant_organization(
169
- access_scope,
170
- auth_result.scope_access_via.is_super_admin || false,
171
- );
172
- } else if (auth_result.scope_access_via.is_super_admin) {
173
- // Super admin accessing scope they're not assigned to - fetch scope details
168
+ organization = build_tenant_organization(access_scope);
169
+ } else if (auth_result.permissions.includes(GLOBAL_ADMIN_PERMISSION)) {
170
+ // Global admin accessing a scope they aren't directly assigned to — fetch scope details
174
171
  const hazoConnect = get_hazo_connect_instance();
175
172
  const scope_result = await get_scope_by_id(hazoConnect, scope_id);
176
173
  if (scope_result.success && scope_result.scope) {
@@ -179,8 +176,7 @@ export async function hazo_get_tenant_auth(
179
176
  name: scope_result.scope.name,
180
177
  slug: null, // Could fetch from scope if slug column exists
181
178
  level: scope_result.scope.level,
182
- role_id: "", // Super admin doesn't have a role in the scope
183
- is_super_admin: true,
179
+ role_id: "", // Global admin doesn't have a role assignment in the scope
184
180
  branding: scope_result.scope.logo_url
185
181
  ? {
186
182
  logo_url: scope_result.scope.logo_url,
@@ -11,6 +11,15 @@ import { create_app_logger } from "../app_logger.js";
11
11
  // section: constants
12
12
  const DEFAULT_CONFIG_FILE = "config/hazo_auth_config.ini";
13
13
 
14
+ // section: config_instance_cache
15
+ // Cache HazoConfig instances by resolved file path so we never construct (and re-parse)
16
+ // more than one instance per config file per server process. This prevents repeated
17
+ // warnings when multiple getters read the same file in a single request, and eliminates
18
+ // redundant synchronous file I/O.
19
+ const _config_instance_cache = new Map<string, HazoConfig>();
20
+ // Track paths that failed to construct so we don't retry and warn repeatedly.
21
+ const _config_failed_paths = new Set<string>();
22
+
14
23
  // section: helpers
15
24
  /**
16
25
  * Gets the default config file path
@@ -24,6 +33,35 @@ function get_config_file_path(custom_path?: string): string {
24
33
  return path.resolve(process.cwd(), DEFAULT_CONFIG_FILE);
25
34
  }
26
35
 
36
+ /**
37
+ * Returns a cached HazoConfig instance for the given path, constructing one if needed.
38
+ * Returns null if construction fails (logs the error once on first failure).
39
+ */
40
+ function get_config_instance(
41
+ config_path: string,
42
+ logger: ReturnType<typeof create_app_logger>,
43
+ ): HazoConfig | null {
44
+ const cached = _config_instance_cache.get(config_path);
45
+ if (cached) return cached;
46
+ if (_config_failed_paths.has(config_path)) return null;
47
+
48
+ try {
49
+ const instance = new HazoConfig({ filePath: config_path });
50
+ _config_instance_cache.set(config_path, instance);
51
+ return instance;
52
+ } catch (error) {
53
+ _config_failed_paths.add(config_path);
54
+ const error_message = error instanceof Error ? error.message : "Unknown error";
55
+ logger.warn("config_loader_construct_failed", {
56
+ filename: "config_loader.server.ts",
57
+ line_number: 0,
58
+ config_path,
59
+ error: error_message,
60
+ });
61
+ return null;
62
+ }
63
+ }
64
+
27
65
  /**
28
66
  * Reads a section from the config file
29
67
  * @param section_name - Name of the section to read (e.g., "hazo_auth__register_layout")
@@ -41,10 +79,10 @@ export function read_config_section(
41
79
  return undefined;
42
80
  }
43
81
 
82
+ const hazo_config = get_config_instance(config_path, logger);
83
+ if (!hazo_config) return undefined;
84
+
44
85
  try {
45
- const hazo_config = new HazoConfig({
46
- filePath: config_path,
47
- });
48
86
  return hazo_config.getSection(section_name);
49
87
  } catch (error) {
50
88
  const error_message = error instanceof Error ? error.message : "Unknown error";
@@ -1,5 +1,5 @@
1
1
  // file_description: Zod-validated config loader for hazo_auth core settings.
2
- // Covers server-critical sections ([hazo_auth__tokens], [hazo_auth__cookies], [hazo_auth__rate_limit], [log.overrides]).
2
+ // Covers server-critical sections ([hazo_auth__tokens], [hazo_auth__cookies], [hazo_auth__rate_limit], [log_overrides]).
3
3
  // UI sections (login_layout, register_layout, etc.) are still handled by config_loader.server.ts.
4
4
  import { z } from 'zod';
5
5
  import { loadConfig } from 'hazo_core';
@@ -13,3 +13,5 @@ export const HAZO_AUTH_PERMISSIONS = {
13
13
  } as const;
14
14
 
15
15
  export const ALL_ADMIN_PERMISSIONS: string[] = Object.values(HAZO_AUTH_PERMISSIONS);
16
+
17
+ export const GLOBAL_ADMIN_PERMISSION = "hazo_org_global_admin";