(
sessionToken: string,
opts?: ValidateSessionTokenOptions
)
| 423 | } |
| 424 | |
| 425 | async validateSessionToken( |
| 426 | sessionToken: string, |
| 427 | opts?: ValidateSessionTokenOptions |
| 428 | ): Promise<{ sessionId: string } | null> { |
| 429 | const normalizedToken = normalizeOptionalString(sessionToken); |
| 430 | if (!normalizedToken) { |
| 431 | return null; |
| 432 | } |
| 433 | |
| 434 | const tokenHash = hashSessionToken(normalizedToken); |
| 435 | const now = Date.now(); |
| 436 | const normalizedUserAgent = normalizeOptionalString(opts?.userAgent); |
| 437 | const normalizedIpAddress = normalizeIpAddress(opts?.ipAddress); |
| 438 | |
| 439 | await using _lock = await this.sessionsMutex.acquire(); |
| 440 | |
| 441 | const data = await this.loadPersistedSessionsLocked(); |
| 442 | const session = data.sessions.find((candidate) => candidate.tokenHash === tokenHash); |
| 443 | if (!session) { |
| 444 | return null; |
| 445 | } |
| 446 | |
| 447 | const sessionAgeMs = now - session.createdAtMs; |
| 448 | const sessionExpired = |
| 449 | !Number.isFinite(session.createdAtMs) || |
| 450 | !Number.isFinite(sessionAgeMs) || |
| 451 | sessionAgeMs >= SERVER_AUTH_SESSION_MAX_AGE_MS; |
| 452 | if (sessionExpired) { |
| 453 | data.sessions = data.sessions.filter((candidate) => candidate.id !== session.id); |
| 454 | |
| 455 | try { |
| 456 | await this.savePersistedSessionsLocked(data); |
| 457 | } catch (error) { |
| 458 | // Validation should still fail closed for expired sessions even if pruning |
| 459 | // persistence fails. |
| 460 | log.warn("Failed to prune expired server auth session", { |
| 461 | sessionId: session.id, |
| 462 | error: getErrorMessage(error), |
| 463 | }); |
| 464 | } |
| 465 | |
| 466 | return null; |
| 467 | } |
| 468 | |
| 469 | const shouldPersistLastUsed = |
| 470 | !Number.isFinite(session.lastUsedAtMs) || |
| 471 | now - session.lastUsedAtMs >= SESSION_LAST_USED_PERSIST_INTERVAL_MS; |
| 472 | |
| 473 | const shouldPersistUserAgent = normalizedUserAgent && session.userAgent !== normalizedUserAgent; |
| 474 | const shouldPersistIpAddress = normalizedIpAddress && session.ipAddress !== normalizedIpAddress; |
| 475 | |
| 476 | if (shouldPersistLastUsed || shouldPersistUserAgent || shouldPersistIpAddress) { |
| 477 | session.lastUsedAtMs = now; |
| 478 | if (normalizedUserAgent) { |
| 479 | session.userAgent = normalizedUserAgent; |
| 480 | session.label = buildSessionLabel(normalizedUserAgent); |
| 481 | } |
| 482 | if (normalizedIpAddress) { |
no test coverage detected