import { AuthSessionId, AuthStandardClientScopes, AuthEnvironmentScopes, type AuthClientMetadata, type AuthClientSession, type AuthEnvironmentScope, type ClientSurface, type ServerAuthSessionMethod,} from "@t3tools/contracts";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as Option from "effect/Option"; import * as ServerConfig from "../config.ts";import * as ServerEnvironment from "../environment/ServerEnvironment.ts";import * as AuthSessions from "../persistence/AuthSessions.ts";import * as ServerSecretStore from "./ServerSecretStore.ts";import { base64UrlDecodeUtf8, base64UrlEncode, resolveLegacySessionCookieName, resolveSessionCookieName, signPayload, timingSafeEqualBase64Url,} from "./utils.ts"; export interface IssuedSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: ServerAuthSessionMethod; readonly client: AuthClientMetadata; readonly expiresAt: DateTime.DateTime; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly proofKeyThumbprint?: string;} export interface VerifiedSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: ServerAuthSessionMethod; readonly client: AuthClientMetadata; readonly expiresAt?: DateTime.DateTime; readonly subject: string; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly proofKeyThumbprint?: string;} export type SessionCredentialChange = | { readonly type: "clientUpserted"; readonly clientSession: AuthClientSession; } | { readonly type: "clientRemoved"; readonly sessionId: AuthSessionId; }; export class MalformedSessionTokenError extends Schema.TaggedErrorClass<MalformedSessionTokenError>()( "MalformedSessionTokenError", {},) { override get message(): string { return "Malformed session token."; }} export class InvalidSessionTokenSignatureError extends Schema.TaggedErrorClass<InvalidSessionTokenSignatureError>()( "InvalidSessionTokenSignatureError", {},) { override get message(): string { return "Invalid session token signature."; }} export class InvalidSessionTokenPayloadError extends Schema.TaggedErrorClass<InvalidSessionTokenPayloadError>()( "InvalidSessionTokenPayloadError", { cause: Schema.Defect(), },) { override get message(): string { return "Invalid session token payload."; }} export class SessionTokenExpiredError extends Schema.TaggedErrorClass<SessionTokenExpiredError>()( "SessionTokenExpiredError", { sessionId: AuthSessionId, expiresAt: Schema.DateTimeUtc, observedAt: Schema.DateTimeUtc, },) { override get message(): string { return "Session token expired."; }} export class UnknownSessionTokenError extends Schema.TaggedErrorClass<UnknownSessionTokenError>()( "UnknownSessionTokenError", { sessionId: AuthSessionId, },) { override get message(): string { return "Unknown session token."; }} export class SessionTokenRevokedError extends Schema.TaggedErrorClass<SessionTokenRevokedError>()( "SessionTokenRevokedError", { sessionId: AuthSessionId, revokedAt: Schema.DateTimeUtc, },) { override get message(): string { return "Session token revoked."; }} export class InvalidSessionExpirationClaimError extends Schema.TaggedErrorClass<InvalidSessionExpirationClaimError>()( "InvalidSessionExpirationClaimError", { sessionId: AuthSessionId, expirationClaim: Schema.Number, },) { override get message(): string { return "Invalid `exp` claim"; }} export class MalformedWebSocketTokenError extends Schema.TaggedErrorClass<MalformedWebSocketTokenError>()( "MalformedWebSocketTokenError", {},) { override get message(): string { return "Malformed websocket token."; }} export class InvalidWebSocketTokenSignatureError extends Schema.TaggedErrorClass<InvalidWebSocketTokenSignatureError>()( "InvalidWebSocketTokenSignatureError", {},) { override get message(): string { return "Invalid websocket token signature."; }} export class InvalidWebSocketTokenPayloadError extends Schema.TaggedErrorClass<InvalidWebSocketTokenPayloadError>()( "InvalidWebSocketTokenPayloadError", { cause: Schema.Defect(), },) { override get message(): string { return "Invalid websocket token payload."; }} export class WebSocketTokenExpiredError extends Schema.TaggedErrorClass<WebSocketTokenExpiredError>()( "WebSocketTokenExpiredError", { sessionId: AuthSessionId, expiresAt: Schema.DateTimeUtc, observedAt: Schema.DateTimeUtc, },) { override get message(): string { return "Websocket token expired."; }} export class UnknownWebSocketSessionError extends Schema.TaggedErrorClass<UnknownWebSocketSessionError>()( "UnknownWebSocketSessionError", { sessionId: AuthSessionId, },) { override get message(): string { return "Unknown websocket session."; }} export class WebSocketSessionExpiredError extends Schema.TaggedErrorClass<WebSocketSessionExpiredError>()( "WebSocketSessionExpiredError", { sessionId: AuthSessionId, expiresAt: Schema.DateTimeUtc, observedAt: Schema.DateTimeUtc, },) { override get message(): string { return "Websocket session expired."; }} export class WebSocketSessionRevokedError extends Schema.TaggedErrorClass<WebSocketSessionRevokedError>()( "WebSocketSessionRevokedError", { sessionId: AuthSessionId, revokedAt: Schema.DateTimeUtc, },) { override get message(): string { return "Websocket session revoked."; }} export const SessionCredentialInvalidError = Schema.Union([ MalformedSessionTokenError, InvalidSessionTokenSignatureError, InvalidSessionTokenPayloadError, SessionTokenExpiredError, UnknownSessionTokenError, SessionTokenRevokedError, InvalidSessionExpirationClaimError, MalformedWebSocketTokenError, InvalidWebSocketTokenSignatureError, InvalidWebSocketTokenPayloadError, WebSocketTokenExpiredError, UnknownWebSocketSessionError, WebSocketSessionExpiredError, WebSocketSessionRevokedError,]);export type SessionCredentialInvalidError = typeof SessionCredentialInvalidError.Type;export const isSessionCredentialInvalidError = Schema.is(SessionCredentialInvalidError); const sessionCredentialInternalErrorContext = { cause: Schema.Defect(),}; export class SessionClaimsEncodingError extends Schema.TaggedErrorClass<SessionClaimsEncodingError>()( "SessionClaimsEncodingError", { sessionId: AuthSessionId, operation: Schema.Literals(["encode_session_claims", "encode_websocket_claims"]), ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to encode claims"; }} export class SessionCredentialIssueError extends Schema.TaggedErrorClass<SessionCredentialIssueError>()( "SessionCredentialIssueError", { sessionId: Schema.optional(AuthSessionId), ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to issue session credential."; }} export class SessionCredentialVerificationError extends Schema.TaggedErrorClass<SessionCredentialVerificationError>()( "SessionCredentialVerificationError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to verify session credential."; }} export class WebSocketTokenIssueError extends Schema.TaggedErrorClass<WebSocketTokenIssueError>()( "WebSocketTokenIssueError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to issue websocket token."; }} export class WebSocketTokenVerificationError extends Schema.TaggedErrorClass<WebSocketTokenVerificationError>()( "WebSocketTokenVerificationError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to verify websocket token."; }} export class ActiveSessionsListError extends Schema.TaggedErrorClass<ActiveSessionsListError>()( "ActiveSessionsListError", { ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to list active sessions."; }} export class SessionRevocationError extends Schema.TaggedErrorClass<SessionRevocationError>()( "SessionRevocationError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to revoke session."; }} export class OtherSessionsRevocationError extends Schema.TaggedErrorClass<OtherSessionsRevocationError>()( "OtherSessionsRevocationError", { currentSessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, },) { override get message(): string { return "Failed to revoke other sessions."; }} export const SessionCredentialInternalError = Schema.Union([ SessionClaimsEncodingError, SessionCredentialIssueError, SessionCredentialVerificationError, WebSocketTokenIssueError, WebSocketTokenVerificationError, ActiveSessionsListError, SessionRevocationError, OtherSessionsRevocationError,]);export type SessionCredentialInternalError = typeof SessionCredentialInternalError.Type;export const isSessionCredentialInternalError = Schema.is(SessionCredentialInternalError); export const SessionCredentialError = Schema.Union([ SessionCredentialInvalidError, SessionCredentialInternalError,]);export type SessionCredentialError = typeof SessionCredentialError.Type;export const isSessionCredentialError = Schema.is(SessionCredentialError); export class SessionStore extends Context.Service< SessionStore, { readonly cookieName: string; readonly legacyCookieName: string | undefined; readonly issue: (input?: { readonly ttl?: Duration.Duration; readonly subject?: string; readonly method?: ServerAuthSessionMethod; readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; readonly client?: AuthClientMetadata; readonly proofKeyThumbprint?: string; /** * Atomically revoke active sessions with the same subject and method * before storing this session. */ readonly replaceActiveForSubjectAndMethod?: boolean; }) => Effect.Effect<IssuedSession, SessionCredentialInternalError>; readonly verify: (token: string) => Effect.Effect<VerifiedSession, SessionCredentialError>; readonly issueWebSocketToken: ( sessionId: AuthSessionId, input?: { readonly ttl?: Duration.Duration; }, ) => Effect.Effect< { readonly token: string; readonly expiresAt: DateTime.DateTime; }, SessionCredentialInternalError >; readonly verifyWebSocketToken: ( token: string, ) => Effect.Effect<VerifiedSession, SessionCredentialError>; readonly listActive: () => Effect.Effect< ReadonlyArray<AuthClientSession>, SessionCredentialInternalError >; readonly streamChanges: Stream.Stream<SessionCredentialChange>; readonly revoke: ( sessionId: AuthSessionId, ) => Effect.Effect<boolean, SessionCredentialInternalError>; readonly revokeAllExcept: ( sessionId: AuthSessionId, ) => Effect.Effect<number, SessionCredentialInternalError>; readonly markConnected: (sessionId: AuthSessionId) => Effect.Effect<void, never>; readonly markDisconnected: (sessionId: AuthSessionId) => Effect.Effect<void, never>; readonly recordClientConnection: ( sessionId: AuthSessionId, client: { readonly surface?: ClientSurface | undefined; readonly appVersion?: string | undefined; }, ) => Effect.Effect<void, never>; }>()("t3/auth/SessionStore") {} const SIGNING_SECRET_NAME = "server-signing-key";const DEFAULT_SESSION_TTL = Duration.days(30);const DEFAULT_WEBSOCKET_TOKEN_TTL = Duration.minutes(5); const SessionClaims = Schema.Struct({ v: Schema.Literal(1), kind: Schema.Literal("session"), sid: AuthSessionId, sub: Schema.String, scopes: AuthEnvironmentScopes, method: Schema.Literals(["browser-session-cookie", "bearer-access-token", "dpop-access-token"]), jkt: Schema.optionalKey(Schema.String), iat: Schema.Number, exp: Schema.Number,});type SessionClaims = typeof SessionClaims.Type; const WebSocketClaims = Schema.Struct({ v: Schema.Literal(1), kind: Schema.Literal("websocket"), sid: AuthSessionId, iat: Schema.Number, exp: Schema.Number,});type WebSocketClaims = typeof WebSocketClaims.Type; const decodeSessionClaims = Schema.decodeUnknownEffect(Schema.fromJsonString(SessionClaims));const decodeWebSocketClaims = Schema.decodeUnknownEffect(Schema.fromJsonString(WebSocketClaims)); function createDefaultClientMetadata(): AuthClientMetadata { return { deviceType: "unknown", };} function toClientMetadata(record: { readonly label: string | null; readonly ipAddress: string | null; readonly userAgent: string | null; readonly deviceType: AuthClientMetadata["deviceType"]; readonly os: string | null; readonly browser: string | null;}): AuthClientMetadata { return { ...(record.label ? { label: record.label } : {}), ...(record.ipAddress ? { ipAddress: record.ipAddress } : {}), ...(record.userAgent ? { userAgent: record.userAgent } : {}), deviceType: record.deviceType, ...(record.os ? { os: record.os } : {}), ...(record.browser ? { browser: record.browser } : {}), };} function toAuthClientSession(input: Omit<AuthClientSession, "current">): AuthClientSession { return { ...input, current: false, };} export const make = Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const serverConfig = yield* ServerConfig.ServerConfig; const serverEnvironment = yield* ServerEnvironment.ServerEnvironmentIdentity; const secretStore = yield* ServerSecretStore.ServerSecretStore; const authSessions = yield* AuthSessions.AuthSessionRepository; const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32); const connectedSessionsRef = yield* Ref.make(new Map<AuthSessionId, number>()); const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>(); const cookieInput = { mode: serverConfig.mode, port: serverConfig.port, host: serverConfig.host, instanceKey: serverConfig.stateDir, environmentId: yield* serverEnvironment.getEnvironmentId, development: serverConfig.devUrl !== undefined, } as const; const cookieName = resolveSessionCookieName(cookieInput); const legacyCookieName = resolveLegacySessionCookieName(cookieInput); const emitUpsert = (clientSession: AuthClientSession) => PubSub.publish(changesPubSub, { type: "clientUpserted",Lines 1–500 of 986. Load more
import { AuthSessionId, AuthStandardClientScopes, AuthEnvironmentScopes, type AuthClientMetadata, type AuthClientSession, type AuthEnvironmentScope, type ClientSurface, type ServerAuthSessionMethod, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as Option from "effect/Option"; import * as ServerConfig from "../config.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as AuthSessions from "../persistence/AuthSessions.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; import { base64UrlDecodeUtf8, base64UrlEncode, resolveLegacySessionCookieName, resolveSessionCookieName, signPayload, timingSafeEqualBase64Url, } from "./utils.ts"; export interface IssuedSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: ServerAuthSessionMethod; readonly client: AuthClientMetadata; readonly expiresAt: DateTime.DateTime; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly proofKeyThumbprint?: string; } export interface VerifiedSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: ServerAuthSessionMethod; readonly client: AuthClientMetadata; readonly expiresAt?: DateTime.DateTime; readonly subject: string; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly proofKeyThumbprint?: string; } export type SessionCredentialChange = | { readonly type: "clientUpserted"; readonly clientSession: AuthClientSession; } | { readonly type: "clientRemoved"; readonly sessionId: AuthSessionId; }; export class MalformedSessionTokenError extends Schema.TaggedErrorClass<MalformedSessionTokenError>()( "MalformedSessionTokenError", {}, ) { override get message(): string { return "Malformed session token."; } } export class InvalidSessionTokenSignatureError extends Schema.TaggedErrorClass<InvalidSessionTokenSignatureError>()( "InvalidSessionTokenSignatureError", {}, ) { override get message(): string { return "Invalid session token signature."; } } export class InvalidSessionTokenPayloadError extends Schema.TaggedErrorClass<InvalidSessionTokenPayloadError>()( "InvalidSessionTokenPayloadError", { cause: Schema.Defect(), }, ) { override get message(): string { return "Invalid session token payload."; } } export class SessionTokenExpiredError extends Schema.TaggedErrorClass<SessionTokenExpiredError>()( "SessionTokenExpiredError", { sessionId: AuthSessionId, expiresAt: Schema.DateTimeUtc, observedAt: Schema.DateTimeUtc, }, ) { override get message(): string { return "Session token expired."; } } export class UnknownSessionTokenError extends Schema.TaggedErrorClass<UnknownSessionTokenError>()( "UnknownSessionTokenError", { sessionId: AuthSessionId, }, ) { override get message(): string { return "Unknown session token."; } } export class SessionTokenRevokedError extends Schema.TaggedErrorClass<SessionTokenRevokedError>()( "SessionTokenRevokedError", { sessionId: AuthSessionId, revokedAt: Schema.DateTimeUtc, }, ) { override get message(): string { return "Session token revoked."; } } export class InvalidSessionExpirationClaimError extends Schema.TaggedErrorClass<InvalidSessionExpirationClaimError>()( "InvalidSessionExpirationClaimError", { sessionId: AuthSessionId, expirationClaim: Schema.Number, }, ) { override get message(): string { return "Invalid `exp` claim"; } } export class MalformedWebSocketTokenError extends Schema.TaggedErrorClass<MalformedWebSocketTokenError>()( "MalformedWebSocketTokenError", {}, ) { override get message(): string { return "Malformed websocket token."; } } export class InvalidWebSocketTokenSignatureError extends Schema.TaggedErrorClass<InvalidWebSocketTokenSignatureError>()( "InvalidWebSocketTokenSignatureError", {}, ) { override get message(): string { return "Invalid websocket token signature."; } } export class InvalidWebSocketTokenPayloadError extends Schema.TaggedErrorClass<InvalidWebSocketTokenPayloadError>()( "InvalidWebSocketTokenPayloadError", { cause: Schema.Defect(), }, ) { override get message(): string { return "Invalid websocket token payload."; } } export class WebSocketTokenExpiredError extends Schema.TaggedErrorClass<WebSocketTokenExpiredError>()( "WebSocketTokenExpiredError", { sessionId: AuthSessionId, expiresAt: Schema.DateTimeUtc, observedAt: Schema.DateTimeUtc, }, ) { override get message(): string { return "Websocket token expired."; } } export class UnknownWebSocketSessionError extends Schema.TaggedErrorClass<UnknownWebSocketSessionError>()( "UnknownWebSocketSessionError", { sessionId: AuthSessionId, }, ) { override get message(): string { return "Unknown websocket session."; } } export class WebSocketSessionExpiredError extends Schema.TaggedErrorClass<WebSocketSessionExpiredError>()( "WebSocketSessionExpiredError", { sessionId: AuthSessionId, expiresAt: Schema.DateTimeUtc, observedAt: Schema.DateTimeUtc, }, ) { override get message(): string { return "Websocket session expired."; } } export class WebSocketSessionRevokedError extends Schema.TaggedErrorClass<WebSocketSessionRevokedError>()( "WebSocketSessionRevokedError", { sessionId: AuthSessionId, revokedAt: Schema.DateTimeUtc, }, ) { override get message(): string { return "Websocket session revoked."; } } export const SessionCredentialInvalidError = Schema.Union([ MalformedSessionTokenError, InvalidSessionTokenSignatureError, InvalidSessionTokenPayloadError, SessionTokenExpiredError, UnknownSessionTokenError, SessionTokenRevokedError, InvalidSessionExpirationClaimError, MalformedWebSocketTokenError, InvalidWebSocketTokenSignatureError, InvalidWebSocketTokenPayloadError, WebSocketTokenExpiredError, UnknownWebSocketSessionError, WebSocketSessionExpiredError, WebSocketSessionRevokedError, ]); export type SessionCredentialInvalidError = typeof SessionCredentialInvalidError.Type; export const isSessionCredentialInvalidError = Schema.is(SessionCredentialInvalidError); const sessionCredentialInternalErrorContext = { cause: Schema.Defect(), }; export class SessionClaimsEncodingError extends Schema.TaggedErrorClass<SessionClaimsEncodingError>()( "SessionClaimsEncodingError", { sessionId: AuthSessionId, operation: Schema.Literals(["encode_session_claims", "encode_websocket_claims"]), ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to encode claims"; } } export class SessionCredentialIssueError extends Schema.TaggedErrorClass<SessionCredentialIssueError>()( "SessionCredentialIssueError", { sessionId: Schema.optional(AuthSessionId), ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to issue session credential."; } } export class SessionCredentialVerificationError extends Schema.TaggedErrorClass<SessionCredentialVerificationError>()( "SessionCredentialVerificationError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to verify session credential."; } } export class WebSocketTokenIssueError extends Schema.TaggedErrorClass<WebSocketTokenIssueError>()( "WebSocketTokenIssueError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to issue websocket token."; } } export class WebSocketTokenVerificationError extends Schema.TaggedErrorClass<WebSocketTokenVerificationError>()( "WebSocketTokenVerificationError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to verify websocket token."; } } export class ActiveSessionsListError extends Schema.TaggedErrorClass<ActiveSessionsListError>()( "ActiveSessionsListError", { ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to list active sessions."; } } export class SessionRevocationError extends Schema.TaggedErrorClass<SessionRevocationError>()( "SessionRevocationError", { sessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to revoke session."; } } export class OtherSessionsRevocationError extends Schema.TaggedErrorClass<OtherSessionsRevocationError>()( "OtherSessionsRevocationError", { currentSessionId: AuthSessionId, ...sessionCredentialInternalErrorContext, }, ) { override get message(): string { return "Failed to revoke other sessions."; } } export const SessionCredentialInternalError = Schema.Union([ SessionClaimsEncodingError, SessionCredentialIssueError, SessionCredentialVerificationError, WebSocketTokenIssueError, WebSocketTokenVerificationError, ActiveSessionsListError, SessionRevocationError, OtherSessionsRevocationError, ]); export type SessionCredentialInternalError = typeof SessionCredentialInternalError.Type; export const isSessionCredentialInternalError = Schema.is(SessionCredentialInternalError); export const SessionCredentialError = Schema.Union([ SessionCredentialInvalidError, SessionCredentialInternalError, ]); export type SessionCredentialError = typeof SessionCredentialError.Type; export const isSessionCredentialError = Schema.is(SessionCredentialError); export class SessionStore extends Context.Service< SessionStore, { readonly cookieName: string; readonly legacyCookieName: string | undefined; readonly issue: (input?: { readonly ttl?: Duration.Duration; readonly subject?: string; readonly method?: ServerAuthSessionMethod; readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; readonly client?: AuthClientMetadata; readonly proofKeyThumbprint?: string; /** * Atomically revoke active sessions with the same subject and method * before storing this session. */ readonly replaceActiveForSubjectAndMethod?: boolean; }) => Effect.Effect<IssuedSession, SessionCredentialInternalError>; readonly verify: (token: string) => Effect.Effect<VerifiedSession, SessionCredentialError>; readonly issueWebSocketToken: ( sessionId: AuthSessionId, input?: { readonly ttl?: Duration.Duration; }, ) => Effect.Effect< { readonly token: string; readonly expiresAt: DateTime.DateTime; }, SessionCredentialInternalError >; readonly verifyWebSocketToken: ( token: string, ) => Effect.Effect<VerifiedSession, SessionCredentialError>; readonly listActive: () => Effect.Effect< ReadonlyArray<AuthClientSession>, SessionCredentialInternalError >; readonly streamChanges: Stream.Stream<SessionCredentialChange>; readonly revoke: ( sessionId: AuthSessionId, ) => Effect.Effect<boolean, SessionCredentialInternalError>; readonly revokeAllExcept: ( sessionId: AuthSessionId, ) => Effect.Effect<number, SessionCredentialInternalError>; readonly markConnected: (sessionId: AuthSessionId) => Effect.Effect<void, never>; readonly markDisconnected: (sessionId: AuthSessionId) => Effect.Effect<void, never>; readonly recordClientConnection: ( sessionId: AuthSessionId, client: { readonly surface?: ClientSurface | undefined; readonly appVersion?: string | undefined; }, ) => Effect.Effect<void, never>; } >()("t3/auth/SessionStore") {} const SIGNING_SECRET_NAME = "server-signing-key"; const DEFAULT_SESSION_TTL = Duration.days(30); const DEFAULT_WEBSOCKET_TOKEN_TTL = Duration.minutes(5); const SessionClaims = Schema.Struct({ v: Schema.Literal(1), kind: Schema.Literal("session"), sid: AuthSessionId, sub: Schema.String, scopes: AuthEnvironmentScopes, method: Schema.Literals(["browser-session-cookie", "bearer-access-token", "dpop-access-token"]), jkt: Schema.optionalKey(Schema.String), iat: Schema.Number, exp: Schema.Number, }); type SessionClaims = typeof SessionClaims.Type; const WebSocketClaims = Schema.Struct({ v: Schema.Literal(1), kind: Schema.Literal("websocket"), sid: AuthSessionId, iat: Schema.Number, exp: Schema.Number, }); type WebSocketClaims = typeof WebSocketClaims.Type; const decodeSessionClaims = Schema.decodeUnknownEffect(Schema.fromJsonString(SessionClaims)); const decodeWebSocketClaims = Schema.decodeUnknownEffect(Schema.fromJsonString(WebSocketClaims)); function createDefaultClientMetadata(): AuthClientMetadata { return { deviceType: "unknown", }; } function toClientMetadata(record: { readonly label: string | null; readonly ipAddress: string | null; readonly userAgent: string | null; readonly deviceType: AuthClientMetadata["deviceType"]; readonly os: string | null; readonly browser: string | null; }): AuthClientMetadata { return { ...(record.label ? { label: record.label } : {}), ...(record.ipAddress ? { ipAddress: record.ipAddress } : {}), ...(record.userAgent ? { userAgent: record.userAgent } : {}), deviceType: record.deviceType, ...(record.os ? { os: record.os } : {}), ...(record.browser ? { browser: record.browser } : {}), }; } function toAuthClientSession(input: Omit<AuthClientSession, "current">): AuthClientSession { return { ...input, current: false, }; } export const make = Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const serverConfig = yield* ServerConfig.ServerConfig; const serverEnvironment = yield* ServerEnvironment.ServerEnvironmentIdentity; const secretStore = yield* ServerSecretStore.ServerSecretStore; const authSessions = yield* AuthSessions.AuthSessionRepository; const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32); const connectedSessionsRef = yield* Ref.make(new Map<AuthSessionId, number>()); const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>(); const cookieInput = { mode: serverConfig.mode, port: serverConfig.port, host: serverConfig.host, instanceKey: serverConfig.stateDir, environmentId: yield* serverEnvironment.getEnvironmentId, development: serverConfig.devUrl !== undefined, } as const; const cookieName = resolveSessionCookieName(cookieInput); const legacyCookieName = resolveLegacySessionCookieName(cookieInput); const emitUpsert = (clientSession: AuthClientSession) => PubSub.publish(changesPubSub, { type: "clientUpserted",