@eminuckan/spine 0.3.6 → 0.3.9
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/CHANGELOG.md +38 -0
- package/README.md +39 -6
- package/dist/api-client/api-config.server.d.ts +11 -1
- package/dist/api-client/api-config.server.d.ts.map +1 -1
- package/dist/api-client/fetch-client.server.d.ts.map +1 -1
- package/dist/api-client/index.d.ts +1 -1
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/server.mjs +1 -1
- package/dist/api-client/types.d.ts +9 -1
- package/dist/api-client/types.d.ts.map +1 -1
- package/dist/auth/auth.server.d.ts.map +1 -1
- package/dist/auth/callback-errors.d.ts +42 -0
- package/dist/auth/callback-errors.d.ts.map +1 -0
- package/dist/auth/oauth-state-config.d.ts +7 -0
- package/dist/auth/oauth-state-config.d.ts.map +1 -0
- package/dist/auth/redis-session-storage.server.d.ts +3 -1
- package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
- package/dist/auth/route-protection.server.d.ts.map +1 -1
- package/dist/auth/server.d.ts +1 -0
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/server.mjs +3 -3
- package/dist/auth/token-refresh.server.d.ts.map +1 -1
- package/dist/react-router/server.mjs +5 -5
- package/dist/{redis-session-storage.server-DGgQcJ9B.mjs → redis-session-storage.server-CI_IYIn8.mjs} +46 -15
- package/dist/redis-session-storage.server-CI_IYIn8.mjs.map +1 -0
- package/dist/{server-DPCu40ud.mjs → server-Cw86FdT-.mjs} +273 -64
- package/dist/server-Cw86FdT-.mjs.map +1 -0
- package/dist/{server-ByScGRXA.mjs → server-pnsLybIH.mjs} +63 -16
- package/dist/server-pnsLybIH.mjs.map +1 -0
- package/dist/server.mjs +5 -5
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.mjs +11 -0
- package/dist/tanstack-start/server.d.ts +9 -0
- package/dist/tanstack-start/server.d.ts.map +1 -0
- package/dist/tanstack-start/server.mjs +17 -0
- package/dist/tenant/server.mjs +47 -7
- package/dist/tenant/server.mjs.map +1 -1
- package/dist/tenant/tenant-cookie.server.d.ts +5 -1
- package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
- package/dist/tenant/tenant.server.d.ts +9 -1
- package/dist/tenant/tenant.server.d.ts.map +1 -1
- package/dist/types-DqCXinAk.mjs.map +1 -1
- package/docs/adapters.md +64 -1
- package/docs/architecture.md +6 -1
- package/docs/module-reference.md +5 -0
- package/package.json +13 -4
- package/dist/redis-session-storage.server-DGgQcJ9B.mjs.map +0 -1
- package/dist/server-ByScGRXA.mjs.map +0 -1
- package/dist/server-DPCu40ud.mjs.map +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LOG_LEVEL_PRIORITY, LogLevel, Logger, createLogger, defaultLoggerConfig, logger } from "../logging/index.mjs";
|
|
2
|
+
import { t as PermissionChecker } from "../permission-service-DUyKomU3.mjs";
|
|
3
|
+
import { _ as initializePermissions, a as ProtectedSection, c as useHasPermission, d as usePermission, f as usePermissionChecker, g as getPermissions, h as getPermissionChecker, i as ProtectedLink, l as useHasPermissions, m as PermissionInitializer, n as ProtectedAction, o as useAllPermissions, p as usePermissions, r as ProtectedButton, s as useHasModulePermission, t as PERMISSION_MODULES, u as useModulePermission, v as usePermissionStore } from "../permissions-C5dTP9y4.mjs";
|
|
4
|
+
import { t as evaluateAccessRequirement } from "../access-BHy1v5sr.mjs";
|
|
5
|
+
import { TenantProvider, configureTenantClient, configureTenantEndpoints, getTenantCookieName, initializeTenantStore, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetTenantClientConfig, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useOrganization, useTenant, useTenantData, useTenantStore, useTenantSwitcher } from "../tenant/index.mjs";
|
|
6
|
+
import { IdentityContextProvider, configureIdentityEndpoints, configureIdentityStore, initialIdentityContext, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, resetIdentityStoreConfig, useHasEntitlement, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useUserPermissions, useUserTenants } from "../identity/index.mjs";
|
|
7
|
+
import { t as isApiClientError } from "../types-DqCXinAk.mjs";
|
|
8
|
+
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "../api-client-CB7XZaMg.mjs";
|
|
9
|
+
import { a as getIdentitySignalRClient, i as createIdentitySignalRClient, n as SignalRClient, o as initializeSignalR, r as cleanupSignalR, t as IdentitySignalRClient } from "../signalr-C-TA8nEE.mjs";
|
|
10
|
+
import { i as invalidationHelpers, n as createQueryClient, r as createQueryKeyFactory, t as cachePresets } from "../query-client-_xB300gc.mjs";
|
|
11
|
+
export { ApiError, ErrorCodes, ErrorHandler, IdentityContextProvider, IdentitySignalRClient, LOG_LEVEL_PRIORITY, LogLevel, Logger, PERMISSION_MODULES, PermissionChecker, PermissionInitializer, ProtectedAction, ProtectedButton, ProtectedLink, ProtectedSection, SignalRClient, TenantProvider, cachePresets, calculateBackoffDelay, cleanupSignalR, configureIdentityEndpoints, configureIdentityStore, configureTenantClient, configureTenantEndpoints, createIdentitySignalRClient, createLogger, createQueryClient, createQueryKeyFactory, createRetryFetch, defaultLoggerConfig, evaluateAccessRequirement, fetchWithTimeout, getIdentitySignalRClient, getPermissionChecker, getPermissions, getTenantCookieName, handleApiError, initialIdentityContext, initializePermissions, initializeSignalR, initializeTenantStore, invalidationHelpers, isApiClientError, isRetryableError, logger, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetIdentityStoreConfig, resetTenantClientConfig, useAllPermissions, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useHasEntitlement, useHasModulePermission, useHasPermission, useHasPermissions, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useModulePermission, useOrganization, usePermission, usePermissionChecker, usePermissionStore, usePermissions, useTenant, useTenantData, useTenantStore, useTenantSwitcher, useUserPermissions, useUserTenants, withRetry };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack Start server adapter exports.
|
|
3
|
+
*
|
|
4
|
+
* Server functions, middleware, and route handlers stay app-owned. The
|
|
5
|
+
* adapter only aliases Spine's framework-neutral server contract so native
|
|
6
|
+
* Web Request/Response behavior is preserved.
|
|
7
|
+
*/
|
|
8
|
+
export * from '../server';
|
|
9
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/tanstack-start/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LOG_LEVEL_PRIORITY, LogLevel, Logger, createLogger, defaultLoggerConfig, logger } from "../logging/index.mjs";
|
|
2
|
+
import { t as PermissionChecker } from "../permission-service-DUyKomU3.mjs";
|
|
3
|
+
import { _ as initializePermissions, a as ProtectedSection, c as useHasPermission, d as usePermission, f as usePermissionChecker, g as getPermissions, h as getPermissionChecker, i as ProtectedLink, l as useHasPermissions, m as PermissionInitializer, n as ProtectedAction, o as useAllPermissions, p as usePermissions, r as ProtectedButton, s as useHasModulePermission, t as PERMISSION_MODULES, u as useModulePermission, v as usePermissionStore } from "../permissions-C5dTP9y4.mjs";
|
|
4
|
+
import { t as evaluateAccessRequirement } from "../access-BHy1v5sr.mjs";
|
|
5
|
+
import { TenantProvider, configureTenantClient, configureTenantEndpoints, getTenantCookieName, initializeTenantStore, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetTenantClientConfig, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useOrganization, useTenant, useTenantData, useTenantStore, useTenantSwitcher } from "../tenant/index.mjs";
|
|
6
|
+
import { IdentityContextProvider, configureIdentityEndpoints, configureIdentityStore, initialIdentityContext, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, resetIdentityStoreConfig, useHasEntitlement, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useUserPermissions, useUserTenants } from "../identity/index.mjs";
|
|
7
|
+
import { t as isApiClientError } from "../types-DqCXinAk.mjs";
|
|
8
|
+
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "../api-client-CB7XZaMg.mjs";
|
|
9
|
+
import { a as getIdentitySignalRClient, i as createIdentitySignalRClient, n as SignalRClient, o as initializeSignalR, r as cleanupSignalR, t as IdentitySignalRClient } from "../signalr-C-TA8nEE.mjs";
|
|
10
|
+
import { i as invalidationHelpers, n as createQueryClient, r as createQueryKeyFactory, t as cachePresets } from "../query-client-_xB300gc.mjs";
|
|
11
|
+
import { S as updateAuthSession, _ as isSessionValid, a as createOAuthState, b as listCurrentUserAuthSessions, c as destroyAuthSessionById, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, h as getCurrentAuthSessionId, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, m as getAuthSessionById, n as cleanupExpiredOAuthStates, o as deleteOAuthState, p as getAuthSession, r as closeRedisConnection, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, u as destroyAuthSessionsBySessionState, v as listAuthSessionDataForUser, x as requireAuthSession, y as listAuthSessionsForUser } from "../redis-session-storage.server-CI_IYIn8.mjs";
|
|
12
|
+
import { A as resetAuthClaimMapping, C as handleCallback, D as logout, E as login, M as OAuthCallbackFailureCodes, N as isOAuthCallbackError, O as refreshTokens, P as isRecoverableOAuthCallbackError, S as handleBackChannelLogout, T as hasAuthError, _ as createAuthorizationResponseUrl, a as configureRouteProtection, b as getAuthError, c as protectRoute, d as resetRouteProtectionConfig, f as shouldRefreshToken, g as configureAuthClaimMapping, h as clearAuthServerCache, i as authRoute, j as OAuthCallbackError, k as requireAuth, l as protectedRoute, m as clearAuthErrorHeaders, n as isTokenExpiredResponse, o as onboardingRoute, p as subscriptionRoute, r as shouldAttemptTokenRefresh, s as policyRoute, t as attemptTokenRefresh, u as publicRoute, v as extractUserInfo, w as handleFrontChannelLogout, x as getUser, y as getAccessToken } from "../server-Cw86FdT-.mjs";
|
|
13
|
+
import { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, isTenantMember, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant } from "../tenant/server.mjs";
|
|
14
|
+
import { clearIdentityContextCache, configureIdentityAPIFetcher, configurePermissionFetcher, contextToUserInfo, fetchIdentityContext, fetchUserPermissions, getIdentityContext, hasContextVersionChanged, updateContextVersion } from "../identity/server.mjs";
|
|
15
|
+
import { n as createFetchMiddleware, r as createAPIConfigFactory, t as createApiClient } from "../server-pnsLybIH.mjs";
|
|
16
|
+
import { checkPermission, configurePermissionRouteProtection, requirePermission, resetPermissionRouteProtection, withPermission } from "../server.mjs";
|
|
17
|
+
export { AUTH_ERROR_COOKIE_PREFIX, ApiError, ErrorCodes, ErrorHandler, IdentityContextProvider, IdentitySignalRClient, LOG_LEVEL_PRIORITY, LogLevel, Logger, OAuthCallbackError, OAuthCallbackFailureCodes, PERMISSION_MODULES, PermissionChecker, PermissionInitializer, ProtectedAction, ProtectedButton, ProtectedLink, ProtectedSection, SignalRClient, TenantProvider, attemptTokenRefresh, authRoute, cachePresets, calculateBackoffDelay, checkPermission, cleanupExpiredOAuthStates, cleanupSignalR, clearActiveTenant, clearAuthErrorHeaders, clearAuthServerCache, clearCurrentTenant, clearIdentityContextCache, closeRedisConnection, configureAuthClaimMapping, configureIdentityAPIFetcher, configureIdentityContextFetcher, configureIdentityEndpoints, configureIdentityStore, configurePermissionFetcher, configurePermissionRouteProtection, configureRouteProtection, configureTenantClient, configureTenantCookie, configureTenantEndpoints, configureTenantResolution, contextToUserInfo, createAPIConfigFactory, createApiClient, createAuthSession, createAuthorizationResponseUrl, createFetchMiddleware, createIdentitySignalRClient, createLogger, createOAuthState, createQueryClient, createQueryKeyFactory, createRetryFetch, defaultLoggerConfig, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, evaluateAccessRequirement, extractUserInfo, fetchIdentityContext, fetchUserPermissions, fetchWithTimeout, getAccessToken, getActiveTenant, getAuthError, getAuthSession, getAuthSessionById, getAvailableTenants, getCurrentAuthSessionId, getCurrentTenant, getIdentityContext, getIdentitySignalRClient, getOAuthState, getPermissionChecker, getPermissions, getTenantCookieName, getTenantFromIdentityContext, getUser, handleApiError, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, hasContextVersionChanged, initialIdentityContext, initializePermissions, initializeSignalR, initializeTenant, initializeTenantStore, invalidationHelpers, isApiClientError, isOAuthCallbackError, isRecoverableOAuthCallbackError, isRetryableError, isSessionValid, isTenantMember, isTokenExpiredResponse, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, logger, login, logout, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, normalizeTenantDataPayload, normalizeTenantSwitchPayload, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, refreshTokens, requireAuth, requireAuthSession, requirePermission, resetAuthClaimMapping, resetIdentityStoreConfig, resetPermissionRouteProtection, resetRouteProtectionConfig, resetTenantClientConfig, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute, updateAuthSession, updateContextVersion, useAllPermissions, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useHasEntitlement, useHasModulePermission, useHasPermission, useHasPermissions, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useModulePermission, useOrganization, usePermission, usePermissionChecker, usePermissionStore, usePermissions, useTenant, useTenantData, useTenantStore, useTenantSwitcher, useUserPermissions, useUserTenants, withPermission, withRetry };
|
package/dist/tenant/server.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as getAuthSession } from "../redis-session-storage.server-
|
|
1
|
+
import { p as getAuthSession } from "../redis-session-storage.server-CI_IYIn8.mjs";
|
|
2
2
|
//#region src/tenant/tenant-cookie.server.ts
|
|
3
3
|
const DEFAULT_TENANT_COOKIE_CONFIG = {
|
|
4
4
|
name: "__spine_tenant",
|
|
@@ -21,7 +21,11 @@ function readCookieValue(cookieHeader, name) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Read the active tenant selector from the cookie.
|
|
25
|
+
*
|
|
26
|
+
* This is intentionally a raw cookie helper. Call `getCurrentTenant` from the
|
|
27
|
+
* tenant server module before using the value in an authorization-sensitive
|
|
28
|
+
* server operation.
|
|
25
29
|
*/
|
|
26
30
|
async function getActiveTenant(request) {
|
|
27
31
|
const config = getTenantCookieConfig();
|
|
@@ -70,6 +74,7 @@ function resetTenantCookieConfig() {
|
|
|
70
74
|
* Server-side tenant management.
|
|
71
75
|
*/
|
|
72
76
|
let identityContextFetcher = null;
|
|
77
|
+
const identityContextResolutionRequests = /* @__PURE__ */ new WeakSet();
|
|
73
78
|
let resolveInitialTenant = ({ identityContext }) => {
|
|
74
79
|
const membership = identityContext?.memberships?.[0];
|
|
75
80
|
return membership?.tenantId ?? membership?.id ?? null;
|
|
@@ -97,13 +102,28 @@ function resetTenantResolutionConfig() {
|
|
|
97
102
|
};
|
|
98
103
|
resolveAvailableTenants = ({ identityContext }) => identityContext?.memberships?.map((membership) => membership.tenantId ?? membership.id).filter((tenantId) => Boolean(tenantId)) ?? [];
|
|
99
104
|
}
|
|
105
|
+
function isIdentityContextResolutionInProgress(request) {
|
|
106
|
+
return identityContextResolutionRequests.has(request);
|
|
107
|
+
}
|
|
108
|
+
async function fetchConfiguredIdentityContext(request) {
|
|
109
|
+
const fetcher = identityContextFetcher;
|
|
110
|
+
if (!fetcher || isIdentityContextResolutionInProgress(request)) return null;
|
|
111
|
+
identityContextResolutionRequests.add(request);
|
|
112
|
+
try {
|
|
113
|
+
return await fetcher(request);
|
|
114
|
+
} finally {
|
|
115
|
+
identityContextResolutionRequests.delete(request);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
100
118
|
/**
|
|
101
119
|
* Get current active tenant from cookie
|
|
102
120
|
*/
|
|
103
|
-
async function getCurrentTenant(request) {
|
|
121
|
+
async function getCurrentTenant(request, requestedTenantId) {
|
|
122
|
+
if (isIdentityContextResolutionInProgress(request)) return null;
|
|
104
123
|
try {
|
|
124
|
+
if (requestedTenantId) return await isTenantMember(request, requestedTenantId) ? requestedTenantId : null;
|
|
105
125
|
const tenantFromCookie = await getActiveTenant(request);
|
|
106
|
-
if (tenantFromCookie) return tenantFromCookie;
|
|
126
|
+
if (tenantFromCookie && await isTenantMember(request, tenantFromCookie)) return tenantFromCookie;
|
|
107
127
|
return null;
|
|
108
128
|
} catch (error) {
|
|
109
129
|
console.error("Error getting current tenant:", error);
|
|
@@ -114,14 +134,16 @@ async function getCurrentTenant(request) {
|
|
|
114
134
|
* Get tenant from identity context
|
|
115
135
|
*/
|
|
116
136
|
async function getTenantFromIdentityContext(request) {
|
|
137
|
+
if (isIdentityContextResolutionInProgress(request)) return null;
|
|
117
138
|
try {
|
|
118
139
|
const sessionData = await getAuthSession(request);
|
|
119
140
|
if (!sessionData?.user?.sub) return null;
|
|
141
|
+
if (typeof sessionData.expiresAt === "number" && Date.now() >= sessionData.expiresAt) return null;
|
|
120
142
|
if (!identityContextFetcher) {
|
|
121
143
|
console.warn("Identity context fetcher not configured");
|
|
122
144
|
return null;
|
|
123
145
|
}
|
|
124
|
-
const context = await
|
|
146
|
+
const context = await fetchConfiguredIdentityContext(request);
|
|
125
147
|
return resolveInitialTenant({
|
|
126
148
|
request,
|
|
127
149
|
session: sessionData,
|
|
@@ -136,14 +158,16 @@ async function getTenantFromIdentityContext(request) {
|
|
|
136
158
|
* Get available tenants from identity context
|
|
137
159
|
*/
|
|
138
160
|
async function getAvailableTenants(request) {
|
|
161
|
+
if (isIdentityContextResolutionInProgress(request)) return [];
|
|
139
162
|
try {
|
|
140
163
|
const sessionData = await getAuthSession(request);
|
|
141
164
|
if (!sessionData?.user?.sub) return [];
|
|
165
|
+
if (typeof sessionData.expiresAt === "number" && Date.now() >= sessionData.expiresAt) return [];
|
|
142
166
|
if (!identityContextFetcher) {
|
|
143
167
|
console.warn("Identity context fetcher not configured");
|
|
144
168
|
return [];
|
|
145
169
|
}
|
|
146
|
-
const context = await
|
|
170
|
+
const context = await fetchConfiguredIdentityContext(request);
|
|
147
171
|
return resolveAvailableTenants({
|
|
148
172
|
request,
|
|
149
173
|
session: sessionData,
|
|
@@ -155,6 +179,17 @@ async function getAvailableTenants(request) {
|
|
|
155
179
|
}
|
|
156
180
|
}
|
|
157
181
|
/**
|
|
182
|
+
* Validate a tenant selector against the current identity membership set.
|
|
183
|
+
*
|
|
184
|
+
* Cookies and request options are selectors only; this check is the boundary
|
|
185
|
+
* before a tenant ID is used in a server-authoritative operation such as an
|
|
186
|
+
* API header or a session-backed tenant switch.
|
|
187
|
+
*/
|
|
188
|
+
async function isTenantMember(request, tenantId) {
|
|
189
|
+
if (!tenantId || isIdentityContextResolutionInProgress(request)) return false;
|
|
190
|
+
return (await getAvailableTenants(request)).includes(tenantId);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
158
193
|
* Set current active tenant
|
|
159
194
|
*/
|
|
160
195
|
async function setCurrentTenant(request, tenantId) {
|
|
@@ -164,6 +199,11 @@ async function setCurrentTenant(request, tenantId) {
|
|
|
164
199
|
success: false,
|
|
165
200
|
error: "No active session"
|
|
166
201
|
};
|
|
202
|
+
if (!await isTenantMember(request, tenantId)) return {
|
|
203
|
+
headers: new Headers(),
|
|
204
|
+
success: false,
|
|
205
|
+
error: "Tenant is not available for the active user"
|
|
206
|
+
};
|
|
167
207
|
const cookieValue = await setActiveTenant(tenantId);
|
|
168
208
|
const headers = new Headers();
|
|
169
209
|
headers.append("Set-Cookie", cookieValue);
|
|
@@ -213,6 +253,6 @@ async function clearCurrentTenant() {
|
|
|
213
253
|
return headers;
|
|
214
254
|
}
|
|
215
255
|
//#endregion
|
|
216
|
-
export { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant };
|
|
256
|
+
export { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, isTenantMember, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant };
|
|
217
257
|
|
|
218
258
|
//# sourceMappingURL=server.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/tenant/tenant-cookie.server.ts","../../src/tenant/tenant.server.ts"],"sourcesContent":["/**\n * Tenant Cookie Server Module\n * \n * Cookie-based tenant storage.\n */\n\nexport type TenantCookieSameSite = 'Strict' | 'Lax' | 'None';\n\nexport interface TenantCookieConfig {\n name?: string;\n maxAge?: number;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: TenantCookieSameSite;\n}\n\nconst DEFAULT_TENANT_COOKIE_CONFIG: Required<TenantCookieConfig> = {\n name: '__spine_tenant',\n maxAge: 60 * 60 * 24 * 365,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n httpOnly: false,\n sameSite: 'Lax',\n};\n\nlet tenantCookieConfig: Required<TenantCookieConfig> = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n};\n\nfunction getTenantCookieConfig(): Required<TenantCookieConfig> {\n return tenantCookieConfig;\n}\n\nfunction readCookieValue(cookieHeader: string, name: string): string | null {\n for (const segment of cookieHeader.split(';')) {\n const [rawName, ...rawValue] = segment.split('=');\n if (rawName?.trim() !== name || rawValue.length === 0) {\n continue;\n }\n\n return decodeURIComponent(rawValue.join('='));\n }\n\n return null;\n}\n\n/**\n * Get active tenant from cookie\n */\nexport async function getActiveTenant(request: Request): Promise<string | null> {\n const config = getTenantCookieConfig();\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n return readCookieValue(cookies, config.name);\n}\n\n/**\n * Set active tenant cookie value\n */\nexport async function setActiveTenant(tenantId: string): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=${encodeURIComponent(tenantId)}; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=${config.maxAge}${secure}`;\n}\n\n/**\n * Clear active tenant cookie value\n */\nexport async function clearActiveTenant(): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=0${secure}`;\n}\n\n/**\n * Configure tenant cookie name\n */\nexport function configureTenantCookie(config: TenantCookieConfig): void {\n tenantCookieConfig = {\n ...tenantCookieConfig,\n ...config,\n };\n}\n\n/**\n * Reset tenant cookie config back to defaults.\n */\nexport function resetTenantCookieConfig(): void {\n tenantCookieConfig = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n };\n}\n","/**\n * Tenant Server Module\n * \n * Server-side tenant management.\n */\n\nimport { getAuthSession } from '../auth/redis-session-storage.server';\nimport { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-cookie.server';\nimport type { SessionData } from '../auth/types';\n\n/**\n * Identity context fetcher type (to avoid circular dependency)\n */\nexport type IdentityContextFetcher = (request: Request) => Promise<{\n memberships?: Array<{ tenantId?: string; id?: string; [key: string]: unknown }>;\n [key: string]: unknown;\n} | null>;\n\nexport interface TenantResolutionContext {\n request: Request;\n session: SessionData;\n identityContext: Awaited<ReturnType<IdentityContextFetcher>>;\n}\n\nexport interface TenantResolutionConfig {\n identityContextFetcher?: IdentityContextFetcher;\n resolveInitialTenant?: (context: TenantResolutionContext) => Promise<string | null> | string | null;\n resolveAvailableTenants?: (context: TenantResolutionContext) => Promise<string[]> | string[];\n}\n\nlet identityContextFetcher: IdentityContextFetcher | null = null;\nlet resolveInitialTenant: NonNullable<TenantResolutionConfig['resolveInitialTenant']> =\n ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\nlet resolveAvailableTenants: NonNullable<TenantResolutionConfig['resolveAvailableTenants']> =\n ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n\n/**\n * Configure identity context fetcher\n */\nexport function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void {\n identityContextFetcher = fetcher;\n}\n\n/**\n * Configure tenant resolution for custom backend identity contracts.\n */\nexport function configureTenantResolution(config: TenantResolutionConfig): void {\n if (config.identityContextFetcher) {\n identityContextFetcher = config.identityContextFetcher;\n }\n if (config.resolveInitialTenant) {\n resolveInitialTenant = config.resolveInitialTenant;\n }\n if (config.resolveAvailableTenants) {\n resolveAvailableTenants = config.resolveAvailableTenants;\n }\n}\n\nexport function resetTenantResolutionConfig(): void {\n identityContextFetcher = null;\n resolveInitialTenant = ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\n resolveAvailableTenants = ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n}\n\n/**\n * Get current active tenant from cookie\n */\nexport async function getCurrentTenant(request: Request): Promise<string | null> {\n try {\n const tenantFromCookie = await getActiveTenant(request);\n if (tenantFromCookie) {\n return tenantFromCookie;\n }\n return null;\n } catch (error) {\n console.error('Error getting current tenant:', error);\n return null;\n }\n}\n\n/**\n * Get tenant from identity context\n */\nexport async function getTenantFromIdentityContext(request: Request): Promise<string | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return null;\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return null;\n }\n\n const context = await identityContextFetcher(request);\n return resolveInitialTenant({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting tenant from identity context:', error);\n return null;\n }\n}\n\n/**\n * Get available tenants from identity context\n */\nexport async function getAvailableTenants(request: Request): Promise<string[]> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return [];\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return [];\n }\n\n const context = await identityContextFetcher(request);\n return resolveAvailableTenants({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting available tenants:', error);\n return [];\n }\n}\n\n/**\n * Set current active tenant\n */\nexport async function setCurrentTenant(\n request: Request,\n tenantId: string\n): Promise<{ headers: Headers; success: boolean; error?: string }> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user) {\n return { headers: new Headers(), success: false, error: 'No active session' };\n }\n\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n\n return { headers, success: true };\n } catch (error) {\n console.error('Failed to set current tenant:', error);\n return {\n headers: new Headers(),\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Initialize tenant on first login\n */\nexport async function initializeTenant(\n request: Request\n): Promise<{ headers: Headers; tenantId: string } | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user) {\n return null;\n }\n\n // Check if tenant is already set in cookie\n const currentTenant = await getCurrentTenant(request);\n if (currentTenant) {\n return null;\n }\n\n // Get tenant from identity context\n const tenantId = await getTenantFromIdentityContext(request);\n\n if (tenantId) {\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return { headers, tenantId };\n }\n\n return null;\n } catch (error) {\n console.error('Failed to initialize tenant:', error);\n return null;\n }\n}\n\n/**\n * Clear current tenant\n */\nexport async function clearCurrentTenant(): Promise<Headers> {\n const cookieValue = await clearActiveTenant();\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return headers;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/tenant/tenant-cookie.server.ts","../../src/tenant/tenant.server.ts"],"sourcesContent":["/**\n * Tenant Cookie Server Module\n * \n * Cookie-based tenant storage.\n */\n\nexport type TenantCookieSameSite = 'Strict' | 'Lax' | 'None';\n\nexport interface TenantCookieConfig {\n name?: string;\n maxAge?: number;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: TenantCookieSameSite;\n}\n\nconst DEFAULT_TENANT_COOKIE_CONFIG: Required<TenantCookieConfig> = {\n name: '__spine_tenant',\n maxAge: 60 * 60 * 24 * 365,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n httpOnly: false,\n sameSite: 'Lax',\n};\n\nlet tenantCookieConfig: Required<TenantCookieConfig> = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n};\n\nfunction getTenantCookieConfig(): Required<TenantCookieConfig> {\n return tenantCookieConfig;\n}\n\nfunction readCookieValue(cookieHeader: string, name: string): string | null {\n for (const segment of cookieHeader.split(';')) {\n const [rawName, ...rawValue] = segment.split('=');\n if (rawName?.trim() !== name || rawValue.length === 0) {\n continue;\n }\n\n return decodeURIComponent(rawValue.join('='));\n }\n\n return null;\n}\n\n/**\n * Read the active tenant selector from the cookie.\n *\n * This is intentionally a raw cookie helper. Call `getCurrentTenant` from the\n * tenant server module before using the value in an authorization-sensitive\n * server operation.\n */\nexport async function getActiveTenant(request: Request): Promise<string | null> {\n const config = getTenantCookieConfig();\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n return readCookieValue(cookies, config.name);\n}\n\n/**\n * Set active tenant cookie value\n */\nexport async function setActiveTenant(tenantId: string): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=${encodeURIComponent(tenantId)}; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=${config.maxAge}${secure}`;\n}\n\n/**\n * Clear active tenant cookie value\n */\nexport async function clearActiveTenant(): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=0${secure}`;\n}\n\n/**\n * Configure tenant cookie name\n */\nexport function configureTenantCookie(config: TenantCookieConfig): void {\n tenantCookieConfig = {\n ...tenantCookieConfig,\n ...config,\n };\n}\n\n/**\n * Reset tenant cookie config back to defaults.\n */\nexport function resetTenantCookieConfig(): void {\n tenantCookieConfig = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n };\n}\n","/**\n * Tenant Server Module\n * \n * Server-side tenant management.\n */\n\nimport { getAuthSession } from '../auth/redis-session-storage.server';\nimport { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-cookie.server';\nimport type { SessionData } from '../auth/types';\n\n/**\n * Identity context fetcher type (to avoid circular dependency)\n */\nexport type IdentityContextFetcher = (request: Request) => Promise<{\n memberships?: Array<{ tenantId?: string; id?: string; [key: string]: unknown }>;\n [key: string]: unknown;\n} | null>;\n\nexport interface TenantResolutionContext {\n request: Request;\n session: SessionData;\n identityContext: Awaited<ReturnType<IdentityContextFetcher>>;\n}\n\nexport interface TenantResolutionConfig {\n identityContextFetcher?: IdentityContextFetcher;\n resolveInitialTenant?: (context: TenantResolutionContext) => Promise<string | null> | string | null;\n resolveAvailableTenants?: (context: TenantResolutionContext) => Promise<string[]> | string[];\n}\n\nlet identityContextFetcher: IdentityContextFetcher | null = null;\nconst identityContextResolutionRequests = new WeakSet<Request>();\nlet resolveInitialTenant: NonNullable<TenantResolutionConfig['resolveInitialTenant']> =\n ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\nlet resolveAvailableTenants: NonNullable<TenantResolutionConfig['resolveAvailableTenants']> =\n ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n\n/**\n * Configure identity context fetcher\n */\nexport function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void {\n identityContextFetcher = fetcher;\n}\n\n/**\n * Configure tenant resolution for custom backend identity contracts.\n */\nexport function configureTenantResolution(config: TenantResolutionConfig): void {\n if (config.identityContextFetcher) {\n identityContextFetcher = config.identityContextFetcher;\n }\n if (config.resolveInitialTenant) {\n resolveInitialTenant = config.resolveInitialTenant;\n }\n if (config.resolveAvailableTenants) {\n resolveAvailableTenants = config.resolveAvailableTenants;\n }\n}\n\nexport function resetTenantResolutionConfig(): void {\n identityContextFetcher = null;\n resolveInitialTenant = ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\n resolveAvailableTenants = ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n}\n\nfunction isIdentityContextResolutionInProgress(request: Request): boolean {\n return identityContextResolutionRequests.has(request);\n}\n\nasync function fetchConfiguredIdentityContext(\n request: Request,\n): Promise<Awaited<ReturnType<IdentityContextFetcher>>> {\n const fetcher = identityContextFetcher;\n if (!fetcher || isIdentityContextResolutionInProgress(request)) {\n return null;\n }\n\n identityContextResolutionRequests.add(request);\n try {\n return await fetcher(request);\n } finally {\n identityContextResolutionRequests.delete(request);\n }\n}\n\n/**\n * Get current active tenant from cookie\n */\nexport async function getCurrentTenant(\n request: Request,\n requestedTenantId?: string,\n): Promise<string | null> {\n // Identity fetchers commonly build their request through the same tenant\n // resolver. A nested selector is not authorization evidence and must not\n // recurse into another membership fetch.\n if (isIdentityContextResolutionInProgress(request)) {\n return null;\n }\n\n try {\n if (requestedTenantId) {\n return await isTenantMember(request, requestedTenantId) ? requestedTenantId : null;\n }\n\n const tenantFromCookie = await getActiveTenant(request);\n if (tenantFromCookie && await isTenantMember(request, tenantFromCookie)) {\n return tenantFromCookie;\n }\n\n return null;\n } catch (error) {\n console.error('Error getting current tenant:', error);\n return null;\n }\n}\n\n/**\n * Get tenant from identity context\n */\nexport async function getTenantFromIdentityContext(request: Request): Promise<string | null> {\n if (isIdentityContextResolutionInProgress(request)) {\n return null;\n }\n\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return null;\n }\n\n if (typeof sessionData.expiresAt === 'number' && Date.now() >= sessionData.expiresAt) {\n return null;\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return null;\n }\n\n const context = await fetchConfiguredIdentityContext(request);\n return resolveInitialTenant({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting tenant from identity context:', error);\n return null;\n }\n}\n\n/**\n * Get available tenants from identity context\n */\nexport async function getAvailableTenants(request: Request): Promise<string[]> {\n if (isIdentityContextResolutionInProgress(request)) {\n return [];\n }\n\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return [];\n }\n\n if (typeof sessionData.expiresAt === 'number' && Date.now() >= sessionData.expiresAt) {\n return [];\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return [];\n }\n\n const context = await fetchConfiguredIdentityContext(request);\n return resolveAvailableTenants({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting available tenants:', error);\n return [];\n }\n}\n\n/**\n * Validate a tenant selector against the current identity membership set.\n *\n * Cookies and request options are selectors only; this check is the boundary\n * before a tenant ID is used in a server-authoritative operation such as an\n * API header or a session-backed tenant switch.\n */\nexport async function isTenantMember(request: Request, tenantId: string): Promise<boolean> {\n if (!tenantId || isIdentityContextResolutionInProgress(request)) {\n return false;\n }\n\n const availableTenants = await getAvailableTenants(request);\n return availableTenants.includes(tenantId);\n}\n\n/**\n * Set current active tenant\n */\nexport async function setCurrentTenant(\n request: Request,\n tenantId: string\n): Promise<{ headers: Headers; success: boolean; error?: string }> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user) {\n return { headers: new Headers(), success: false, error: 'No active session' };\n }\n\n if (!await isTenantMember(request, tenantId)) {\n return {\n headers: new Headers(),\n success: false,\n error: 'Tenant is not available for the active user',\n };\n }\n\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n\n return { headers, success: true };\n } catch (error) {\n console.error('Failed to set current tenant:', error);\n return {\n headers: new Headers(),\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Initialize tenant on first login\n */\nexport async function initializeTenant(\n request: Request\n): Promise<{ headers: Headers; tenantId: string } | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user) {\n return null;\n }\n\n // Check if tenant is already set in cookie\n const currentTenant = await getCurrentTenant(request);\n if (currentTenant) {\n return null;\n }\n\n // Get tenant from identity context\n const tenantId = await getTenantFromIdentityContext(request);\n\n if (tenantId) {\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return { headers, tenantId };\n }\n\n return null;\n } catch (error) {\n console.error('Failed to initialize tenant:', error);\n return null;\n }\n}\n\n/**\n * Clear current tenant\n */\nexport async function clearCurrentTenant(): Promise<Headers> {\n const cookieValue = await clearActiveTenant();\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return headers;\n}\n"],"mappings":";;AAiBA,MAAM,+BAA6D;CACjE,MAAM;CACN,QAAQ,OAAU,KAAK;CACvB,MAAM;CACN,QAAQ,QAAQ,IAAI,aAAa;CACjC,UAAU;CACV,UAAU;AACZ;AAEA,IAAI,qBAAmD,EACrD,GAAG,6BACL;AAEA,SAAS,wBAAsD;CAC7D,OAAO;AACT;AAEA,SAAS,gBAAgB,cAAsB,MAA6B;CAC1E,KAAK,MAAM,WAAW,aAAa,MAAM,GAAG,GAAG;EAC7C,MAAM,CAAC,SAAS,GAAG,YAAY,QAAQ,MAAM,GAAG;EAChD,IAAI,SAAS,KAAK,MAAM,QAAQ,SAAS,WAAW,GAClD;EAGF,OAAO,mBAAmB,SAAS,KAAK,GAAG,CAAC;CAC9C;CAEA,OAAO;AACT;;;;;;;;AASA,eAAsB,gBAAgB,SAA0C;CAC9E,MAAM,SAAS,sBAAsB;CACrC,MAAM,UAAU,QAAQ,QAAQ,IAAI,QAAQ;CAC5C,IAAI,CAAC,SAAS,OAAO;CAErB,OAAO,gBAAgB,SAAS,OAAO,IAAI;AAC7C;;;;AAKA,eAAsB,gBAAgB,UAAmC;CACvE,MAAM,SAAS,sBAAsB;CACrC,MAAM,SAAS,OAAO,SAAS,aAAa;CAC5C,MAAM,WAAW,OAAO,WAAW,eAAe;CAClD,OAAO,GAAG,OAAO,KAAK,GAAG,mBAAmB,QAAQ,EAAE,SAAS,OAAO,OAAO,SAAS,aAAa,OAAO,SAAS,YAAY,OAAO,SAAS;AACjJ;;;;AAKA,eAAsB,oBAAqC;CACzD,MAAM,SAAS,sBAAsB;CACrC,MAAM,SAAS,OAAO,SAAS,aAAa;CAC5C,MAAM,WAAW,OAAO,WAAW,eAAe;CAClD,OAAO,GAAG,OAAO,KAAK,UAAU,OAAO,OAAO,SAAS,aAAa,OAAO,SAAS,aAAa;AACnG;;;;AAKA,SAAgB,sBAAsB,QAAkC;CACtE,qBAAqB;EACnB,GAAG;EACH,GAAG;CACL;AACF;;;;AAKA,SAAgB,0BAAgC;CAC9C,qBAAqB,EACnB,GAAG,6BACL;AACF;;;;;;;;ACrEA,IAAI,yBAAwD;AAC5D,MAAM,oDAAoC,IAAI,QAAiB;AAC/D,IAAI,wBACD,EAAE,sBAAsB;CACvB,MAAM,aAAa,iBAAiB,cAAc;CAClD,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD;AACF,IAAI,2BACD,EAAE,sBACD,iBAAiB,aACb,KAAK,eAAe,WAAW,YAAY,WAAW,EAAE,CAAC,CAC1D,QAAQ,aAAiC,QAAQ,QAAQ,CAAC,KAAK,CAAC;;;;AAKvE,SAAgB,gCAAgC,SAAuC;CACrF,yBAAyB;AAC3B;;;;AAKA,SAAgB,0BAA0B,QAAsC;CAC9E,IAAI,OAAO,wBACT,yBAAyB,OAAO;CAElC,IAAI,OAAO,sBACT,uBAAuB,OAAO;CAEhC,IAAI,OAAO,yBACT,0BAA0B,OAAO;AAErC;AAEA,SAAgB,8BAAoC;CAClD,yBAAyB;CACzB,wBAAwB,EAAE,sBAAsB;EAC9C,MAAM,aAAa,iBAAiB,cAAc;EAClD,OAAO,YAAY,YAAY,YAAY,MAAM;CACnD;CACA,2BAA2B,EAAE,sBAC3B,iBAAiB,aACb,KAAK,eAAe,WAAW,YAAY,WAAW,EAAE,CAAC,CAC1D,QAAQ,aAAiC,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvE;AAEA,SAAS,sCAAsC,SAA2B;CACxE,OAAO,kCAAkC,IAAI,OAAO;AACtD;AAEA,eAAe,+BACb,SACsD;CACtD,MAAM,UAAU;CAChB,IAAI,CAAC,WAAW,sCAAsC,OAAO,GAC3D,OAAO;CAGT,kCAAkC,IAAI,OAAO;CAC7C,IAAI;EACF,OAAO,MAAM,QAAQ,OAAO;CAC9B,UAAU;EACR,kCAAkC,OAAO,OAAO;CAClD;AACF;;;;AAKA,eAAsB,iBACpB,SACA,mBACwB;CAIxB,IAAI,sCAAsC,OAAO,GAC/C,OAAO;CAGT,IAAI;EACF,IAAI,mBACF,OAAO,MAAM,eAAe,SAAS,iBAAiB,IAAI,oBAAoB;EAGhF,MAAM,mBAAmB,MAAM,gBAAgB,OAAO;EACtD,IAAI,oBAAoB,MAAM,eAAe,SAAS,gBAAgB,GACpE,OAAO;EAGT,OAAO;CACT,SAAS,OAAO;EACd,QAAQ,MAAM,iCAAiC,KAAK;EACpD,OAAO;CACT;AACF;;;;AAKA,eAAsB,6BAA6B,SAA0C;CAC3F,IAAI,sCAAsC,OAAO,GAC/C,OAAO;CAGT,IAAI;EACF,MAAM,cAAc,MAAM,eAAe,OAAO;EAChD,IAAI,CAAC,aAAa,MAAM,KACtB,OAAO;EAGT,IAAI,OAAO,YAAY,cAAc,YAAY,KAAK,IAAI,KAAK,YAAY,WACzE,OAAO;EAGT,IAAI,CAAC,wBAAwB;GAC3B,QAAQ,KAAK,yCAAyC;GACtD,OAAO;EACT;EAEA,MAAM,UAAU,MAAM,+BAA+B,OAAO;EAC5D,OAAO,qBAAqB;GAAE;GAAS,SAAS;GAAa,iBAAiB;EAAQ,CAAC;CACzF,SAAS,OAAO;EACd,QAAQ,MAAM,+CAA+C,KAAK;EAClE,OAAO;CACT;AACF;;;;AAKA,eAAsB,oBAAoB,SAAqC;CAC7E,IAAI,sCAAsC,OAAO,GAC/C,OAAO,CAAC;CAGV,IAAI;EACF,MAAM,cAAc,MAAM,eAAe,OAAO;EAChD,IAAI,CAAC,aAAa,MAAM,KACtB,OAAO,CAAC;EAGV,IAAI,OAAO,YAAY,cAAc,YAAY,KAAK,IAAI,KAAK,YAAY,WACzE,OAAO,CAAC;EAGV,IAAI,CAAC,wBAAwB;GAC3B,QAAQ,KAAK,yCAAyC;GACtD,OAAO,CAAC;EACV;EAEA,MAAM,UAAU,MAAM,+BAA+B,OAAO;EAC5D,OAAO,wBAAwB;GAAE;GAAS,SAAS;GAAa,iBAAiB;EAAQ,CAAC;CAC5F,SAAS,OAAO;EACd,QAAQ,MAAM,oCAAoC,KAAK;EACvD,OAAO,CAAC;CACV;AACF;;;;;;;;AASA,eAAsB,eAAe,SAAkB,UAAoC;CACzF,IAAI,CAAC,YAAY,sCAAsC,OAAO,GAC5D,OAAO;CAIT,QAAO,MADwB,oBAAoB,OAAO,EAAA,CAClC,SAAS,QAAQ;AAC3C;;;;AAKA,eAAsB,iBACpB,SACA,UACiE;CACjE,IAAI;EAGF,IAAI,EAAC,MAFqB,eAAe,OAAO,EAAA,CAE/B,MACf,OAAO;GAAE,SAAS,IAAI,QAAQ;GAAG,SAAS;GAAO,OAAO;EAAoB;EAG9E,IAAI,CAAC,MAAM,eAAe,SAAS,QAAQ,GACzC,OAAO;GACL,SAAS,IAAI,QAAQ;GACrB,SAAS;GACT,OAAO;EACT;EAGF,MAAM,cAAc,MAAM,gBAAgB,QAAQ;EAClD,MAAM,UAAU,IAAI,QAAQ;EAC5B,QAAQ,OAAO,cAAc,WAAW;EAExC,OAAO;GAAE;GAAS,SAAS;EAAK;CAClC,SAAS,OAAO;EACd,QAAQ,MAAM,iCAAiC,KAAK;EACpD,OAAO;GACL,SAAS,IAAI,QAAQ;GACrB,SAAS;GACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;EAClD;CACF;AACF;;;;AAKA,eAAsB,iBACpB,SACwD;CACxD,IAAI;EAEF,IAAI,EAAC,MADqB,eAAe,OAAO,EAAA,EAC9B,MAChB,OAAO;EAKT,IAAI,MADwB,iBAAiB,OAAO,GAElD,OAAO;EAIT,MAAM,WAAW,MAAM,6BAA6B,OAAO;EAE3D,IAAI,UAAU;GACZ,MAAM,cAAc,MAAM,gBAAgB,QAAQ;GAClD,MAAM,UAAU,IAAI,QAAQ;GAC5B,QAAQ,OAAO,cAAc,WAAW;GACxC,OAAO;IAAE;IAAS;GAAS;EAC7B;EAEA,OAAO;CACT,SAAS,OAAO;EACd,QAAQ,MAAM,gCAAgC,KAAK;EACnD,OAAO;CACT;AACF;;;;AAKA,eAAsB,qBAAuC;CAC3D,MAAM,cAAc,MAAM,kBAAkB;CAC5C,MAAM,UAAU,IAAI,QAAQ;CAC5B,QAAQ,OAAO,cAAc,WAAW;CACxC,OAAO;AACT"}
|
|
@@ -13,7 +13,11 @@ export interface TenantCookieConfig {
|
|
|
13
13
|
sameSite?: TenantCookieSameSite;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Read the active tenant selector from the cookie.
|
|
17
|
+
*
|
|
18
|
+
* This is intentionally a raw cookie helper. Call `getCurrentTenant` from the
|
|
19
|
+
* tenant server module before using the value in an authorization-sensitive
|
|
20
|
+
* server operation.
|
|
17
21
|
*/
|
|
18
22
|
export declare function getActiveTenant(request: Request): Promise<string | null>;
|
|
19
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-cookie.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant-cookie.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAgCD
|
|
1
|
+
{"version":3,"file":"tenant-cookie.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant-cookie.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAgCD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAM9E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAKzD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAKtE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C"}
|
|
@@ -37,7 +37,7 @@ export declare function resetTenantResolutionConfig(): void;
|
|
|
37
37
|
/**
|
|
38
38
|
* Get current active tenant from cookie
|
|
39
39
|
*/
|
|
40
|
-
export declare function getCurrentTenant(request: Request): Promise<string | null>;
|
|
40
|
+
export declare function getCurrentTenant(request: Request, requestedTenantId?: string): Promise<string | null>;
|
|
41
41
|
/**
|
|
42
42
|
* Get tenant from identity context
|
|
43
43
|
*/
|
|
@@ -46,6 +46,14 @@ export declare function getTenantFromIdentityContext(request: Request): Promise<
|
|
|
46
46
|
* Get available tenants from identity context
|
|
47
47
|
*/
|
|
48
48
|
export declare function getAvailableTenants(request: Request): Promise<string[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Validate a tenant selector against the current identity membership set.
|
|
51
|
+
*
|
|
52
|
+
* Cookies and request options are selectors only; this check is the boundary
|
|
53
|
+
* before a tenant ID is used in a server-authoritative operation such as an
|
|
54
|
+
* API header or a session-backed tenant switch.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isTenantMember(request: Request, tenantId: string): Promise<boolean>;
|
|
49
57
|
/**
|
|
50
58
|
* Set current active tenant
|
|
51
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GAAG,IAAI,CAAC,CAAC;AAEV,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAsB;IACrC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACpG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAC9F;
|
|
1
|
+
{"version":3,"file":"tenant.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GAAG,IAAI,CAAC,CAAC;AAEV,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAsB;IACrC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACpG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAC9F;AAeD;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAErF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAU9E;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAUlD;AAsBD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuBxB;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0B3F;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0B7E;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BjE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA4BxD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC,CAK3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-DqCXinAk.mjs","names":[],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (request: Request) => Promise<string | null>;\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiB,OAAyC;CACxE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAEnE"}
|
|
1
|
+
{"version":3,"file":"types-DqCXinAk.mjs","names":[],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (\n request: Request,\n requestedTenantId?: string,\n) => Promise<string | null>;\n\n/**\n * Validate a tenant selector against the current server-side membership set.\n * Tenant cookies and explicit request overrides must not be treated as\n * authorization evidence by themselves.\n */\nexport type ValidateTenantFn = (request: Request, tenantId: string) => Promise<boolean> | boolean;\n\n/** Resolve the current server-side membership IDs for a request. */\nexport type GetAvailableTenantsFn = (request: Request) => Promise<readonly string[]> | readonly string[];\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiB,OAAyC;CACxE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAEnE"}
|
package/docs/adapters.md
CHANGED
|
@@ -13,7 +13,9 @@ Core primitives should not know about:
|
|
|
13
13
|
|
|
14
14
|
Adapters solve that by providing a framework-shaped surface without polluting the core package.
|
|
15
15
|
|
|
16
|
-
## Current
|
|
16
|
+
## Current Adapters
|
|
17
|
+
|
|
18
|
+
### React Router
|
|
17
19
|
|
|
18
20
|
Available entry points:
|
|
19
21
|
|
|
@@ -28,6 +30,67 @@ Benefits:
|
|
|
28
30
|
- adapter-specific helpers can be added later without changing import strategy
|
|
29
31
|
- future adapters can follow the same pattern
|
|
30
32
|
|
|
33
|
+
### TanStack Start
|
|
34
|
+
|
|
35
|
+
Available entry points:
|
|
36
|
+
|
|
37
|
+
- `@eminuckan/spine/tanstack-start`
|
|
38
|
+
- `@eminuckan/spine/tanstack-start/server`
|
|
39
|
+
|
|
40
|
+
These are also thin aliases over the framework-neutral client and server
|
|
41
|
+
exports. Spine does not import `@tanstack/react-start`, `@tanstack/react-router`,
|
|
42
|
+
or expose `createServerFn`/`createFileRoute` from its adapter. TanStack Start
|
|
43
|
+
server functions, middleware, route handlers, redirects, and CSRF configuration
|
|
44
|
+
remain owned by the consuming app.
|
|
45
|
+
|
|
46
|
+
Keep server-only request access inside the app-owned function module and return
|
|
47
|
+
only a safe projection across the RPC boundary:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// app/lib/viewer.functions.ts
|
|
51
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
52
|
+
import { getRequest } from '@tanstack/react-start/server';
|
|
53
|
+
import { authRoute } from '@eminuckan/spine/tanstack-start/server';
|
|
54
|
+
|
|
55
|
+
export const getViewerStatus = createServerFn({ method: 'GET' }).handler(() =>
|
|
56
|
+
authRoute(getRequest(), () => ({ authenticated: true })),
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use server routes for HTTP auth endpoints and return Spine's native
|
|
61
|
+
`Response` unchanged so status, redirects, and repeated `Set-Cookie` headers
|
|
62
|
+
survive the framework boundary:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// app/routes/auth.callback.tsx
|
|
66
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
67
|
+
import { handleCallback } from '@eminuckan/spine/tanstack-start/server';
|
|
68
|
+
|
|
69
|
+
export const Route = createFileRoute('/auth/callback')({
|
|
70
|
+
server: {
|
|
71
|
+
handlers: {
|
|
72
|
+
GET: ({ request }) => handleCallback(request),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a normal callback with a non-empty `code` and `state`, a missing or
|
|
79
|
+
expired local OAuth transaction is exposed as `OAuthCallbackError` with
|
|
80
|
+
`isRecoverableOAuthCallbackError(error) === true`. The consuming adapter owns
|
|
81
|
+
loop prevention: record a signed one-shot retry marker, start at most one new
|
|
82
|
+
`login` flow, and render a terminal error on a second stale callback. Never
|
|
83
|
+
retry state mismatches, malformed callbacks, provider/access errors,
|
|
84
|
+
token-exchange failures, storage/configuration failures, or application
|
|
85
|
+
actions. When handling a typed failure, append every value from
|
|
86
|
+
`error.cleanupSetCookies` (or iterate `error.cleanupHeaders`) so repeated
|
|
87
|
+
`Set-Cookie` headers are not collapsed.
|
|
88
|
+
|
|
89
|
+
Do not return `SessionData`, access tokens, refresh tokens, or raw request
|
|
90
|
+
context from a server function or route. Keep tenant membership checks and
|
|
91
|
+
product-specific redirects in the app-owned wrapper, and configure TanStack
|
|
92
|
+
Start's CSRF middleware when the app defines a custom `src/start.ts`.
|
|
93
|
+
|
|
31
94
|
## Import Strategy
|
|
32
95
|
|
|
33
96
|
### Use root/core imports when
|
package/docs/architecture.md
CHANGED
|
@@ -47,8 +47,13 @@ Examples:
|
|
|
47
47
|
|
|
48
48
|
- `@eminuckan/spine/react-router`
|
|
49
49
|
- `@eminuckan/spine/react-router/server`
|
|
50
|
+
- `@eminuckan/spine/tanstack-start`
|
|
51
|
+
- `@eminuckan/spine/tanstack-start/server`
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
The React Router and TanStack Start adapters are intentionally thin. That is a
|
|
54
|
+
feature, not a gap: they keep the core honest while leaving framework glue and
|
|
55
|
+
product policy in the consuming app. TanStack Start server functions, route
|
|
56
|
+
handlers, redirects, and CSRF configuration remain app-owned.
|
|
52
57
|
|
|
53
58
|
### 3. Application Adapters
|
|
54
59
|
|
package/docs/module-reference.md
CHANGED
|
@@ -9,11 +9,16 @@ Server entry point:
|
|
|
9
9
|
- `@eminuckan/spine/auth/server`
|
|
10
10
|
- `@eminuckan/spine/server`
|
|
11
11
|
- `@eminuckan/spine/react-router/server`
|
|
12
|
+
- `@eminuckan/spine/tanstack-start/server`
|
|
12
13
|
|
|
13
14
|
Key exports:
|
|
14
15
|
|
|
15
16
|
- `login`
|
|
16
17
|
- `handleCallback`
|
|
18
|
+
- `OAuthCallbackError`
|
|
19
|
+
- `OAuthCallbackFailureCodes`
|
|
20
|
+
- `isOAuthCallbackError`
|
|
21
|
+
- `isRecoverableOAuthCallbackError`
|
|
17
22
|
- `logout`
|
|
18
23
|
- `getUser`
|
|
19
24
|
- `requireAuth`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eminuckan/spine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Framework-agnostic SaaS primitives for auth, identity, multi-tenancy, permissions, and API access.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
"types": "./dist/react-router/server.d.ts",
|
|
26
26
|
"import": "./dist/react-router/server.mjs"
|
|
27
27
|
},
|
|
28
|
+
"./tanstack-start": {
|
|
29
|
+
"types": "./dist/tanstack-start/index.d.ts",
|
|
30
|
+
"import": "./dist/tanstack-start/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./tanstack-start/server": {
|
|
33
|
+
"types": "./dist/tanstack-start/server.d.ts",
|
|
34
|
+
"import": "./dist/tanstack-start/server.mjs"
|
|
35
|
+
},
|
|
28
36
|
"./auth": {
|
|
29
37
|
"types": "./dist/auth/index.d.ts",
|
|
30
38
|
"import": "./dist/auth/index.mjs"
|
|
@@ -113,8 +121,8 @@
|
|
|
113
121
|
}
|
|
114
122
|
},
|
|
115
123
|
"dependencies": {
|
|
116
|
-
"@microsoft/signalr": "^
|
|
117
|
-
"ioredis": "^
|
|
124
|
+
"@microsoft/signalr": "^10.0.11",
|
|
125
|
+
"ioredis": "^6.0.0",
|
|
118
126
|
"jose": "^6.2.2",
|
|
119
127
|
"openid-client": "^6.8.3",
|
|
120
128
|
"zustand": "^5.0.2"
|
|
@@ -128,11 +136,12 @@
|
|
|
128
136
|
"tsdown": "^0.22.4",
|
|
129
137
|
"typescript": "^5.7.0",
|
|
130
138
|
"typescript-eslint": "^8.57.0",
|
|
139
|
+
"unrun": "0.2.39",
|
|
131
140
|
"vitest": "^4.1.8"
|
|
132
141
|
},
|
|
133
142
|
"pnpm": {
|
|
134
143
|
"overrides": {
|
|
135
|
-
"@microsoft/signalr>ws": "7.5.
|
|
144
|
+
"@microsoft/signalr>ws": "7.5.13",
|
|
136
145
|
"ajv@6.12.6": "6.14.0",
|
|
137
146
|
"brace-expansion@1.1.12": "1.1.13",
|
|
138
147
|
"brace-expansion@5.0.4": "5.0.6",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-session-storage.server-DGgQcJ9B.mjs","names":[],"sources":["../src/auth/redis-session-storage.server.ts"],"sourcesContent":["/**\n * Redis Session Storage Server Module\n *\n * Redis-backed session storage for authentication.\n * Provides secure session management with automatic expiration,\n * without depending on a framework-specific session implementation.\n */\n\nimport Redis from 'ioredis';\nimport { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport type { AuthSessionSummary, SessionData, OAuthState } from './types';\nimport { logger } from '../logging';\n\n// ============================================================================\n// Redis Client Setup\n// ============================================================================\n\nlet redis: Redis | null = null;\n\nfunction getRedis(): Redis {\n if (!redis) {\n redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');\n\n redis.on('error', (err) => {\n logger.error('Redis Client Error', err);\n });\n\n redis.on('connect', () => {\n logger.info('Redis connected successfully');\n });\n }\n return redis;\n}\n\n// ============================================================================\n// Redis Key Patterns\n// ============================================================================\n\nconst KEY_PREFIX = process.env.REDIS_KEY_PREFIX || '';\n\nconst REDIS_KEYS = {\n oauthState: (stateId: string) => `${KEY_PREFIX}oauth:state:${stateId}`,\n session: (sessionId: string) => `${KEY_PREFIX}session:${sessionId}`,\n sessionByUser: (userId: string) => `${KEY_PREFIX}session:index:user:${userId}`,\n sessionBySid: (sid: string) => `${KEY_PREFIX}session:index:sid:${sid}`,\n sessionBySessionState: (sessionState: string) => `${KEY_PREFIX}session:index:session_state:${sessionState}`,\n};\n\nconst DEFAULT_SESSION_SECRET = 'default-secret-change-in-production';\nconst DEFAULT_SESSION_TTL = 60 * 60 * 24 * 7;\nconst DEFAULT_OAUTH_STATE_TTL = 10 * 60;\n\nconst OAUTH_STATE_TTL = resolvePositiveIntegerEnv('OAUTH_STATE_TTL', DEFAULT_OAUTH_STATE_TTL);\nconst SESSION_TTL = resolvePositiveIntegerEnv('SESSION_DEFAULT_TTL', resolvePositiveIntegerEnv('SESSION_TTL', DEFAULT_SESSION_TTL));\n\nconst SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || '__session_id';\nconst SESSION_SECRET = resolveSessionSecret();\nconst SESSION_COOKIE_SECURE = resolveCookieSecure();\nconst SESSION_COOKIE_PATH = '/';\nconst SESSION_COOKIE_SAME_SITE = resolveCookieSameSite();\nconst SESSION_COOKIE_DOMAIN = process.env.SESSION_COOKIE_DOMAIN?.trim();\nconst SESSION_ENCRYPTION_ENABLED = resolveSessionEncryptionEnabled();\nconst SESSION_ENCRYPTION_KEY = resolveSessionEncryptionKey(SESSION_ENCRYPTION_ENABLED);\n\nexport const AUTH_ERROR_COOKIE_PREFIX = SESSION_COOKIE_NAME.replace('_session', '').replace('__', '');\n\nfunction resolveBooleanEnv(name: string, defaultValue: boolean): boolean {\n const value = process.env[name]?.trim().toLowerCase();\n if (!value) {\n return defaultValue;\n }\n\n if (['1', 'true', 'yes', 'on'].includes(value)) {\n return true;\n }\n\n if (['0', 'false', 'no', 'off'].includes(value)) {\n return false;\n }\n\n throw new Error(`${name} must be a boolean value.`);\n}\n\nfunction resolvePositiveIntegerEnv(name: string, defaultValue: number): number {\n const value = process.env[name]?.trim();\n if (!value) {\n return defaultValue;\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed <= 0) {\n throw new Error(`${name} must be a positive integer.`);\n }\n\n return parsed;\n}\n\nfunction resolveSessionSecret(): string {\n const secret = process.env.SESSION_SECRET?.trim();\n if (!secret || secret === DEFAULT_SESSION_SECRET) {\n if (process.env.NODE_ENV === 'production') {\n throw new Error('SESSION_SECRET must be set to a unique random value in production.');\n }\n\n return DEFAULT_SESSION_SECRET;\n }\n\n if (secret.length < 32) {\n throw new Error('SESSION_SECRET must be at least 32 characters long.');\n }\n\n return secret;\n}\n\nfunction resolveCookieSecure(): boolean {\n return resolveBooleanEnv('SESSION_COOKIE_SECURE', process.env.NODE_ENV === 'production');\n}\n\nfunction resolveCookieSameSite(): 'Lax' | 'Strict' | 'None' {\n const value = process.env.SESSION_COOKIE_SAME_SITE?.trim().toLowerCase();\n const sameSite = value === 'strict' ? 'Strict' : value === 'none' ? 'None' : 'Lax';\n\n if (sameSite === 'None' && !SESSION_COOKIE_SECURE) {\n throw new Error('SESSION_COOKIE_SAME_SITE=None requires SESSION_COOKIE_SECURE=true.');\n }\n\n return sameSite;\n}\n\nfunction resolveSessionEncryptionEnabled(): boolean {\n const enabled = resolveBooleanEnv('SESSION_ENCRYPTION', process.env.NODE_ENV === 'production');\n if (process.env.NODE_ENV === 'production' && !enabled) {\n throw new Error('SESSION_ENCRYPTION cannot be disabled in production.');\n }\n\n return enabled;\n}\n\nfunction decodeBase64Url(value: string): Buffer {\n const normalized = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');\n return Buffer.from(padded, 'base64');\n}\n\nfunction resolveSessionEncryptionKey(enabled: boolean): Buffer | null {\n if (!enabled) {\n return null;\n }\n\n const value = process.env.SESSION_ENCRYPTION_KEY?.trim();\n if (!value) {\n throw new Error('SESSION_ENCRYPTION_KEY is required when session encryption is enabled.');\n }\n\n const key = /^[0-9a-f]{64}$/i.test(value)\n ? Buffer.from(value, 'hex')\n : decodeBase64Url(value);\n\n if (key.length !== 32) {\n throw new Error('SESSION_ENCRYPTION_KEY must decode to exactly 32 bytes. Generate one with: openssl rand -base64 32');\n }\n\n return key;\n}\n\ntype SessionCookieOptions = {\n maxAge: number;\n};\n\nfunction parseCookieHeader(cookieHeader: string | null): Record<string, string> {\n if (!cookieHeader) {\n return {};\n }\n\n return cookieHeader\n .split(';')\n .map((part) => part.trim())\n .filter(Boolean)\n .reduce<Record<string, string>>((cookies, part) => {\n const separatorIndex = part.indexOf('=');\n if (separatorIndex <= 0) {\n return cookies;\n }\n\n const name = part.slice(0, separatorIndex).trim();\n const value = part.slice(separatorIndex + 1).trim();\n if (!name) {\n return cookies;\n }\n\n cookies[name] = value;\n return cookies;\n }, {});\n}\n\nfunction createCookieSignature(value: string, secret: string): string {\n return createHmac('sha256', secret).update(value).digest('base64url');\n}\n\nfunction safelyCompareValues(left: string, right: string): boolean {\n const leftBuffer = Buffer.from(left);\n const rightBuffer = Buffer.from(right);\n\n if (leftBuffer.length !== rightBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signSessionId(sessionId: string): string {\n const signature = createCookieSignature(sessionId, SESSION_SECRET);\n return `${sessionId}.${signature}`;\n}\n\nfunction unsignSessionId(cookieValue: string | undefined): string | null {\n if (!cookieValue) {\n return null;\n }\n\n let decodedValue: string;\n try {\n decodedValue = decodeURIComponent(cookieValue);\n } catch {\n return null;\n }\n\n const separatorIndex = decodedValue.lastIndexOf('.');\n if (separatorIndex <= 0 || separatorIndex === decodedValue.length - 1) {\n return null;\n }\n\n const sessionId = decodedValue.slice(0, separatorIndex);\n const signature = decodedValue.slice(separatorIndex + 1);\n const expectedSignature = createCookieSignature(sessionId, SESSION_SECRET);\n\n return safelyCompareValues(signature, expectedSignature) ? sessionId : null;\n}\n\nfunction serializeSessionCookieValue(sessionId: string, options: SessionCookieOptions): string {\n const signedValue = encodeURIComponent(signSessionId(sessionId));\n const segments = [\n `${SESSION_COOKIE_NAME}=${signedValue}`,\n `Max-Age=${options.maxAge}`,\n `Path=${SESSION_COOKIE_PATH}`,\n `SameSite=${SESSION_COOKIE_SAME_SITE}`,\n 'HttpOnly',\n ];\n\n if (SESSION_COOKIE_DOMAIN) {\n segments.push(`Domain=${SESSION_COOKIE_DOMAIN}`);\n }\n\n if (SESSION_COOKIE_SECURE) {\n segments.push('Secure');\n }\n\n return segments.join('; ');\n}\n\nfunction serializeExpiredSessionCookie(): string {\n const segments = [\n `${SESSION_COOKIE_NAME}=`,\n 'Max-Age=0',\n `Path=${SESSION_COOKIE_PATH}`,\n `SameSite=${SESSION_COOKIE_SAME_SITE}`,\n 'HttpOnly',\n ];\n\n if (SESSION_COOKIE_DOMAIN) {\n segments.push(`Domain=${SESSION_COOKIE_DOMAIN}`);\n }\n\n if (SESSION_COOKIE_SECURE) {\n segments.push('Secure');\n }\n\n return segments.join('; ');\n}\n\nasync function getSessionIdFromRequest(request: Request): Promise<string | null> {\n const cookies = parseCookieHeader(request.headers.get('Cookie'));\n return unsignSessionId(cookies[SESSION_COOKIE_NAME]);\n}\n\nfunction getSessionIndexKeys(data: SessionData): string[] {\n const keys = new Set<string>();\n\n if (data.userId) {\n keys.add(REDIS_KEYS.sessionByUser(data.userId));\n }\n\n if (data.sid) {\n keys.add(REDIS_KEYS.sessionBySid(data.sid));\n }\n\n if (data.sessionState) {\n keys.add(REDIS_KEYS.sessionBySessionState(data.sessionState));\n }\n\n return Array.from(keys);\n}\n\nasync function registerSessionIndexes(sessionId: string, data: SessionData): Promise<void> {\n const indexKeys = getSessionIndexKeys(data);\n if (indexKeys.length === 0) {\n return;\n }\n\n const redisClient = getRedis();\n const pipeline = redisClient.pipeline();\n\n for (const indexKey of indexKeys) {\n pipeline.sadd(indexKey, sessionId);\n pipeline.expire(indexKey, SESSION_TTL);\n }\n\n await pipeline.exec();\n}\n\nasync function unregisterSessionIndexes(sessionId: string, data: SessionData): Promise<void> {\n const indexKeys = getSessionIndexKeys(data);\n if (indexKeys.length === 0) {\n return;\n }\n\n const redisClient = getRedis();\n const pipeline = redisClient.pipeline();\n\n for (const indexKey of indexKeys) {\n pipeline.srem(indexKey, sessionId);\n }\n\n await pipeline.exec();\n}\n\nfunction parseSessionData(sessionData: string | null): SessionData | null {\n if (!sessionData) {\n return null;\n }\n\n try {\n return JSON.parse(decodeSessionData(sessionData)) as SessionData;\n } catch (error) {\n logger.error('Failed to parse session data', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\nfunction encodeSessionData(data: SessionData): string {\n const plaintext = JSON.stringify(data);\n if (!SESSION_ENCRYPTION_KEY) {\n return plaintext;\n }\n\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', SESSION_ENCRYPTION_KEY, iv);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return [\n 'enc:v1',\n iv.toString('base64url'),\n tag.toString('base64url'),\n ciphertext.toString('base64url'),\n ].join(':');\n}\n\nfunction decodeSessionData(value: string): string {\n if (!value.startsWith('enc:v1:')) {\n if (SESSION_ENCRYPTION_ENABLED) {\n logger.warn('Reading legacy plaintext auth session payload while session encryption is enabled.');\n }\n\n return value;\n }\n\n if (!SESSION_ENCRYPTION_KEY) {\n throw new Error('Cannot decrypt auth session payload because SESSION_ENCRYPTION_KEY is not configured.');\n }\n\n const [, version, ivValue, tagValue, ciphertextValue] = value.split(':');\n if (version !== 'v1' || !ivValue || !tagValue || !ciphertextValue) {\n throw new Error('Invalid encrypted auth session payload format.');\n }\n\n const decipher = createDecipheriv('aes-256-gcm', SESSION_ENCRYPTION_KEY, Buffer.from(ivValue, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagValue, 'base64url'));\n\n return Buffer.concat([\n decipher.update(Buffer.from(ciphertextValue, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n}\n\nasync function readSessionData(sessionId: string): Promise<SessionData | null> {\n try {\n const sessionData = await getRedis().get(REDIS_KEYS.session(sessionId));\n if (!sessionData) {\n logger.debug('Session not found in Redis', { sessionId });\n return null;\n }\n\n await getRedis().expire(REDIS_KEYS.session(sessionId), SESSION_TTL);\n return parseSessionData(sessionData);\n } catch (error) {\n logger.error('Failed to read session from Redis', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\nasync function writeSessionData(sessionId: string, data: SessionData): Promise<void> {\n try {\n const existingSessionData = parseSessionData(await getRedis().get(REDIS_KEYS.session(sessionId)));\n if (existingSessionData) {\n await unregisterSessionIndexes(sessionId, existingSessionData);\n }\n\n await getRedis().setex(REDIS_KEYS.session(sessionId), SESSION_TTL, encodeSessionData(data));\n await registerSessionIndexes(sessionId, data);\n logger.debug('Updated Redis session', { sessionId });\n } catch (error) {\n logger.error('Failed to update session in Redis', error instanceof Error ? error : undefined);\n throw error;\n }\n}\n\nasync function deleteSessionData(sessionId: string): Promise<void> {\n try {\n const existingSessionData = parseSessionData(await getRedis().get(REDIS_KEYS.session(sessionId)));\n if (existingSessionData) {\n await unregisterSessionIndexes(sessionId, existingSessionData);\n }\n\n await getRedis().del(REDIS_KEYS.session(sessionId));\n logger.debug('Deleted Redis session', { sessionId });\n } catch (error) {\n logger.error('Failed to delete session from Redis', error instanceof Error ? error : undefined);\n }\n}\n\nfunction mergeSessionData(existingData: SessionData, updates: Partial<SessionData>): SessionData {\n const nextData = { ...existingData } as Record<string, unknown>;\n\n Object.entries(updates).forEach(([key, value]) => {\n if (value === undefined) {\n delete nextData[key];\n return;\n }\n\n nextData[key] = value;\n });\n\n return nextData as SessionData;\n}\n\nasync function getSessionIdsFromIndex(indexKey: string): Promise<string[]> {\n const sessionIds = await getRedis().smembers(indexKey);\n const activeSessionIds: string[] = [];\n const staleSessionIds: string[] = [];\n\n for (const sessionId of sessionIds) {\n const exists = await getRedis().exists(REDIS_KEYS.session(sessionId));\n if (exists) {\n activeSessionIds.push(sessionId);\n } else {\n staleSessionIds.push(sessionId);\n }\n }\n\n if (staleSessionIds.length > 0) {\n await getRedis().srem(indexKey, ...staleSessionIds);\n }\n\n return activeSessionIds;\n}\n\nfunction toAuthSessionSummary(\n sessionId: string,\n session: SessionData,\n currentSessionId?: string | null\n): AuthSessionSummary {\n return {\n sessionId,\n userId: session.userId,\n email: session.user?.email,\n name: session.user?.name,\n issuer: session.issuer,\n sid: session.sid,\n sessionState: session.sessionState,\n clientId: session.clientId,\n ipAddress: session.ipAddress,\n userAgent: session.userAgent,\n createdAt: session.createdAt,\n lastActivity: session.lastActivity,\n expiresAt: session.expiresAt,\n isCurrent: currentSessionId ? sessionId === currentSessionId : undefined,\n };\n}\n\nasync function listSessionDataByIds(sessionIds: string[]): Promise<Array<{ sessionId: string; data: SessionData }>> {\n const sessions: Array<{ sessionId: string; data: SessionData }> = [];\n\n for (const sessionId of sessionIds) {\n const data = await readSessionData(sessionId);\n if (data) {\n sessions.push({ sessionId, data });\n }\n }\n\n return sessions;\n}\n\nfunction getRequestIpAddress(request: Request): string | undefined {\n const forwardedFor = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();\n return (\n forwardedFor ||\n request.headers.get('cf-connecting-ip')?.trim() ||\n request.headers.get('x-real-ip')?.trim() ||\n undefined\n );\n}\n\n// ============================================================================\n// Session Helper Functions\n// ============================================================================\n\nexport async function createAuthSession(\n request: Request,\n sessionData: Partial<SessionData>\n): Promise<Headers> {\n const currentSessionId = await getSessionIdFromRequest(request);\n const nextSessionId = crypto.randomUUID();\n const nextSessionData = mergeSessionData({}, {\n ...sessionData,\n sessionId: nextSessionId,\n ipAddress: sessionData.ipAddress ?? getRequestIpAddress(request),\n userAgent: sessionData.userAgent ?? request.headers.get('user-agent') ?? undefined,\n });\n\n logger.debug('Creating auth session', { dataKeys: Object.keys(sessionData) });\n\n if (currentSessionId) {\n await deleteSessionData(currentSessionId);\n }\n\n await writeSessionData(nextSessionId, nextSessionData);\n\n return new Headers({\n 'Set-Cookie': serializeSessionCookieValue(nextSessionId, { maxAge: SESSION_TTL }),\n });\n}\n\nexport async function getAuthSession(request: Request): Promise<SessionData> {\n const sessionId = await getSessionIdFromRequest(request);\n if (!sessionId) {\n return {};\n }\n\n const session = await readSessionData(sessionId);\n if (!session) {\n return {};\n }\n\n return {\n sessionId,\n userId: session.userId,\n accessToken: session.accessToken,\n refreshToken: session.refreshToken,\n idToken: session.idToken,\n expiresAt: session.expiresAt,\n issuer: session.issuer,\n sid: session.sid,\n sessionState: session.sessionState,\n clientId: session.clientId,\n ipAddress: session.ipAddress,\n userAgent: session.userAgent,\n user: session.user,\n createdAt: session.createdAt,\n lastActivity: session.lastActivity,\n };\n}\n\nexport async function updateAuthSession(\n request: Request,\n updates: Partial<SessionData>\n): Promise<Headers> {\n const currentSessionId = await getSessionIdFromRequest(request);\n const currentSessionData = currentSessionId\n ? (await readSessionData(currentSessionId)) ?? {}\n : {};\n const nextSessionId = currentSessionId ?? crypto.randomUUID();\n const nextSessionData = mergeSessionData(currentSessionData, {\n ...updates,\n sessionId: nextSessionId,\n });\n\n await writeSessionData(nextSessionId, nextSessionData);\n\n return new Headers({\n 'Set-Cookie': serializeSessionCookieValue(nextSessionId, { maxAge: SESSION_TTL }),\n });\n}\n\nexport async function destroyAuthSession(request: Request): Promise<Headers> {\n const sessionId = await getSessionIdFromRequest(request);\n if (sessionId) {\n await deleteSessionData(sessionId);\n }\n\n return new Headers({\n 'Set-Cookie': serializeExpiredSessionCookie(),\n });\n}\n\nexport async function getCurrentAuthSessionId(request: Request): Promise<string | null> {\n return await getSessionIdFromRequest(request);\n}\n\nexport async function getAuthSessionById(sessionId: string): Promise<SessionData | null> {\n const session = await readSessionData(sessionId);\n return session ? { ...session, sessionId } : null;\n}\n\nexport async function listAuthSessionDataForUser(\n userId: string\n): Promise<Array<{ sessionId: string; data: SessionData }>> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionByUser(userId));\n return await listSessionDataByIds(sessionIds);\n}\n\nexport async function listAuthSessionsForUser(\n userId: string,\n currentSessionId?: string | null\n): Promise<AuthSessionSummary[]> {\n const sessions = await listAuthSessionDataForUser(userId);\n\n return sessions\n .map(({ sessionId, data }) => toAuthSessionSummary(sessionId, data, currentSessionId))\n .sort((left, right) => (right.lastActivity ?? right.createdAt ?? 0) - (left.lastActivity ?? left.createdAt ?? 0));\n}\n\nexport async function listCurrentUserAuthSessions(request: Request): Promise<AuthSessionSummary[]> {\n const currentSession = await getAuthSession(request);\n if (!currentSession.userId) {\n return [];\n }\n\n return await listAuthSessionsForUser(currentSession.userId, currentSession.sessionId);\n}\n\nexport async function destroyAuthSessionById(sessionId: string): Promise<boolean> {\n const session = await readSessionData(sessionId);\n if (!session) {\n return false;\n }\n\n await deleteSessionData(sessionId);\n return true;\n}\n\nexport async function destroyAuthSessionsForUser(userId: string): Promise<number> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionByUser(userId));\n let destroyedSessions = 0;\n\n for (const sessionId of sessionIds) {\n if (await destroyAuthSessionById(sessionId)) {\n destroyedSessions += 1;\n }\n }\n\n return destroyedSessions;\n}\n\nexport async function destroyAuthSessionsBySid(sid: string): Promise<number> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionBySid(sid));\n let destroyedSessions = 0;\n\n for (const sessionId of sessionIds) {\n if (await destroyAuthSessionById(sessionId)) {\n destroyedSessions += 1;\n }\n }\n\n return destroyedSessions;\n}\n\nexport async function destroyAuthSessionsBySessionState(sessionState: string): Promise<number> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionBySessionState(sessionState));\n let destroyedSessions = 0;\n\n for (const sessionId of sessionIds) {\n if (await destroyAuthSessionById(sessionId)) {\n destroyedSessions += 1;\n }\n }\n\n return destroyedSessions;\n}\n\nexport async function destroyAuthSessionsByIdentitySession(params: {\n sid?: string;\n sessionState?: string;\n userId?: string;\n}): Promise<number> {\n if (params.sid) {\n return await destroyAuthSessionsBySid(params.sid);\n }\n\n if (params.sessionState) {\n return await destroyAuthSessionsBySessionState(params.sessionState);\n }\n\n if (params.userId) {\n return await destroyAuthSessionsForUser(params.userId);\n }\n\n return 0;\n}\n\nexport async function requireAuthSession(request: Request): Promise<SessionData> {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.userId || !sessionData.accessToken) {\n throw new Response('Unauthorized', { status: 401 });\n }\n\n return sessionData;\n}\n\nexport async function isSessionValid(request: Request): Promise<boolean> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user || !sessionData.accessToken) {\n return false;\n }\n\n if (sessionData.expiresAt && Date.now() >= sessionData.expiresAt) {\n return false;\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\n// ============================================================================\n// OAuth State Management\n// ============================================================================\n\nexport async function createOAuthState(state: OAuthState): Promise<string> {\n const stateId = crypto.randomUUID();\n const key = REDIS_KEYS.oauthState(stateId);\n\n await getRedis().setex(key, OAUTH_STATE_TTL, JSON.stringify(state));\n\n logger.debug('OAuth state created', { stateId, ttl: OAUTH_STATE_TTL });\n\n return stateId;\n}\n\nexport async function getOAuthState(stateId: string): Promise<OAuthState | null> {\n const key = REDIS_KEYS.oauthState(stateId);\n const data = await getRedis().get(key);\n\n if (!data) {\n logger.warn('OAuth state not found or expired', { stateId });\n return null;\n }\n\n try {\n return JSON.parse(data);\n } catch (error) {\n logger.error('Failed to parse OAuth state', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\nexport async function deleteOAuthState(stateId: string): Promise<void> {\n const key = REDIS_KEYS.oauthState(stateId);\n await getRedis().del(key);\n logger.debug('OAuth state deleted', { stateId });\n}\n\nexport async function cleanupExpiredOAuthStates(): Promise<number> {\n const pattern = REDIS_KEYS.oauthState('*');\n const keys = await getRedis().keys(pattern);\n\n let cleaned = 0;\n for (const key of keys) {\n const ttl = await getRedis().ttl(key);\n if (ttl === -1) {\n await getRedis().del(key);\n cleaned++;\n }\n }\n\n logger.info('OAuth state cleanup completed', { cleaned, total: keys.length });\n return cleaned;\n}\n\n// ============================================================================\n// Cleanup\n// ============================================================================\n\nexport async function closeRedisConnection(): Promise<void> {\n if (redis) {\n await redis.quit();\n redis = null;\n logger.info('Redis connection closed');\n }\n}\n"],"mappings":"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"}
|