@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,250 @@
|
|
|
1
|
+
import { logger } from "./logging-_h2mi3L9.js";
|
|
2
|
+
import Redis from "ioredis";
|
|
3
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
//#region src/auth/redis-session-storage.server.ts
|
|
6
|
+
let redis = null;
|
|
7
|
+
function getRedis() {
|
|
8
|
+
if (!redis) {
|
|
9
|
+
redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
|
|
10
|
+
redis.on("error", (err) => {
|
|
11
|
+
logger.error("Redis Client Error", err);
|
|
12
|
+
});
|
|
13
|
+
redis.on("connect", () => {
|
|
14
|
+
logger.info("Redis connected successfully");
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return redis;
|
|
18
|
+
}
|
|
19
|
+
const KEY_PREFIX = process.env.REDIS_KEY_PREFIX || "";
|
|
20
|
+
const REDIS_KEYS = {
|
|
21
|
+
oauthState: (stateId) => `${KEY_PREFIX}oauth:state:${stateId}`,
|
|
22
|
+
session: (sessionId) => `${KEY_PREFIX}session:${sessionId}`
|
|
23
|
+
};
|
|
24
|
+
const OAUTH_STATE_TTL = 10 * 60;
|
|
25
|
+
const SESSION_TTL = 60 * 60 * 24 * 7;
|
|
26
|
+
const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || "__session_id";
|
|
27
|
+
const SESSION_SECRET = process.env.SESSION_SECRET || "default-secret-change-in-production";
|
|
28
|
+
const SESSION_COOKIE_SECURE = process.env.NODE_ENV === "production";
|
|
29
|
+
const SESSION_COOKIE_PATH = "/";
|
|
30
|
+
const SESSION_COOKIE_SAME_SITE = "Lax";
|
|
31
|
+
const AUTH_ERROR_COOKIE_PREFIX = SESSION_COOKIE_NAME.replace("_session", "").replace("__", "");
|
|
32
|
+
function parseCookieHeader(cookieHeader) {
|
|
33
|
+
if (!cookieHeader) return {};
|
|
34
|
+
return cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).reduce((cookies, part) => {
|
|
35
|
+
const separatorIndex = part.indexOf("=");
|
|
36
|
+
if (separatorIndex <= 0) return cookies;
|
|
37
|
+
const name = part.slice(0, separatorIndex).trim();
|
|
38
|
+
const value = part.slice(separatorIndex + 1).trim();
|
|
39
|
+
if (!name) return cookies;
|
|
40
|
+
cookies[name] = value;
|
|
41
|
+
return cookies;
|
|
42
|
+
}, {});
|
|
43
|
+
}
|
|
44
|
+
function createCookieSignature(value, secret) {
|
|
45
|
+
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
46
|
+
}
|
|
47
|
+
function safelyCompareValues(left, right) {
|
|
48
|
+
const leftBuffer = Buffer.from(left);
|
|
49
|
+
const rightBuffer = Buffer.from(right);
|
|
50
|
+
if (leftBuffer.length !== rightBuffer.length) return false;
|
|
51
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
52
|
+
}
|
|
53
|
+
function signSessionId(sessionId) {
|
|
54
|
+
const signature = createCookieSignature(sessionId, SESSION_SECRET);
|
|
55
|
+
return `${sessionId}.${signature}`;
|
|
56
|
+
}
|
|
57
|
+
function unsignSessionId(cookieValue) {
|
|
58
|
+
if (!cookieValue) return null;
|
|
59
|
+
let decodedValue;
|
|
60
|
+
try {
|
|
61
|
+
decodedValue = decodeURIComponent(cookieValue);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const separatorIndex = decodedValue.lastIndexOf(".");
|
|
66
|
+
if (separatorIndex <= 0 || separatorIndex === decodedValue.length - 1) return null;
|
|
67
|
+
const sessionId = decodedValue.slice(0, separatorIndex);
|
|
68
|
+
const signature = decodedValue.slice(separatorIndex + 1);
|
|
69
|
+
const expectedSignature = createCookieSignature(sessionId, SESSION_SECRET);
|
|
70
|
+
return safelyCompareValues(signature, expectedSignature) ? sessionId : null;
|
|
71
|
+
}
|
|
72
|
+
function serializeSessionCookieValue(sessionId, options) {
|
|
73
|
+
const signedValue = encodeURIComponent(signSessionId(sessionId));
|
|
74
|
+
const segments = [
|
|
75
|
+
`${SESSION_COOKIE_NAME}=${signedValue}`,
|
|
76
|
+
`Max-Age=${options.maxAge}`,
|
|
77
|
+
`Path=${SESSION_COOKIE_PATH}`,
|
|
78
|
+
`SameSite=${SESSION_COOKIE_SAME_SITE}`,
|
|
79
|
+
"HttpOnly"
|
|
80
|
+
];
|
|
81
|
+
if (SESSION_COOKIE_SECURE) segments.push("Secure");
|
|
82
|
+
return segments.join("; ");
|
|
83
|
+
}
|
|
84
|
+
function serializeExpiredSessionCookie() {
|
|
85
|
+
const segments = [
|
|
86
|
+
`${SESSION_COOKIE_NAME}=`,
|
|
87
|
+
"Max-Age=0",
|
|
88
|
+
`Path=${SESSION_COOKIE_PATH}`,
|
|
89
|
+
`SameSite=${SESSION_COOKIE_SAME_SITE}`,
|
|
90
|
+
"HttpOnly"
|
|
91
|
+
];
|
|
92
|
+
if (SESSION_COOKIE_SECURE) segments.push("Secure");
|
|
93
|
+
return segments.join("; ");
|
|
94
|
+
}
|
|
95
|
+
async function getSessionIdFromRequest(request) {
|
|
96
|
+
const cookies = parseCookieHeader(request.headers.get("Cookie"));
|
|
97
|
+
return unsignSessionId(cookies[SESSION_COOKIE_NAME]);
|
|
98
|
+
}
|
|
99
|
+
async function readSessionData(sessionId) {
|
|
100
|
+
try {
|
|
101
|
+
const sessionData = await getRedis().get(REDIS_KEYS.session(sessionId));
|
|
102
|
+
if (!sessionData) {
|
|
103
|
+
logger.debug("Session not found in Redis", { sessionId });
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
await getRedis().expire(REDIS_KEYS.session(sessionId), SESSION_TTL);
|
|
107
|
+
return JSON.parse(sessionData);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error("Failed to read session from Redis", error instanceof Error ? error : void 0);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function writeSessionData(sessionId, data) {
|
|
114
|
+
try {
|
|
115
|
+
const sessionData = JSON.stringify(data);
|
|
116
|
+
await getRedis().setex(REDIS_KEYS.session(sessionId), SESSION_TTL, sessionData);
|
|
117
|
+
logger.debug("Updated Redis session", { sessionId });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger.error("Failed to update session in Redis", error instanceof Error ? error : void 0);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function deleteSessionData(sessionId) {
|
|
124
|
+
try {
|
|
125
|
+
await getRedis().del(REDIS_KEYS.session(sessionId));
|
|
126
|
+
logger.debug("Deleted Redis session", { sessionId });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger.error("Failed to delete session from Redis", error instanceof Error ? error : void 0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function mergeSessionData(existingData, updates) {
|
|
132
|
+
const nextData = { ...existingData };
|
|
133
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
134
|
+
if (value === void 0) {
|
|
135
|
+
delete nextData[key];
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
nextData[key] = value;
|
|
139
|
+
});
|
|
140
|
+
return nextData;
|
|
141
|
+
}
|
|
142
|
+
async function createAuthSession(request, sessionData) {
|
|
143
|
+
const currentSessionId = await getSessionIdFromRequest(request);
|
|
144
|
+
const currentSessionData = currentSessionId ? await readSessionData(currentSessionId) ?? {} : {};
|
|
145
|
+
const nextSessionId = currentSessionId ?? crypto.randomUUID();
|
|
146
|
+
const nextSessionData = mergeSessionData(currentSessionData, sessionData);
|
|
147
|
+
logger.debug("Creating auth session", { dataKeys: Object.keys(sessionData) });
|
|
148
|
+
await writeSessionData(nextSessionId, nextSessionData);
|
|
149
|
+
return new Headers({ "Set-Cookie": serializeSessionCookieValue(nextSessionId, { maxAge: SESSION_TTL }) });
|
|
150
|
+
}
|
|
151
|
+
async function getAuthSession(request) {
|
|
152
|
+
const sessionId = await getSessionIdFromRequest(request);
|
|
153
|
+
if (!sessionId) return {};
|
|
154
|
+
const session = await readSessionData(sessionId);
|
|
155
|
+
if (!session) return {};
|
|
156
|
+
return {
|
|
157
|
+
userId: session.userId,
|
|
158
|
+
accessToken: session.accessToken,
|
|
159
|
+
refreshToken: session.refreshToken,
|
|
160
|
+
idToken: session.idToken,
|
|
161
|
+
expiresAt: session.expiresAt,
|
|
162
|
+
user: session.user,
|
|
163
|
+
lastActivity: session.lastActivity
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function updateAuthSession(request, updates) {
|
|
167
|
+
const currentSessionId = await getSessionIdFromRequest(request);
|
|
168
|
+
const currentSessionData = currentSessionId ? await readSessionData(currentSessionId) ?? {} : {};
|
|
169
|
+
const nextSessionId = currentSessionId ?? crypto.randomUUID();
|
|
170
|
+
const nextSessionData = mergeSessionData(currentSessionData, updates);
|
|
171
|
+
await writeSessionData(nextSessionId, nextSessionData);
|
|
172
|
+
return new Headers({ "Set-Cookie": serializeSessionCookieValue(nextSessionId, { maxAge: SESSION_TTL }) });
|
|
173
|
+
}
|
|
174
|
+
async function destroyAuthSession(request) {
|
|
175
|
+
const sessionId = await getSessionIdFromRequest(request);
|
|
176
|
+
if (sessionId) await deleteSessionData(sessionId);
|
|
177
|
+
return new Headers({ "Set-Cookie": serializeExpiredSessionCookie() });
|
|
178
|
+
}
|
|
179
|
+
async function requireAuthSession(request) {
|
|
180
|
+
const sessionData = await getAuthSession(request);
|
|
181
|
+
if (!sessionData.userId || !sessionData.accessToken) throw new Response("Unauthorized", { status: 401 });
|
|
182
|
+
return sessionData;
|
|
183
|
+
}
|
|
184
|
+
async function isSessionValid(request) {
|
|
185
|
+
try {
|
|
186
|
+
const sessionData = await getAuthSession(request);
|
|
187
|
+
if (!sessionData.user || !sessionData.accessToken) return false;
|
|
188
|
+
if (sessionData.expiresAt && Date.now() >= sessionData.expiresAt) return false;
|
|
189
|
+
return true;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function createOAuthState(state) {
|
|
195
|
+
const stateId = crypto.randomUUID();
|
|
196
|
+
const key = REDIS_KEYS.oauthState(stateId);
|
|
197
|
+
await getRedis().setex(key, OAUTH_STATE_TTL, JSON.stringify(state));
|
|
198
|
+
logger.debug("OAuth state created", {
|
|
199
|
+
stateId,
|
|
200
|
+
ttl: OAUTH_STATE_TTL
|
|
201
|
+
});
|
|
202
|
+
return stateId;
|
|
203
|
+
}
|
|
204
|
+
async function getOAuthState(stateId) {
|
|
205
|
+
const key = REDIS_KEYS.oauthState(stateId);
|
|
206
|
+
const data = await getRedis().get(key);
|
|
207
|
+
if (!data) {
|
|
208
|
+
logger.warn("OAuth state not found or expired", { stateId });
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(data);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
logger.error("Failed to parse OAuth state", error instanceof Error ? error : void 0);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function deleteOAuthState(stateId) {
|
|
219
|
+
const key = REDIS_KEYS.oauthState(stateId);
|
|
220
|
+
await getRedis().del(key);
|
|
221
|
+
logger.debug("OAuth state deleted", { stateId });
|
|
222
|
+
}
|
|
223
|
+
async function cleanupExpiredOAuthStates() {
|
|
224
|
+
const pattern = REDIS_KEYS.oauthState("*");
|
|
225
|
+
const keys = await getRedis().keys(pattern);
|
|
226
|
+
let cleaned = 0;
|
|
227
|
+
for (const key of keys) {
|
|
228
|
+
const ttl = await getRedis().ttl(key);
|
|
229
|
+
if (ttl === -1) {
|
|
230
|
+
await getRedis().del(key);
|
|
231
|
+
cleaned++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
logger.info("OAuth state cleanup completed", {
|
|
235
|
+
cleaned,
|
|
236
|
+
total: keys.length
|
|
237
|
+
});
|
|
238
|
+
return cleaned;
|
|
239
|
+
}
|
|
240
|
+
async function closeRedisConnection() {
|
|
241
|
+
if (redis) {
|
|
242
|
+
await redis.quit();
|
|
243
|
+
redis = null;
|
|
244
|
+
logger.info("Redis connection closed");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//#endregion
|
|
249
|
+
export { AUTH_ERROR_COOKIE_PREFIX as AUTH_ERROR_COOKIE_PREFIX$1, cleanupExpiredOAuthStates as cleanupExpiredOAuthStates$1, closeRedisConnection as closeRedisConnection$1, createAuthSession as createAuthSession$1, createOAuthState as createOAuthState$1, deleteOAuthState as deleteOAuthState$1, destroyAuthSession as destroyAuthSession$1, getAuthSession as getAuthSession$1, getOAuthState as getOAuthState$1, isSessionValid as isSessionValid$1, requireAuthSession as requireAuthSession$1, updateAuthSession as updateAuthSession$1 };
|
|
250
|
+
//# sourceMappingURL=redis-session-storage.server-DhufyPjd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-session-storage.server-DhufyPjd.js","names":["redis: Redis | null","stateId: string","sessionId: string","cookieHeader: string | null","value: string","secret: string","left: string","right: string","cookieValue: string | undefined","decodedValue: string","options: SessionCookieOptions","request: Request","data: SessionData","existingData: SessionData","updates: Partial<SessionData>","sessionData: Partial<SessionData>","state: OAuthState"],"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 { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { 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};\n\nconst OAUTH_STATE_TTL = 10 * 60;\nconst SESSION_TTL = 60 * 60 * 24 * 7;\n\nconst SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || '__session_id';\nconst SESSION_SECRET = process.env.SESSION_SECRET || 'default-secret-change-in-production';\nconst SESSION_COOKIE_SECURE = process.env.NODE_ENV === 'production';\nconst SESSION_COOKIE_PATH = '/';\nconst SESSION_COOKIE_SAME_SITE = 'Lax';\n\nexport const AUTH_ERROR_COOKIE_PREFIX = SESSION_COOKIE_NAME.replace('_session', '').replace('__', '');\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_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_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\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 JSON.parse(sessionData) as 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 sessionData = JSON.stringify(data);\n await getRedis().setex(REDIS_KEYS.session(sessionId), SESSION_TTL, sessionData);\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 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\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 currentSessionData = currentSessionId\n ? (await readSessionData(currentSessionId)) ?? {}\n : {};\n const nextSessionId = currentSessionId ?? crypto.randomUUID();\n const nextSessionData = mergeSessionData(currentSessionData, sessionData);\n\n logger.debug('Creating auth session', { dataKeys: Object.keys(sessionData) });\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 userId: session.userId,\n accessToken: session.accessToken,\n refreshToken: session.refreshToken,\n idToken: session.idToken,\n expiresAt: session.expiresAt,\n user: session.user,\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, updates);\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 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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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PermissionCheckOptions, PermissionCode } from "./index-BIJOC8Fb.js";
|
|
2
|
+
|
|
3
|
+
//#region src/permissions/route-protection.server.d.ts
|
|
4
|
+
interface PermissionRouteSession {
|
|
5
|
+
user?: {
|
|
6
|
+
sub?: string;
|
|
7
|
+
email?: string | null;
|
|
8
|
+
} | null;
|
|
9
|
+
accessToken?: string | null;
|
|
10
|
+
}
|
|
11
|
+
interface PermissionRouteContext {
|
|
12
|
+
permissions: PermissionCode[];
|
|
13
|
+
currentTenant?: string | null;
|
|
14
|
+
}
|
|
15
|
+
interface ForbiddenPermissionDetails {
|
|
16
|
+
request: Request;
|
|
17
|
+
permission: PermissionCode | PermissionCode[];
|
|
18
|
+
permissionsToCheck: PermissionCode[];
|
|
19
|
+
missingPermissions?: PermissionCode[];
|
|
20
|
+
options: PermissionCheckOptions;
|
|
21
|
+
session: PermissionRouteSession;
|
|
22
|
+
context: PermissionRouteContext;
|
|
23
|
+
}
|
|
24
|
+
interface PermissionRouteProtectionConfig {
|
|
25
|
+
getSession?: (request: Request) => Promise<PermissionRouteSession>;
|
|
26
|
+
resolveContext?: (request: Request, session: PermissionRouteSession) => Promise<PermissionRouteContext>;
|
|
27
|
+
createUnauthorizedResponse?: (request: Request, session: PermissionRouteSession) => Response;
|
|
28
|
+
createForbiddenResponse?: (details: ForbiddenPermissionDetails) => Response;
|
|
29
|
+
}
|
|
30
|
+
declare function configurePermissionRouteProtection(config: PermissionRouteProtectionConfig): void;
|
|
31
|
+
declare function resetPermissionRouteProtection(): void;
|
|
32
|
+
declare function requirePermission(request: Request, permission: PermissionCode | PermissionCode[], options?: PermissionCheckOptions): Promise<void>;
|
|
33
|
+
declare function checkPermission(request: Request, permission: PermissionCode | PermissionCode[], options?: PermissionCheckOptions): Promise<boolean>;
|
|
34
|
+
declare function withPermission<TLoaderData>(permission: PermissionCode | PermissionCode[], loader: (args: any) => Promise<TLoaderData>, options?: PermissionCheckOptions): (args: any) => Promise<TLoaderData>;
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//# sourceMappingURL=route-protection.server.d.ts.map
|
|
38
|
+
|
|
39
|
+
export { ForbiddenPermissionDetails, PermissionRouteContext, PermissionRouteProtectionConfig, PermissionRouteSession, checkPermission as checkPermission$1, configurePermissionRouteProtection as configurePermissionRouteProtection$1, requirePermission as requirePermission$1, resetPermissionRouteProtection as resetPermissionRouteProtection$1, withPermission as withPermission$1 };
|
|
40
|
+
//# sourceMappingURL=server-BoeUiXSh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-BoeUiXSh.d.ts","names":[],"sources":["../src/permissions/route-protection.server.ts"],"sourcesContent":null,"mappings":";;;UAGiB,sBAAA;;IAAA,GAAA,CAAA,EAAA,MAAA;IAQA,KAAA,CAAA,EAAA,MAAA,GAAA,IAAsB;EAKtB,CAAA,GAAA,IAAA;EAA0B,WAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;AAE7B,UAPG,sBAAA,CAOH;EAAc,WAAG,EANhB,cAMgB,EAAA;EAAc,aACvB,CAAA,EAAA,MAAA,GAAA,IAAA;;AAEX,UALM,0BAAA,CAKN;EAAsB,OACtB,EALA,OAKA;EAAsB,UACtB,EALG,cAKH,GALoB,cAKpB,EAAA;EAAsB,kBAAA,EAJX,cAIW,EAAA;EAGhB,kBAAA,CAAA,EANM,cAMyB,EAAA;EAAA,OAAA,EALrC,sBAKqC;EAAA,OACvB,EALd,sBAKc;EAAO,OAAa,EAJlC,sBAIkC;;AAEhC,UAHI,+BAAA,CAGJ;EAAO,UACP,CAAA,EAAA,CAAA,OAAA,EAHY,OAGZ,EAAA,GAHwB,OAGxB,CAHgC,sBAGhC,CAAA;EAAsB,cACpB,CAAA,EAAA,CAAA,OAAA,EAFF,OAEE,EAAA,OAAA,EADF,sBACE,EAAA,GAAR,OAAQ,CAAA,sBAAA,CAAA;EAAsB,0BAA9B,CAAA,EAAA,CAAA,OAAA,EAEM,OAFN,EAAA,OAAA,EAGM,sBAHN,EAAA,GAIA,QAJA;EAAO,uBAED,CAAA,EAAA,CAAA,OAAA,EAGyB,0BAHzB,EAAA,GAGwD,QAHxD;;AAEN,iBA+CS,kCAAA,CA/CT,MAAA,EAgDG,+BAhDH,CAAA,EAAA,IAAA;AAC+B,iBAuDtB,8BAAA,CAAA,CAvDsB,EAAA,IAAA;AAA+B,iBA2E/C,iBAAA,CA3E+C,OAAA,EA4E1D,OA5E0D,EAAA,UAAA,EA6EvD,cA7EuD,GA6EtC,cA7EsC,EAAA,EAAA,OAAA,CAAA,EA8E1D,sBA9E0D,CAAA,EA+ElE,OA/EkE,CAAA,IAAA,CAAA;AAAQ,iBAuGvD,eAAA,CAvGuD,OAAA,EAwGlE,OAxGkE,EAAA,UAAA,EAyG/D,cAzG+D,GAyG9C,cAzG8C,EAAA,EAAA,OAAA,CAAA,EA0GlE,sBA1GkE,CAAA,EA2G1E,OA3G0E,CAAA,OAAA,CAAA;AA8C7D,iBAsEA,cAtEA,CAAA,WAAkC,CAAA,CAAA,UACxC,EAsEI,cAtEJ,GAsEqB,cAtEU,EAAA,EAAA,MAAA,EAAA,CAAA,IAAA,EAAA,GAAA,EAAA,GAuEhB,OAvEgB,CAuER,WAvEQ,CAAA,EAAA,OAAA,CAAA,EAwE7B,sBAxE6B,CAAA,EAAA,CAAA,IAAA,EAAA,GAAA,EAAA,GA0Eb,OA1Ea,CA0EL,WA1EK,CAAA;;;AAQzC"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { getAuthSession$1 as getAuthSession } from "./redis-session-storage.server-DhufyPjd.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tenant/tenant-cookie.server.ts
|
|
4
|
+
const DEFAULT_TENANT_COOKIE_CONFIG = {
|
|
5
|
+
name: "__active-org",
|
|
6
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
7
|
+
path: "/",
|
|
8
|
+
secure: process.env.NODE_ENV === "production",
|
|
9
|
+
httpOnly: false,
|
|
10
|
+
sameSite: "Lax"
|
|
11
|
+
};
|
|
12
|
+
let tenantCookieConfig = { ...DEFAULT_TENANT_COOKIE_CONFIG };
|
|
13
|
+
function getTenantCookieConfig() {
|
|
14
|
+
return tenantCookieConfig;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get active tenant from cookie
|
|
18
|
+
*/
|
|
19
|
+
async function getActiveTenant(request) {
|
|
20
|
+
const config = getTenantCookieConfig();
|
|
21
|
+
const cookies = request.headers.get("Cookie");
|
|
22
|
+
if (!cookies) return null;
|
|
23
|
+
const match = cookies.match(new RegExp(`${config.name}=([^;]+)`));
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
return decodeURIComponent(match[1]);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Set active tenant cookie value
|
|
29
|
+
*/
|
|
30
|
+
async function setActiveTenant(tenantId) {
|
|
31
|
+
const config = getTenantCookieConfig();
|
|
32
|
+
const secure = config.secure ? "; Secure" : "";
|
|
33
|
+
const httpOnly = config.httpOnly ? "; HttpOnly" : "";
|
|
34
|
+
return `${config.name}=${encodeURIComponent(tenantId)}; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=${config.maxAge}${secure}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Clear active tenant cookie value
|
|
38
|
+
*/
|
|
39
|
+
async function clearActiveTenant() {
|
|
40
|
+
const config = getTenantCookieConfig();
|
|
41
|
+
const secure = config.secure ? "; Secure" : "";
|
|
42
|
+
const httpOnly = config.httpOnly ? "; HttpOnly" : "";
|
|
43
|
+
return `${config.name}=; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=0${secure}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Configure tenant cookie name
|
|
47
|
+
*/
|
|
48
|
+
function configureTenantCookie(config) {
|
|
49
|
+
tenantCookieConfig = {
|
|
50
|
+
...tenantCookieConfig,
|
|
51
|
+
...config
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reset tenant cookie config back to defaults.
|
|
56
|
+
*/
|
|
57
|
+
function resetTenantCookieConfig() {
|
|
58
|
+
tenantCookieConfig = { ...DEFAULT_TENANT_COOKIE_CONFIG };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/tenant/tenant.server.ts
|
|
63
|
+
let identityContextFetcher = null;
|
|
64
|
+
/**
|
|
65
|
+
* Configure identity context fetcher
|
|
66
|
+
*/
|
|
67
|
+
function configureIdentityContextFetcher(fetcher) {
|
|
68
|
+
identityContextFetcher = fetcher;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get current active tenant from cookie
|
|
72
|
+
*/
|
|
73
|
+
async function getCurrentTenant(request) {
|
|
74
|
+
try {
|
|
75
|
+
const tenantFromCookie = await getActiveTenant(request);
|
|
76
|
+
if (tenantFromCookie) return tenantFromCookie;
|
|
77
|
+
return null;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Error getting current tenant:", error);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get tenant from identity context
|
|
85
|
+
*/
|
|
86
|
+
async function getTenantFromIdentityContext(request) {
|
|
87
|
+
try {
|
|
88
|
+
const sessionData = await getAuthSession(request);
|
|
89
|
+
if (!sessionData?.user?.sub) return null;
|
|
90
|
+
if (!identityContextFetcher) {
|
|
91
|
+
console.warn("Identity context fetcher not configured");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const context = await identityContextFetcher(request);
|
|
95
|
+
if (context?.memberships && context.memberships.length > 0) return context.memberships[0].tenantId;
|
|
96
|
+
return null;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Error getting tenant from identity context:", error);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get available tenants from identity context
|
|
104
|
+
*/
|
|
105
|
+
async function getAvailableTenants(request) {
|
|
106
|
+
try {
|
|
107
|
+
const sessionData = await getAuthSession(request);
|
|
108
|
+
if (!sessionData?.user?.sub) return [];
|
|
109
|
+
if (!identityContextFetcher) {
|
|
110
|
+
console.warn("Identity context fetcher not configured");
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const context = await identityContextFetcher(request);
|
|
114
|
+
if (context?.memberships && context.memberships.length > 0) return context.memberships.map((m) => m.tenantId);
|
|
115
|
+
return [];
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Error getting available tenants:", error);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Set current active tenant
|
|
123
|
+
*/
|
|
124
|
+
async function setCurrentTenant(request, tenantId) {
|
|
125
|
+
try {
|
|
126
|
+
const sessionData = await getAuthSession(request);
|
|
127
|
+
if (!sessionData.user) return {
|
|
128
|
+
headers: new Headers(),
|
|
129
|
+
success: false,
|
|
130
|
+
error: "No active session"
|
|
131
|
+
};
|
|
132
|
+
const cookieValue = await setActiveTenant(tenantId);
|
|
133
|
+
const headers = new Headers();
|
|
134
|
+
headers.append("Set-Cookie", cookieValue);
|
|
135
|
+
return {
|
|
136
|
+
headers,
|
|
137
|
+
success: true
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Failed to set current tenant:", error);
|
|
141
|
+
return {
|
|
142
|
+
headers: new Headers(),
|
|
143
|
+
success: false,
|
|
144
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Initialize tenant on first login
|
|
150
|
+
*/
|
|
151
|
+
async function initializeTenant(request) {
|
|
152
|
+
try {
|
|
153
|
+
const sessionData = await getAuthSession(request);
|
|
154
|
+
if (!sessionData?.user) return null;
|
|
155
|
+
const currentTenant = await getCurrentTenant(request);
|
|
156
|
+
if (currentTenant) return null;
|
|
157
|
+
const tenantId = await getTenantFromIdentityContext(request);
|
|
158
|
+
if (tenantId) {
|
|
159
|
+
const cookieValue = await setActiveTenant(tenantId);
|
|
160
|
+
const headers = new Headers();
|
|
161
|
+
headers.append("Set-Cookie", cookieValue);
|
|
162
|
+
return {
|
|
163
|
+
headers,
|
|
164
|
+
tenantId
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error("Failed to initialize tenant:", error);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Clear current tenant
|
|
175
|
+
*/
|
|
176
|
+
async function clearCurrentTenant() {
|
|
177
|
+
const cookieValue = await clearActiveTenant();
|
|
178
|
+
const headers = new Headers();
|
|
179
|
+
headers.append("Set-Cookie", cookieValue);
|
|
180
|
+
return headers;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
export { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, resetTenantCookieConfig, setActiveTenant, setCurrentTenant };
|
|
185
|
+
//# sourceMappingURL=server-C-DiVP9e.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-C-DiVP9e.js","names":["DEFAULT_TENANT_COOKIE_CONFIG: Required<TenantCookieConfig>","tenantCookieConfig: Required<TenantCookieConfig>","request: Request","tenantId: string","config: TenantCookieConfig","identityContextFetcher: IdentityContextFetcher | null","fetcher: IdentityContextFetcher","request: Request","tenantId: string"],"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: '__active-org',\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\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 const match = cookies.match(new RegExp(`${config.name}=([^;]+)`));\n if (!match) return null;\n\n return decodeURIComponent(match[1]);\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';\n\n/**\n * Identity context fetcher type (to avoid circular dependency)\n */\nexport type IdentityContextFetcher = (request: Request) => Promise<{\n memberships?: Array<{ tenantId: string }>;\n} | null>;\n\nlet identityContextFetcher: IdentityContextFetcher | null = null;\n\n/**\n * Configure identity context fetcher\n */\nexport function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void {\n identityContextFetcher = fetcher;\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\n if (context?.memberships && context.memberships.length > 0) {\n return context.memberships[0].tenantId;\n }\n\n return null;\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\n if (context?.memberships && context.memberships.length > 0) {\n return context.memberships.map((m) => m.tenantId);\n }\n\n return [];\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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"}
|