| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | import * as SqlClient from "effect/sql/SqlClient"; |
| 7 | import * as SqlSchema from "effect/sql/SqlSchema"; |
| 8 | |
| 9 | import { |
| 10 | AuthClientMetadataDeviceType, |
| 11 | AuthEnvironmentScopes, |
| 12 | AuthSessionId, |
| 13 | ClientSurface, |
| 14 | ServerAuthSessionMethod, |
| 15 | } from "@t3tools/contracts"; |
| 16 | |
| 17 | import { |
| 18 | type AuthSessionRepositoryError, |
| 19 | PersistenceDecodeError, |
| 20 | type PersistenceErrorCorrelation, |
| 21 | PersistenceSqlError, |
| 22 | } from "./Errors.ts"; |
| 23 | |
| 24 | export const AuthSessionClientMetadataRecord = Schema.Struct({ |
| 25 | label: Schema.NullOr(Schema.String), |
| 26 | ipAddress: Schema.NullOr(Schema.String), |
| 27 | userAgent: Schema.NullOr(Schema.String), |
| 28 | deviceType: AuthClientMetadataDeviceType, |
| 29 | os: Schema.NullOr(Schema.String), |
| 30 | browser: Schema.NullOr(Schema.String), |
| 31 | }); |
| 32 | export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type; |
| 33 | |
| 34 | export const AuthSessionRecord = Schema.Struct({ |
| 35 | sessionId: AuthSessionId, |
| 36 | parentSessionId: Schema.optionalKey(AuthSessionId), |
| 37 | deviceId: Schema.optionalKey(Schema.String), |
| 38 | subject: Schema.String, |
| 39 | scopes: AuthEnvironmentScopes, |
| 40 | method: ServerAuthSessionMethod, |
| 41 | client: AuthSessionClientMetadataRecord, |
| 42 | issuedAt: Schema.DateTimeUtcFromString, |
| 43 | expiresAt: Schema.DateTimeUtcFromString, |
| 44 | lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), |
| 45 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), |
| 46 | }); |
| 47 | export type AuthSessionRecord = typeof AuthSessionRecord.Type; |
| 48 | |
| 49 | export const CreateAuthSessionInput = Schema.Struct({ |
| 50 | sessionId: AuthSessionId, |
| 51 | parentSessionId: Schema.optionalKey(AuthSessionId), |
| 52 | deviceId: Schema.optionalKey(Schema.String), |
| 53 | subject: Schema.String, |
| 54 | scopes: AuthEnvironmentScopes, |
| 55 | method: ServerAuthSessionMethod, |
| 56 | client: AuthSessionClientMetadataRecord, |
| 57 | issuedAt: Schema.DateTimeUtcFromString, |
| 58 | expiresAt: Schema.DateTimeUtcFromString, |
| 59 | }); |
| 60 | export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type; |
| 61 | |
| 62 | export const CreateReplacingActiveAuthSessionInput = Schema.Struct({ |
| 63 | session: CreateAuthSessionInput, |
| 64 | revokedAt: Schema.DateTimeUtcFromString, |
| 65 | replaceSessionId: Schema.optionalKey(AuthSessionId), |
| 66 | }); |
| 67 | export type CreateReplacingActiveAuthSessionInput = |
| 68 | typeof CreateReplacingActiveAuthSessionInput.Type; |
| 69 | |
| 70 | export const GetAuthSessionByIdInput = Schema.Struct({ |
| 71 | sessionId: AuthSessionId, |
| 72 | }); |
| 73 | export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type; |
| 74 | |
| 75 | export const ListActiveAuthSessionsInput = Schema.Struct({ |
| 76 | now: Schema.DateTimeUtcFromString, |
| 77 | connectedSessionIds: Schema.optionalKey(Schema.Array(AuthSessionId)), |
| 78 | }); |
| 79 | export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type; |
| 80 | |
| 81 | export const RevokeAuthSessionInput = Schema.Struct({ |
| 82 | sessionId: AuthSessionId, |
| 83 | revokedAt: Schema.DateTimeUtcFromString, |
| 84 | }); |
| 85 | export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type; |
| 86 | |
| 87 | export const RevokeOtherAuthSessionsInput = Schema.Struct({ |
| 88 | currentSessionId: AuthSessionId, |
| 89 | revokedAt: Schema.DateTimeUtcFromString, |
| 90 | }); |
| 91 | export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type; |
| 92 | |
| 93 | export const SetAuthSessionLastConnectedAtInput = Schema.Struct({ |
| 94 | sessionId: AuthSessionId, |
| 95 | lastConnectedAt: Schema.DateTimeUtcFromString, |
| 96 | }); |
| 97 | export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type; |
| 98 | |
| 99 | export const SetAuthSessionClientConnectionInput = Schema.Struct({ |
| 100 | sessionId: AuthSessionId, |
| 101 | surface: Schema.NullOr(ClientSurface), |
| 102 | appVersion: Schema.NullOr(Schema.String), |
| 103 | }); |
| 104 | export type SetAuthSessionClientConnectionInput = typeof SetAuthSessionClientConnectionInput.Type; |
| 105 | |
| 106 | export class AuthSessionRepository extends Context.Service< |
| 107 | AuthSessionRepository, |
| 108 | { |
| 109 | readonly create: ( |
| 110 | input: CreateAuthSessionInput, |
| 111 | ) => Effect.Effect<void, AuthSessionRepositoryError>; |
| 112 | readonly createReplacingActive: ( |
| 113 | input: CreateReplacingActiveAuthSessionInput, |
| 114 | ) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>; |
| 115 | readonly createIfAbsent: ( |
| 116 | input: CreateAuthSessionInput, |
| 117 | ) => Effect.Effect<void, AuthSessionRepositoryError>; |
| 118 | readonly getById: ( |
| 119 | input: GetAuthSessionByIdInput, |
| 120 | ) => Effect.Effect<Option.Option<AuthSessionRecord>, AuthSessionRepositoryError>; |
| 121 | readonly listActive: ( |
| 122 | input: ListActiveAuthSessionsInput, |
| 123 | ) => Effect.Effect<ReadonlyArray<AuthSessionRecord>, AuthSessionRepositoryError>; |
| 124 | readonly revoke: ( |
| 125 | input: RevokeAuthSessionInput, |
| 126 | ) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>; |
| 127 | readonly revokeAllExcept: ( |
| 128 | input: RevokeOtherAuthSessionsInput, |
| 129 | ) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>; |
| 130 | readonly setLastConnectedAt: ( |
| 131 | input: SetAuthSessionLastConnectedAtInput, |
| 132 | ) => Effect.Effect<void, AuthSessionRepositoryError>; |
| 133 | readonly setClientConnection: ( |
| 134 | input: SetAuthSessionClientConnectionInput, |
| 135 | ) => Effect.Effect<void, AuthSessionRepositoryError>; |
| 136 | } |
| 137 | >()("t3/persistence/AuthSessions/AuthSessionRepository") {} |
| 138 | |
| 139 | const AuthSessionDbRow = Schema.Struct({ |
| 140 | sessionId: AuthSessionId, |
| 141 | parentSessionId: Schema.NullOr(AuthSessionId), |
| 142 | deviceId: Schema.NullOr(Schema.String), |
| 143 | subject: Schema.String, |
| 144 | scopes: Schema.fromJsonString(AuthEnvironmentScopes), |
| 145 | method: ServerAuthSessionMethod, |
| 146 | clientLabel: Schema.NullOr(Schema.String), |
| 147 | clientIpAddress: Schema.NullOr(Schema.String), |
| 148 | clientUserAgent: Schema.NullOr(Schema.String), |
| 149 | clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]), |
| 150 | clientOs: Schema.NullOr(Schema.String), |
| 151 | clientBrowser: Schema.NullOr(Schema.String), |
| 152 | issuedAt: Schema.DateTimeUtcFromString, |
| 153 | expiresAt: Schema.DateTimeUtcFromString, |
| 154 | lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), |
| 155 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), |
| 156 | }); |
| 157 | |
| 158 | const AuthSessionRawDbRow = Schema.Struct({ |
| 159 | sessionId: Schema.String, |
| 160 | parentSessionId: Schema.Unknown, |
| 161 | deviceId: Schema.Unknown, |
| 162 | subject: Schema.Unknown, |
| 163 | scopes: Schema.Unknown, |
| 164 | method: Schema.Unknown, |
| 165 | clientLabel: Schema.Unknown, |
| 166 | clientIpAddress: Schema.Unknown, |
| 167 | clientUserAgent: Schema.Unknown, |
| 168 | clientDeviceType: Schema.Unknown, |
| 169 | clientOs: Schema.Unknown, |
| 170 | clientBrowser: Schema.Unknown, |
| 171 | issuedAt: Schema.Unknown, |
| 172 | expiresAt: Schema.Unknown, |
| 173 | lastConnectedAt: Schema.Unknown, |
| 174 | revokedAt: Schema.Unknown, |
| 175 | }); |
| 176 | |
| 177 | const decodeAuthSessionDbRow = Schema.decodeUnknownEffect(AuthSessionDbRow); |
| 178 | |
| 179 | function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord { |
| 180 | return { |
| 181 | sessionId: row.sessionId, |
| 182 | ...(row.parentSessionId === null ? {} : { parentSessionId: row.parentSessionId }), |
| 183 | ...(row.deviceId === null ? {} : { deviceId: row.deviceId }), |
| 184 | subject: row.subject, |
| 185 | scopes: row.scopes, |
| 186 | method: row.method, |
| 187 | client: { |
| 188 | label: row.clientLabel, |
| 189 | ipAddress: row.clientIpAddress, |
| 190 | userAgent: row.clientUserAgent, |
| 191 | deviceType: row.clientDeviceType, |
| 192 | os: row.clientOs, |
| 193 | browser: row.clientBrowser, |
| 194 | }, |
| 195 | issuedAt: row.issuedAt, |
| 196 | expiresAt: row.expiresAt, |
| 197 | lastConnectedAt: row.lastConnectedAt, |
| 198 | revokedAt: row.revokedAt, |
| 199 | }; |
| 200 | } |
| 201 | |
| 202 | function toPersistenceSqlOrDecodeError( |
| 203 | sqlOperation: string, |
| 204 | decodeOperation: string, |
| 205 | correlation?: PersistenceErrorCorrelation, |
| 206 | ) { |
| 207 | return (cause: unknown): AuthSessionRepositoryError => |
| 208 | Schema.isSchemaError(cause) |
| 209 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation) |
| 210 | : new PersistenceSqlError({ |
| 211 | operation: sqlOperation, |
| 212 | ...(correlation === undefined ? {} : { correlation }), |
| 213 | cause, |
| 214 | }); |
| 215 | } |
| 216 | |
| 217 | /** @public Service construction is part of the canonical Effect module API. */ |
| 218 | export const make = Effect.gen(function* () { |
| 219 | const sql = yield* SqlClient.SqlClient; |
| 220 | |
| 221 | const insertSessionRow = (ignoreExisting: boolean) => |
| 222 | SqlSchema.void({ |
| 223 | Request: CreateAuthSessionInput, |
| 224 | execute: (input) => |
| 225 | sql` |
| 226 | INSERT INTO auth_sessions ( |
| 227 | session_id, |
| 228 | parent_session_id, |
| 229 | device_id, |
| 230 | subject, |
| 231 | scopes, |
| 232 | method, |
| 233 | client_label, |
| 234 | client_ip_address, |
| 235 | client_user_agent, |
| 236 | client_device_type, |
| 237 | client_os, |
| 238 | client_browser, |
| 239 | issued_at, |
| 240 | expires_at, |
| 241 | revoked_at |
| 242 | ) |
| 243 | VALUES ( |
| 244 | ${input.sessionId}, |
| 245 | ${input.parentSessionId ?? null}, |
| 246 | ${input.deviceId ?? null}, |
| 247 | ${input.subject}, |
| 248 | ${JSON.stringify(input.scopes)}, |
| 249 | ${input.method}, |
| 250 | ${input.client.label}, |
| 251 | ${input.client.ipAddress}, |
| 252 | ${input.client.userAgent}, |
| 253 | ${input.client.deviceType}, |
| 254 | ${input.client.os}, |
| 255 | ${input.client.browser}, |
| 256 | ${input.issuedAt}, |
| 257 | ${input.expiresAt}, |
| 258 | NULL |
| 259 | ) |
| 260 | ${ignoreExisting ? sql`ON CONFLICT(session_id) DO NOTHING` : sql``} |
| 261 | `, |
| 262 | }); |
| 263 | const createSessionRow = insertSessionRow(false); |
| 264 | const createSessionRowIfAbsent = insertSessionRow(true); |
| 265 | |
| 266 | const getSessionRowById = SqlSchema.findOneOption({ |
| 267 | Request: GetAuthSessionByIdInput, |
| 268 | Result: AuthSessionRawDbRow, |
| 269 | execute: ({ sessionId }) => |
| 270 | sql` |
| 271 | SELECT |
| 272 | session_id AS "sessionId", |
| 273 | parent_session_id AS "parentSessionId", |
| 274 | device_id AS "deviceId", |
| 275 | subject AS "subject", |
| 276 | scopes AS "scopes", |
| 277 | method AS "method", |
| 278 | client_label AS "clientLabel", |
| 279 | client_ip_address AS "clientIpAddress", |
| 280 | client_user_agent AS "clientUserAgent", |
| 281 | client_device_type AS "clientDeviceType", |
| 282 | client_os AS "clientOs", |
| 283 | client_browser AS "clientBrowser", |
| 284 | issued_at AS "issuedAt", |
| 285 | expires_at AS "expiresAt", |
| 286 | last_connected_at AS "lastConnectedAt", |
| 287 | revoked_at AS "revokedAt" |
| 288 | FROM auth_sessions |
| 289 | WHERE session_id = ${sessionId} |
| 290 | AND (device_id IS NULL OR EXISTS ( |
| 291 | SELECT 1 FROM auth_devices AS device |
| 292 | WHERE device.device_id = auth_sessions.device_id AND device.revoked_at IS NULL |
| 293 | )) |
| 294 | `, |
| 295 | }); |
| 296 | |
| 297 | const revokeActiveSessionsForReplacement = SqlSchema.findAll({ |
| 298 | Request: CreateReplacingActiveAuthSessionInput, |
| 299 | Result: Schema.Struct({ sessionId: AuthSessionId }), |
| 300 | execute: ({ session, revokedAt, replaceSessionId }) => |
| 301 | sql` |
| 302 | UPDATE auth_sessions |
| 303 | SET revoked_at = ${revokedAt} |
| 304 | WHERE ${ |
| 305 | replaceSessionId === undefined |
| 306 | ? sql`subject = ${session.subject}` |
| 307 | : sql`session_id = ${replaceSessionId}` |
| 308 | } |
| 309 | AND method = ${session.method} |
| 310 | AND revoked_at IS NULL |
| 311 | AND expires_at > ${revokedAt} |
| 312 | RETURNING session_id AS "sessionId" |
| 313 | `, |
| 314 | }); |
| 315 | |
| 316 | const listActiveSessionRows = SqlSchema.findAll({ |
| 317 | Request: ListActiveAuthSessionsInput, |
| 318 | Result: AuthSessionRawDbRow, |
| 319 | execute: ({ now, connectedSessionIds = [] }) => |
| 320 | sql` |
| 321 | SELECT |
| 322 | session_id AS "sessionId", |
| 323 | parent_session_id AS "parentSessionId", |
| 324 | device_id AS "deviceId", |
| 325 | subject AS "subject", |
| 326 | scopes AS "scopes", |
| 327 | method AS "method", |
| 328 | client_label AS "clientLabel", |
| 329 | client_ip_address AS "clientIpAddress", |
| 330 | client_user_agent AS "clientUserAgent", |
| 331 | client_device_type AS "clientDeviceType", |
| 332 | client_os AS "clientOs", |
| 333 | client_browser AS "clientBrowser", |
| 334 | issued_at AS "issuedAt", |
| 335 | expires_at AS "expiresAt", |
| 336 | last_connected_at AS "lastConnectedAt", |
| 337 | revoked_at AS "revokedAt" |
| 338 | FROM auth_sessions |
| 339 | WHERE revoked_at IS NULL |
| 340 | AND (parent_session_id IS NULL OR EXISTS ( |
| 341 | SELECT 1 FROM auth_sessions AS parent |
| 342 | WHERE parent.session_id = auth_sessions.parent_session_id |
| 343 | AND parent.revoked_at IS NULL |
| 344 | AND parent.expires_at > ${now} |
| 345 | )) |
| 346 | AND (expires_at > ${now} OR ${sql.in("session_id", connectedSessionIds)}) |
| 347 | ORDER BY issued_at DESC, session_id DESC |
| 348 | `, |
| 349 | }); |
| 350 | |
| 351 | const setLastConnectedAtRow = SqlSchema.void({ |
| 352 | Request: SetAuthSessionLastConnectedAtInput, |
| 353 | execute: ({ sessionId, lastConnectedAt }) => |
| 354 | sql` |
| 355 | UPDATE auth_sessions |
| 356 | SET last_connected_at = ${lastConnectedAt} |
| 357 | WHERE session_id = ${sessionId} |
| 358 | AND revoked_at IS NULL |
| 359 | `, |
| 360 | }); |
| 361 | |
| 362 | // COALESCE keeps the previous value when a client reports only one field, so |
| 363 | // a partial report never nulls out data a fuller client stored earlier. |
| 364 | const setClientConnectionRow = SqlSchema.void({ |
| 365 | Request: SetAuthSessionClientConnectionInput, |
| 366 | execute: ({ sessionId, surface, appVersion }) => |
| 367 | sql` |
| 368 | UPDATE auth_sessions |
| 369 | SET client_surface = COALESCE(${surface}, client_surface), |
| 370 | client_app_version = COALESCE(${appVersion}, client_app_version) |
| 371 | WHERE session_id = ${sessionId} |
| 372 | AND revoked_at IS NULL |
| 373 | `, |
| 374 | }); |
| 375 | |
| 376 | const revokeSessionRows = SqlSchema.findAll({ |
| 377 | Request: RevokeAuthSessionInput, |
| 378 | Result: Schema.Struct({ sessionId: AuthSessionId }), |
| 379 | execute: ({ sessionId, revokedAt }) => |
| 380 | sql` |
| 381 | UPDATE auth_sessions |
| 382 | SET revoked_at = ${revokedAt} |
| 383 | WHERE (session_id = ${sessionId} OR parent_session_id = ${sessionId}) |
| 384 | AND revoked_at IS NULL |
| 385 | RETURNING session_id AS "sessionId" |
| 386 | `, |
| 387 | }); |
| 388 | |
| 389 | const revokeOtherSessionRows = SqlSchema.findAll({ |
| 390 | Request: RevokeOtherAuthSessionsInput, |
| 391 | Result: Schema.Struct({ sessionId: AuthSessionId }), |
| 392 | execute: ({ currentSessionId, revokedAt }) => |
| 393 | sql` |
| 394 | UPDATE auth_sessions |
| 395 | SET revoked_at = ${revokedAt} |
| 396 | WHERE session_id <> ${currentSessionId} |
| 397 | AND revoked_at IS NULL |
| 398 | RETURNING session_id AS "sessionId" |
| 399 | `, |
| 400 | }); |
| 401 | |
| 402 | const create: AuthSessionRepository["Service"]["create"] = (input) => |
| 403 | createSessionRow(input).pipe( |
| 404 | Effect.mapError( |
| 405 | toPersistenceSqlOrDecodeError( |
| 406 | "AuthSessionRepository.create:query", |
| 407 | "AuthSessionRepository.create:encodeRequest", |
| 408 | { sessionId: input.sessionId }, |
| 409 | ), |
| 410 | ), |
| 411 | ); |
| 412 | |
| 413 | const createReplacingActive: AuthSessionRepository["Service"]["createReplacingActive"] = ( |
| 414 | input, |
| 415 | ) => |
| 416 | sql |
| 417 | .withTransaction( |
| 418 | revokeActiveSessionsForReplacement(input).pipe( |
| 419 | Effect.flatMap((revokedRows) => |
| 420 | createSessionRow(input.session).pipe( |
| 421 | Effect.as(revokedRows.map((row) => row.sessionId)), |
| 422 | ), |
| 423 | ), |
| 424 | ), |
| 425 | ) |
| 426 | .pipe( |
| 427 | Effect.mapError( |
| 428 | toPersistenceSqlOrDecodeError( |
| 429 | "AuthSessionRepository.createReplacingActive:query", |
| 430 | "AuthSessionRepository.createReplacingActive:encodeRequest", |
| 431 | { sessionId: input.session.sessionId }, |
| 432 | ), |
| 433 | ), |
| 434 | ); |
| 435 | |
| 436 | const createIfAbsent: AuthSessionRepository["Service"]["createIfAbsent"] = (input) => |
| 437 | createSessionRowIfAbsent(input).pipe( |
| 438 | Effect.mapError( |
| 439 | toPersistenceSqlOrDecodeError( |
| 440 | "AuthSessionRepository.createIfAbsent:query", |
| 441 | "AuthSessionRepository.createIfAbsent:encodeRequest", |
| 442 | { sessionId: input.sessionId }, |
| 443 | ), |
| 444 | ), |
| 445 | ); |
| 446 | |
| 447 | const getById: AuthSessionRepository["Service"]["getById"] = (input) => |
| 448 | getSessionRowById(input).pipe( |
| 449 | Effect.mapError( |
| 450 | toPersistenceSqlOrDecodeError( |
| 451 | "AuthSessionRepository.getById:query", |
| 452 | "AuthSessionRepository.getById:decodeRow", |
| 453 | { sessionId: input.sessionId }, |
| 454 | ), |
| 455 | ), |
| 456 | Effect.flatMap((rowOption) => |
| 457 | Option.match(rowOption, { |
| 458 | onNone: () => Effect.succeedNone, |
| 459 | onSome: (row) => |
| 460 | decodeAuthSessionDbRow(row).pipe( |
| 461 | Effect.mapError((cause) => |
| 462 | PersistenceDecodeError.fromSchemaError( |
| 463 | "AuthSessionRepository.getById:decodeRow", |
| 464 | cause, |
| 465 | { sessionId: input.sessionId }, |
| 466 | ), |
| 467 | ), |
| 468 | Effect.map((decodedRow) => Option.some(toAuthSessionRecord(decodedRow))), |
| 469 | ), |
| 470 | }), |
| 471 | ), |
| 472 | ); |
| 473 | |
| 474 | const listActive: AuthSessionRepository["Service"]["listActive"] = (input) => |
| 475 | listActiveSessionRows(input).pipe( |
| 476 | Effect.mapError( |
| 477 | toPersistenceSqlOrDecodeError( |
| 478 | "AuthSessionRepository.listActive:query", |
| 479 | "AuthSessionRepository.listActive:decodeRows", |
| 480 | ), |
| 481 | ), |
| 482 | Effect.flatMap((rows) => |
| 483 | Effect.forEach(rows, (row) => |
| 484 | decodeAuthSessionDbRow(row).pipe( |
| 485 | Effect.mapError((cause) => |
| 486 | PersistenceDecodeError.fromSchemaError( |
| 487 | "AuthSessionRepository.listActive:decodeRows", |
| 488 | cause, |
| 489 | { sessionId: row.sessionId }, |
| 490 | ), |
| 491 | ), |
| 492 | Effect.map(toAuthSessionRecord), |
| 493 | ), |
| 494 | ), |
| 495 | ), |
| 496 | ); |
| 497 | |
| 498 | const revoke: AuthSessionRepository["Service"]["revoke"] = (input) => |
| 499 | revokeSessionRows(input).pipe( |
| 500 | Effect.mapError( |
| 501 | toPersistenceSqlOrDecodeError( |
| 502 | "AuthSessionRepository.revoke:query", |
| 503 | "AuthSessionRepository.revoke:decodeRows", |
| 504 | { sessionId: input.sessionId }, |
| 505 | ), |
| 506 | ), |
| 507 | Effect.map((rows) => rows.map((row) => row.sessionId)), |
| 508 | ); |
| 509 | |
| 510 | const revokeAllExcept: AuthSessionRepository["Service"]["revokeAllExcept"] = (input) => |
| 511 | revokeOtherSessionRows(input).pipe( |
| 512 | Effect.mapError( |
| 513 | toPersistenceSqlOrDecodeError( |
| 514 | "AuthSessionRepository.revokeAllExcept:query", |
| 515 | "AuthSessionRepository.revokeAllExcept:decodeRows", |
| 516 | { currentSessionId: input.currentSessionId }, |
| 517 | ), |
| 518 | ), |
| 519 | Effect.map((rows) => rows.map((row) => row.sessionId)), |
| 520 | ); |
| 521 | |
| 522 | const setLastConnectedAt: AuthSessionRepository["Service"]["setLastConnectedAt"] = (input) => |
| 523 | setLastConnectedAtRow(input).pipe( |
| 524 | Effect.mapError( |
| 525 | toPersistenceSqlOrDecodeError( |
| 526 | "AuthSessionRepository.setLastConnectedAt:query", |
| 527 | "AuthSessionRepository.setLastConnectedAt:encodeRequest", |
| 528 | { sessionId: input.sessionId }, |
| 529 | ), |
| 530 | ), |
| 531 | ); |
| 532 | |
| 533 | const setClientConnection: AuthSessionRepository["Service"]["setClientConnection"] = (input) => |
| 534 | setClientConnectionRow(input).pipe( |
| 535 | Effect.mapError( |
| 536 | toPersistenceSqlOrDecodeError( |
| 537 | "AuthSessionRepository.setClientConnection:query", |
| 538 | "AuthSessionRepository.setClientConnection:encodeRequest", |
| 539 | { sessionId: input.sessionId }, |
| 540 | ), |
| 541 | ), |
| 542 | ); |
| 543 | |
| 544 | return { |
| 545 | create, |
| 546 | createReplacingActive, |
| 547 | createIfAbsent, |
| 548 | getById, |
| 549 | listActive, |
| 550 | revoke, |
| 551 | revokeAllExcept, |
| 552 | setLastConnectedAt, |
| 553 | setClientConnection, |
| 554 | } satisfies AuthSessionRepository["Service"]; |
| 555 | }); |
| 556 | |
| 557 | export const layer = Layer.effect(AuthSessionRepository, make); |