hazo_auth 5.1.11 → 5.1.13

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.
@@ -10,6 +10,7 @@ import type {
10
10
  HazoAuthUser,
11
11
  HazoAuthOptions,
12
12
  ScopeAccessInfo,
13
+ ScopeDetails,
13
14
  } from "./auth_types";
14
15
  import { PermissionError, ScopeAccessError } from "./auth_types.js";
15
16
  import { get_auth_cache } from "./auth_cache.js";
@@ -72,11 +73,6 @@ function get_client_ip(request: NextRequest): string {
72
73
  return "unknown";
73
74
  }
74
75
 
75
- /**
76
- * Fetches user data and permissions from database
77
- * @param user_id - User ID
78
- * @returns Object with user, permissions, and role_ids
79
- */
80
76
  /**
81
77
  * CRUD service options for hazo_user_scopes table
82
78
  * This table uses a composite primary key (user_id, scope_id) and no 'id' column
@@ -86,10 +82,63 @@ const USER_SCOPES_CRUD_OPTIONS = {
86
82
  autoId: false as const,
87
83
  };
88
84
 
85
+ /**
86
+ * Fetches full scope details for user's scope assignments
87
+ * Joins hazo_user_scopes with hazo_scopes to get name, slug, branding
88
+ * @param user_id - User ID
89
+ * @returns Array of scope details with branding information
90
+ */
91
+ async function fetch_user_scope_details(user_id: string): Promise<ScopeDetails[]> {
92
+ const hazoConnect = get_hazo_connect_instance();
93
+ const user_scopes_service = createCrudService(
94
+ hazoConnect,
95
+ "hazo_user_scopes",
96
+ USER_SCOPES_CRUD_OPTIONS,
97
+ );
98
+ const scopes_service = createCrudService(hazoConnect, "hazo_scopes");
99
+
100
+ const user_scope_assignments = await user_scopes_service.findBy({ user_id });
101
+ if (!Array.isArray(user_scope_assignments) || user_scope_assignments.length === 0) {
102
+ return [];
103
+ }
104
+
105
+ const scope_details: ScopeDetails[] = [];
106
+ for (const assignment of user_scope_assignments) {
107
+ const scope_id = assignment.scope_id as string;
108
+ const role_id = assignment.role_id as string;
109
+
110
+ const scopes = await scopes_service.findBy({ id: scope_id });
111
+ if (Array.isArray(scopes) && scopes.length > 0) {
112
+ const scope = scopes[0];
113
+ scope_details.push({
114
+ id: scope_id,
115
+ name: scope.name as string,
116
+ slug: (scope.slug as string) || null,
117
+ level: scope.level as string,
118
+ parent_id: scope.parent_id as string | null,
119
+ role_id,
120
+ logo_url: (scope.logo_url as string) || null,
121
+ primary_color: (scope.primary_color as string) || null,
122
+ secondary_color: (scope.secondary_color as string) || null,
123
+ tagline: (scope.tagline as string) || null,
124
+ });
125
+ }
126
+ }
127
+
128
+ return scope_details;
129
+ }
130
+
131
+ /**
132
+ * Fetches user data and permissions from database
133
+ * @param user_id - User ID
134
+ * @returns Object with user, permissions, role_ids, and scopes
135
+ */
136
+
89
137
  async function fetch_user_data_from_db(user_id: string): Promise<{
90
138
  user: HazoAuthUser;
91
139
  permissions: string[];
92
140
  role_ids: string[];
141
+ scopes: ScopeDetails[];
93
142
  }> {
94
143
  const hazoConnect = get_hazo_connect_instance();
95
144
  const users_service = createCrudService(hazoConnect, "hazo_users");
@@ -186,7 +235,10 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
186
235
 
187
236
  const permissions = Array.from(permissions_set);
188
237
 
189
- return { user, permissions, role_ids };
238
+ // v5.2: Fetch full scope details for caching
239
+ const scopes = await fetch_user_scope_details(user_id);
240
+
241
+ return { user, permissions, role_ids, scopes };
190
242
  }
191
243
 
192
244
  /**
@@ -430,8 +482,8 @@ export async function hazo_get_auth(
430
482
  permissions = user_data.permissions;
431
483
  role_ids = user_data.role_ids;
432
484
 
433
- // Update cache
434
- cache.set(user_id, user, permissions, role_ids);
485
+ // Update cache (v5.2: includes scope details for tenant auth)
486
+ cache.set(user_id, user, permissions, role_ids, user_data.scopes);
435
487
  } catch (error) {
436
488
  const error_message =
437
489
  error instanceof Error ? error.message : "Unknown error";
@@ -0,0 +1,267 @@
1
+ // file_description: tenant-aware authentication function that extracts scope from request headers/cookies
2
+ // section: imports
3
+ import { NextRequest } from "next/server";
4
+ import { hazo_get_auth } from "./hazo_get_auth.server.js";
5
+ import { get_auth_cache } from "./auth_cache.js";
6
+ import { get_scope_by_id } from "../services/scope_service.js";
7
+ import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
8
+ import { get_cookie_name } from "../cookies_config.server.js";
9
+ import { get_auth_utility_config } from "../auth_utility_config.server.js";
10
+ import type {
11
+ TenantAuthOptions,
12
+ TenantAuthResult,
13
+ RequiredTenantAuthResult,
14
+ TenantOrganization,
15
+ ScopeDetails,
16
+ } from "./auth_types";
17
+ import {
18
+ AuthenticationRequiredError,
19
+ TenantRequiredError,
20
+ TenantAccessDeniedError,
21
+ } from "./auth_types.js";
22
+
23
+ // section: constants
24
+
25
+ /**
26
+ * Default header name for scope ID
27
+ */
28
+ const DEFAULT_SCOPE_HEADER = "X-Hazo-Scope-Id";
29
+
30
+ /**
31
+ * Base cookie name for scope ID (will have prefix applied)
32
+ */
33
+ const BASE_SCOPE_COOKIE = "hazo_auth_scope_id";
34
+
35
+ // section: helpers
36
+
37
+ /**
38
+ * Extracts scope ID from request headers or cookies
39
+ * Priority: Header > Cookie
40
+ * @param request - NextRequest object
41
+ * @param options - TenantAuthOptions for customization
42
+ * @returns Scope ID if found, undefined otherwise
43
+ */
44
+ export function extract_scope_id_from_request(
45
+ request: NextRequest,
46
+ options: TenantAuthOptions,
47
+ ): string | undefined {
48
+ // Check header first
49
+ const header_name = options.scope_header_name || DEFAULT_SCOPE_HEADER;
50
+ const header_value = request.headers.get(header_name);
51
+ if (header_value) {
52
+ return header_value;
53
+ }
54
+
55
+ // Check cookie (with prefix)
56
+ const cookie_name = options.scope_cookie_name || get_cookie_name(BASE_SCOPE_COOKIE);
57
+ const cookie_value = request.cookies.get(cookie_name)?.value;
58
+ return cookie_value;
59
+ }
60
+
61
+ /**
62
+ * Builds TenantOrganization from scope details and access info
63
+ * @param scope_details - Full scope details from cache
64
+ * @param is_super_admin - Whether user is accessing as super admin
65
+ * @returns TenantOrganization object
66
+ */
67
+ function build_tenant_organization(
68
+ scope_details: ScopeDetails,
69
+ is_super_admin: boolean,
70
+ ): TenantOrganization {
71
+ return {
72
+ id: scope_details.id,
73
+ name: scope_details.name,
74
+ slug: scope_details.slug,
75
+ level: scope_details.level,
76
+ role_id: scope_details.role_id,
77
+ is_super_admin,
78
+ branding:
79
+ scope_details.logo_url || scope_details.primary_color
80
+ ? {
81
+ logo_url: scope_details.logo_url,
82
+ primary_color: scope_details.primary_color,
83
+ secondary_color: scope_details.secondary_color,
84
+ tagline: scope_details.tagline,
85
+ }
86
+ : undefined,
87
+ };
88
+ }
89
+
90
+ // section: main_functions
91
+
92
+ /**
93
+ * Tenant-aware authentication function
94
+ *
95
+ * Extracts tenant/scope context from request headers or cookies,
96
+ * validates access, and returns enriched result with organization info.
97
+ *
98
+ * Header priority: X-Hazo-Scope-Id > Cookie
99
+ *
100
+ * @param request - NextRequest object
101
+ * @param options - TenantAuthOptions for customization
102
+ * @returns TenantAuthResult with user, permissions, organization, and user_scopes
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const auth = await hazo_get_tenant_auth(request);
107
+ * if (auth.authenticated && auth.organization) {
108
+ * // Access tenant-specific data
109
+ * const data = await getData(auth.organization.id);
110
+ * }
111
+ * ```
112
+ */
113
+ export async function hazo_get_tenant_auth(
114
+ request: NextRequest,
115
+ options: TenantAuthOptions = {},
116
+ ): Promise<TenantAuthResult> {
117
+ // Extract scope_id from request
118
+ const scope_id = extract_scope_id_from_request(request, options);
119
+
120
+ // Build hazo_get_auth options, passing through scope_id if present
121
+ const auth_options = {
122
+ ...options,
123
+ scope_id: scope_id || options.scope_id, // Allow explicit override via options
124
+ };
125
+
126
+ // Call base hazo_get_auth
127
+ const auth_result = await hazo_get_auth(request, auth_options);
128
+
129
+ // Handle unauthenticated case
130
+ if (!auth_result.authenticated) {
131
+ return {
132
+ authenticated: false,
133
+ user: null,
134
+ permissions: [],
135
+ permission_ok: false,
136
+ organization: null,
137
+ organization_id: null,
138
+ user_scopes: [],
139
+ scope_ok: false,
140
+ };
141
+ }
142
+
143
+ // Get user's scopes from cache (already populated by hazo_get_auth)
144
+ const config = get_auth_utility_config();
145
+ const cache = get_auth_cache(
146
+ config.cache_max_users,
147
+ config.cache_ttl_minutes,
148
+ config.cache_max_age_minutes,
149
+ );
150
+ const cached = cache.get(auth_result.user.id);
151
+
152
+ // User scopes from cache or empty array
153
+ const user_scopes: ScopeDetails[] = cached?.scopes || [];
154
+
155
+ // Build organization info if scope access was successful
156
+ let organization: TenantOrganization | null = null;
157
+
158
+ if (scope_id && auth_result.scope_ok && auth_result.scope_access_via) {
159
+ // Find the scope in user's scopes that matches the access_via scope
160
+ const access_scope = user_scopes.find(
161
+ (s) => s.id === auth_result.scope_access_via?.scope_id,
162
+ );
163
+
164
+ if (access_scope) {
165
+ organization = build_tenant_organization(
166
+ access_scope,
167
+ auth_result.scope_access_via.is_super_admin || false,
168
+ );
169
+ } else if (auth_result.scope_access_via.is_super_admin) {
170
+ // Super admin accessing scope they're not assigned to - fetch scope details
171
+ const hazoConnect = get_hazo_connect_instance();
172
+ const scope_result = await get_scope_by_id(hazoConnect, scope_id);
173
+ if (scope_result.success && scope_result.scope) {
174
+ organization = {
175
+ id: scope_result.scope.id,
176
+ name: scope_result.scope.name,
177
+ slug: null, // Could fetch from scope if slug column exists
178
+ level: scope_result.scope.level,
179
+ role_id: "", // Super admin doesn't have a role in the scope
180
+ is_super_admin: true,
181
+ branding: scope_result.scope.logo_url
182
+ ? {
183
+ logo_url: scope_result.scope.logo_url,
184
+ primary_color: scope_result.scope.primary_color,
185
+ secondary_color: scope_result.scope.secondary_color,
186
+ tagline: scope_result.scope.tagline,
187
+ }
188
+ : undefined,
189
+ };
190
+ }
191
+ }
192
+ }
193
+
194
+ return {
195
+ authenticated: true,
196
+ user: auth_result.user,
197
+ permissions: auth_result.permissions,
198
+ permission_ok: auth_result.permission_ok,
199
+ missing_permissions: auth_result.missing_permissions,
200
+ organization,
201
+ organization_id: organization?.id || null,
202
+ user_scopes,
203
+ scope_ok: auth_result.scope_ok,
204
+ scope_access_via: auth_result.scope_access_via,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Strict tenant authentication helper
210
+ *
211
+ * Wraps hazo_get_tenant_auth and throws appropriate errors:
212
+ * - AuthenticationRequiredError (401) if not authenticated
213
+ * - TenantRequiredError (403) if no tenant context in request
214
+ * - TenantAccessDeniedError (403) if user lacks access to requested tenant
215
+ *
216
+ * @param request - NextRequest object
217
+ * @param options - TenantAuthOptions for customization
218
+ * @returns RequiredTenantAuthResult with guaranteed non-null organization
219
+ * @throws AuthenticationRequiredError, TenantRequiredError, TenantAccessDeniedError
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * try {
224
+ * const auth = await require_tenant_auth(request);
225
+ * // auth.organization is guaranteed non-null here
226
+ * const data = await getData(auth.organization.id);
227
+ * } catch (error) {
228
+ * if (error instanceof HazoAuthError) {
229
+ * return NextResponse.json(
230
+ * { error: error.message, code: error.code },
231
+ * { status: error.status_code }
232
+ * );
233
+ * }
234
+ * throw error;
235
+ * }
236
+ * ```
237
+ */
238
+ export async function require_tenant_auth(
239
+ request: NextRequest,
240
+ options: TenantAuthOptions = {},
241
+ ): Promise<RequiredTenantAuthResult> {
242
+ const result = await hazo_get_tenant_auth(request, options);
243
+
244
+ // Check authentication
245
+ if (!result.authenticated) {
246
+ throw new AuthenticationRequiredError();
247
+ }
248
+
249
+ // Extract scope_id from request for error messages
250
+ const scope_id = extract_scope_id_from_request(request, options);
251
+
252
+ // Check if scope was requested but access denied
253
+ if (scope_id && !result.scope_ok) {
254
+ throw new TenantAccessDeniedError(scope_id, result.user_scopes);
255
+ }
256
+
257
+ // Check if organization context is required but missing
258
+ if (!result.organization) {
259
+ throw new TenantRequiredError(
260
+ "No organization context provided. Include X-Hazo-Scope-Id header or scope cookie.",
261
+ result.user_scopes,
262
+ );
263
+ }
264
+
265
+ // Type assertion: at this point we know organization is non-null
266
+ return result as RequiredTenantAuthResult;
267
+ }
@@ -11,6 +11,26 @@ export {
11
11
  } from "./auth_utils.server.js";
12
12
  export type { AuthResult, AuthUser } from "./auth_utils.server";
13
13
 
14
+ // section: tenant_auth_exports
15
+ export {
16
+ hazo_get_tenant_auth,
17
+ require_tenant_auth,
18
+ extract_scope_id_from_request,
19
+ } from "./hazo_get_tenant_auth.server.js";
20
+ export type {
21
+ ScopeDetails,
22
+ TenantOrganization,
23
+ TenantAuthOptions,
24
+ TenantAuthResult,
25
+ RequiredTenantAuthResult,
26
+ } from "./auth_types";
27
+ export {
28
+ HazoAuthError,
29
+ AuthenticationRequiredError,
30
+ TenantRequiredError,
31
+ TenantAccessDeniedError,
32
+ } from "./auth_types.js";
33
+
14
34
  // section: client_exports
15
35
  export { get_server_auth_user } from "./server_auth.js";
16
36
  export type { ServerAuthResult } from "./server_auth";
@@ -25,6 +25,7 @@ export const BASE_COOKIE_NAMES = {
25
25
  USER_EMAIL: "hazo_auth_user_email",
26
26
  SESSION: "hazo_auth_session",
27
27
  DEV_LOCK: "hazo_auth_dev_lock",
28
+ SCOPE_ID: "hazo_auth_scope_id", // v5.2: Tenant context cookie for multi-tenancy
28
29
  } as const;
29
30
 
30
31
  // section: main_function
@@ -1,12 +1,14 @@
1
- import type { HazoAuthUser } from "./auth_types";
1
+ import type { HazoAuthUser, ScopeDetails } from "./auth_types";
2
2
  /**
3
3
  * Cache entry structure
4
4
  * v5.x: role_ids are now string UUIDs (from hazo_user_scopes)
5
+ * v5.2: Added scopes with full details for multi-tenancy support
5
6
  */
6
7
  type CacheEntry = {
7
8
  user: HazoAuthUser;
8
9
  permissions: string[];
9
10
  role_ids: string[];
11
+ scopes: ScopeDetails[];
10
12
  timestamp: number;
11
13
  cache_version: number;
12
14
  };
@@ -35,8 +37,9 @@ declare class AuthCache {
35
37
  * @param user - User data
36
38
  * @param permissions - User permissions
37
39
  * @param role_ids - User role IDs (v5.x: string UUIDs)
40
+ * @param scopes - User scope details with full information (v5.2+)
38
41
  */
39
- set(user_id: string, user: HazoAuthUser, permissions: string[], role_ids: string[]): void;
42
+ set(user_id: string, user: HazoAuthUser, permissions: string[], role_ids: string[], scopes?: ScopeDetails[]): void;
40
43
  /**
41
44
  * Invalidates cache for a specific user
42
45
  * @param user_id - User ID to invalidate
@@ -52,6 +55,12 @@ declare class AuthCache {
52
55
  * Invalidates all cache entries
53
56
  */
54
57
  invalidate_all(): void;
58
+ /**
59
+ * Invalidates cache entries for users who have access to specific scopes
60
+ * Used when scope details change (name, branding, etc.)
61
+ * @param scope_ids - Array of scope IDs to invalidate
62
+ */
63
+ invalidate_by_scope_ids(scope_ids: string[]): void;
55
64
  /**
56
65
  * Gets the maximum cache version for a set of roles
57
66
  * Used to determine if cache entry is stale
@@ -1 +1 @@
1
- {"version":3,"file":"auth_cache.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD;;;GAGG;AACH,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE,YAAY,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,cAAM,SAAS;IACb,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAsB;gBAG5C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM;IASzB;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IA+B5C;;;;;;;OAOG;IACH,GAAG,CACD,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,EAAE,MAAM,EAAE,GACjB,IAAI;IAyBP;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAItC;;;;OAIG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;IAqB7C;;OAEG;IACH,cAAc,IAAI,IAAI;IAItB;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;;OAGG;IACH,SAAS,IAAI;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB;CAMF;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAc,EACxB,WAAW,GAAE,MAAW,EACxB,eAAe,GAAE,MAAW,GAC3B,SAAS,CAKX;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
1
+ {"version":3,"file":"auth_cache.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAI/D;;;;GAIG;AACH,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE,YAAY,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,cAAM,SAAS;IACb,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAsB;gBAG5C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM;IASzB;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IA+B5C;;;;;;;;OAQG;IACH,GAAG,CACD,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,GAAE,YAAY,EAAO,GAC1B,IAAI;IA0BP;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAItC;;;;OAIG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;IAqB7C;;OAEG;IACH,cAAc,IAAI,IAAI;IAItB;;;;OAIG;IACH,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI;IAalD;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;;OAGG;IACH,SAAS,IAAI;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB;CAMF;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAc,EACxB,WAAW,GAAE,MAAW,EACxB,eAAe,GAAE,MAAW,GAC3B,SAAS,CAKX;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -47,8 +47,9 @@ class AuthCache {
47
47
  * @param user - User data
48
48
  * @param permissions - User permissions
49
49
  * @param role_ids - User role IDs (v5.x: string UUIDs)
50
+ * @param scopes - User scope details with full information (v5.2+)
50
51
  */
51
- set(user_id, user, permissions, role_ids) {
52
+ set(user_id, user, permissions, role_ids, scopes = []) {
52
53
  // Evict LRU entries if cache is full
53
54
  while (this.cache.size >= this.max_size) {
54
55
  const first_key = this.cache.keys().next().value;
@@ -65,6 +66,7 @@ class AuthCache {
65
66
  user,
66
67
  permissions,
67
68
  role_ids,
69
+ scopes,
68
70
  timestamp: Date.now(),
69
71
  cache_version,
70
72
  };
@@ -106,6 +108,23 @@ class AuthCache {
106
108
  invalidate_all() {
107
109
  this.cache.clear();
108
110
  }
111
+ /**
112
+ * Invalidates cache entries for users who have access to specific scopes
113
+ * Used when scope details change (name, branding, etc.)
114
+ * @param scope_ids - Array of scope IDs to invalidate
115
+ */
116
+ invalidate_by_scope_ids(scope_ids) {
117
+ const entries_to_remove = [];
118
+ for (const [user_id, entry] of this.cache.entries()) {
119
+ const has_scope = entry.scopes.some((s) => scope_ids.includes(s.id));
120
+ if (has_scope) {
121
+ entries_to_remove.push(user_id);
122
+ }
123
+ }
124
+ for (const user_id of entries_to_remove) {
125
+ this.cache.delete(user_id);
126
+ }
127
+ }
109
128
  /**
110
129
  * Gets the maximum cache version for a set of roles
111
130
  * Used to determine if cache entry is stale
@@ -85,4 +85,117 @@ export declare class ScopeAccessError extends Error {
85
85
  scope_name?: string;
86
86
  }>);
87
87
  }
88
+ /**
89
+ * Full scope details with branding information
90
+ * Used in cache and tenant auth results for multi-tenancy support
91
+ */
92
+ export type ScopeDetails = {
93
+ id: string;
94
+ name: string;
95
+ slug: string | null;
96
+ level: string;
97
+ parent_id: string | null;
98
+ role_id: string;
99
+ logo_url: string | null;
100
+ primary_color: string | null;
101
+ secondary_color: string | null;
102
+ tagline: string | null;
103
+ };
104
+ /**
105
+ * Tenant/organization information returned in tenant auth results
106
+ * Simplified view of scope for API responses
107
+ */
108
+ export type TenantOrganization = {
109
+ id: string;
110
+ name: string;
111
+ slug: string | null;
112
+ level: string;
113
+ role_id: string;
114
+ is_super_admin: boolean;
115
+ branding?: {
116
+ logo_url: string | null;
117
+ primary_color: string | null;
118
+ secondary_color: string | null;
119
+ tagline: string | null;
120
+ };
121
+ };
122
+ /**
123
+ * Options for hazo_get_tenant_auth function
124
+ * Extends HazoAuthOptions with tenant-specific configuration
125
+ */
126
+ export type TenantAuthOptions = HazoAuthOptions & {
127
+ /**
128
+ * Header name to check for scope ID (default: "X-Hazo-Scope-Id")
129
+ */
130
+ scope_header_name?: string;
131
+ /**
132
+ * Cookie name to check for scope ID (uses cookie prefix if not specified)
133
+ */
134
+ scope_cookie_name?: string;
135
+ };
136
+ /**
137
+ * Result type for hazo_get_tenant_auth function
138
+ * Extends HazoAuthResult with tenant-specific information
139
+ */
140
+ export type TenantAuthResult = {
141
+ authenticated: true;
142
+ user: HazoAuthUser;
143
+ permissions: string[];
144
+ permission_ok: boolean;
145
+ missing_permissions?: string[];
146
+ organization: TenantOrganization | null;
147
+ /** Shorthand for organization?.id - commonly used for DB query filters */
148
+ organization_id: string | null;
149
+ user_scopes: ScopeDetails[];
150
+ scope_ok?: boolean;
151
+ scope_access_via?: ScopeAccessInfo;
152
+ } | {
153
+ authenticated: false;
154
+ user: null;
155
+ permissions: [];
156
+ permission_ok: false;
157
+ organization: null;
158
+ /** Shorthand for organization?.id - commonly used for DB query filters */
159
+ organization_id: null;
160
+ user_scopes: [];
161
+ scope_ok?: false;
162
+ };
163
+ /**
164
+ * Guaranteed authenticated result with non-null organization
165
+ * Returned by require_tenant_auth when validation passes
166
+ */
167
+ export type RequiredTenantAuthResult = TenantAuthResult & {
168
+ authenticated: true;
169
+ organization: TenantOrganization;
170
+ };
171
+ /**
172
+ * Base error class for all hazo_auth errors
173
+ * Provides error code and HTTP status code for API responses
174
+ */
175
+ export declare class HazoAuthError extends Error {
176
+ readonly code: string;
177
+ readonly status_code: number;
178
+ constructor(message: string, code: string, status_code: number);
179
+ }
180
+ /**
181
+ * Error thrown when authentication is required but user is not authenticated
182
+ */
183
+ export declare class AuthenticationRequiredError extends HazoAuthError {
184
+ constructor(message?: string);
185
+ }
186
+ /**
187
+ * Error thrown when a tenant/scope context is required but not provided
188
+ */
189
+ export declare class TenantRequiredError extends HazoAuthError {
190
+ readonly user_scopes: ScopeDetails[];
191
+ constructor(message?: string, user_scopes?: ScopeDetails[]);
192
+ }
193
+ /**
194
+ * Error thrown when user lacks access to the requested tenant/scope
195
+ */
196
+ export declare class TenantAccessDeniedError extends HazoAuthError {
197
+ readonly scope_id: string;
198
+ readonly user_scopes: ScopeDetails[];
199
+ constructor(scope_id: string, user_scopes?: ScopeDetails[]);
200
+ }
88
201
  //# sourceMappingURL=auth_types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth_types.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IAEnC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,aAAa,EAAE,IAAI,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,eAAe,CAAC;CACpC,GACD;IACE,aAAa,EAAE,KAAK,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,EAAE,CAAC;IAChB,aAAa,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC;AAEN;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IAE/B,mBAAmB,EAAE,MAAM,EAAE;IAC7B,gBAAgB,EAAE,MAAM,EAAE;IAC1B,oBAAoB,EAAE,MAAM,EAAE;IAC9B,qBAAqB,CAAC,EAAE,MAAM;IAC9B,uBAAuB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;gBAJ7C,mBAAmB,EAAE,MAAM,EAAE,EAC7B,gBAAgB,EAAE,MAAM,EAAE,EAC1B,oBAAoB,EAAE,MAAM,EAAE,EAC9B,qBAAqB,CAAC,EAAE,MAAM,YAAA,EAC9B,uBAAuB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,YAAA;CAKvD;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IAEhC,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;gBAD7D,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAKvE"}
1
+ {"version":3,"file":"auth_types.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_types.ts"],"names":[],"mappings":"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"}
@@ -28,3 +28,46 @@ export class ScopeAccessError extends Error {
28
28
  this.name = "ScopeAccessError";
29
29
  }
30
30
  }
31
+ // section: tenant_error_classes
32
+ /**
33
+ * Base error class for all hazo_auth errors
34
+ * Provides error code and HTTP status code for API responses
35
+ */
36
+ export class HazoAuthError extends Error {
37
+ constructor(message, code, status_code) {
38
+ super(message);
39
+ this.code = code;
40
+ this.status_code = status_code;
41
+ this.name = "HazoAuthError";
42
+ }
43
+ }
44
+ /**
45
+ * Error thrown when authentication is required but user is not authenticated
46
+ */
47
+ export class AuthenticationRequiredError extends HazoAuthError {
48
+ constructor(message = "Authentication required") {
49
+ super(message, "AUTHENTICATION_REQUIRED", 401);
50
+ this.name = "AuthenticationRequiredError";
51
+ }
52
+ }
53
+ /**
54
+ * Error thrown when a tenant/scope context is required but not provided
55
+ */
56
+ export class TenantRequiredError extends HazoAuthError {
57
+ constructor(message = "Tenant context required", user_scopes = []) {
58
+ super(message, "TENANT_REQUIRED", 403);
59
+ this.user_scopes = user_scopes;
60
+ this.name = "TenantRequiredError";
61
+ }
62
+ }
63
+ /**
64
+ * Error thrown when user lacks access to the requested tenant/scope
65
+ */
66
+ export class TenantAccessDeniedError extends HazoAuthError {
67
+ constructor(scope_id, user_scopes = []) {
68
+ super(`Access denied to scope: ${scope_id}`, "TENANT_ACCESS_DENIED", 403);
69
+ this.scope_id = scope_id;
70
+ this.user_scopes = user_scopes;
71
+ this.name = "TenantAccessDeniedError";
72
+ }
73
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"hazo_get_auth.server.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/hazo_get_auth.server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EACV,cAAc,EAEd,eAAe,EAEhB,MAAM,cAAc,CAAC;AAmTtB;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAmNzB"}
1
+ {"version":3,"file":"hazo_get_auth.server.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/hazo_get_auth.server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EACV,cAAc,EAEd,eAAe,EAGhB,MAAM,cAAc,CAAC;AAsWtB;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAmNzB"}