@eminuckan/spine 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/api-client/index.d.ts +3 -0
- package/dist/api-client/index.js +4 -0
- package/dist/api-client/server.d.ts +4 -0
- package/dist/api-client/server.js +5 -0
- package/dist/api-client-Ftxqm3Ep.js +212 -0
- package/dist/api-client-Ftxqm3Ep.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +0 -0
- package/dist/auth/server.d.ts +3 -0
- package/dist/auth/server.js +6 -0
- package/dist/auth.server-C979TR6W.js +724 -0
- package/dist/auth.server-C979TR6W.js.map +1 -0
- package/dist/auth.server-DU-x2LPA.js +5 -0
- package/dist/identity/index.d.ts +4 -0
- package/dist/identity/index.js +3 -0
- package/dist/identity/server.d.ts +4 -0
- package/dist/identity/server.js +4 -0
- package/dist/identity-CFa1txoj.js +235 -0
- package/dist/identity-CFa1txoj.js.map +1 -0
- package/dist/index-BIJOC8Fb.d.ts +272 -0
- package/dist/index-BIJOC8Fb.d.ts.map +1 -0
- package/dist/index-Bt9XE6gj.d.ts +104 -0
- package/dist/index-Bt9XE6gj.d.ts.map +1 -0
- package/dist/index-C2hGpzGr.d.ts +74 -0
- package/dist/index-C2hGpzGr.d.ts.map +1 -0
- package/dist/index-CfQsb3l_.d.ts +170 -0
- package/dist/index-CfQsb3l_.d.ts.map +1 -0
- package/dist/index-D6RK_5vm.d.ts +199 -0
- package/dist/index-D6RK_5vm.d.ts.map +1 -0
- package/dist/index-DrttYPAj.d.ts +30 -0
- package/dist/index-DrttYPAj.d.ts.map +1 -0
- package/dist/index-geY2ynnN.d.ts +141 -0
- package/dist/index-geY2ynnN.d.ts.map +1 -0
- package/dist/index-kr8Wk7v5.d.ts +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/logging/index.d.ts +3 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging-_h2mi3L9.js +118 -0
- package/dist/logging-_h2mi3L9.js.map +1 -0
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions-D5ScGGnd.js +281 -0
- package/dist/permissions-D5ScGGnd.js.map +1 -0
- package/dist/query-client/index.d.ts +2 -0
- package/dist/query-client/index.js +3 -0
- package/dist/query-client-DRPQIKHX.js +174 -0
- package/dist/query-client-DRPQIKHX.js.map +1 -0
- package/dist/react-router/index.d.ts +12 -0
- package/dist/react-router/index.js +10 -0
- package/dist/react-router/server.d.ts +18 -0
- package/dist/react-router/server.js +17 -0
- package/dist/redis-session-storage.server-DhufyPjd.js +250 -0
- package/dist/redis-session-storage.server-DhufyPjd.js.map +1 -0
- package/dist/server-BoeUiXSh.d.ts +40 -0
- package/dist/server-BoeUiXSh.d.ts.map +1 -0
- package/dist/server-C-DiVP9e.js +185 -0
- package/dist/server-C-DiVP9e.js.map +1 -0
- package/dist/server-CLbksnvg.d.ts +144 -0
- package/dist/server-CLbksnvg.d.ts.map +1 -0
- package/dist/server-CNp6gjFE.js +78 -0
- package/dist/server-CNp6gjFE.js.map +1 -0
- package/dist/server-CXGaRaiS.d.ts +256 -0
- package/dist/server-CXGaRaiS.d.ts.map +1 -0
- package/dist/server-CiSbg-9C.js +352 -0
- package/dist/server-CiSbg-9C.js.map +1 -0
- package/dist/server-DLgH3FRR.js +312 -0
- package/dist/server-DLgH3FRR.js.map +1 -0
- package/dist/server-DazqdM63.d.ts +93 -0
- package/dist/server-DazqdM63.d.ts.map +1 -0
- package/dist/server-iWm3oeCM.js +145 -0
- package/dist/server-iWm3oeCM.js.map +1 -0
- package/dist/server-zEKMDpP7.d.ts +80 -0
- package/dist/server-zEKMDpP7.d.ts.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +17 -0
- package/dist/signalr/index.d.ts +4 -0
- package/dist/signalr/index.js +3 -0
- package/dist/signalr-BJGcXc0m.js +274 -0
- package/dist/signalr-BJGcXc0m.js.map +1 -0
- package/dist/src-D7mh9MKA.js +0 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.js +3 -0
- package/dist/tenant/server.d.ts +3 -0
- package/dist/tenant/server.js +5 -0
- package/dist/tenant-C6KPaSCX.js +256 -0
- package/dist/tenant-C6KPaSCX.js.map +1 -0
- package/dist/types-BTjFzIhv.d.ts +31 -0
- package/dist/types-BTjFzIhv.d.ts.map +1 -0
- package/dist/types-DoIfDhl6.d.ts +55 -0
- package/dist/types-DoIfDhl6.d.ts.map +1 -0
- package/dist/types-DufLRY7A.d.ts +164 -0
- package/dist/types-DufLRY7A.d.ts.map +1 -0
- package/dist/types-uDrDMkCq.d.ts +93 -0
- package/dist/types-uDrDMkCq.d.ts.map +1 -0
- package/package.json +147 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { logger } from "./logging-_h2mi3L9.js";
|
|
2
|
+
import { getAuthSession$1 as getAuthSession } from "./redis-session-storage.server-DhufyPjd.js";
|
|
3
|
+
import { createRedirectResponse, getUser$1 as getUser, login$1 as login, refreshTokens$1 as refreshTokens, requireAuth$1 as requireAuth } from "./auth.server-C979TR6W.js";
|
|
4
|
+
|
|
5
|
+
//#region src/auth/route-protection.server.ts
|
|
6
|
+
const REFRESH_THRESHOLD = 5 * 60 * 1e3;
|
|
7
|
+
/**
|
|
8
|
+
* Check if token needs refresh
|
|
9
|
+
*/
|
|
10
|
+
async function shouldRefreshToken(request) {
|
|
11
|
+
try {
|
|
12
|
+
const sessionData = await getAuthSession(request);
|
|
13
|
+
if (!sessionData.refreshToken || sessionData.refreshToken.length === 0) {
|
|
14
|
+
if (sessionData.expiresAt && Date.now() >= sessionData.expiresAt) {
|
|
15
|
+
logger.info("Token expired and no refresh token, forcing app-specific logout");
|
|
16
|
+
const { logout } = await import("./auth.server-DU-x2LPA.js");
|
|
17
|
+
const logoutRequest = await createAutomaticLogoutRequest(request, "expired-no-refresh-token");
|
|
18
|
+
throw await logout(logoutRequest);
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (!sessionData.expiresAt || !sessionData.accessToken) return false;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
let jwtExpiry = null;
|
|
25
|
+
try {
|
|
26
|
+
const tokenParts = sessionData.accessToken.split(".");
|
|
27
|
+
if (tokenParts.length === 3) {
|
|
28
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString());
|
|
29
|
+
if (payload.exp) jwtExpiry = payload.exp * 1e3;
|
|
30
|
+
}
|
|
31
|
+
} catch {}
|
|
32
|
+
const earliestExpiry = jwtExpiry && jwtExpiry < sessionData.expiresAt ? jwtExpiry : sessionData.expiresAt;
|
|
33
|
+
const timeUntilEarliestExpiry = earliestExpiry - now;
|
|
34
|
+
return timeUntilEarliestExpiry <= REFRESH_THRESHOLD;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.error("Error checking token refresh", error instanceof Error ? error : void 0);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Auto-refresh tokens if needed
|
|
42
|
+
*/
|
|
43
|
+
async function autoRefreshTokens(request) {
|
|
44
|
+
try {
|
|
45
|
+
const needsRefresh = await shouldRefreshToken(request);
|
|
46
|
+
if (needsRefresh) {
|
|
47
|
+
logger.info("Auto-refreshing tokens due to expiry");
|
|
48
|
+
const result = await refreshTokens(request);
|
|
49
|
+
if (result.success) logger.info("Auto-refresh completed successfully");
|
|
50
|
+
else {
|
|
51
|
+
logger.error("Auto-refresh failed", void 0, { error: result.error });
|
|
52
|
+
if (result.shouldLogout) {
|
|
53
|
+
logger.error("Refresh token invalid, forcing app-specific logout");
|
|
54
|
+
const { logout } = await import("./auth.server-DU-x2LPA.js");
|
|
55
|
+
const logoutRequest = await createAutomaticLogoutRequest(request, "refresh-failed");
|
|
56
|
+
throw await logout(logoutRequest);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let protectionConfig = {};
|
|
66
|
+
/**
|
|
67
|
+
* Configure route protection with identity/tenant/subscription checkers
|
|
68
|
+
*/
|
|
69
|
+
function configureRouteProtection(config) {
|
|
70
|
+
protectionConfig = config;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reset route protection to its built-in defaults.
|
|
74
|
+
*/
|
|
75
|
+
function resetRouteProtectionConfig() {
|
|
76
|
+
protectionConfig = {};
|
|
77
|
+
}
|
|
78
|
+
function createRedirectFromConfig(redirect) {
|
|
79
|
+
return createRedirectResponse(redirect.location, {
|
|
80
|
+
headers: redirect.headers,
|
|
81
|
+
status: redirect.status
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function getLoginReturnUrl(request, currentPath, protection) {
|
|
85
|
+
if (protectionConfig.getLoginReturnUrl) return protectionConfig.getLoginReturnUrl({
|
|
86
|
+
request,
|
|
87
|
+
currentPath,
|
|
88
|
+
protection
|
|
89
|
+
});
|
|
90
|
+
switch (protection) {
|
|
91
|
+
case "onboarding-required": return "/onboarding";
|
|
92
|
+
case "subscription-required": return "/subscription";
|
|
93
|
+
case "public": return "/";
|
|
94
|
+
case "auth":
|
|
95
|
+
default: {
|
|
96
|
+
const url = new URL(request.url);
|
|
97
|
+
return url.pathname === "/" ? "/" : `${url.pathname}${url.search}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function createAutomaticLogoutRequest(request, reason) {
|
|
102
|
+
if (protectionConfig.createAutomaticLogoutRequest) return protectionConfig.createAutomaticLogoutRequest({
|
|
103
|
+
request,
|
|
104
|
+
reason
|
|
105
|
+
});
|
|
106
|
+
const logoutUrl = new URL(request.url);
|
|
107
|
+
logoutUrl.pathname = "/auth/logout";
|
|
108
|
+
logoutUrl.searchParams.set("client_id_only", "true");
|
|
109
|
+
return new Request(logoutUrl.toString(), { headers: request.headers });
|
|
110
|
+
}
|
|
111
|
+
function createRouteResolverContext(request, user, currentPath, protection) {
|
|
112
|
+
let identityContextPromise = null;
|
|
113
|
+
let currentTenantPromise = null;
|
|
114
|
+
let initializeTenantPromise = null;
|
|
115
|
+
return {
|
|
116
|
+
request,
|
|
117
|
+
user,
|
|
118
|
+
currentPath,
|
|
119
|
+
protection,
|
|
120
|
+
getIdentityContext: async (forceRefresh = false) => {
|
|
121
|
+
if (!protectionConfig.identityChecker) return null;
|
|
122
|
+
if (forceRefresh) return protectionConfig.identityChecker.getIdentityContext(request, user.sub, true);
|
|
123
|
+
if (!identityContextPromise) identityContextPromise = protectionConfig.identityChecker.getIdentityContext(request, user.sub);
|
|
124
|
+
return identityContextPromise;
|
|
125
|
+
},
|
|
126
|
+
getCurrentTenant: async () => {
|
|
127
|
+
if (!protectionConfig.tenantChecker) return null;
|
|
128
|
+
if (!currentTenantPromise) currentTenantPromise = protectionConfig.tenantChecker.getCurrentTenant(request);
|
|
129
|
+
return currentTenantPromise;
|
|
130
|
+
},
|
|
131
|
+
initializeTenant: async () => {
|
|
132
|
+
if (!protectionConfig.tenantChecker) return null;
|
|
133
|
+
if (!initializeTenantPromise) initializeTenantPromise = protectionConfig.tenantChecker.initializeTenant(request);
|
|
134
|
+
return initializeTenantPromise;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function applyConfiguredRouteResolution(request, user, currentPath, protection) {
|
|
139
|
+
if (!protectionConfig.resolveRoute) return false;
|
|
140
|
+
const context = createRouteResolverContext(request, user, currentPath, protection);
|
|
141
|
+
try {
|
|
142
|
+
const result = await protectionConfig.resolveRoute(context);
|
|
143
|
+
if (result) throw createRedirectFromConfig(result);
|
|
144
|
+
return true;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
147
|
+
if (!protectionConfig.handleRouteError) throw error;
|
|
148
|
+
const recovery = await protectionConfig.handleRouteError({
|
|
149
|
+
...context,
|
|
150
|
+
error
|
|
151
|
+
});
|
|
152
|
+
if (recovery) throw createRedirectFromConfig(recovery);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check user status (onboarding, subscription)
|
|
158
|
+
*/
|
|
159
|
+
async function checkUserStatus(request, user, currentPath) {
|
|
160
|
+
if (!protectionConfig.identityChecker) {
|
|
161
|
+
logger.warn("Identity checker not configured, skipping status check");
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const context = await protectionConfig.identityChecker.getIdentityContext(request, user.sub);
|
|
166
|
+
logger.debug("User status check", {
|
|
167
|
+
userId: user.sub,
|
|
168
|
+
currentPath,
|
|
169
|
+
hasAnyMembership: context.hasAnyMembership,
|
|
170
|
+
isOnboarded: context.isOnboarded,
|
|
171
|
+
hasSubscription: context.hasSubscription
|
|
172
|
+
});
|
|
173
|
+
if (!context.hasAnyMembership) {
|
|
174
|
+
if (currentPath !== "/onboarding") {
|
|
175
|
+
logger.info("User has no memberships, redirecting to onboarding");
|
|
176
|
+
throw createRedirectResponse("/onboarding");
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (context.hasAnyMembership) {
|
|
181
|
+
if (currentPath === "/onboarding") {
|
|
182
|
+
logger.info("User has memberships, redirecting from onboarding to subscription");
|
|
183
|
+
throw createRedirectResponse("/subscription");
|
|
184
|
+
}
|
|
185
|
+
if (currentPath === "/subscription") return null;
|
|
186
|
+
if (protectionConfig.tenantChecker && protectionConfig.subscriptionChecker) {
|
|
187
|
+
let currentTenant = await protectionConfig.tenantChecker.getCurrentTenant(request);
|
|
188
|
+
if (!currentTenant) {
|
|
189
|
+
const initResult = await protectionConfig.tenantChecker.initializeTenant(request);
|
|
190
|
+
if (initResult) currentTenant = initResult.tenantId;
|
|
191
|
+
}
|
|
192
|
+
if (currentTenant) {
|
|
193
|
+
const hasSubscription = await protectionConfig.subscriptionChecker.checkTenantSubscriptionStatus(request, currentTenant);
|
|
194
|
+
if (!hasSubscription) {
|
|
195
|
+
logger.info("Tenant has no subscription, redirecting to subscription");
|
|
196
|
+
throw createRedirectResponse("/subscription");
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
logger.info("Could not initialize tenant, redirecting to subscription");
|
|
200
|
+
throw createRedirectResponse("/subscription");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
207
|
+
logger.error("Error checking user status", error instanceof Error ? error : void 0);
|
|
208
|
+
if (currentPath !== "/onboarding") throw createRedirectResponse("/onboarding");
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Main route protection function
|
|
214
|
+
*/
|
|
215
|
+
async function protectRoute(request, protection, loaderFn) {
|
|
216
|
+
const url = new URL(request.url);
|
|
217
|
+
switch (protection) {
|
|
218
|
+
case "public": try {
|
|
219
|
+
await autoRefreshTokens(request);
|
|
220
|
+
const user = await getUser(request);
|
|
221
|
+
return loaderFn(user ?? void 0);
|
|
222
|
+
} catch {
|
|
223
|
+
return loaderFn(void 0);
|
|
224
|
+
}
|
|
225
|
+
case "auth": try {
|
|
226
|
+
await autoRefreshTokens(request);
|
|
227
|
+
const user = await requireAuth(request);
|
|
228
|
+
const handledByConfig = await applyConfiguredRouteResolution(request, user, url.pathname, "auth");
|
|
229
|
+
if (!handledByConfig) await checkUserStatus(request, user, url.pathname);
|
|
230
|
+
return loaderFn(user);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
233
|
+
throw await login(request, getLoginReturnUrl(request, url.pathname, "auth"));
|
|
234
|
+
}
|
|
235
|
+
case "onboarding-required": try {
|
|
236
|
+
await autoRefreshTokens(request);
|
|
237
|
+
const user = await requireAuth(request);
|
|
238
|
+
const handledByConfig = await applyConfiguredRouteResolution(request, user, url.pathname, "onboarding-required");
|
|
239
|
+
if (!handledByConfig && protectionConfig.identityChecker) {
|
|
240
|
+
const context = await protectionConfig.identityChecker.getIdentityContext(request, user.sub);
|
|
241
|
+
if (context.hasAnyMembership) throw createRedirectResponse("/subscription");
|
|
242
|
+
}
|
|
243
|
+
return loaderFn(user);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
246
|
+
throw await login(request, getLoginReturnUrl(request, url.pathname, "onboarding-required"));
|
|
247
|
+
}
|
|
248
|
+
case "subscription-required": try {
|
|
249
|
+
await autoRefreshTokens(request);
|
|
250
|
+
const user = await requireAuth(request);
|
|
251
|
+
const handledByConfig = await applyConfiguredRouteResolution(request, user, url.pathname, "subscription-required");
|
|
252
|
+
if (!handledByConfig && protectionConfig.identityChecker) {
|
|
253
|
+
const context = await protectionConfig.identityChecker.getIdentityContext(request, user.sub);
|
|
254
|
+
if (!context.hasAnyMembership) throw createRedirectResponse("/onboarding");
|
|
255
|
+
}
|
|
256
|
+
if (protectionConfig.tenantChecker) {
|
|
257
|
+
const currentTenant = await protectionConfig.tenantChecker.getCurrentTenant(request);
|
|
258
|
+
if (!currentTenant) await protectionConfig.tenantChecker.initializeTenant(request);
|
|
259
|
+
}
|
|
260
|
+
return loaderFn(user);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
263
|
+
throw await login(request, getLoginReturnUrl(request, url.pathname, "subscription-required"));
|
|
264
|
+
}
|
|
265
|
+
default: throw new Error(`Unknown protection level: ${protection}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Public route (optional auth)
|
|
270
|
+
*/
|
|
271
|
+
const publicRoute = (request, loaderFn) => protectRoute(request, "public", loaderFn);
|
|
272
|
+
/**
|
|
273
|
+
* Auth-protected route
|
|
274
|
+
*/
|
|
275
|
+
const authRoute = (request, loaderFn) => protectRoute(request, "auth", loaderFn);
|
|
276
|
+
/**
|
|
277
|
+
* Onboarding route
|
|
278
|
+
*/
|
|
279
|
+
const onboardingRoute = (request, loaderFn) => protectRoute(request, "onboarding-required", loaderFn);
|
|
280
|
+
/**
|
|
281
|
+
* Subscription route
|
|
282
|
+
*/
|
|
283
|
+
const subscriptionRoute = (request, loaderFn) => protectRoute(request, "subscription-required", loaderFn);
|
|
284
|
+
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/auth/token-refresh.server.ts
|
|
287
|
+
/**
|
|
288
|
+
* Attempt to refresh the access token
|
|
289
|
+
*/
|
|
290
|
+
async function attemptTokenRefresh(request) {
|
|
291
|
+
try {
|
|
292
|
+
logger.info("Attempting token refresh due to 401 response");
|
|
293
|
+
const sessionData = await getAuthSession(request);
|
|
294
|
+
if (!sessionData.refreshToken) {
|
|
295
|
+
logger.warn("No refresh token available in session");
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: "No refresh token available",
|
|
299
|
+
shouldLogout: true
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const refreshResult = await refreshTokens(request, sessionData.refreshToken);
|
|
303
|
+
if (!refreshResult.success) {
|
|
304
|
+
logger.error("Token refresh failed", void 0, {
|
|
305
|
+
error: refreshResult.error,
|
|
306
|
+
shouldLogout: refreshResult.shouldLogout
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
error: refreshResult.error,
|
|
311
|
+
shouldLogout: refreshResult.shouldLogout
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const tokens = refreshResult.tokens;
|
|
315
|
+
if (!tokens || !tokens.access_token) {
|
|
316
|
+
logger.error("Token refresh succeeded but no access token returned");
|
|
317
|
+
return {
|
|
318
|
+
success: false,
|
|
319
|
+
error: "No access token in refresh response",
|
|
320
|
+
shouldLogout: true
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
logger.info("Token refresh successful");
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
newAccessToken: tokens.access_token
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
logger.error("Token refresh attempt failed", error instanceof Error ? error : void 0);
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
333
|
+
shouldLogout: true
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Check if a response indicates token expiration
|
|
339
|
+
*/
|
|
340
|
+
function isTokenExpiredResponse(response) {
|
|
341
|
+
return response.status === 401;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Check if token refresh should be attempted
|
|
345
|
+
*/
|
|
346
|
+
function shouldAttemptTokenRefresh(response, hasAuth) {
|
|
347
|
+
return hasAuth && isTokenExpiredResponse(response);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
//#endregion
|
|
351
|
+
export { attemptTokenRefresh, authRoute, configureRouteProtection, isTokenExpiredResponse, onboardingRoute, protectRoute, publicRoute, resetRouteProtectionConfig, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute };
|
|
352
|
+
//# sourceMappingURL=server-CiSbg-9C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-CiSbg-9C.js","names":["request: Request","jwtExpiry: number | null","protectionConfig: RouteProtectionConfig<any>","config: RouteProtectionConfig<TIdentityContext>","redirect: RouteProtectionRedirect","currentPath: string","protection: ProtectionLevel","reason: AutomaticLogoutRequestContext['reason']","user: UserInfo","identityContextPromise: Promise<any | null> | null","currentTenantPromise: Promise<string | null> | null","initializeTenantPromise: Promise<{ headers: Headers; tenantId: string } | null> | null","loaderFn: (user?: UserInfo) => Promise<T> | T","loaderFn: (user: UserInfo) => Promise<T> | T","request: Request","response: Response","hasAuth: boolean"],"sources":["../src/auth/route-protection.server.ts","../src/auth/token-refresh.server.ts"],"sourcesContent":["/**\n * Route Protection Server Module\n * \n * Provides route protection utilities for different authentication levels.\n */\n\nimport { getUser, requireAuth, refreshTokens, login } from './auth.server';\nimport { getAuthSession } from './redis-session-storage.server';\nimport type { ProtectionLevel, UserInfo } from './types';\nimport { logger } from '../logging';\nimport { createRedirectResponse } from '../http/response';\n\n// Token refresh threshold (5 minutes before expiry)\nconst REFRESH_THRESHOLD = 5 * 60 * 1000;\n\n/**\n * Check if token needs refresh\n */\nexport async function shouldRefreshToken(request: Request): Promise<boolean> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.refreshToken || sessionData.refreshToken.length === 0) {\n if (sessionData.expiresAt && Date.now() >= sessionData.expiresAt) {\n logger.info('Token expired and no refresh token, forcing app-specific logout');\n const { logout } = await import('./auth.server');\n const logoutRequest = await createAutomaticLogoutRequest(request, 'expired-no-refresh-token');\n throw await logout(logoutRequest);\n }\n return false;\n }\n\n if (!sessionData.expiresAt || !sessionData.accessToken) {\n return false;\n }\n\n const now = Date.now();\n\n // Check JWT expiry\n let jwtExpiry: number | null = null;\n try {\n const tokenParts = sessionData.accessToken.split('.');\n if (tokenParts.length === 3) {\n const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());\n if (payload.exp) {\n jwtExpiry = payload.exp * 1000;\n }\n }\n } catch {\n // Ignore JWT decode errors\n }\n\n const earliestExpiry = jwtExpiry && jwtExpiry < sessionData.expiresAt \n ? jwtExpiry \n : sessionData.expiresAt;\n const timeUntilEarliestExpiry = earliestExpiry - now;\n\n return timeUntilEarliestExpiry <= REFRESH_THRESHOLD;\n } catch (error) {\n logger.error('Error checking token refresh', error instanceof Error ? error : undefined);\n return false;\n }\n}\n\n/**\n * Auto-refresh tokens if needed\n */\nasync function autoRefreshTokens(request: Request): Promise<void> {\n try {\n const needsRefresh = await shouldRefreshToken(request);\n\n if (needsRefresh) {\n logger.info('Auto-refreshing tokens due to expiry');\n const result = await refreshTokens(request);\n\n if (result.success) {\n logger.info('Auto-refresh completed successfully');\n } else {\n logger.error('Auto-refresh failed', undefined, { error: result.error });\n\n if (result.shouldLogout) {\n logger.error('Refresh token invalid, forcing app-specific logout');\n const { logout } = await import('./auth.server');\n const logoutRequest = await createAutomaticLogoutRequest(request, 'refresh-failed');\n throw await logout(logoutRequest);\n }\n }\n }\n } catch (error) {\n if (error instanceof Response && error.status >= 300 && error.status < 400) {\n throw error;\n }\n throw error;\n }\n}\n\n/**\n * Identity context check configuration\n */\nexport interface DefaultRouteIdentityContext {\n hasAnyMembership: boolean;\n hasSubscription: boolean;\n isOnboarded: boolean;\n}\n\nexport interface IdentityContextChecker<TIdentityContext = DefaultRouteIdentityContext> {\n getIdentityContext: (\n request: Request,\n userId: string,\n forceRefresh?: boolean\n ) => Promise<DefaultRouteIdentityContext & TIdentityContext>;\n}\n\n/**\n * Tenant checker configuration\n */\nexport interface TenantChecker {\n getCurrentTenant: (request: Request) => Promise<string | null>;\n initializeTenant: (request: Request) => Promise<{ headers: Headers; tenantId: string } | null>;\n}\n\n/**\n * Subscription checker configuration\n */\nexport interface SubscriptionChecker {\n checkTenantSubscriptionStatus: (request: Request, tenantId: string) => Promise<boolean>;\n}\n\nexport interface RouteProtectionRedirect {\n location: string;\n headers?: HeadersInit;\n status?: number;\n}\n\nexport interface AutomaticLogoutRequestContext {\n request: Request;\n reason: 'expired-no-refresh-token' | 'refresh-failed';\n}\n\nexport interface LoginReturnUrlContext {\n request: Request;\n currentPath: string;\n protection: ProtectionLevel;\n}\n\nexport interface RouteProtectionResolverContext<TIdentityContext = DefaultRouteIdentityContext> {\n request: Request;\n user: UserInfo;\n currentPath: string;\n protection: ProtectionLevel;\n getIdentityContext: (forceRefresh?: boolean) => Promise<TIdentityContext | null>;\n getCurrentTenant: () => Promise<string | null>;\n initializeTenant: () => Promise<{ headers: Headers; tenantId: string } | null>;\n}\n\nexport interface RouteProtectionErrorHandlerContext<TIdentityContext = DefaultRouteIdentityContext>\n extends RouteProtectionResolverContext<TIdentityContext> {\n error: unknown;\n}\n\n/**\n * Route protection configuration\n */\nexport interface RouteProtectionConfig<TIdentityContext = DefaultRouteIdentityContext> {\n identityChecker?: IdentityContextChecker<TIdentityContext>;\n tenantChecker?: TenantChecker;\n subscriptionChecker?: SubscriptionChecker;\n resolveRoute?: (\n context: RouteProtectionResolverContext<TIdentityContext>\n ) => Promise<RouteProtectionRedirect | null | void> | RouteProtectionRedirect | null | void;\n handleRouteError?: (\n context: RouteProtectionErrorHandlerContext<TIdentityContext>\n ) => Promise<RouteProtectionRedirect | null | void> | RouteProtectionRedirect | null | void;\n getLoginReturnUrl?: (context: LoginReturnUrlContext) => string;\n createAutomaticLogoutRequest?: (\n context: AutomaticLogoutRequestContext\n ) => Promise<Request> | Request;\n}\n\nlet protectionConfig: RouteProtectionConfig<any> = {};\n\n/**\n * Configure route protection with identity/tenant/subscription checkers\n */\nexport function configureRouteProtection<TIdentityContext = DefaultRouteIdentityContext>(\n config: RouteProtectionConfig<TIdentityContext>\n): void {\n protectionConfig = config;\n}\n\n/**\n * Reset route protection to its built-in defaults.\n */\nexport function resetRouteProtectionConfig(): void {\n protectionConfig = {};\n}\n\nfunction createRedirectFromConfig(redirect: RouteProtectionRedirect): Response {\n return createRedirectResponse(redirect.location, {\n headers: redirect.headers,\n status: redirect.status,\n });\n}\n\nfunction getLoginReturnUrl(\n request: Request,\n currentPath: string,\n protection: ProtectionLevel\n): string {\n if (protectionConfig.getLoginReturnUrl) {\n return protectionConfig.getLoginReturnUrl({\n request,\n currentPath,\n protection,\n });\n }\n\n switch (protection) {\n case 'onboarding-required':\n return '/onboarding';\n case 'subscription-required':\n return '/subscription';\n case 'public':\n return '/';\n case 'auth':\n default: {\n const url = new URL(request.url);\n return url.pathname === '/' ? '/' : `${url.pathname}${url.search}`;\n }\n }\n}\n\nasync function createAutomaticLogoutRequest(\n request: Request,\n reason: AutomaticLogoutRequestContext['reason']\n): Promise<Request> {\n if (protectionConfig.createAutomaticLogoutRequest) {\n return protectionConfig.createAutomaticLogoutRequest({ request, reason });\n }\n\n const logoutUrl = new URL(request.url);\n logoutUrl.pathname = '/auth/logout';\n logoutUrl.searchParams.set('client_id_only', 'true');\n\n return new Request(logoutUrl.toString(), {\n headers: request.headers,\n });\n}\n\nfunction createRouteResolverContext(\n request: Request,\n user: UserInfo,\n currentPath: string,\n protection: ProtectionLevel\n): RouteProtectionResolverContext<any> {\n let identityContextPromise: Promise<any | null> | null = null;\n let currentTenantPromise: Promise<string | null> | null = null;\n let initializeTenantPromise: Promise<{ headers: Headers; tenantId: string } | null> | null = null;\n\n return {\n request,\n user,\n currentPath,\n protection,\n getIdentityContext: async (forceRefresh = false) => {\n if (!protectionConfig.identityChecker) {\n return null;\n }\n\n if (forceRefresh) {\n return protectionConfig.identityChecker.getIdentityContext(request, user.sub, true);\n }\n\n if (!identityContextPromise) {\n identityContextPromise = protectionConfig.identityChecker.getIdentityContext(request, user.sub);\n }\n\n return identityContextPromise;\n },\n getCurrentTenant: async () => {\n if (!protectionConfig.tenantChecker) {\n return null;\n }\n\n if (!currentTenantPromise) {\n currentTenantPromise = protectionConfig.tenantChecker.getCurrentTenant(request);\n }\n\n return currentTenantPromise;\n },\n initializeTenant: async () => {\n if (!protectionConfig.tenantChecker) {\n return null;\n }\n\n if (!initializeTenantPromise) {\n initializeTenantPromise = protectionConfig.tenantChecker.initializeTenant(request);\n }\n\n return initializeTenantPromise;\n },\n };\n}\n\nasync function applyConfiguredRouteResolution(\n request: Request,\n user: UserInfo,\n currentPath: string,\n protection: ProtectionLevel\n): Promise<boolean> {\n if (!protectionConfig.resolveRoute) {\n return false;\n }\n\n const context = createRouteResolverContext(request, user, currentPath, protection);\n\n try {\n const result = await protectionConfig.resolveRoute(context);\n if (result) {\n throw createRedirectFromConfig(result);\n }\n\n return true;\n } catch (error) {\n if (error instanceof Response && error.status >= 300 && error.status < 400) {\n throw error;\n }\n\n if (!protectionConfig.handleRouteError) {\n throw error;\n }\n\n const recovery = await protectionConfig.handleRouteError({\n ...context,\n error,\n });\n\n if (recovery) {\n throw createRedirectFromConfig(recovery);\n }\n\n throw error;\n }\n}\n\n/**\n * Check user status (onboarding, subscription)\n */\nasync function checkUserStatus(\n request: Request, \n user: UserInfo, \n currentPath: string\n): Promise<Response | null> {\n if (!protectionConfig.identityChecker) {\n logger.warn('Identity checker not configured, skipping status check');\n return null;\n }\n\n try {\n const context = await protectionConfig.identityChecker.getIdentityContext(request, user.sub);\n\n logger.debug('User status check', {\n userId: user.sub,\n currentPath,\n hasAnyMembership: context.hasAnyMembership,\n isOnboarded: context.isOnboarded,\n hasSubscription: context.hasSubscription,\n });\n\n // Check if user needs onboarding\n if (!context.hasAnyMembership) {\n if (currentPath !== '/onboarding') {\n logger.info('User has no memberships, redirecting to onboarding');\n throw createRedirectResponse('/onboarding');\n }\n return null;\n }\n\n // Check subscription status\n if (context.hasAnyMembership) {\n if (currentPath === '/onboarding') {\n logger.info('User has memberships, redirecting from onboarding to subscription');\n throw createRedirectResponse('/subscription');\n }\n\n if (currentPath === '/subscription') {\n return null;\n }\n\n // Check tenant subscription\n if (protectionConfig.tenantChecker && protectionConfig.subscriptionChecker) {\n let currentTenant = await protectionConfig.tenantChecker.getCurrentTenant(request);\n\n if (!currentTenant) {\n const initResult = await protectionConfig.tenantChecker.initializeTenant(request);\n if (initResult) {\n currentTenant = initResult.tenantId;\n }\n }\n\n if (currentTenant) {\n const hasSubscription = await protectionConfig.subscriptionChecker.checkTenantSubscriptionStatus(\n request,\n currentTenant\n );\n\n if (!hasSubscription) {\n logger.info('Tenant has no subscription, redirecting to subscription');\n throw createRedirectResponse('/subscription');\n }\n } else {\n logger.info('Could not initialize tenant, redirecting to subscription');\n throw createRedirectResponse('/subscription');\n }\n }\n }\n\n return null;\n } catch (error) {\n if (error instanceof Response && error.status >= 300 && error.status < 400) {\n throw error;\n }\n\n logger.error('Error checking user status', error instanceof Error ? error : undefined);\n\n if (currentPath !== '/onboarding') {\n throw createRedirectResponse('/onboarding');\n }\n\n return null;\n }\n}\n\n/**\n * Main route protection function\n */\nexport async function protectRoute<T>(\n request: Request,\n protection: ProtectionLevel,\n loaderFn: (user?: UserInfo) => Promise<T> | T\n): Promise<T> {\n const url = new URL(request.url);\n\n switch (protection) {\n case 'public':\n try {\n await autoRefreshTokens(request);\n const user = await getUser(request);\n return loaderFn(user ?? undefined);\n } catch {\n return loaderFn(undefined);\n }\n\n case 'auth':\n try {\n await autoRefreshTokens(request);\n const user = await requireAuth(request);\n const handledByConfig = await applyConfiguredRouteResolution(request, user, url.pathname, 'auth');\n if (!handledByConfig) {\n await checkUserStatus(request, user, url.pathname);\n }\n return loaderFn(user);\n } catch (error) {\n if (error instanceof Response && error.status >= 300 && error.status < 400) {\n throw error;\n }\n throw await login(request, getLoginReturnUrl(request, url.pathname, 'auth'));\n }\n\n case 'onboarding-required':\n try {\n await autoRefreshTokens(request);\n const user = await requireAuth(request);\n\n const handledByConfig = await applyConfiguredRouteResolution(\n request,\n user,\n url.pathname,\n 'onboarding-required'\n );\n if (!handledByConfig && protectionConfig.identityChecker) {\n const context = await protectionConfig.identityChecker.getIdentityContext(request, user.sub);\n if (context.hasAnyMembership) {\n throw createRedirectResponse('/subscription');\n }\n }\n\n return loaderFn(user);\n } catch (error) {\n if (error instanceof Response && error.status >= 300 && error.status < 400) {\n throw error;\n }\n throw await login(request, getLoginReturnUrl(request, url.pathname, 'onboarding-required'));\n }\n\n case 'subscription-required':\n try {\n await autoRefreshTokens(request);\n const user = await requireAuth(request);\n\n const handledByConfig = await applyConfiguredRouteResolution(\n request,\n user,\n url.pathname,\n 'subscription-required'\n );\n if (!handledByConfig && protectionConfig.identityChecker) {\n const context = await protectionConfig.identityChecker.getIdentityContext(request, user.sub);\n if (!context.hasAnyMembership) {\n throw createRedirectResponse('/onboarding');\n }\n }\n\n // Initialize tenant if needed\n if (protectionConfig.tenantChecker) {\n const currentTenant = await protectionConfig.tenantChecker.getCurrentTenant(request);\n if (!currentTenant) {\n await protectionConfig.tenantChecker.initializeTenant(request);\n }\n }\n\n return loaderFn(user);\n } catch (error) {\n if (error instanceof Response && error.status >= 300 && error.status < 400) {\n throw error;\n }\n throw await login(request, getLoginReturnUrl(request, url.pathname, 'subscription-required'));\n }\n\n default:\n throw new Error(`Unknown protection level: ${protection}`);\n }\n}\n\n// ============================================================================\n// Convenience Functions\n// ============================================================================\n\n/**\n * Public route (optional auth)\n */\nexport const publicRoute = <T>(\n request: Request,\n loaderFn: (user?: UserInfo) => Promise<T> | T\n) => protectRoute(request, 'public', loaderFn);\n\n/**\n * Auth-protected route\n */\nexport const authRoute = <T>(\n request: Request,\n loaderFn: (user: UserInfo) => Promise<T> | T\n) => protectRoute(request, 'auth', loaderFn as (user?: UserInfo) => Promise<T> | T);\n\n/**\n * Onboarding route\n */\nexport const onboardingRoute = <T>(\n request: Request,\n loaderFn: (user: UserInfo) => Promise<T> | T\n) => protectRoute(request, 'onboarding-required', loaderFn as (user?: UserInfo) => Promise<T> | T);\n\n/**\n * Subscription route\n */\nexport const subscriptionRoute = <T>(\n request: Request,\n loaderFn: (user: UserInfo) => Promise<T> | T\n) => protectRoute(request, 'subscription-required', loaderFn as (user?: UserInfo) => Promise<T> | T);\n","/**\n * Token Refresh Server Module\n */\n\nimport { refreshTokens } from './auth.server';\nimport { getAuthSession } from './redis-session-storage.server';\nimport type { TokenRefreshResult } from './types';\nimport { logger } from '../logging';\n\n/**\n * Attempt to refresh the access token\n */\nexport async function attemptTokenRefresh(request: Request): Promise<TokenRefreshResult> {\n try {\n logger.info('Attempting token refresh due to 401 response');\n\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.refreshToken) {\n logger.warn('No refresh token available in session');\n return {\n success: false,\n error: 'No refresh token available',\n shouldLogout: true,\n };\n }\n\n const refreshResult = await refreshTokens(request, sessionData.refreshToken);\n\n if (!refreshResult.success) {\n logger.error('Token refresh failed', undefined, {\n error: refreshResult.error,\n shouldLogout: refreshResult.shouldLogout,\n });\n\n return {\n success: false,\n error: refreshResult.error,\n shouldLogout: refreshResult.shouldLogout,\n };\n }\n\n const tokens = refreshResult.tokens;\n if (!tokens || !tokens.access_token) {\n logger.error('Token refresh succeeded but no access token returned');\n return {\n success: false,\n error: 'No access token in refresh response',\n shouldLogout: true,\n };\n }\n\n logger.info('Token refresh successful');\n\n return {\n success: true,\n newAccessToken: tokens.access_token,\n };\n } catch (error) {\n logger.error('Token refresh attempt failed', error instanceof Error ? error : undefined);\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n shouldLogout: true,\n };\n }\n}\n\n/**\n * Check if a response indicates token expiration\n */\nexport function isTokenExpiredResponse(response: Response): boolean {\n return response.status === 401;\n}\n\n/**\n * Check if token refresh should be attempted\n */\nexport function shouldAttemptTokenRefresh(response: Response, hasAuth: boolean): boolean {\n return hasAuth && isTokenExpiredResponse(response);\n}\n"],"mappings":"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"}
|