@eminuckan/spine 0.2.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 (99) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +425 -0
  4. package/dist/api-client/index.d.ts +3 -0
  5. package/dist/api-client/index.js +4 -0
  6. package/dist/api-client/server.d.ts +4 -0
  7. package/dist/api-client/server.js +5 -0
  8. package/dist/api-client-Ftxqm3Ep.js +212 -0
  9. package/dist/api-client-Ftxqm3Ep.js.map +1 -0
  10. package/dist/auth/index.d.ts +2 -0
  11. package/dist/auth/index.js +0 -0
  12. package/dist/auth/server.d.ts +3 -0
  13. package/dist/auth/server.js +6 -0
  14. package/dist/auth.server-C979TR6W.js +724 -0
  15. package/dist/auth.server-C979TR6W.js.map +1 -0
  16. package/dist/auth.server-DU-x2LPA.js +5 -0
  17. package/dist/identity/index.d.ts +4 -0
  18. package/dist/identity/index.js +3 -0
  19. package/dist/identity/server.d.ts +4 -0
  20. package/dist/identity/server.js +4 -0
  21. package/dist/identity-CFa1txoj.js +235 -0
  22. package/dist/identity-CFa1txoj.js.map +1 -0
  23. package/dist/index-BIJOC8Fb.d.ts +272 -0
  24. package/dist/index-BIJOC8Fb.d.ts.map +1 -0
  25. package/dist/index-Bt9XE6gj.d.ts +104 -0
  26. package/dist/index-Bt9XE6gj.d.ts.map +1 -0
  27. package/dist/index-C2hGpzGr.d.ts +74 -0
  28. package/dist/index-C2hGpzGr.d.ts.map +1 -0
  29. package/dist/index-CfQsb3l_.d.ts +170 -0
  30. package/dist/index-CfQsb3l_.d.ts.map +1 -0
  31. package/dist/index-D6RK_5vm.d.ts +199 -0
  32. package/dist/index-D6RK_5vm.d.ts.map +1 -0
  33. package/dist/index-DrttYPAj.d.ts +30 -0
  34. package/dist/index-DrttYPAj.d.ts.map +1 -0
  35. package/dist/index-geY2ynnN.d.ts +141 -0
  36. package/dist/index-geY2ynnN.d.ts.map +1 -0
  37. package/dist/index-kr8Wk7v5.d.ts +1 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.js +10 -0
  40. package/dist/logging/index.d.ts +3 -0
  41. package/dist/logging/index.js +3 -0
  42. package/dist/logging-_h2mi3L9.js +118 -0
  43. package/dist/logging-_h2mi3L9.js.map +1 -0
  44. package/dist/permissions/index.d.ts +2 -0
  45. package/dist/permissions/index.js +3 -0
  46. package/dist/permissions-D5ScGGnd.js +281 -0
  47. package/dist/permissions-D5ScGGnd.js.map +1 -0
  48. package/dist/query-client/index.d.ts +2 -0
  49. package/dist/query-client/index.js +3 -0
  50. package/dist/query-client-DRPQIKHX.js +174 -0
  51. package/dist/query-client-DRPQIKHX.js.map +1 -0
  52. package/dist/react-router/index.d.ts +12 -0
  53. package/dist/react-router/index.js +10 -0
  54. package/dist/react-router/server.d.ts +18 -0
  55. package/dist/react-router/server.js +17 -0
  56. package/dist/redis-session-storage.server-DhufyPjd.js +250 -0
  57. package/dist/redis-session-storage.server-DhufyPjd.js.map +1 -0
  58. package/dist/server-BoeUiXSh.d.ts +40 -0
  59. package/dist/server-BoeUiXSh.d.ts.map +1 -0
  60. package/dist/server-C-DiVP9e.js +185 -0
  61. package/dist/server-C-DiVP9e.js.map +1 -0
  62. package/dist/server-CLbksnvg.d.ts +144 -0
  63. package/dist/server-CLbksnvg.d.ts.map +1 -0
  64. package/dist/server-CNp6gjFE.js +78 -0
  65. package/dist/server-CNp6gjFE.js.map +1 -0
  66. package/dist/server-CXGaRaiS.d.ts +256 -0
  67. package/dist/server-CXGaRaiS.d.ts.map +1 -0
  68. package/dist/server-CiSbg-9C.js +352 -0
  69. package/dist/server-CiSbg-9C.js.map +1 -0
  70. package/dist/server-DLgH3FRR.js +312 -0
  71. package/dist/server-DLgH3FRR.js.map +1 -0
  72. package/dist/server-DazqdM63.d.ts +93 -0
  73. package/dist/server-DazqdM63.d.ts.map +1 -0
  74. package/dist/server-iWm3oeCM.js +145 -0
  75. package/dist/server-iWm3oeCM.js.map +1 -0
  76. package/dist/server-zEKMDpP7.d.ts +80 -0
  77. package/dist/server-zEKMDpP7.d.ts.map +1 -0
  78. package/dist/server.d.ts +18 -0
  79. package/dist/server.js +17 -0
  80. package/dist/signalr/index.d.ts +4 -0
  81. package/dist/signalr/index.js +3 -0
  82. package/dist/signalr-BJGcXc0m.js +274 -0
  83. package/dist/signalr-BJGcXc0m.js.map +1 -0
  84. package/dist/src-D7mh9MKA.js +0 -0
  85. package/dist/tenant/index.d.ts +3 -0
  86. package/dist/tenant/index.js +3 -0
  87. package/dist/tenant/server.d.ts +3 -0
  88. package/dist/tenant/server.js +5 -0
  89. package/dist/tenant-C6KPaSCX.js +256 -0
  90. package/dist/tenant-C6KPaSCX.js.map +1 -0
  91. package/dist/types-BTjFzIhv.d.ts +31 -0
  92. package/dist/types-BTjFzIhv.d.ts.map +1 -0
  93. package/dist/types-DoIfDhl6.d.ts +55 -0
  94. package/dist/types-DoIfDhl6.d.ts.map +1 -0
  95. package/dist/types-DufLRY7A.d.ts +164 -0
  96. package/dist/types-DufLRY7A.d.ts.map +1 -0
  97. package/dist/types-uDrDMkCq.d.ts +93 -0
  98. package/dist/types-uDrDMkCq.d.ts.map +1 -0
  99. package/package.json +147 -0
@@ -0,0 +1,312 @@
1
+ import { calculateBackoffDelay, isRetryableError } from "./api-client-Ftxqm3Ep.js";
2
+
3
+ //#region src/api-client/api-config.server.ts
4
+ /**
5
+ * Base URL Configuration
6
+ * Single unified base URL for all API operations
7
+ */
8
+ const DEFAULT_API_BASE_URL = process.env.API_BASE_URL || "https://localhost:5001";
9
+ /**
10
+ * Default Headers
11
+ * Applied to all API requests
12
+ */
13
+ const DEFAULT_HEADERS = {
14
+ Accept: "application/json, application/problem+json",
15
+ "Content-Type": "application/json"
16
+ };
17
+ /**
18
+ * Create API Config Factory
19
+ *
20
+ * Creates a factory function that generates API configurations
21
+ * with the provided auth and tenant resolvers.
22
+ */
23
+ function createAPIConfigFactory(getAccessToken, getCurrentTenant, logger, factoryOptions = {}) {
24
+ const { baseURL = DEFAULT_API_BASE_URL, defaultHeaders = DEFAULT_HEADERS, userAgent = "MimirCore/1.0", timeout = 3e4 } = factoryOptions;
25
+ /**
26
+ * Resolve tenant ID from request or options
27
+ */
28
+ async function resolveTenantId(request, options) {
29
+ if (options.tenantId) {
30
+ logger?.debug?.("Using explicit tenant ID", { tenantId: options.tenantId });
31
+ return options.tenantId;
32
+ }
33
+ try {
34
+ const tenantId = await getCurrentTenant(request);
35
+ if (tenantId) logger?.debug?.("Resolved tenant from context", { tenantId });
36
+ return tenantId;
37
+ } catch (error) {
38
+ logger?.warn?.("Failed to resolve current tenant", error instanceof Error ? error : void 0);
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Creates unified API configuration for all services.
44
+ *
45
+ * This function automatically:
46
+ * - Adds authentication token to Authorization header
47
+ * - Injects tenant context as X-Tenant-Id header
48
+ * - Configures base URL from environment
49
+ * - Applies default headers
50
+ *
51
+ * @param request - The incoming request object
52
+ * @param options - Configuration options
53
+ * @returns API configuration object
54
+ * @throws Error if required authentication or tenant context is missing
55
+ *
56
+ * @example
57
+ * ` ypescript
58
+ * // Standard usage with auth and tenant
59
+ * const config = await createAPIConfig(request);
60
+ * const api = new IdentityApi(config);
61
+ *
62
+ * // Without tenant requirement
63
+ * const config = await createAPIConfig(request, { requireTenant: false });
64
+ *
65
+ * // With explicit tenant override
66
+ * const config = await createAPIConfig(request, { tenantId: 'specific-tenant-id' });
67
+ * `
68
+ */
69
+ async function createAPIConfig(request, options = {}) {
70
+ const { requireTenant = true, includeAuth = true, customHeaders = {} } = options;
71
+ const headers = {
72
+ ...defaultHeaders,
73
+ "User-Agent": userAgent,
74
+ ...customHeaders
75
+ };
76
+ let accessToken;
77
+ if (includeAuth) {
78
+ const token = await getAccessToken(request);
79
+ if (!token) {
80
+ logger?.error?.("No access token available for API request");
81
+ throw new Error("No access token available");
82
+ }
83
+ accessToken = token;
84
+ headers.Authorization = `Bearer ${token}`;
85
+ logger?.debug?.("Added authorization header to API config");
86
+ }
87
+ const tenantId = await resolveTenantId(request, options);
88
+ if (requireTenant) {
89
+ if (!tenantId) {
90
+ logger?.error?.("Tenant context required but not available");
91
+ throw new Error("Tenant context is required for this operation");
92
+ }
93
+ headers["X-Tenant-Id"] = tenantId;
94
+ logger?.debug?.("Added tenant context header", { tenantId });
95
+ }
96
+ logger?.debug?.("API config created", {
97
+ basePath: baseURL,
98
+ hasAuth: includeAuth,
99
+ hasTenant: !!tenantId,
100
+ requireTenant
101
+ });
102
+ return {
103
+ basePath: baseURL,
104
+ accessToken,
105
+ headers,
106
+ tenantId: tenantId || "",
107
+ baseOptions: { timeout }
108
+ };
109
+ }
110
+ /**
111
+ * Get the configured API base URL
112
+ */
113
+ function getAPIBaseURL() {
114
+ return baseURL;
115
+ }
116
+ return {
117
+ createAPIConfig,
118
+ getAPIBaseURL
119
+ };
120
+ }
121
+
122
+ //#endregion
123
+ //#region src/api-client/axios-setup.server.ts
124
+ /**
125
+ * Global token refresh queue - prevents concurrent refresh attempts
126
+ *
127
+ * When multiple requests get 401 simultaneously:
128
+ * 1. First request starts the refresh, creates a promise
129
+ * 2. Other requests wait for the same promise
130
+ * 3. When refresh completes, all waiting requests get the new token
131
+ * 4. This prevents refresh token rotation race conditions
132
+ */
133
+ const refreshPromiseMap = new Map();
134
+ /**
135
+ * Get or create a token refresh promise for a session
136
+ * Ensures only one refresh happens at a time per session
137
+ */
138
+ function getOrCreateRefreshPromise(sessionKey, refreshFn, logger) {
139
+ const existingPromise = refreshPromiseMap.get(sessionKey);
140
+ if (existingPromise) {
141
+ logger?.info?.("Token refresh already in progress, waiting for existing refresh");
142
+ return existingPromise;
143
+ }
144
+ logger?.info?.("Starting new token refresh");
145
+ const refreshPromise = refreshFn().finally(() => {
146
+ refreshPromiseMap.delete(sessionKey);
147
+ });
148
+ refreshPromiseMap.set(sessionKey, refreshPromise);
149
+ return refreshPromise;
150
+ }
151
+ /**
152
+ * Extract session key from request for refresh deduplication
153
+ */
154
+ function getSessionKeyFromRequest(request) {
155
+ const cookie = request.headers.get("cookie") || "";
156
+ return `session:${cookie.slice(0, 50)}`;
157
+ }
158
+ /**
159
+ * Setup Axios interceptors for auto-generated clients
160
+ *
161
+ * Features:
162
+ * - Automatic token refresh on 401
163
+ * - Retry logic with exponential backoff
164
+ * - ProblemDetails error parsing
165
+ * - Timeout management
166
+ * - Comprehensive logging
167
+ *
168
+ * @param axiosInstance - The Axios instance from auto-generated client
169
+ * @param request - The incoming request object (for token refresh)
170
+ * @param options - Configuration options
171
+ *
172
+ * @example
173
+ * ` ypescript
174
+ * const config = await createAPIConfig(request);
175
+ * const api = new IdentityApi(config);
176
+ * setupAxiosInterceptors(api.axios, request, { logger, attemptTokenRefresh });
177
+ *
178
+ * // Now all API calls have automatic retry, token refresh, error handling
179
+ * const users = await api.getUsers(tenantId);
180
+ * `
181
+ */
182
+ function setupAxiosInterceptors(axiosInstance, request, options = {}) {
183
+ const { logger, attemptTokenRefresh, retryConfig = {}, addRequestId = true } = options;
184
+ const { maxRetries = 3, baseDelay = 1e3, maxDelay = 1e4 } = retryConfig;
185
+ if (axiosInstance._interceptorsSetup) {
186
+ logger?.debug?.("Interceptors already setup, skipping");
187
+ return;
188
+ }
189
+ axiosInstance._interceptorsSetup = true;
190
+ if (addRequestId) axiosInstance.interceptors.request.use((config) => {
191
+ config.headers["X-Request-Id"] = crypto.randomUUID();
192
+ logger?.debug?.("Axios request initiated", {
193
+ method: config.method,
194
+ url: config.url,
195
+ requestId: config.headers["X-Request-Id"]
196
+ });
197
+ return config;
198
+ }, (error) => {
199
+ logger?.error?.("Axios request setup failed", error);
200
+ return Promise.reject(error);
201
+ });
202
+ if (attemptTokenRefresh) axiosInstance.interceptors.response.use((response) => response, async (error) => {
203
+ const originalRequest = error.config;
204
+ if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
205
+ originalRequest._retry = true;
206
+ logger?.info?.("Received 401 from backend, attempting token refresh");
207
+ const sessionKey = getSessionKeyFromRequest(request);
208
+ const refreshResult = await getOrCreateRefreshPromise(sessionKey, () => attemptTokenRefresh(request), logger);
209
+ if (refreshResult.success && refreshResult.newAccessToken) {
210
+ logger?.info?.("Token refresh successful, retrying original request");
211
+ originalRequest.headers.Authorization = `Bearer ${refreshResult.newAccessToken}`;
212
+ return axiosInstance(originalRequest);
213
+ } else if (refreshResult.shouldLogout) {
214
+ logger?.error?.("Token refresh failed - refresh token expired or invalid");
215
+ const logoutError = new Error("REFRESH_TOKEN_EXPIRED");
216
+ logoutError.shouldLogout = true;
217
+ logoutError.originalError = error;
218
+ return Promise.reject(logoutError);
219
+ } else logger?.error?.("Token refresh failed but not requiring logout", void 0, { error: refreshResult.error });
220
+ }
221
+ return Promise.reject(error);
222
+ });
223
+ axiosInstance.interceptors.response.use((response) => response, async (error) => {
224
+ const config = error.config;
225
+ if (!config) return Promise.reject(error);
226
+ if (error.response?.status === 401) {
227
+ logger?.debug?.("401 detected, not retrying (handled by token refresh interceptor)");
228
+ return Promise.reject(error);
229
+ }
230
+ if (error.response?.status === 409) {
231
+ logger?.debug?.("409 Conflict detected, will not retry");
232
+ return Promise.reject(error);
233
+ }
234
+ if (config._retryCount === void 0) config._retryCount = 0;
235
+ const shouldRetry = isRetryableError(error) && config._retryCount < maxRetries;
236
+ if (shouldRetry) {
237
+ config._retryCount++;
238
+ const delay = calculateBackoffDelay(config._retryCount, baseDelay, maxDelay);
239
+ logger?.info?.("Retrying request", {
240
+ attempt: config._retryCount,
241
+ maxRetries,
242
+ delay,
243
+ error: error.message
244
+ });
245
+ await new Promise((resolve) => setTimeout(resolve, delay));
246
+ return axiosInstance(config);
247
+ }
248
+ return Promise.reject(error);
249
+ });
250
+ axiosInstance.interceptors.response.use((response) => response, (error) => {
251
+ if (error.response?.data) {
252
+ const problemDetails = error.response.data;
253
+ logger?.error?.("API error (ProblemDetails)", void 0, {
254
+ code: problemDetails.title,
255
+ status: problemDetails.status,
256
+ detail: problemDetails.detail,
257
+ traceId: problemDetails.traceId
258
+ });
259
+ error.problemDetails = problemDetails;
260
+ }
261
+ return Promise.reject(error);
262
+ });
263
+ }
264
+ /**
265
+ * Helper to create and setup an API client in one call
266
+ *
267
+ * @param request - The incoming request object
268
+ * @param ClientClass - The auto-generated API client class
269
+ * @param createAPIConfig - Function to create API configuration
270
+ * @param ConfigurationClass - The Configuration class from generated API
271
+ * @param options - Configuration options
272
+ * @returns Configured API client instance with interceptors
273
+ *
274
+ * @example
275
+ * ` ypescript
276
+ * import { IdentityApi, Configuration } from './generated-api';
277
+ *
278
+ * const { client: api, tenantId } = await createEnhancedClient(
279
+ * request,
280
+ * IdentityApi,
281
+ * createAPIConfig,
282
+ * Configuration,
283
+ * { logger, attemptTokenRefresh }
284
+ * );
285
+ * const users = await api.getUsers(tenantId);
286
+ * `
287
+ */
288
+ async function createEnhancedClient(request, ClientClass, createAPIConfig, ConfigurationClass, options = {}) {
289
+ const { requireTenant, includeAuth, tenantId, customHeaders,...axiosOptions } = options;
290
+ const config = await createAPIConfig(request, {
291
+ requireTenant,
292
+ includeAuth,
293
+ tenantId,
294
+ customHeaders
295
+ });
296
+ const configuration = new ConfigurationClass({
297
+ basePath: config.basePath,
298
+ accessToken: config.accessToken,
299
+ baseOptions: { headers: config.headers }
300
+ });
301
+ const client = new ClientClass(configuration);
302
+ const axiosInstance = client.axios;
303
+ if (axiosInstance) setupAxiosInterceptors(axiosInstance, request, axiosOptions);
304
+ return {
305
+ client,
306
+ tenantId: config.tenantId
307
+ };
308
+ }
309
+
310
+ //#endregion
311
+ export { createAPIConfigFactory, createEnhancedClient, setupAxiosInterceptors };
312
+ //# sourceMappingURL=server-DLgH3FRR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-DLgH3FRR.js","names":["DEFAULT_HEADERS: Record<string, string>","getAccessToken: GetAccessTokenFn","getCurrentTenant: GetCurrentTenantFn","logger?: APILogger","factoryOptions: APIConfigFactoryOptions","request: Request","options: CreateAPIConfigOptions","headers: Record<string, string>","accessToken: string | undefined","sessionKey: string","refreshFn: () => Promise<TokenRefreshResult>","logger?: APILogger","request: Request","axiosInstance: AxiosInstance","options: AxiosSetupOptions","config: InternalAxiosRequestConfig","error: AxiosError","error: AxiosError<ProblemDetails>","ClientClass: new (configuration?: any, basePath?: string, axios?: AxiosInstance) => T","createAPIConfig: (request: Request, options?: any) => Promise<any>","ConfigurationClass: new (params?: any) => any","options: CreateEnhancedClientOptions"],"sources":["../src/api-client/api-config.server.ts","../src/api-client/axios-setup.server.ts"],"sourcesContent":["/**\n * API Configuration Server Module\n *\n * Centralized configuration for all API clients,\n * automatically handling authentication tokens and tenant context.\n */\n\nimport type { APIConfig, CreateAPIConfigOptions, GetAccessTokenFn, GetCurrentTenantFn, APILogger } from './types';\n\n/**\n * Base URL Configuration\n * Single unified base URL for all API operations\n */\nconst DEFAULT_API_BASE_URL = process.env.API_BASE_URL || 'https://localhost:5001';\n\n/**\n * Default Headers\n * Applied to all API requests\n */\nconst DEFAULT_HEADERS: Record<string, string> = {\n Accept: 'application/json, application/problem+json',\n 'Content-Type': 'application/json',\n};\n\n/**\n * API Config Factory Options\n */\nexport interface APIConfigFactoryOptions {\n /**\n * Base URL for API requests\n */\n baseURL?: string;\n /**\n * Default headers applied to all requests\n */\n defaultHeaders?: Record<string, string>;\n /**\n * User agent string\n */\n userAgent?: string;\n /**\n * Default timeout in milliseconds\n */\n timeout?: number;\n}\n\n/**\n * Create API Config Factory\n *\n * Creates a factory function that generates API configurations\n * with the provided auth and tenant resolvers.\n */\nexport function createAPIConfigFactory(\n getAccessToken: GetAccessTokenFn,\n getCurrentTenant: GetCurrentTenantFn,\n logger?: APILogger,\n factoryOptions: APIConfigFactoryOptions = {}\n) {\n const {\n baseURL = DEFAULT_API_BASE_URL,\n defaultHeaders = DEFAULT_HEADERS,\n userAgent = 'MimirCore/1.0',\n timeout = 30000,\n } = factoryOptions;\n\n /**\n * Resolve tenant ID from request or options\n */\n async function resolveTenantId(\n request: Request,\n options: CreateAPIConfigOptions\n ): Promise<string | null> {\n // Use explicit tenant ID if provided\n if (options.tenantId) {\n logger?.debug?.('Using explicit tenant ID', { tenantId: options.tenantId });\n return options.tenantId;\n }\n\n // Resolve from current tenant context\n try {\n const tenantId = await getCurrentTenant(request);\n if (tenantId) {\n logger?.debug?.('Resolved tenant from context', { tenantId });\n }\n return tenantId;\n } catch (error) {\n logger?.warn?.('Failed to resolve current tenant', error instanceof Error ? error : undefined);\n return null;\n }\n }\n\n /**\n * Creates unified API configuration for all services.\n *\n * This function automatically:\n * - Adds authentication token to Authorization header\n * - Injects tenant context as X-Tenant-Id header\n * - Configures base URL from environment\n * - Applies default headers\n *\n * @param request - The incoming request object\n * @param options - Configuration options\n * @returns API configuration object\n * @throws Error if required authentication or tenant context is missing\n *\n * @example\n * `\typescript\n * // Standard usage with auth and tenant\n * const config = await createAPIConfig(request);\n * const api = new IdentityApi(config);\n *\n * // Without tenant requirement\n * const config = await createAPIConfig(request, { requireTenant: false });\n *\n * // With explicit tenant override\n * const config = await createAPIConfig(request, { tenantId: 'specific-tenant-id' });\n * `\n */\n async function createAPIConfig(\n request: Request,\n options: CreateAPIConfigOptions = {}\n ): Promise<APIConfig> {\n const {\n requireTenant = true,\n includeAuth = true,\n customHeaders = {},\n } = options;\n\n // Start with default headers\n const headers: Record<string, string> = {\n ...defaultHeaders,\n 'User-Agent': userAgent,\n ...customHeaders,\n };\n\n let accessToken: string | undefined;\n\n // Add authentication if required\n if (includeAuth) {\n const token = await getAccessToken(request);\n if (!token) {\n logger?.error?.('No access token available for API request');\n throw new Error('No access token available');\n }\n\n accessToken = token;\n headers.Authorization = `Bearer ${token}`;\n logger?.debug?.('Added authorization header to API config');\n }\n\n // Resolve tenant context\n const tenantId = await resolveTenantId(request, options);\n\n // Add tenant header if required\n if (requireTenant) {\n if (!tenantId) {\n logger?.error?.('Tenant context required but not available');\n throw new Error('Tenant context is required for this operation');\n }\n\n headers['X-Tenant-Id'] = tenantId;\n logger?.debug?.('Added tenant context header', { tenantId });\n }\n\n logger?.debug?.('API config created', {\n basePath: baseURL,\n hasAuth: includeAuth,\n hasTenant: !!tenantId,\n requireTenant,\n });\n\n return {\n basePath: baseURL,\n accessToken,\n headers,\n tenantId: tenantId || '',\n baseOptions: {\n timeout,\n },\n };\n }\n\n /**\n * Get the configured API base URL\n */\n function getAPIBaseURL(): string {\n return baseURL;\n }\n\n return {\n createAPIConfig,\n getAPIBaseURL,\n };\n}\n\nexport type APIConfigFactory = ReturnType<typeof createAPIConfigFactory>;\n","/**\n * Axios Interceptor Setup for Auto-Generated API Clients\n *\n * This module enhances auto-generated Axios-based API clients with:\n * - Automatic token refresh on 401 (serialized to prevent race conditions)\n * - Retry logic with exponential backoff\n * - ProblemDetails error parsing\n * - Timeout management\n * - Comprehensive logging\n *\n * CRITICAL: Token refresh is serialized using a global promise queue.\n * This prevents race conditions when multiple concurrent requests get 401\n * and all try to refresh the token simultaneously (which would fail due\n * to refresh token rotation - the second refresh would use an invalid token).\n */\n\nimport type { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';\nimport type { ProblemDetails, RetryConfig, APILogger, TokenRefreshFn, TokenRefreshResult } from './types';\nimport { calculateBackoffDelay, isRetryableError } from './retry-handler';\n\n/**\n * Global token refresh queue - prevents concurrent refresh attempts\n * \n * When multiple requests get 401 simultaneously:\n * 1. First request starts the refresh, creates a promise\n * 2. Other requests wait for the same promise\n * 3. When refresh completes, all waiting requests get the new token\n * 4. This prevents refresh token rotation race conditions\n */\nconst refreshPromiseMap = new Map<string, Promise<TokenRefreshResult>>();\n\n/**\n * Get or create a token refresh promise for a session\n * Ensures only one refresh happens at a time per session\n */\nfunction getOrCreateRefreshPromise(\n sessionKey: string,\n refreshFn: () => Promise<TokenRefreshResult>,\n logger?: APILogger\n): Promise<TokenRefreshResult> {\n // Check if there's already a refresh in progress for this session\n const existingPromise = refreshPromiseMap.get(sessionKey);\n if (existingPromise) {\n logger?.info?.('Token refresh already in progress, waiting for existing refresh');\n return existingPromise;\n }\n\n // Create new refresh promise\n logger?.info?.('Starting new token refresh');\n const refreshPromise = refreshFn().finally(() => {\n // Clean up after refresh completes (success or failure)\n refreshPromiseMap.delete(sessionKey);\n });\n\n refreshPromiseMap.set(sessionKey, refreshPromise);\n return refreshPromise;\n}\n\n/**\n * Extract session key from request for refresh deduplication\n */\nfunction getSessionKeyFromRequest(request: Request): string {\n // Use cookie header as session identifier (contains session ID)\n const cookie = request.headers.get('cookie') || '';\n // Use first 50 chars of cookie as key\n return `session:${cookie.slice(0, 50)}`;\n}\n\n/**\n * Axios Setup Options\n */\nexport interface AxiosSetupOptions {\n /**\n * Logger instance for logging\n */\n logger?: APILogger;\n /**\n * Token refresh function\n */\n attemptTokenRefresh?: TokenRefreshFn;\n /**\n * Retry configuration\n */\n retryConfig?: RetryConfig;\n /**\n * Whether to add request ID header\n */\n addRequestId?: boolean;\n}\n\n/**\n * Setup Axios interceptors for auto-generated clients\n *\n * Features:\n * - Automatic token refresh on 401\n * - Retry logic with exponential backoff\n * - ProblemDetails error parsing\n * - Timeout management\n * - Comprehensive logging\n *\n * @param axiosInstance - The Axios instance from auto-generated client\n * @param request - The incoming request object (for token refresh)\n * @param options - Configuration options\n *\n * @example\n * `\typescript\n * const config = await createAPIConfig(request);\n * const api = new IdentityApi(config);\n * setupAxiosInterceptors(api.axios, request, { logger, attemptTokenRefresh });\n *\n * // Now all API calls have automatic retry, token refresh, error handling\n * const users = await api.getUsers(tenantId);\n * `\n */\nexport function setupAxiosInterceptors(\n axiosInstance: AxiosInstance,\n request: Request,\n options: AxiosSetupOptions = {}\n): void {\n const {\n logger,\n attemptTokenRefresh,\n retryConfig = {},\n addRequestId = true,\n } = options;\n\n const {\n maxRetries = 3,\n baseDelay = 1000,\n maxDelay = 10000,\n } = retryConfig;\n\n // CRITICAL: Check if interceptors are already setup to prevent duplicate interceptors\n // Multiple interceptors cause exponential request multiplication\n if ((axiosInstance as any)._interceptorsSetup) {\n logger?.debug?.('Interceptors already setup, skipping');\n return;\n }\n (axiosInstance as any)._interceptorsSetup = true;\n\n // Request interceptor: Add request ID for tracing\n if (addRequestId) {\n axiosInstance.interceptors.request.use(\n (config: InternalAxiosRequestConfig) => {\n config.headers['X-Request-Id'] = crypto.randomUUID();\n\n logger?.debug?.('Axios request initiated', {\n method: config.method,\n url: config.url,\n requestId: config.headers['X-Request-Id'] as string,\n });\n\n return config;\n },\n (error) => {\n logger?.error?.('Axios request setup failed', error);\n return Promise.reject(error);\n }\n );\n }\n\n // Response interceptor: Token refresh on 401\n if (attemptTokenRefresh) {\n axiosInstance.interceptors.response.use(\n (response) => response,\n async (error: AxiosError) => {\n const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n // Check if 401 and should attempt token refresh\n if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {\n originalRequest._retry = true;\n\n logger?.info?.('Received 401 from backend, attempting token refresh');\n\n // CRITICAL: Use serialized token refresh to prevent race conditions\n // Multiple concurrent 401s will share the same refresh promise\n const sessionKey = getSessionKeyFromRequest(request);\n const refreshResult = await getOrCreateRefreshPromise(\n sessionKey,\n () => attemptTokenRefresh(request),\n logger\n );\n\n if (refreshResult.success && refreshResult.newAccessToken) {\n logger?.info?.('Token refresh successful, retrying original request');\n\n // Update authorization header with new token\n originalRequest.headers.Authorization = `Bearer ${refreshResult.newAccessToken}`;\n\n // Retry the original request with new token\n return axiosInstance(originalRequest);\n } else if (refreshResult.shouldLogout) {\n logger?.error?.('Token refresh failed - refresh token expired or invalid');\n\n // Enhance error with logout flag for API route to handle\n const logoutError = new Error('REFRESH_TOKEN_EXPIRED') as any;\n logoutError.shouldLogout = true;\n logoutError.originalError = error;\n\n return Promise.reject(logoutError);\n } else {\n logger?.error?.('Token refresh failed but not requiring logout', undefined, {\n error: refreshResult.error,\n });\n }\n }\n\n return Promise.reject(error);\n }\n );\n }\n\n // Response interceptor: Retry logic with exponential backoff\n axiosInstance.interceptors.response.use(\n (response) => response,\n async (error: AxiosError) => {\n const config = error.config as InternalAxiosRequestConfig & { _retryCount?: number };\n\n if (!config) {\n return Promise.reject(error);\n }\n\n // CRITICAL: Never retry 401 errors - they're handled by token refresh interceptor\n if (error.response?.status === 401) {\n logger?.debug?.('401 detected, not retrying (handled by token refresh interceptor)');\n return Promise.reject(error);\n }\n\n // CRITICAL: Never retry 409 Conflict errors (concurrent modification)\n // These indicate a business logic conflict that won't be resolved by retrying\n if (error.response?.status === 409) {\n logger?.debug?.('409 Conflict detected, will not retry');\n return Promise.reject(error);\n }\n\n if (config._retryCount === undefined) {\n config._retryCount = 0;\n }\n\n const shouldRetry = isRetryableError(error) && config._retryCount < maxRetries;\n\n if (shouldRetry) {\n config._retryCount++;\n\n const delay = calculateBackoffDelay(config._retryCount, baseDelay, maxDelay);\n\n logger?.info?.('Retrying request', {\n attempt: config._retryCount,\n maxRetries,\n delay,\n error: error.message,\n });\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n return axiosInstance(config);\n }\n\n return Promise.reject(error);\n }\n );\n\n // Response interceptor: Error handling with ProblemDetails parsing\n axiosInstance.interceptors.response.use(\n (response) => response,\n (error: AxiosError<ProblemDetails>) => {\n if (error.response?.data) {\n const problemDetails = error.response.data;\n\n logger?.error?.('API error (ProblemDetails)', undefined, {\n code: problemDetails.title,\n status: problemDetails.status,\n detail: problemDetails.detail,\n traceId: problemDetails.traceId,\n });\n\n // Enhance error with parsed ProblemDetails\n (error as any).problemDetails = problemDetails;\n }\n\n return Promise.reject(error);\n }\n );\n}\n\n/**\n * Create Enhanced Client Options\n */\nexport interface CreateEnhancedClientOptions extends AxiosSetupOptions {\n /**\n * Whether tenant is required\n */\n requireTenant?: boolean;\n /**\n * Whether to include auth\n */\n includeAuth?: boolean;\n /**\n * Explicit tenant ID override\n */\n tenantId?: string;\n /**\n * Custom headers\n */\n customHeaders?: Record<string, string>;\n}\n\n/**\n * Helper to create and setup an API client in one call\n *\n * @param request - The incoming request object\n * @param ClientClass - The auto-generated API client class\n * @param createAPIConfig - Function to create API configuration\n * @param ConfigurationClass - The Configuration class from generated API\n * @param options - Configuration options\n * @returns Configured API client instance with interceptors\n *\n * @example\n * `\typescript\n * import { IdentityApi, Configuration } from './generated-api';\n *\n * const { client: api, tenantId } = await createEnhancedClient(\n * request,\n * IdentityApi,\n * createAPIConfig,\n * Configuration,\n * { logger, attemptTokenRefresh }\n * );\n * const users = await api.getUsers(tenantId);\n * `\n */\nexport async function createEnhancedClient<T>(\n request: Request,\n ClientClass: new (configuration?: any, basePath?: string, axios?: AxiosInstance) => T,\n createAPIConfig: (request: Request, options?: any) => Promise<any>,\n ConfigurationClass: new (params?: any) => any,\n options: CreateEnhancedClientOptions = {}\n): Promise<{ client: T; tenantId: string }> {\n const { requireTenant, includeAuth, tenantId, customHeaders, ...axiosOptions } = options;\n\n const config = await createAPIConfig(request, {\n requireTenant,\n includeAuth,\n tenantId,\n customHeaders,\n });\n\n // Create Configuration object for auto-generated clients\n const configuration = new ConfigurationClass({\n basePath: config.basePath,\n accessToken: config.accessToken,\n baseOptions: {\n headers: config.headers,\n },\n });\n\n const client = new ClientClass(configuration);\n\n // Access protected axios instance via type assertion\n const axiosInstance = (client as any).axios as AxiosInstance | undefined;\n\n // Setup interceptors for token refresh and retry logic\n // IMPORTANT: Only setup once per instance to prevent duplication\n if (axiosInstance) {\n setupAxiosInterceptors(axiosInstance, request, axiosOptions);\n }\n\n return { client, tenantId: config.tenantId };\n}\n"],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ //#region src/tenant/tenant.server.d.ts
2
+ /**
3
+ * Tenant Server Module
4
+ *
5
+ * Server-side tenant management.
6
+ */
7
+ /**
8
+ * Identity context fetcher type (to avoid circular dependency)
9
+ */
10
+ type IdentityContextFetcher = (request: Request) => Promise<{
11
+ memberships?: Array<{
12
+ tenantId: string;
13
+ }>;
14
+ } | null>;
15
+ /**
16
+ * Configure identity context fetcher
17
+ */
18
+ declare function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void;
19
+ /**
20
+ * Get current active tenant from cookie
21
+ */
22
+ declare function getCurrentTenant(request: Request): Promise<string | null>;
23
+ /**
24
+ * Get tenant from identity context
25
+ */
26
+ declare function getTenantFromIdentityContext(request: Request): Promise<string | null>;
27
+ /**
28
+ * Get available tenants from identity context
29
+ */
30
+ declare function getAvailableTenants(request: Request): Promise<string[]>;
31
+ /**
32
+ * Set current active tenant
33
+ */
34
+ declare function setCurrentTenant(request: Request, tenantId: string): Promise<{
35
+ headers: Headers;
36
+ success: boolean;
37
+ error?: string;
38
+ }>;
39
+ /**
40
+ * Initialize tenant on first login
41
+ */
42
+ declare function initializeTenant(request: Request): Promise<{
43
+ headers: Headers;
44
+ tenantId: string;
45
+ } | null>;
46
+ /**
47
+ * Clear current tenant
48
+ */
49
+ declare function clearCurrentTenant(): Promise<Headers>;
50
+
51
+ //#endregion
52
+ //#region src/tenant/tenant-cookie.server.d.ts
53
+ //# sourceMappingURL=tenant.server.d.ts.map
54
+ /**
55
+ * Tenant Cookie Server Module
56
+ *
57
+ * Cookie-based tenant storage.
58
+ */
59
+ type TenantCookieSameSite = 'Strict' | 'Lax' | 'None';
60
+ interface TenantCookieConfig {
61
+ name?: string;
62
+ maxAge?: number;
63
+ path?: string;
64
+ secure?: boolean;
65
+ httpOnly?: boolean;
66
+ sameSite?: TenantCookieSameSite;
67
+ }
68
+ /**
69
+ * Get active tenant from cookie
70
+ */
71
+ declare function getActiveTenant(request: Request): Promise<string | null>;
72
+ /**
73
+ * Set active tenant cookie value
74
+ */
75
+ declare function setActiveTenant(tenantId: string): Promise<string>;
76
+ /**
77
+ * Clear active tenant cookie value
78
+ */
79
+ declare function clearActiveTenant(): Promise<string>;
80
+ /**
81
+ * Configure tenant cookie name
82
+ */
83
+ declare function configureTenantCookie(config: TenantCookieConfig): void;
84
+ /**
85
+ * Reset tenant cookie config back to defaults.
86
+ */
87
+ declare function resetTenantCookieConfig(): void;
88
+
89
+ //#endregion
90
+ //# sourceMappingURL=tenant-cookie.server.d.ts.map
91
+
92
+ export { IdentityContextFetcher, TenantCookieConfig, TenantCookieSameSite, clearActiveTenant as clearActiveTenant$1, clearCurrentTenant as clearCurrentTenant$1, configureIdentityContextFetcher as configureIdentityContextFetcher$1, configureTenantCookie as configureTenantCookie$1, getActiveTenant as getActiveTenant$1, getAvailableTenants as getAvailableTenants$1, getCurrentTenant as getCurrentTenant$1, getTenantFromIdentityContext as getTenantFromIdentityContext$1, initializeTenant as initializeTenant$1, resetTenantCookieConfig as resetTenantCookieConfig$1, setActiveTenant as setActiveTenant$1, setCurrentTenant as setCurrentTenant$1 };
93
+ //# sourceMappingURL=server-DazqdM63.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-DazqdM63.d.ts","names":[],"sources":["../src/tenant/tenant.server.ts","../src/tenant/tenant-cookie.server.ts"],"sourcesContent":null,"mappings":";;;;;;;;;KAYY,sBAAA,aAAmC,YAAY;EAA/C,WAAA,CAAA,EACI,KADJ,CAAA;IAAsB,QAAA,EAAA,MAAA;EAAA,CAAA,CAAA;CAAoB,GAAA,IACtC,CAAA;;AADkD;;iBASlD,+BAAA,UAAyC;;AAAzD;;iBAOsB,gBAAA,UAA0B,UAAU;;AAA1D;;AAAgD,iBAgB1B,4BAAA,CAhB0B,OAAA,EAgBY,OAhBZ,CAAA,EAgBsB,OAhBtB,CAAA,MAAA,GAAA,IAAA,CAAA;;AAAiB;;iBA4C3C,mBAAA,UAA6B,UAAU;;AA5B7D;;AAA4D,iBAwDtC,gBAAA,CAxDsC,OAAA,EAyDjD,OAzDiD,EAAA,QAAA,EAAA,MAAA,CAAA,EA2DzD,OA3DyD,CAAA;EAAO,OAAG,EA2DhD,OA3DgD;EAAO,OAAA,EAAA,OAAA;;;;AA4B7E;;AAAmD,iBAyD7B,gBAAA,CAzD6B,OAAA,EA0DxC,OA1DwC,CAAA,EA2DhD,OA3DgD,CAAA;EAAO,OAAG,EA2DvC,OA3DuC;EAAO,QAAA,EAAA,MAAA;;;;AA4BpE;AAAsC,iBAgEhB,kBAAA,CAAA,CAhEgB,EAgEM,OAhEN,CAgEc,OAhEd,CAAA;;;;;;;;;;KC9F1B,oBAAA;UAEK,kBAAA;;;EDIL,IAAA,CAAA,EAAA,MAAA;EAAsB,MAAA,CAAA,EAAA,OAAA;EAAA,QAAa,CAAA,EAAA,OAAA;EAAO,QACtC,CAAA,ECCH,oBDDG;;AADkD;;;iBCyB5C,eAAA,UAAyB,UAAU;ADhBzD;;;iBC8BsB,eAAA,oBAAmC;ADvBzD;;;AAA0D,iBCiCpC,iBAAA,CAAA,CDjCoC,ECiCf,ODjCe,CAAA,MAAA,CAAA;AAAO;;;iBC2CjD,qBAAA,SAA8B;AD3B9C;;;AAAsE,iBCqCtD,uBAAA,CAAA,CDrCsD,EAAA,IAAA;;;AAAO"}
@@ -0,0 +1,145 @@
1
+ import { logger } from "./logging-_h2mi3L9.js";
2
+
3
+ //#region src/identity/identity-context.server.ts
4
+ let identityAPIFetcher = null;
5
+ /**
6
+ * Configure identity API fetcher
7
+ */
8
+ function configureIdentityAPIFetcher(fetcher) {
9
+ identityAPIFetcher = fetcher;
10
+ }
11
+ const contextCache = new Map();
12
+ const CONTEXT_CACHE_TTL = 5 * 60 * 1e3;
13
+ /**
14
+ * Fetch identity context from backend
15
+ */
16
+ async function fetchIdentityContext(request) {
17
+ if (!identityAPIFetcher) throw new Error("Identity API fetcher not configured. Call configureIdentityAPIFetcher first.");
18
+ try {
19
+ logger.info("Fetching identity context");
20
+ const context = await identityAPIFetcher(request);
21
+ logger.debug("Identity context fetched", {
22
+ hasAnyMembership: context.hasAnyMembership,
23
+ hasSubscription: context.hasSubscription,
24
+ isOnboarded: context.isOnboarded,
25
+ membershipCount: context.memberships?.length || 0
26
+ });
27
+ return context;
28
+ } catch (error) {
29
+ logger.error("Failed to fetch identity context", error instanceof Error ? error : void 0);
30
+ if (error && typeof error === "object" && "response" in error && error.response?.status === 404 && error.response?.data?.title === "CurrentUserMissing") {
31
+ logger.error("CurrentUserMissing error detected - session is invalid");
32
+ const invalidSessionError = new Error("INVALID_SESSION");
33
+ invalidSessionError.isInvalidSession = true;
34
+ throw invalidSessionError;
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ /**
40
+ * Get cached identity context or fetch fresh if needed
41
+ */
42
+ async function getIdentityContext(request, userId, forceRefresh = false) {
43
+ const cacheKey = userId;
44
+ const cached = contextCache.get(cacheKey);
45
+ const now = Date.now();
46
+ if (!forceRefresh && cached && now - cached.timestamp < CONTEXT_CACHE_TTL) {
47
+ logger.debug("Using cached identity context for user", { userId });
48
+ return cached.context;
49
+ }
50
+ const context = await fetchIdentityContext(request);
51
+ contextCache.set(cacheKey, {
52
+ context,
53
+ timestamp: now,
54
+ version: context.contextVersion ?? 0
55
+ });
56
+ logger.debug("Cached fresh identity context for user", { userId });
57
+ return context;
58
+ }
59
+ /**
60
+ * Clear identity context cache for user
61
+ */
62
+ function clearIdentityContextCache(userId) {
63
+ if (userId) {
64
+ contextCache.delete(userId);
65
+ logger.debug("Cleared identity context cache for user", { userId });
66
+ } else {
67
+ contextCache.clear();
68
+ logger.debug("Cleared all identity context cache");
69
+ }
70
+ }
71
+ let permissionFetcher = null;
72
+ /**
73
+ * Configure permission fetcher
74
+ */
75
+ function configurePermissionFetcher(fetcher) {
76
+ permissionFetcher = fetcher;
77
+ }
78
+ /**
79
+ * Fetch user permissions for a specific tenant
80
+ */
81
+ async function fetchUserPermissions(request, tenantId) {
82
+ if (!tenantId) {
83
+ logger.warn("No tenant ID provided, skipping permission fetch");
84
+ return [];
85
+ }
86
+ if (!permissionFetcher) throw new Error("Permission fetcher not configured. Call configurePermissionFetcher first.");
87
+ try {
88
+ return await permissionFetcher(request, tenantId);
89
+ } catch (error) {
90
+ logger.error("Failed to fetch user permissions", error instanceof Error ? error : void 0);
91
+ return [];
92
+ }
93
+ }
94
+ /**
95
+ * Convert raw identity context into a tenant-aware user info shape.
96
+ */
97
+ async function contextToUserInfo(context, options = {}) {
98
+ const memberships = context.memberships || [];
99
+ const tenants = memberships.map((membership) => membership.tenantId);
100
+ const resolvedCurrentTenant = options.currentTenant && tenants.includes(options.currentTenant) ? options.currentTenant : tenants[0] || void 0;
101
+ let permissions = [];
102
+ if (resolvedCurrentTenant && options.request) permissions = await fetchUserPermissions(options.request, resolvedCurrentTenant);
103
+ return {
104
+ tenants,
105
+ memberships,
106
+ currentTenant: resolvedCurrentTenant,
107
+ isOnboarded: context.isOnboarded ?? false,
108
+ permissions,
109
+ hasSubscription: context.hasSubscription ?? false,
110
+ userId: context.userId,
111
+ email: context.email,
112
+ firstName: context.firstName,
113
+ lastName: context.lastName,
114
+ displayName: context.displayName,
115
+ profileImageUrl: context.profileImageUrl,
116
+ phoneNumber: context.phoneNumber,
117
+ timeZone: context.timeZone,
118
+ addresses: context.addresses,
119
+ hasOwnerMembership: context.hasOwnerMembership ?? false
120
+ };
121
+ }
122
+ /**
123
+ * Check if a cached context version differs from the latest version.
124
+ */
125
+ function hasContextVersionChanged(userId, newVersion) {
126
+ const cached = contextCache.get(userId);
127
+ return !cached || cached.version !== newVersion;
128
+ }
129
+ /**
130
+ * Mark a cached identity context as stale after a version change.
131
+ */
132
+ function updateContextVersion(userId, newVersion) {
133
+ const cached = contextCache.get(userId);
134
+ if (!cached) return;
135
+ cached.version = newVersion;
136
+ cached.timestamp = 0;
137
+ logger.debug("Marked identity context as stale for user", {
138
+ userId,
139
+ version: newVersion
140
+ });
141
+ }
142
+
143
+ //#endregion
144
+ export { clearIdentityContextCache, configureIdentityAPIFetcher, configurePermissionFetcher, contextToUserInfo, fetchIdentityContext, fetchUserPermissions, getIdentityContext, hasContextVersionChanged, updateContextVersion };
145
+ //# sourceMappingURL=server-iWm3oeCM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-iWm3oeCM.js","names":["identityAPIFetcher: IdentityAPIFetcher | null","fetcher: IdentityAPIFetcher","request: Request","error: unknown","userId: string","userId?: string","permissionFetcher: PermissionFetcher | null","fetcher: PermissionFetcher","tenantId: string","context: IdentityContextData","options: ContextToUserInfoOptions","permissions: string[]","newVersion: number"],"sources":["../src/identity/identity-context.server.ts"],"sourcesContent":["/**\n * Identity Context Server Module\n * \n * Server-side identity context management.\n */\n\nimport type { AddressData, IdentityContextData, TenantMembership } from './types';\nimport { logger } from '../logging';\n\n/**\n * Identity API fetcher type (to avoid coupling with specific API client)\n */\nexport type IdentityAPIFetcher = (request: Request) => Promise<IdentityContextData>;\n\nlet identityAPIFetcher: IdentityAPIFetcher | null = null;\n\n/**\n * Configure identity API fetcher\n */\nexport function configureIdentityAPIFetcher(fetcher: IdentityAPIFetcher): void {\n identityAPIFetcher = fetcher;\n}\n\n// In-memory cache for identity context\nconst contextCache = new Map<\n string,\n {\n context: IdentityContextData;\n timestamp: number;\n version: number;\n }\n>();\n\nconst CONTEXT_CACHE_TTL = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Fetch identity context from backend\n */\nexport async function fetchIdentityContext(request: Request): Promise<IdentityContextData> {\n if (!identityAPIFetcher) {\n throw new Error('Identity API fetcher not configured. Call configureIdentityAPIFetcher first.');\n }\n\n try {\n logger.info('Fetching identity context');\n const context = await identityAPIFetcher(request);\n \n logger.debug('Identity context fetched', {\n hasAnyMembership: context.hasAnyMembership,\n hasSubscription: context.hasSubscription,\n isOnboarded: context.isOnboarded,\n membershipCount: context.memberships?.length || 0,\n });\n\n return context;\n } catch (error: unknown) {\n logger.error('Failed to fetch identity context', error instanceof Error ? error : undefined);\n\n // Check if this is a CurrentUserMissing error\n if (\n error &&\n typeof error === 'object' &&\n 'response' in error &&\n (error as { response?: { status?: number; data?: { title?: string } } }).response?.status === 404 &&\n (error as { response?: { data?: { title?: string } } }).response?.data?.title === 'CurrentUserMissing'\n ) {\n logger.error('CurrentUserMissing error detected - session is invalid');\n const invalidSessionError = new Error('INVALID_SESSION');\n (invalidSessionError as Error & { isInvalidSession: boolean }).isInvalidSession = true;\n throw invalidSessionError;\n }\n\n throw error;\n }\n}\n\n/**\n * Get cached identity context or fetch fresh if needed\n */\nexport async function getIdentityContext(\n request: Request,\n userId: string,\n forceRefresh = false\n): Promise<IdentityContextData> {\n const cacheKey = userId;\n const cached = contextCache.get(cacheKey);\n const now = Date.now();\n\n // Return cached if valid and not forcing refresh\n if (!forceRefresh && cached && now - cached.timestamp < CONTEXT_CACHE_TTL) {\n logger.debug('Using cached identity context for user', { userId });\n return cached.context;\n }\n\n // Fetch fresh context\n const context = await fetchIdentityContext(request);\n\n // Cache the result\n contextCache.set(cacheKey, {\n context,\n timestamp: now,\n version: context.contextVersion ?? 0,\n });\n\n logger.debug('Cached fresh identity context for user', { userId });\n return context;\n}\n\n/**\n * Clear identity context cache for user\n */\nexport function clearIdentityContextCache(userId?: string): void {\n if (userId) {\n contextCache.delete(userId);\n logger.debug('Cleared identity context cache for user', { userId });\n } else {\n contextCache.clear();\n logger.debug('Cleared all identity context cache');\n }\n}\n\n/**\n * Permission fetcher type\n */\nexport type PermissionFetcher = (request: Request, tenantId: string) => Promise<string[]>;\n\nlet permissionFetcher: PermissionFetcher | null = null;\n\n/**\n * Configure permission fetcher\n */\nexport function configurePermissionFetcher(fetcher: PermissionFetcher): void {\n permissionFetcher = fetcher;\n}\n\n/**\n * Fetch user permissions for a specific tenant\n */\nexport async function fetchUserPermissions(request: Request, tenantId: string): Promise<string[]> {\n if (!tenantId) {\n logger.warn('No tenant ID provided, skipping permission fetch');\n return [];\n }\n\n if (!permissionFetcher) {\n throw new Error('Permission fetcher not configured. Call configurePermissionFetcher first.');\n }\n\n try {\n return await permissionFetcher(request, tenantId);\n } catch (error) {\n logger.error('Failed to fetch user permissions', error instanceof Error ? error : undefined);\n return [];\n }\n}\n\nexport interface UserContextInfo {\n tenants: string[];\n memberships: TenantMembership[];\n currentTenant?: string;\n isOnboarded: boolean;\n permissions: string[];\n hasSubscription: boolean;\n userId?: string;\n email?: string;\n firstName?: string | null;\n lastName?: string | null;\n displayName?: string | null;\n profileImageUrl?: string | null;\n phoneNumber?: string | null;\n timeZone?: string;\n addresses?: AddressData[];\n hasOwnerMembership: boolean;\n}\n\nexport interface ContextToUserInfoOptions {\n currentTenant?: string | null;\n request?: Request;\n}\n\n/**\n * Convert raw identity context into a tenant-aware user info shape.\n */\nexport async function contextToUserInfo(\n context: IdentityContextData,\n options: ContextToUserInfoOptions = {}\n): Promise<UserContextInfo> {\n const memberships = context.memberships || [];\n const tenants = memberships.map((membership) => membership.tenantId);\n const resolvedCurrentTenant =\n options.currentTenant && tenants.includes(options.currentTenant)\n ? options.currentTenant\n : tenants[0] || undefined;\n\n let permissions: string[] = [];\n if (resolvedCurrentTenant && options.request) {\n permissions = await fetchUserPermissions(options.request, resolvedCurrentTenant);\n }\n\n return {\n tenants,\n memberships,\n currentTenant: resolvedCurrentTenant,\n isOnboarded: context.isOnboarded ?? false,\n permissions,\n hasSubscription: context.hasSubscription ?? false,\n userId: context.userId,\n email: context.email,\n firstName: context.firstName,\n lastName: context.lastName,\n displayName: context.displayName,\n profileImageUrl: context.profileImageUrl,\n phoneNumber: context.phoneNumber,\n timeZone: context.timeZone,\n addresses: context.addresses,\n hasOwnerMembership: context.hasOwnerMembership ?? false,\n };\n}\n\n/**\n * Check if a cached context version differs from the latest version.\n */\nexport function hasContextVersionChanged(userId: string, newVersion: number): boolean {\n const cached = contextCache.get(userId);\n return !cached || cached.version !== newVersion;\n}\n\n/**\n * Mark a cached identity context as stale after a version change.\n */\nexport function updateContextVersion(userId: string, newVersion: number): void {\n const cached = contextCache.get(userId);\n if (!cached) return;\n\n cached.version = newVersion;\n cached.timestamp = 0;\n logger.debug('Marked identity context as stale for user', {\n userId,\n version: newVersion,\n });\n}\n"],"mappings":"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"}