hazo_auth 5.1.11 → 5.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1296,7 +1296,13 @@ export default function Page() {
1296
1296
 
1297
1297
  ## Authentication Service
1298
1298
 
1299
- The `hazo_auth` package provides a comprehensive authentication and authorization system with role-based access control (RBAC). The main authentication utility is `hazo_get_auth`, which provides user details, permissions, and permission checking with built-in caching and rate limiting.
1299
+ The `hazo_auth` package provides a comprehensive authentication and authorization system with role-based access control (RBAC). The main authentication utilities are:
1300
+
1301
+ - **`hazo_get_auth`** - Standard authentication with user details, permissions, and caching
1302
+ - **`hazo_get_tenant_auth`** - Tenant-aware authentication that extracts scope context from request headers or cookies
1303
+ - **`require_tenant_auth`** - Strict tenant authentication with typed error handling
1304
+
1305
+ These utilities provide user details, permissions, and permission checking with built-in caching and rate limiting.
1300
1306
 
1301
1307
  ### Client-Side API Endpoint (Recommended)
1302
1308
 
@@ -1461,7 +1467,205 @@ export async function proxy(request: NextRequest) {
1461
1467
 
1462
1468
  ### Server-Side Functions
1463
1469
 
1464
- #### `hazo_get_auth` (Recommended)
1470
+ #### `hazo_get_tenant_auth` (Recommended for Multi-Tenant Apps)
1471
+
1472
+ **New:** Tenant-aware authentication function that extracts scope context from request headers or cookies and returns enriched result with organization information.
1473
+
1474
+ **Location:** `src/lib/auth/hazo_get_tenant_auth.server.ts`
1475
+
1476
+ **Scope Context Extraction:**
1477
+ - **Header (priority):** `X-Hazo-Scope-Id` (configurable via `scope_header_name`)
1478
+ - **Cookie (fallback):** `hazo_auth_scope_id` (with prefix if configured)
1479
+
1480
+ **Function Signature:**
1481
+ ```typescript
1482
+ import { hazo_get_tenant_auth } from "hazo_auth";
1483
+ import type { TenantAuthResult, TenantAuthOptions } from "hazo_auth";
1484
+
1485
+ async function hazo_get_tenant_auth(
1486
+ request: NextRequest,
1487
+ options?: TenantAuthOptions
1488
+ ): Promise<TenantAuthResult>
1489
+ ```
1490
+
1491
+ **Options:**
1492
+ - `required_permissions?: string[]` - Array of permission names to check
1493
+ - `strict?: boolean` - If `true`, throws errors when checks fail (default: `false`)
1494
+ - `scope_header_name?: string` - Custom header name for scope ID (default: `"X-Hazo-Scope-Id"`)
1495
+ - `scope_cookie_name?: string` - Custom cookie name for scope ID (default: `"hazo_auth_scope_id"`)
1496
+
1497
+ **Return Type:**
1498
+ ```typescript
1499
+ type TenantAuthResult =
1500
+ | {
1501
+ authenticated: true;
1502
+ user: HazoAuthUser;
1503
+ permissions: string[];
1504
+ permission_ok: boolean;
1505
+ missing_permissions?: string[];
1506
+ organization: TenantOrganization | null; // NEW: Tenant context
1507
+ user_scopes: ScopeDetails[]; // NEW: All user's scopes for switching
1508
+ scope_ok: boolean;
1509
+ }
1510
+ | {
1511
+ authenticated: false;
1512
+ user: null;
1513
+ permissions: [];
1514
+ permission_ok: false;
1515
+ organization: null;
1516
+ user_scopes: [];
1517
+ scope_ok: false;
1518
+ };
1519
+
1520
+ type TenantOrganization = {
1521
+ id: string;
1522
+ name: string;
1523
+ slug: string | null; // URL-friendly identifier
1524
+ level: string; // "Company", "Division", etc.
1525
+ role_id: string; // User's role in this scope
1526
+ is_super_admin: boolean;
1527
+ branding?: {
1528
+ logo_url: string | null;
1529
+ primary_color: string | null;
1530
+ secondary_color: string | null;
1531
+ tagline: string | null;
1532
+ };
1533
+ };
1534
+
1535
+ type ScopeDetails = {
1536
+ id: string;
1537
+ name: string;
1538
+ slug: string | null;
1539
+ level: string;
1540
+ parent_id: string | null;
1541
+ role_id: string;
1542
+ logo_url: string | null;
1543
+ primary_color: string | null;
1544
+ secondary_color: string | null;
1545
+ tagline: string | null;
1546
+ };
1547
+ ```
1548
+
1549
+ **Basic Usage Example:**
1550
+ ```typescript
1551
+ // app/api/dashboard/route.ts
1552
+ import { NextRequest, NextResponse } from "next/server";
1553
+ import { hazo_get_tenant_auth } from "hazo_auth";
1554
+
1555
+ export async function GET(request: NextRequest) {
1556
+ const auth = await hazo_get_tenant_auth(request);
1557
+
1558
+ if (!auth.authenticated) {
1559
+ return NextResponse.json({ error: "Authentication required" }, { status: 401 });
1560
+ }
1561
+
1562
+ if (!auth.organization) {
1563
+ return NextResponse.json(
1564
+ {
1565
+ error: "No organization context",
1566
+ available_scopes: auth.user_scopes.map(s => ({ id: s.id, name: s.name }))
1567
+ },
1568
+ { status: 403 }
1569
+ );
1570
+ }
1571
+
1572
+ // Access tenant-specific data
1573
+ const data = await getTenantData(auth.organization.id);
1574
+
1575
+ return NextResponse.json({
1576
+ organization: auth.organization,
1577
+ data,
1578
+ // Include available scopes for UI scope switcher
1579
+ available_scopes: auth.user_scopes,
1580
+ });
1581
+ }
1582
+ ```
1583
+
1584
+ **Strict Mode with Error Handling:**
1585
+ ```typescript
1586
+ import { require_tenant_auth, HazoAuthError } from "hazo_auth";
1587
+
1588
+ export async function GET(request: NextRequest) {
1589
+ try {
1590
+ const auth = await require_tenant_auth(request, {
1591
+ required_permissions: ["view_reports"],
1592
+ });
1593
+
1594
+ // auth.organization is guaranteed non-null here
1595
+ const reports = await getReports(auth.organization.id);
1596
+ return NextResponse.json({ reports });
1597
+ } catch (error) {
1598
+ if (error instanceof HazoAuthError) {
1599
+ return NextResponse.json(
1600
+ {
1601
+ error: error.message,
1602
+ code: error.code,
1603
+ // For TenantAccessDeniedError, includes available_scopes
1604
+ available_scopes: error.available_scopes
1605
+ },
1606
+ { status: error.status_code }
1607
+ );
1608
+ }
1609
+ throw error;
1610
+ }
1611
+ }
1612
+ ```
1613
+
1614
+ **Frontend Integration:**
1615
+ ```typescript
1616
+ // Client sets scope via header or cookie
1617
+ const response = await fetch("/api/dashboard", {
1618
+ headers: {
1619
+ "X-Hazo-Scope-Id": selectedScopeId,
1620
+ },
1621
+ });
1622
+
1623
+ // Or via cookie (set once during scope selection)
1624
+ document.cookie = `hazo_auth_scope_id=${selectedScopeId}; path=/`;
1625
+ ```
1626
+
1627
+ **Error Types:**
1628
+ ```typescript
1629
+ import {
1630
+ AuthenticationRequiredError, // 401 - User not authenticated
1631
+ TenantRequiredError, // 403 - No tenant context provided
1632
+ TenantAccessDeniedError, // 403 - User lacks access to tenant
1633
+ } from "hazo_auth";
1634
+ ```
1635
+
1636
+ #### `require_tenant_auth` (Strict Tenant Auth)
1637
+
1638
+ Helper function that wraps `hazo_get_tenant_auth` and throws typed errors for common failure cases.
1639
+
1640
+ **Throws:**
1641
+ - `AuthenticationRequiredError` (401) - User not authenticated
1642
+ - `TenantRequiredError` (403) - No tenant context in request
1643
+ - `TenantAccessDeniedError` (403) - User lacks access to requested tenant
1644
+
1645
+ **Returns:** `RequiredTenantAuthResult` with guaranteed non-null `organization`
1646
+
1647
+ **Example:**
1648
+ ```typescript
1649
+ export async function GET(request: NextRequest) {
1650
+ try {
1651
+ // organization is guaranteed to exist
1652
+ const { organization, user, permissions } = await require_tenant_auth(request);
1653
+
1654
+ const data = await getData(organization.id);
1655
+ return NextResponse.json(data);
1656
+ } catch (error) {
1657
+ if (error instanceof HazoAuthError) {
1658
+ return NextResponse.json(
1659
+ { error: error.message, code: error.code },
1660
+ { status: error.status_code }
1661
+ );
1662
+ }
1663
+ throw error;
1664
+ }
1665
+ }
1666
+ ```
1667
+
1668
+ #### `hazo_get_auth` (Standard Auth)
1465
1669
 
1466
1670
  The primary authentication utility for server-side use in API routes. Returns user details, permissions, and optionally checks required permissions.
1467
1671
 
@@ -1658,7 +1658,34 @@ WHERE table_name LIKE 'hazo_scopes_%'
1658
1658
  sqlite3 data/hazo_auth.sqlite ".tables" | grep -E "hazo_scopes|hazo_user_scopes|hazo_scope_labels"
1659
1659
  ```
1660
1660
 
1661
- ### Step 7.5: Test HRBAC
1661
+ ### Step 7.5: Add Slug Column to hazo_scopes (Optional - for Tenant Auth)
1662
+
1663
+ If you plan to use tenant-aware authentication with URL-friendly identifiers, add the `slug` column to the `hazo_scopes` table:
1664
+
1665
+ ```bash
1666
+ npm run migrate migrations/012_add_slug_to_hazo_scopes.sql
1667
+ ```
1668
+
1669
+ **Manual Migration (if needed):**
1670
+
1671
+ **PostgreSQL:**
1672
+ ```sql
1673
+ ALTER TABLE hazo_scopes ADD COLUMN slug TEXT;
1674
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1675
+ ```
1676
+
1677
+ **SQLite:**
1678
+ ```sql
1679
+ ALTER TABLE hazo_scopes ADD COLUMN slug TEXT;
1680
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1681
+ ```
1682
+
1683
+ **What is slug?**
1684
+ - URL-friendly identifier for scopes (e.g., "acme-corp", "sales-division")
1685
+ - Enables tenant context via URL paths (e.g., `/org/:slug/dashboard`)
1686
+ - Not enforced as unique to allow flexibility
1687
+
1688
+ ### Step 7.6: Test HRBAC
1662
1689
 
1663
1690
  1. Start your dev server: `npm run dev`
1664
1691
  2. Log in with a user that has `admin_scope_hierarchy_management` permission
@@ -1675,11 +1702,185 @@ sqlite3 data/hazo_auth.sqlite ".tables" | grep -E "hazo_scopes|hazo_user_scopes|
1675
1702
  - [ ] `hazo_scope_labels` (custom labels per org)
1676
1703
  - [ ] Scope ID generator function created (PostgreSQL)
1677
1704
  - [ ] Grants applied (PostgreSQL)
1705
+ - [ ] `slug` column added to hazo_scopes (optional, for tenant auth)
1678
1706
  - [ ] HRBAC tabs visible in User Management
1679
1707
  - [ ] Scope test page works
1680
1708
 
1681
1709
  ---
1682
1710
 
1711
+ ## Phase 8: Tenant-Aware Authentication Setup (Optional)
1712
+
1713
+ Tenant-aware authentication adds scope context to authentication results, enabling multi-tenant applications where users can access multiple organizations/scopes.
1714
+
1715
+ **Skip this phase if:**
1716
+ - Your app doesn't need multi-tenancy
1717
+ - Users don't switch between different scopes/organizations
1718
+
1719
+ ### Step 8.1: Ensure Slug Column Exists
1720
+
1721
+ The tenant auth feature uses the `slug` column for URL-friendly scope identifiers. If you didn't complete Step 7.5, do so now:
1722
+
1723
+ ```bash
1724
+ npm run migrate migrations/012_add_slug_to_hazo_scopes.sql
1725
+ ```
1726
+
1727
+ ### Step 8.2: Use Tenant Auth in API Routes
1728
+
1729
+ Replace `hazo_get_auth` with `hazo_get_tenant_auth` in your API routes:
1730
+
1731
+ **Before (standard auth):**
1732
+ ```typescript
1733
+ import { hazo_get_auth } from "hazo_auth";
1734
+
1735
+ export async function GET(request: NextRequest) {
1736
+ const auth = await hazo_get_auth(request);
1737
+ // No tenant context
1738
+ }
1739
+ ```
1740
+
1741
+ **After (tenant auth):**
1742
+ ```typescript
1743
+ import { hazo_get_tenant_auth } from "hazo_auth";
1744
+
1745
+ export async function GET(request: NextRequest) {
1746
+ const auth = await hazo_get_tenant_auth(request);
1747
+
1748
+ if (auth.authenticated && auth.organization) {
1749
+ // auth.organization contains tenant details
1750
+ // auth.user_scopes contains all scopes user can access (for UI switcher)
1751
+ const data = await getTenantData(auth.organization.id);
1752
+ }
1753
+ }
1754
+ ```
1755
+
1756
+ **Or use strict mode with error handling:**
1757
+ ```typescript
1758
+ import { require_tenant_auth, HazoAuthError } from "hazo_auth";
1759
+
1760
+ export async function GET(request: NextRequest) {
1761
+ try {
1762
+ const auth = await require_tenant_auth(request);
1763
+ // auth.organization is guaranteed non-null
1764
+ return NextResponse.json(await getData(auth.organization.id));
1765
+ } catch (error) {
1766
+ if (error instanceof HazoAuthError) {
1767
+ return NextResponse.json(
1768
+ { error: error.message, code: error.code },
1769
+ { status: error.status_code }
1770
+ );
1771
+ }
1772
+ throw error;
1773
+ }
1774
+ }
1775
+ ```
1776
+
1777
+ ### Step 8.3: Set Scope Context from Frontend
1778
+
1779
+ The frontend needs to send the current scope ID via header or cookie.
1780
+
1781
+ **Option A: Header (recommended - per-request):**
1782
+ ```typescript
1783
+ const response = await fetch("/api/dashboard", {
1784
+ headers: {
1785
+ "X-Hazo-Scope-Id": selectedScopeId,
1786
+ },
1787
+ });
1788
+ ```
1789
+
1790
+ **Option B: Cookie (persistent - set once):**
1791
+ ```typescript
1792
+ // Set cookie when user selects a scope
1793
+ document.cookie = `hazo_auth_scope_id=${selectedScopeId}; path=/`;
1794
+
1795
+ // Then all requests include the scope automatically
1796
+ const response = await fetch("/api/dashboard");
1797
+ ```
1798
+
1799
+ **Custom Configuration (optional):**
1800
+ ```typescript
1801
+ // Use custom header or cookie names
1802
+ const auth = await hazo_get_tenant_auth(request, {
1803
+ scope_header_name: "X-Tenant-Id", // Custom header
1804
+ scope_cookie_name: "my_app_tenant_id", // Custom cookie
1805
+ });
1806
+ ```
1807
+
1808
+ ### Step 8.4: Build Scope Switcher UI (Optional)
1809
+
1810
+ Use the `user_scopes` array to build a scope switcher dropdown:
1811
+
1812
+ ```typescript
1813
+ const auth = await hazo_get_tenant_auth(request);
1814
+
1815
+ // Return available scopes to frontend
1816
+ return NextResponse.json({
1817
+ current_scope: auth.organization,
1818
+ available_scopes: auth.user_scopes.map(s => ({
1819
+ id: s.id,
1820
+ name: s.name,
1821
+ slug: s.slug,
1822
+ level: s.level,
1823
+ branding: {
1824
+ logo_url: s.logo_url,
1825
+ primary_color: s.primary_color,
1826
+ },
1827
+ })),
1828
+ });
1829
+ ```
1830
+
1831
+ **Frontend Scope Switcher:**
1832
+ ```tsx
1833
+ function ScopeSwitcher({ scopes, currentScopeId }) {
1834
+ const handleScopeChange = (scopeId: string) => {
1835
+ // Option 1: Set cookie
1836
+ document.cookie = `hazo_auth_scope_id=${scopeId}; path=/`;
1837
+ window.location.reload();
1838
+
1839
+ // Option 2: Update state and send header on all requests
1840
+ setCurrentScope(scopeId);
1841
+ };
1842
+
1843
+ return (
1844
+ <select value={currentScopeId} onChange={e => handleScopeChange(e.target.value)}>
1845
+ {scopes.map(scope => (
1846
+ <option key={scope.id} value={scope.id}>
1847
+ {scope.name} ({scope.level})
1848
+ </option>
1849
+ ))}
1850
+ </select>
1851
+ );
1852
+ }
1853
+ ```
1854
+
1855
+ ### Step 8.5: Test Tenant Auth
1856
+
1857
+ **Test with cURL:**
1858
+ ```bash
1859
+ # No scope context - should return error or empty organization
1860
+ curl -H "Cookie: hazo_auth_session=YOUR_TOKEN" \
1861
+ http://localhost:3000/api/dashboard | jq
1862
+
1863
+ # With scope context via header
1864
+ curl -H "Cookie: hazo_auth_session=YOUR_TOKEN" \
1865
+ -H "X-Hazo-Scope-Id: SCOPE_UUID" \
1866
+ http://localhost:3000/api/dashboard | jq
1867
+
1868
+ # With scope context via cookie
1869
+ curl -H "Cookie: hazo_auth_session=YOUR_TOKEN; hazo_auth_scope_id=SCOPE_UUID" \
1870
+ http://localhost:3000/api/dashboard | jq
1871
+ ```
1872
+
1873
+ **Tenant Auth Checklist:**
1874
+ - [ ] `slug` column added to `hazo_scopes`
1875
+ - [ ] API routes updated to use `hazo_get_tenant_auth` or `require_tenant_auth`
1876
+ - [ ] Frontend sends scope context via header or cookie
1877
+ - [ ] Tenant auth returns organization details when scope is valid
1878
+ - [ ] Tenant auth returns user_scopes for building scope switcher
1879
+ - [ ] Error handling in place for missing/invalid scope context
1880
+ - [ ] Scope switcher UI built (optional but recommended)
1881
+
1882
+ ---
1883
+
1683
1884
  ## Troubleshooting
1684
1885
 
1685
1886
  ### Issue: Email not sending
@@ -1,17 +1,19 @@
1
1
  // file_description: LRU cache implementation for hazo_get_auth with TTL and size limits
2
2
  // section: imports
3
- import type { HazoAuthUser } from "./auth_types";
3
+ import type { HazoAuthUser, ScopeDetails } from "./auth_types";
4
4
 
5
5
  // section: types
6
6
 
7
7
  /**
8
8
  * Cache entry structure
9
9
  * v5.x: role_ids are now string UUIDs (from hazo_user_scopes)
10
+ * v5.2: Added scopes with full details for multi-tenancy support
10
11
  */
11
12
  type CacheEntry = {
12
13
  user: HazoAuthUser;
13
14
  permissions: string[];
14
15
  role_ids: string[];
16
+ scopes: ScopeDetails[]; // All user's scope assignments with full details
15
17
  timestamp: number; // Unix timestamp in milliseconds
16
18
  cache_version: number; // Version number for smart invalidation
17
19
  };
@@ -83,12 +85,14 @@ class AuthCache {
83
85
  * @param user - User data
84
86
  * @param permissions - User permissions
85
87
  * @param role_ids - User role IDs (v5.x: string UUIDs)
88
+ * @param scopes - User scope details with full information (v5.2+)
86
89
  */
87
90
  set(
88
91
  user_id: string,
89
92
  user: HazoAuthUser,
90
93
  permissions: string[],
91
94
  role_ids: string[],
95
+ scopes: ScopeDetails[] = [],
92
96
  ): void {
93
97
  // Evict LRU entries if cache is full
94
98
  while (this.cache.size >= this.max_size) {
@@ -107,6 +111,7 @@ class AuthCache {
107
111
  user,
108
112
  permissions,
109
113
  role_ids,
114
+ scopes,
110
115
  timestamp: Date.now(),
111
116
  cache_version,
112
117
  };
@@ -155,6 +160,24 @@ class AuthCache {
155
160
  this.cache.clear();
156
161
  }
157
162
 
163
+ /**
164
+ * Invalidates cache entries for users who have access to specific scopes
165
+ * Used when scope details change (name, branding, etc.)
166
+ * @param scope_ids - Array of scope IDs to invalidate
167
+ */
168
+ invalidate_by_scope_ids(scope_ids: string[]): void {
169
+ const entries_to_remove: string[] = [];
170
+ for (const [user_id, entry] of this.cache.entries()) {
171
+ const has_scope = entry.scopes.some((s) => scope_ids.includes(s.id));
172
+ if (has_scope) {
173
+ entries_to_remove.push(user_id);
174
+ }
175
+ }
176
+ for (const user_id of entries_to_remove) {
177
+ this.cache.delete(user_id);
178
+ }
179
+ }
180
+
158
181
  /**
159
182
  * Gets the maximum cache version for a set of roles
160
183
  * Used to determine if cache entry is stale
@@ -100,3 +100,145 @@ export class ScopeAccessError extends Error {
100
100
  this.name = "ScopeAccessError";
101
101
  }
102
102
  }
103
+
104
+ // section: scope_details_types
105
+
106
+ /**
107
+ * Full scope details with branding information
108
+ * Used in cache and tenant auth results for multi-tenancy support
109
+ */
110
+ export type ScopeDetails = {
111
+ id: string;
112
+ name: string;
113
+ slug: string | null;
114
+ level: string;
115
+ parent_id: string | null;
116
+ role_id: string;
117
+ // Branding fields (from hazo_scopes table)
118
+ logo_url: string | null;
119
+ primary_color: string | null;
120
+ secondary_color: string | null;
121
+ tagline: string | null;
122
+ };
123
+
124
+ /**
125
+ * Tenant/organization information returned in tenant auth results
126
+ * Simplified view of scope for API responses
127
+ */
128
+ export type TenantOrganization = {
129
+ id: string;
130
+ name: string;
131
+ slug: string | null;
132
+ level: string;
133
+ role_id: string;
134
+ is_super_admin: boolean;
135
+ branding?: {
136
+ logo_url: string | null;
137
+ primary_color: string | null;
138
+ secondary_color: string | null;
139
+ tagline: string | null;
140
+ };
141
+ };
142
+
143
+ /**
144
+ * Options for hazo_get_tenant_auth function
145
+ * Extends HazoAuthOptions with tenant-specific configuration
146
+ */
147
+ export type TenantAuthOptions = HazoAuthOptions & {
148
+ /**
149
+ * Header name to check for scope ID (default: "X-Hazo-Scope-Id")
150
+ */
151
+ scope_header_name?: string;
152
+ /**
153
+ * Cookie name to check for scope ID (uses cookie prefix if not specified)
154
+ */
155
+ scope_cookie_name?: string;
156
+ };
157
+
158
+ /**
159
+ * Result type for hazo_get_tenant_auth function
160
+ * Extends HazoAuthResult with tenant-specific information
161
+ */
162
+ export type TenantAuthResult =
163
+ | {
164
+ authenticated: true;
165
+ user: HazoAuthUser;
166
+ permissions: string[];
167
+ permission_ok: boolean;
168
+ missing_permissions?: string[];
169
+ organization: TenantOrganization | null;
170
+ user_scopes: ScopeDetails[];
171
+ scope_ok?: boolean;
172
+ scope_access_via?: ScopeAccessInfo;
173
+ }
174
+ | {
175
+ authenticated: false;
176
+ user: null;
177
+ permissions: [];
178
+ permission_ok: false;
179
+ organization: null;
180
+ user_scopes: [];
181
+ scope_ok?: false;
182
+ };
183
+
184
+ /**
185
+ * Guaranteed authenticated result with non-null organization
186
+ * Returned by require_tenant_auth when validation passes
187
+ */
188
+ export type RequiredTenantAuthResult = TenantAuthResult & {
189
+ authenticated: true;
190
+ organization: TenantOrganization;
191
+ };
192
+
193
+ // section: tenant_error_classes
194
+
195
+ /**
196
+ * Base error class for all hazo_auth errors
197
+ * Provides error code and HTTP status code for API responses
198
+ */
199
+ export class HazoAuthError extends Error {
200
+ constructor(
201
+ message: string,
202
+ public readonly code: string,
203
+ public readonly status_code: number,
204
+ ) {
205
+ super(message);
206
+ this.name = "HazoAuthError";
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Error thrown when authentication is required but user is not authenticated
212
+ */
213
+ export class AuthenticationRequiredError extends HazoAuthError {
214
+ constructor(message: string = "Authentication required") {
215
+ super(message, "AUTHENTICATION_REQUIRED", 401);
216
+ this.name = "AuthenticationRequiredError";
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Error thrown when a tenant/scope context is required but not provided
222
+ */
223
+ export class TenantRequiredError extends HazoAuthError {
224
+ constructor(
225
+ message: string = "Tenant context required",
226
+ public readonly user_scopes: ScopeDetails[] = [],
227
+ ) {
228
+ super(message, "TENANT_REQUIRED", 403);
229
+ this.name = "TenantRequiredError";
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Error thrown when user lacks access to the requested tenant/scope
235
+ */
236
+ export class TenantAccessDeniedError extends HazoAuthError {
237
+ constructor(
238
+ public readonly scope_id: string,
239
+ public readonly user_scopes: ScopeDetails[] = [],
240
+ ) {
241
+ super(`Access denied to scope: ${scope_id}`, "TENANT_ACCESS_DENIED", 403);
242
+ this.name = "TenantAccessDeniedError";
243
+ }
244
+ }