dauth-md-node 4.0.0 → 5.0.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/README.md +161 -134
- package/dist/{chunk-4A7BR4EM.mjs → chunk-RKH7YKIR.mjs} +1 -1
- package/dist/chunk-RKH7YKIR.mjs.map +1 -0
- package/dist/index.d.mts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +56 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53 -15
- package/dist/index.mjs.map +1 -1
- package/dist/router.js +13 -59
- package/dist/router.js.map +1 -1
- package/dist/router.mjs +14 -60
- package/dist/router.mjs.map +1 -1
- package/package.json +15 -1
- package/src/api/dauth.api.ts +80 -0
- package/src/csrf.ts +7 -8
- package/src/index.ts +34 -52
- package/src/router.ts +11 -77
- package/src/session.ts +1 -4
- package/dist/chunk-4A7BR4EM.mjs.map +0 -1
package/dist/router.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/router.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure =\n opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ??\n (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(\n await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt)\n );\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(\n `${config.dauthBasePath}/app/${config.domainName}/logout`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n ).catch(() => {});\n }\n clearCookies(res, config);\n return res\n .status(200)\n .send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n // GET /profile-redirect — CSRF required (generates profile code)\n router.get(\n '/profile-redirect',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/profile-code`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n }\n );\n if (!response.ok) {\n return res.status(response.status).send({\n status: 'profile-code-error',\n message: 'Could not generate profile code',\n });\n }\n const data = (await response.json()) as { code: string };\n\n // Build redirect URL to dauth frontend\n const dauthFrontendUrl = opts.dauthUrl\n ? opts.dauthUrl.replace(/\\/+$/, '')\n : process.env.DAUTH_URL\n ? process.env.DAUTH_URL.replace(/\\/+$/, '')\n : process.env.NODE_ENV === 'development'\n ? 'http://localhost:5185'\n : 'https://dauth.ovh';\n\n return res.status(200).send({\n redirectUrl: `${dauthFrontendUrl}/${config.domainName}/update-user?code=${data.code}`,\n });\n }\n );\n\n return router;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(\n req: Request,\n csrfCookieName: string\n): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as\n | string\n | undefined;\n if (!headerToken || !cookieToken) return false;\n return headerToken === cookieToken;\n}\n"],"mappings":"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","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../src/router.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure = opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ?? (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt));\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(`${config.dauthBasePath}/app/${config.domainName}/logout`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }).catch(() => {});\n }\n clearCookies(res, config);\n return res.status(200).send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n return router;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(req: Request, csrfCookieName: string): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as string | undefined;\n if (!headerToken || !cookieToken) return false;\n if (headerToken.length !== cookieToken.length) return false;\n return crypto.timingSafeEqual(\n Buffer.from(headerToken),\n Buffer.from(cookieToken)\n );\n}\n"],"mappings":"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","names":["result"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dauth-md-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Express middleware for JWT verification and session management against the Dauth authentication service",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"author": "David T. Pizarro Frick",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"authentication",
|
|
9
|
+
"passwordless",
|
|
10
|
+
"magic-link",
|
|
11
|
+
"passkey",
|
|
12
|
+
"webauthn",
|
|
13
|
+
"multi-tenant",
|
|
14
|
+
"jwt",
|
|
15
|
+
"express",
|
|
16
|
+
"middleware",
|
|
17
|
+
"dauth"
|
|
18
|
+
],
|
|
6
19
|
"main": "dist/index.js",
|
|
7
20
|
"module": "dist/index.mjs",
|
|
8
21
|
"typings": "dist/index.d.ts",
|
|
@@ -29,6 +42,7 @@
|
|
|
29
42
|
"start": "tsup --watch",
|
|
30
43
|
"build": "tsup",
|
|
31
44
|
"test": "vitest run",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
32
46
|
"typecheck": "tsc --noEmit",
|
|
33
47
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
34
48
|
"prepare": "tsup",
|
package/src/api/dauth.api.ts
CHANGED
|
@@ -5,6 +5,24 @@ interface GetUserResponse {
|
|
|
5
5
|
data: { user?: any; message?: string };
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface TenantUser {
|
|
9
|
+
_id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
lastname: string;
|
|
12
|
+
email: string;
|
|
13
|
+
avatar: { id: string; url: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TenantUserResponse {
|
|
17
|
+
response: { status: number };
|
|
18
|
+
data: { status?: string; data?: TenantUser | null; message?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BatchUsersResponse {
|
|
22
|
+
response: { status: number };
|
|
23
|
+
data: { status?: string; data?: TenantUser[]; message?: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
export async function getUser(
|
|
9
27
|
token: string,
|
|
10
28
|
domainName: string
|
|
@@ -22,3 +40,65 @@ export async function getUser(
|
|
|
22
40
|
const data = (await response.json()) as GetUserResponse['data'];
|
|
23
41
|
return { response: { status: response.status }, data };
|
|
24
42
|
}
|
|
43
|
+
|
|
44
|
+
export async function searchUserByEmail(
|
|
45
|
+
token: string,
|
|
46
|
+
domainName: string,
|
|
47
|
+
email: string
|
|
48
|
+
): Promise<TenantUserResponse> {
|
|
49
|
+
const params = new URLSearchParams({ email });
|
|
50
|
+
const response = await fetch(
|
|
51
|
+
`${getServerBasePath()}/app/${domainName}/users/search?${params}`,
|
|
52
|
+
{
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: token,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const data =
|
|
61
|
+
(await response.json()) as TenantUserResponse['data'];
|
|
62
|
+
return { response: { status: response.status }, data };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getUserById(
|
|
66
|
+
token: string,
|
|
67
|
+
domainName: string,
|
|
68
|
+
userId: string
|
|
69
|
+
): Promise<TenantUserResponse> {
|
|
70
|
+
const response = await fetch(
|
|
71
|
+
`${getServerBasePath()}/app/${domainName}/users/${userId}`,
|
|
72
|
+
{
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: token,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
const data =
|
|
81
|
+
(await response.json()) as TenantUserResponse['data'];
|
|
82
|
+
return { response: { status: response.status }, data };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function batchGetUsers(
|
|
86
|
+
token: string,
|
|
87
|
+
domainName: string,
|
|
88
|
+
userIds: string[]
|
|
89
|
+
): Promise<BatchUsersResponse> {
|
|
90
|
+
const response = await fetch(
|
|
91
|
+
`${getServerBasePath()}/app/${domainName}/users/batch`,
|
|
92
|
+
{
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: token,
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({ userIds }),
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
const data =
|
|
102
|
+
(await response.json()) as BatchUsersResponse['data'];
|
|
103
|
+
return { response: { status: response.status }, data };
|
|
104
|
+
}
|
package/src/csrf.ts
CHANGED
|
@@ -5,14 +5,13 @@ export function generateCsrfToken(): string {
|
|
|
5
5
|
return crypto.randomBytes(32).toString('hex');
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function verifyCsrf(
|
|
9
|
-
req: Request,
|
|
10
|
-
csrfCookieName: string
|
|
11
|
-
): boolean {
|
|
8
|
+
export function verifyCsrf(req: Request, csrfCookieName: string): boolean {
|
|
12
9
|
const headerToken = req.headers['x-csrf-token'] as string | undefined;
|
|
13
|
-
const cookieToken = req.cookies?.[csrfCookieName] as
|
|
14
|
-
| string
|
|
15
|
-
| undefined;
|
|
10
|
+
const cookieToken = req.cookies?.[csrfCookieName] as string | undefined;
|
|
16
11
|
if (!headerToken || !cookieToken) return false;
|
|
17
|
-
|
|
12
|
+
if (headerToken.length !== cookieToken.length) return false;
|
|
13
|
+
return crypto.timingSafeEqual(
|
|
14
|
+
Buffer.from(headerToken),
|
|
15
|
+
Buffer.from(cookieToken)
|
|
16
|
+
);
|
|
18
17
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Request, NextFunction, Response as ExpressResponse } from 'express';
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
3
|
import { getUser } from './api/dauth.api';
|
|
4
|
+
export {
|
|
5
|
+
searchUserByEmail,
|
|
6
|
+
getUserById,
|
|
7
|
+
batchGetUsers,
|
|
8
|
+
} from './api/dauth.api';
|
|
9
|
+
export type { TenantUser } from './api/dauth.api';
|
|
4
10
|
import { UserCache } from './cache';
|
|
5
11
|
import type { CacheOptions } from './cache';
|
|
6
12
|
import { deriveEncryptionKey, decryptSessionWithKeys } from './session';
|
|
@@ -33,6 +39,7 @@ export interface IDauthUser {
|
|
|
33
39
|
|
|
34
40
|
export interface IRequestDauth extends Request {
|
|
35
41
|
user: IDauthUser;
|
|
42
|
+
dauthToken: string;
|
|
36
43
|
files: {
|
|
37
44
|
image: { path: string };
|
|
38
45
|
avatar: { path: string };
|
|
@@ -64,12 +71,7 @@ interface TCustomResponse extends ExpressResponse {
|
|
|
64
71
|
export { UserCache };
|
|
65
72
|
export type { CacheOptions };
|
|
66
73
|
|
|
67
|
-
export const dauth = ({
|
|
68
|
-
domainName,
|
|
69
|
-
tsk,
|
|
70
|
-
cache,
|
|
71
|
-
session,
|
|
72
|
-
}: DauthOptions) => {
|
|
74
|
+
export const dauth = ({ domainName, tsk, cache, session }: DauthOptions) => {
|
|
73
75
|
const userCache = cache ? new UserCache(cache) : null;
|
|
74
76
|
|
|
75
77
|
// Lazy-init encryption keys for session cookie mode
|
|
@@ -78,15 +80,10 @@ export const dauth = ({
|
|
|
78
80
|
if (!keysPromise) {
|
|
79
81
|
keysPromise = (async () => {
|
|
80
82
|
const keys: Buffer[] = [];
|
|
81
|
-
keys.push(
|
|
82
|
-
await deriveEncryptionKey(tsk, session?.sessionSalt)
|
|
83
|
-
);
|
|
83
|
+
keys.push(await deriveEncryptionKey(tsk, session?.sessionSalt));
|
|
84
84
|
if (session?.previousTsk) {
|
|
85
85
|
keys.push(
|
|
86
|
-
await deriveEncryptionKey(
|
|
87
|
-
session.previousTsk,
|
|
88
|
-
session.sessionSalt
|
|
89
|
-
)
|
|
86
|
+
await deriveEncryptionKey(session.previousTsk, session.sessionSalt)
|
|
90
87
|
);
|
|
91
88
|
}
|
|
92
89
|
return keys;
|
|
@@ -97,8 +94,7 @@ export const dauth = ({
|
|
|
97
94
|
|
|
98
95
|
function getSessionCookieName(): string {
|
|
99
96
|
if (session?.cookieName) return session.cookieName;
|
|
100
|
-
const secure =
|
|
101
|
-
session?.secure ?? process.env.NODE_ENV !== 'development';
|
|
97
|
+
const secure = session?.secure ?? process.env.NODE_ENV !== 'development';
|
|
102
98
|
return secure ? '__Host-dauth-session' : 'dauth-session';
|
|
103
99
|
}
|
|
104
100
|
|
|
@@ -114,33 +110,27 @@ export const dauth = ({
|
|
|
114
110
|
const cookieName = getSessionCookieName();
|
|
115
111
|
const cookie = req.cookies?.[cookieName];
|
|
116
112
|
if (!cookie) {
|
|
117
|
-
return res
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
message: 'Not authenticated',
|
|
122
|
-
});
|
|
113
|
+
return res.status(401).send({
|
|
114
|
+
status: 'no-session',
|
|
115
|
+
message: 'Not authenticated',
|
|
116
|
+
});
|
|
123
117
|
}
|
|
124
118
|
const keys = await getEncKeys();
|
|
125
119
|
const payload = decryptSessionWithKeys(cookie, keys);
|
|
126
120
|
if (!payload) {
|
|
127
|
-
return res
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
message: 'Invalid session',
|
|
132
|
-
});
|
|
121
|
+
return res.status(401).send({
|
|
122
|
+
status: 'session-invalid',
|
|
123
|
+
message: 'Invalid session',
|
|
124
|
+
});
|
|
133
125
|
}
|
|
134
126
|
token = payload.accessToken;
|
|
135
127
|
} else {
|
|
136
128
|
// Authorization header mode
|
|
137
129
|
if (!req.headers.authorization) {
|
|
138
|
-
return res
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
message: 'Token not found',
|
|
143
|
-
});
|
|
130
|
+
return res.status(403).send({
|
|
131
|
+
status: 'token-not-found',
|
|
132
|
+
message: 'Token not found',
|
|
133
|
+
});
|
|
144
134
|
}
|
|
145
135
|
token = req.headers.authorization.replace(/['"]+/g, '');
|
|
146
136
|
}
|
|
@@ -148,8 +138,7 @@ export const dauth = ({
|
|
|
148
138
|
try {
|
|
149
139
|
jwt.verify(token, tsk);
|
|
150
140
|
} catch (error) {
|
|
151
|
-
const message =
|
|
152
|
-
error instanceof Error ? error.message : 'Token invalid';
|
|
141
|
+
const message = error instanceof Error ? error.message : 'Token invalid';
|
|
153
142
|
|
|
154
143
|
if (message === 'jwt expired') {
|
|
155
144
|
return res
|
|
@@ -159,15 +148,15 @@ export const dauth = ({
|
|
|
159
148
|
if (message === 'invalid signature') {
|
|
160
149
|
return res.status(401).send({
|
|
161
150
|
status: 'tsk-not-invalid',
|
|
162
|
-
message:
|
|
163
|
-
'The TSK variable in the backend middleware is not valid',
|
|
151
|
+
message: 'The TSK variable in the backend middleware is not valid',
|
|
164
152
|
});
|
|
165
153
|
}
|
|
166
|
-
return res
|
|
167
|
-
.status(401)
|
|
168
|
-
.send({ status: 'token-invalid', message });
|
|
154
|
+
return res.status(401).send({ status: 'token-invalid', message });
|
|
169
155
|
}
|
|
170
156
|
|
|
157
|
+
// Expose the verified access token for downstream API calls
|
|
158
|
+
req.dauthToken = token;
|
|
159
|
+
|
|
171
160
|
if (userCache) {
|
|
172
161
|
const cachedUser = userCache.get(token);
|
|
173
162
|
if (cachedUser) {
|
|
@@ -182,15 +171,13 @@ export const dauth = ({
|
|
|
182
171
|
if (getUserFetch.response.status === 404) {
|
|
183
172
|
return res.status(404).send({
|
|
184
173
|
status: 'user-not-found',
|
|
185
|
-
message:
|
|
186
|
-
getUserFetch.data.message ?? 'User does not exist',
|
|
174
|
+
message: getUserFetch.data.message ?? 'User does not exist',
|
|
187
175
|
});
|
|
188
176
|
}
|
|
189
177
|
if (getUserFetch.response.status === 500) {
|
|
190
178
|
return res.status(500).send({
|
|
191
179
|
status: 'error',
|
|
192
|
-
message:
|
|
193
|
-
getUserFetch.data.message ?? 'Dauth server error',
|
|
180
|
+
message: getUserFetch.data.message ?? 'Dauth server error',
|
|
194
181
|
});
|
|
195
182
|
}
|
|
196
183
|
if (getUserFetch.response.status === 200) {
|
|
@@ -202,17 +189,12 @@ export const dauth = ({
|
|
|
202
189
|
}
|
|
203
190
|
return res.status(501).send({
|
|
204
191
|
status: 'request-error',
|
|
205
|
-
message:
|
|
206
|
-
getUserFetch.data.message ?? 'Dauth server error',
|
|
192
|
+
message: getUserFetch.data.message ?? 'Dauth server error',
|
|
207
193
|
});
|
|
208
194
|
} catch (error) {
|
|
209
195
|
const message =
|
|
210
|
-
error instanceof Error
|
|
211
|
-
|
|
212
|
-
: 'Dauth server error';
|
|
213
|
-
return res
|
|
214
|
-
.status(500)
|
|
215
|
-
.send({ status: 'server-error', message });
|
|
196
|
+
error instanceof Error ? error.message : 'Dauth server error';
|
|
197
|
+
return res.status(500).send({ status: 'server-error', message });
|
|
216
198
|
}
|
|
217
199
|
};
|
|
218
200
|
};
|
package/src/router.ts
CHANGED
|
@@ -45,11 +45,9 @@ function clearStaleLocks(): void {
|
|
|
45
45
|
async function resolveConfig(
|
|
46
46
|
opts: DauthRouterOptions
|
|
47
47
|
): Promise<ResolvedConfig> {
|
|
48
|
-
const secure =
|
|
49
|
-
opts.secure ?? process.env.NODE_ENV !== 'development';
|
|
48
|
+
const secure = opts.secure ?? process.env.NODE_ENV !== 'development';
|
|
50
49
|
const cookieName =
|
|
51
|
-
opts.cookieName ??
|
|
52
|
-
(secure ? '__Host-dauth-session' : 'dauth-session');
|
|
50
|
+
opts.cookieName ?? (secure ? '__Host-dauth-session' : 'dauth-session');
|
|
53
51
|
const csrfCookieName =
|
|
54
52
|
opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');
|
|
55
53
|
const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;
|
|
@@ -57,9 +55,7 @@ async function resolveConfig(
|
|
|
57
55
|
const keys: Buffer[] = [];
|
|
58
56
|
keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));
|
|
59
57
|
if (opts.previousTsk) {
|
|
60
|
-
keys.push(
|
|
61
|
-
await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt)
|
|
62
|
-
);
|
|
58
|
+
keys.push(await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt));
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
let dauthBasePath: string;
|
|
@@ -308,21 +304,16 @@ export function dauthRouter(opts: DauthRouterOptions): Router {
|
|
|
308
304
|
const session = readSession(req, config);
|
|
309
305
|
if (session) {
|
|
310
306
|
// Revoke refresh token server-to-server (fire-and-forget)
|
|
311
|
-
fetch(
|
|
312
|
-
|
|
313
|
-
{
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}),
|
|
319
|
-
}
|
|
320
|
-
).catch(() => {});
|
|
307
|
+
fetch(`${config.dauthBasePath}/app/${config.domainName}/logout`, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: { 'Content-Type': 'application/json' },
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
refreshToken: session.refreshToken,
|
|
312
|
+
}),
|
|
313
|
+
}).catch(() => {});
|
|
321
314
|
}
|
|
322
315
|
clearCookies(res, config);
|
|
323
|
-
return res
|
|
324
|
-
.status(200)
|
|
325
|
-
.send({ status: 'success', message: 'Logged out' });
|
|
316
|
+
return res.status(200).send({ status: 'success', message: 'Logged out' });
|
|
326
317
|
});
|
|
327
318
|
|
|
328
319
|
// PATCH /user — CSRF required
|
|
@@ -383,62 +374,5 @@ export function dauthRouter(opts: DauthRouterOptions): Router {
|
|
|
383
374
|
return res.status(response.status).send(data);
|
|
384
375
|
});
|
|
385
376
|
|
|
386
|
-
// GET /profile-redirect — CSRF required (generates profile code)
|
|
387
|
-
router.get(
|
|
388
|
-
'/profile-redirect',
|
|
389
|
-
async (req: Request, res: Response) => {
|
|
390
|
-
const config = await getConfig();
|
|
391
|
-
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
392
|
-
return res.status(403).send({
|
|
393
|
-
status: 'csrf-invalid',
|
|
394
|
-
message: 'CSRF token invalid',
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
const session = readSession(req, config);
|
|
398
|
-
if (!session) {
|
|
399
|
-
return res.status(401).send({
|
|
400
|
-
status: 'no-session',
|
|
401
|
-
message: 'Not authenticated',
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
const refreshed = await maybeRefreshTokens(
|
|
405
|
-
session,
|
|
406
|
-
config,
|
|
407
|
-
res
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
const response = await fetch(
|
|
411
|
-
`${config.dauthBasePath}/app/${config.domainName}/profile-code`,
|
|
412
|
-
{
|
|
413
|
-
method: 'POST',
|
|
414
|
-
headers: {
|
|
415
|
-
'Content-Type': 'application/json',
|
|
416
|
-
Authorization: refreshed.accessToken,
|
|
417
|
-
},
|
|
418
|
-
}
|
|
419
|
-
);
|
|
420
|
-
if (!response.ok) {
|
|
421
|
-
return res.status(response.status).send({
|
|
422
|
-
status: 'profile-code-error',
|
|
423
|
-
message: 'Could not generate profile code',
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
const data = (await response.json()) as { code: string };
|
|
427
|
-
|
|
428
|
-
// Build redirect URL to dauth frontend
|
|
429
|
-
const dauthFrontendUrl = opts.dauthUrl
|
|
430
|
-
? opts.dauthUrl.replace(/\/+$/, '')
|
|
431
|
-
: process.env.DAUTH_URL
|
|
432
|
-
? process.env.DAUTH_URL.replace(/\/+$/, '')
|
|
433
|
-
: process.env.NODE_ENV === 'development'
|
|
434
|
-
? 'http://localhost:5185'
|
|
435
|
-
: 'https://dauth.ovh';
|
|
436
|
-
|
|
437
|
-
return res.status(200).send({
|
|
438
|
-
redirectUrl: `${dauthFrontendUrl}/${config.domainName}/update-user?code=${data.code}`,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
);
|
|
442
|
-
|
|
443
377
|
return router;
|
|
444
378
|
}
|
package/src/session.ts
CHANGED
|
@@ -31,10 +31,7 @@ export async function deriveEncryptionKey(
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function encryptSession(
|
|
35
|
-
payload: SessionPayload,
|
|
36
|
-
key: Buffer
|
|
37
|
-
): string {
|
|
34
|
+
export function encryptSession(payload: SessionPayload, key: Buffer): string {
|
|
38
35
|
const nonce = crypto.randomBytes(12);
|
|
39
36
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
40
37
|
const plaintext = JSON.stringify(payload);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/utils/config.ts","../src/session.ts"],"sourcesContent":["export const apiVersion = 'v1';\nexport const serverDomain = 'dauth.ovh';\n\nexport function getServerBasePath(): string {\n if (process.env.DAUTH_URL) {\n const base = process.env.DAUTH_URL.replace(/\\/+$/, '');\n return `${base}/api/${apiVersion}`;\n }\n\n const isLocalhost = process.env.NODE_ENV === 'development';\n const serverPort = 4012;\n const serverLocalUrl = `http://localhost:${serverPort}/api/${apiVersion}`;\n const serverProdUrl = `https://${serverDomain}/api/${apiVersion}`;\n return isLocalhost ? serverLocalUrl : serverProdUrl;\n}\n","import crypto from 'crypto';\n\nexport interface SessionPayload {\n accessToken: string;\n refreshToken: string;\n}\n\nconst INFO = 'dauth-cookie-enc-v1';\nconst DEFAULT_SALT = Buffer.from(\n 'a3f8c1d7e9b24f6081c5d3a7e2f49b0653d81f7a2e94c0b6d8f3a5e1c7b09d42',\n 'hex'\n);\n\nexport async function deriveEncryptionKey(\n tsk: string,\n salt?: string\n): Promise<Buffer> {\n const saltBuf = salt ? Buffer.from(salt, 'hex') : DEFAULT_SALT;\n return new Promise((resolve, reject) => {\n crypto.hkdf(\n 'sha256',\n Buffer.from(tsk),\n saltBuf,\n INFO,\n 32,\n (err, derivedKey) => {\n if (err) return reject(err);\n resolve(Buffer.from(derivedKey));\n }\n );\n });\n}\n\nexport function encryptSession(\n payload: SessionPayload,\n key: Buffer\n): string {\n const nonce = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);\n const plaintext = JSON.stringify(payload);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n // Format: base64(nonce + ciphertext + authTag)\n return Buffer.concat([nonce, encrypted, authTag]).toString('base64');\n}\n\nexport function decryptSession(\n ciphertext: string,\n key: Buffer\n): SessionPayload | null {\n try {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < 12 + 16) return null; // nonce(12) + authTag(16) minimum\n const nonce = buf.subarray(0, 12);\n const authTag = buf.subarray(buf.length - 16);\n const encrypted = buf.subarray(12, buf.length - 16);\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n return JSON.parse(decrypted.toString('utf8')) as SessionPayload;\n } catch {\n return null;\n }\n}\n\nexport function decryptSessionWithKeys(\n ciphertext: string,\n keys: Buffer[]\n): SessionPayload | null {\n for (const key of keys) {\n const result = decryptSession(ciphertext, key);\n if (result) return result;\n }\n return null;\n}\n"],"mappings":";AAAO,IAAM,aAAa;AACnB,IAAM,eAAe;AAErB,SAAS,oBAA4B;AAC1C,MAAI,QAAQ,IAAI,WAAW;AACzB,UAAM,OAAO,QAAQ,IAAI,UAAU,QAAQ,QAAQ,EAAE;AACrD,WAAO,GAAG,IAAI,QAAQ,UAAU;AAAA,EAClC;AAEA,QAAM,cAAc,QAAQ,IAAI,aAAa;AAC7C,QAAM,aAAa;AACnB,QAAM,iBAAiB,oBAAoB,UAAU,QAAQ,UAAU;AACvE,QAAM,gBAAgB,WAAW,YAAY,QAAQ,UAAU;AAC/D,SAAO,cAAc,iBAAiB;AACxC;;;ACdA,OAAO,YAAY;AAOnB,IAAM,OAAO;AACb,IAAM,eAAe,OAAO;AAAA,EAC1B;AAAA,EACA;AACF;AAEA,eAAsB,oBACpB,KACA,MACiB;AACjB,QAAM,UAAU,OAAO,OAAO,KAAK,MAAM,KAAK,IAAI;AAClD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAO;AAAA,MACL;AAAA,MACA,OAAO,KAAK,GAAG;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA,CAAC,KAAK,eAAe;AACnB,YAAI,IAAK,QAAO,OAAO,GAAG;AAC1B,gBAAQ,OAAO,KAAK,UAAU,CAAC;AAAA,MACjC;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEO,SAAS,eACd,SACA,KACQ;AACR,QAAM,QAAQ,OAAO,YAAY,EAAE;AACnC,QAAM,SAAS,OAAO,eAAe,eAAe,KAAK,KAAK;AAC9D,QAAM,YAAY,KAAK,UAAU,OAAO;AACxC,QAAM,YAAY,OAAO,OAAO;AAAA,IAC9B,OAAO,OAAO,WAAW,MAAM;AAAA,IAC/B,OAAO,MAAM;AAAA,EACf,CAAC;AACD,QAAM,UAAU,OAAO,WAAW;AAElC,SAAO,OAAO,OAAO,CAAC,OAAO,WAAW,OAAO,CAAC,EAAE,SAAS,QAAQ;AACrE;AAEO,SAAS,eACd,YACA,KACuB;AACvB,MAAI;AACF,UAAM,MAAM,OAAO,KAAK,YAAY,QAAQ;AAC5C,QAAI,IAAI,SAAS,KAAK,GAAI,QAAO;AACjC,UAAM,QAAQ,IAAI,SAAS,GAAG,EAAE;AAChC,UAAM,UAAU,IAAI,SAAS,IAAI,SAAS,EAAE;AAC5C,UAAM,YAAY,IAAI,SAAS,IAAI,IAAI,SAAS,EAAE;AAClD,UAAM,WAAW,OAAO,iBAAiB,eAAe,KAAK,KAAK;AAClE,aAAS,WAAW,OAAO;AAC3B,UAAM,YAAY,OAAO,OAAO;AAAA,MAC9B,SAAS,OAAO,SAAS;AAAA,MACzB,SAAS,MAAM;AAAA,IACjB,CAAC;AACD,WAAO,KAAK,MAAM,UAAU,SAAS,MAAM,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,uBACd,YACA,MACuB;AACvB,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,eAAe,YAAY,GAAG;AAC7C,QAAI,OAAQ,QAAO;AAAA,EACrB;AACA,SAAO;AACT;","names":[]}
|