| @@ -7,7 +7,6 @@ import { · 6 unmodified lines |
| 7 | 7 | type AuthEnvironmentScope, |
| 8 | 8 | type ServerAuthSessionMethod, |
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | | −import * as Clock from "effect/Clock"; |
| 11 | 10 | import * as Context from "effect/Context"; |
| 12 | 11 | import * as Crypto from "effect/Crypto"; |
| 13 | 12 | import * as DateTime from "effect/DateTime"; |
| @@ -93,7 +92,11 @@ export class InvalidSessionTokenPayloadError extends Schema.TaggedErrorClass<Inv · 79 unmodified lines |
| 93 | 92 | |
| 94 | 93 | export class SessionTokenExpiredError extends Schema.TaggedErrorClass<SessionTokenExpiredError>()( |
| 95 | 94 | "SessionTokenExpiredError", |
| 96 | | − {}, |
| 95 | + { |
| 96 | + sessionId: AuthSessionId, |
| 97 | + expiresAt: Schema.DateTimeUtc, |
| 98 | + observedAt: Schema.DateTimeUtc, |
| 99 | + }, |
| 97 | 100 | ) { |
| 98 | 101 | override get message(): string { |
| 99 | 102 | return "Session token expired."; |
| @@ -102,7 +105,9 @@ export class SessionTokenExpiredError extends Schema.TaggedErrorClass<SessionTok · 2 unmodified lines |
| 102 | 105 | |
| 103 | 106 | export class UnknownSessionTokenError extends Schema.TaggedErrorClass<UnknownSessionTokenError>()( |
| 104 | 107 | "UnknownSessionTokenError", |
| 105 | | − {}, |
| 108 | + { |
| 109 | + sessionId: AuthSessionId, |
| 110 | + }, |
| 106 | 111 | ) { |
| 107 | 112 | override get message(): string { |
| 108 | 113 | return "Unknown session token."; |
| @@ -111,7 +116,10 @@ export class UnknownSessionTokenError extends Schema.TaggedErrorClass<UnknownSes · 2 unmodified lines |
| 111 | 116 | |
| 112 | 117 | export class SessionTokenRevokedError extends Schema.TaggedErrorClass<SessionTokenRevokedError>()( |
| 113 | 118 | "SessionTokenRevokedError", |
| 114 | | − {}, |
| 119 | + { |
| 120 | + sessionId: AuthSessionId, |
| 121 | + revokedAt: Schema.DateTimeUtc, |
| 122 | + }, |
| 115 | 123 | ) { |
| 116 | 124 | override get message(): string { |
| 117 | 125 | return "Session token revoked."; |
| @@ -120,7 +128,10 @@ export class SessionTokenRevokedError extends Schema.TaggedErrorClass<SessionTok · 2 unmodified lines |
| 120 | 128 | |
| 121 | 129 | export class InvalidSessionExpirationClaimError extends Schema.TaggedErrorClass<InvalidSessionExpirationClaimError>()( |
| 122 | 130 | "InvalidSessionExpirationClaimError", |
| 123 | | − {}, |
| 131 | + { |
| 132 | + sessionId: AuthSessionId, |
| 133 | + expirationClaim: Schema.Number, |
| 134 | + }, |
| 124 | 135 | ) { |
| 125 | 136 | override get message(): string { |
| 126 | 137 | return "Invalid `exp` claim"; |
| @@ -158,7 +169,11 @@ export class InvalidWebSocketTokenPayloadError extends Schema.TaggedErrorClass<I · 31 unmodified lines |
| 158 | 169 | |
| 159 | 170 | export class WebSocketTokenExpiredError extends Schema.TaggedErrorClass<WebSocketTokenExpiredError>()( |
| 160 | 171 | "WebSocketTokenExpiredError", |
| 161 | | − {}, |
| 172 | + { |
| 173 | + sessionId: AuthSessionId, |
| 174 | + expiresAt: Schema.DateTimeUtc, |
| 175 | + observedAt: Schema.DateTimeUtc, |
| 176 | + }, |
| 162 | 177 | ) { |
| 163 | 178 | override get message(): string { |
| 164 | 179 | return "Websocket token expired."; |
| @@ -167,7 +182,9 @@ export class WebSocketTokenExpiredError extends Schema.TaggedErrorClass<WebSocke · 2 unmodified lines |
| 167 | 182 | |
| 168 | 183 | export class UnknownWebSocketSessionError extends Schema.TaggedErrorClass<UnknownWebSocketSessionError>()( |
| 169 | 184 | "UnknownWebSocketSessionError", |
| 170 | | − {}, |
| 185 | + { |
| 186 | + sessionId: AuthSessionId, |
| 187 | + }, |
| 171 | 188 | ) { |
| 172 | 189 | override get message(): string { |
| 173 | 190 | return "Unknown websocket session."; |
| @@ -176,7 +193,11 @@ export class UnknownWebSocketSessionError extends Schema.TaggedErrorClass<Unknow · 2 unmodified lines |
| 176 | 193 | |
| 177 | 194 | export class WebSocketSessionExpiredError extends Schema.TaggedErrorClass<WebSocketSessionExpiredError>()( |
| 178 | 195 | "WebSocketSessionExpiredError", |
| 179 | | − {}, |
| 196 | + { |
| 197 | + sessionId: AuthSessionId, |
| 198 | + expiresAt: Schema.DateTimeUtc, |
| 199 | + observedAt: Schema.DateTimeUtc, |
| 200 | + }, |
| 180 | 201 | ) { |
| 181 | 202 | override get message(): string { |
| 182 | 203 | return "Websocket session expired."; |
| @@ -185,7 +206,10 @@ export class WebSocketSessionExpiredError extends Schema.TaggedErrorClass<WebSoc · 2 unmodified lines |
| 185 | 206 | |
| 186 | 207 | export class WebSocketSessionRevokedError extends Schema.TaggedErrorClass<WebSocketSessionRevokedError>()( |
| 187 | 208 | "WebSocketSessionRevokedError", |
| 188 | | − {}, |
| 209 | + { |
| 210 | + sessionId: AuthSessionId, |
| 211 | + revokedAt: Schema.DateTimeUtc, |
| 212 | + }, |
| 189 | 213 | ) { |
| 190 | 214 | override get message(): string { |
| 191 | 215 | return "Websocket session revoked."; |
| @@ -218,6 +242,7 @@ const sessionCredentialInternalErrorContext = { · 26 unmodified lines |
| 218 | 242 | export class SessionClaimsEncodingError extends Schema.TaggedErrorClass<SessionClaimsEncodingError>()( |
| 219 | 243 | "SessionClaimsEncodingError", |
| 220 | 244 | { |
| 245 | + sessionId: AuthSessionId, |
| 221 | 246 | operation: Schema.Literals(["encode_session_claims", "encode_websocket_claims"]), |
| 222 | 247 | ...sessionCredentialInternalErrorContext, |
| 223 | 248 | }, |
| @@ -230,6 +255,7 @@ export class SessionClaimsEncodingError extends Schema.TaggedErrorClass<SessionC · 6 unmodified lines |
| 230 | 255 | export class SessionCredentialIssueError extends Schema.TaggedErrorClass<SessionCredentialIssueError>()( |
| 231 | 256 | "SessionCredentialIssueError", |
| 232 | 257 | { |
| 258 | + sessionId: Schema.optional(AuthSessionId), |
| 233 | 259 | ...sessionCredentialInternalErrorContext, |
| 234 | 260 | }, |
| 235 | 261 | ) { |
| @@ -241,6 +267,7 @@ export class SessionCredentialIssueError extends Schema.TaggedErrorClass<Session · 5 unmodified lines |
| 241 | 267 | export class SessionCredentialVerificationError extends Schema.TaggedErrorClass<SessionCredentialVerificationError>()( |
| 242 | 268 | "SessionCredentialVerificationError", |
| 243 | 269 | { |
| 270 | + sessionId: AuthSessionId, |
| 244 | 271 | ...sessionCredentialInternalErrorContext, |
| 245 | 272 | }, |
| 246 | 273 | ) { |
| @@ -252,6 +279,7 @@ export class SessionCredentialVerificationError extends Schema.TaggedErrorClass< · 5 unmodified lines |
| 252 | 279 | export class WebSocketTokenIssueError extends Schema.TaggedErrorClass<WebSocketTokenIssueError>()( |
| 253 | 280 | "WebSocketTokenIssueError", |
| 254 | 281 | { |
| 282 | + sessionId: AuthSessionId, |
| 255 | 283 | ...sessionCredentialInternalErrorContext, |
| 256 | 284 | }, |
| 257 | 285 | ) { |
| @@ -263,6 +291,7 @@ export class WebSocketTokenIssueError extends Schema.TaggedErrorClass<WebSocketT · 5 unmodified lines |
| 263 | 291 | export class WebSocketTokenVerificationError extends Schema.TaggedErrorClass<WebSocketTokenVerificationError>()( |
| 264 | 292 | "WebSocketTokenVerificationError", |
| 265 | 293 | { |
| 294 | + sessionId: AuthSessionId, |
| 266 | 295 | ...sessionCredentialInternalErrorContext, |
| 267 | 296 | }, |
| 268 | 297 | ) { |
| @@ -285,6 +314,7 @@ export class ActiveSessionsListError extends Schema.TaggedErrorClass<ActiveSessi · 16 unmodified lines |
| 285 | 314 | export class SessionRevocationError extends Schema.TaggedErrorClass<SessionRevocationError>()( |
| 286 | 315 | "SessionRevocationError", |
| 287 | 316 | { |
| 317 | + sessionId: AuthSessionId, |
| 288 | 318 | ...sessionCredentialInternalErrorContext, |
| 289 | 319 | }, |
| 290 | 320 | ) { |
| @@ -296,6 +326,7 @@ export class SessionRevocationError extends Schema.TaggedErrorClass<SessionRevoc · 5 unmodified lines |
| 296 | 326 | export class OtherSessionsRevocationError extends Schema.TaggedErrorClass<OtherSessionsRevocationError>()( |
| 297 | 327 | "OtherSessionsRevocationError", |
| 298 | 328 | { |
| 329 | + currentSessionId: AuthSessionId, |
| 299 | 330 | ...sessionCredentialInternalErrorContext, |
| 300 | 331 | }, |
| 301 | 332 | ) { |
| @@ -539,7 +570,11 @@ export const make = Effect.gen(function* () { · 237 unmodified lines |
| 539 | 570 | const encodeClaims = Schema.encodeEffect(Schema.fromJsonString(SessionClaims)); |
| 540 | 571 | const issue: SessionStore["Service"]["issue"] = Effect.fn("SessionStore.issue")( |
| 541 | 572 | function* (input) { |
| 542 | | − const sessionId = AuthSessionId.make(yield* crypto.randomUUIDv4); |
| 573 | + const sessionId = AuthSessionId.make( |
| 574 | + yield* crypto.randomUUIDv4.pipe( |
| 575 | + Effect.mapError((cause) => new SessionCredentialIssueError({ cause })), |
| 576 | + ), |
| 577 | + ); |
| 543 | 578 | const issuedAt = yield* DateTime.now; |
| 544 | 579 | const expiresAt = DateTime.add(issuedAt, { |
| 545 | 580 | milliseconds: Duration.toMillis(input?.ttl ?? DEFAULT_SESSION_TTL), |
| @@ -559,27 +594,37 @@ export const make = Effect.gen(function* () { · 13 unmodified lines |
| 559 | 594 | const encodedPayload = yield* encodeClaims(claims).pipe( |
| 560 | 595 | Effect.map(base64UrlEncode), |
| 561 | 596 | Effect.mapError( |
| 562 | | − (cause) => new SessionClaimsEncodingError({ operation: "encode_session_claims", cause }), |
| 597 | + (cause) => |
| 598 | + new SessionCredentialIssueError({ |
| 599 | + sessionId, |
| 600 | + cause: new SessionClaimsEncodingError({ |
| 601 | + sessionId, |
| 602 | + operation: "encode_session_claims", |
| 603 | + cause, |
| 604 | + }), |
| 605 | + }), |
| 563 | 606 | ), |
| 564 | 607 | ); |
| 565 | 608 | const signature = signPayload(encodedPayload, signingSecret); |
| 566 | 609 | const client = input?.client ?? createDefaultClientMetadata(); |
| 567 | | − yield* authSessions.create({ |
| 568 | | − sessionId, |
| 569 | | − subject: claims.sub, |
| 570 | | − scopes: claims.scopes, |
| 571 | | − method: claims.method, |
| 572 | | − client: { |
| 573 | | − label: client.label ?? null, |
| 574 | | − ipAddress: client.ipAddress ?? null, |
| 575 | | − userAgent: client.userAgent ?? null, |
| 576 | | − deviceType: client.deviceType, |
| 577 | | − os: client.os ?? null, |
| 578 | | − browser: client.browser ?? null, |
| 579 | | − }, |
| 580 | | − issuedAt, |
| 581 | | − expiresAt, |
| 582 | | − }); |
| 610 | + yield* authSessions |
| 611 | + .create({ |
| 612 | + sessionId, |
| 613 | + subject: claims.sub, |
| 614 | + scopes: claims.scopes, |
| 615 | + method: claims.method, |
| 616 | + client: { |
| 617 | + label: client.label ?? null, |
| 618 | + ipAddress: client.ipAddress ?? null, |
| 619 | + userAgent: client.userAgent ?? null, |
| 620 | + deviceType: client.deviceType, |
| 621 | + os: client.os ?? null, |
| 622 | + browser: client.browser ?? null, |
| 623 | + }, |
| 624 | + issuedAt, |
| 625 | + expiresAt, |
| 626 | + }) |
| 627 | + .pipe(Effect.mapError((cause) => new SessionCredentialIssueError({ sessionId, cause }))); |
| 583 | 628 | yield* emitUpsert( |
| 584 | 629 | toAuthClientSession({ |
| 585 | 630 | sessionId, |
| @@ -604,7 +649,6 @@ export const make = Effect.gen(function* () { · 18 unmodified lines |
| 604 | 649 | ...(claims.jkt ? { proofKeyThumbprint: claims.jkt } : {}), |
| 605 | 650 | } satisfies IssuedSession; |
| 606 | 651 | }, |
| 607 | | − Effect.mapError((cause) => new SessionCredentialIssueError({ cause })), |
| 608 | 652 | ); |
| 609 | 653 | |
| 610 | 654 | const verify: SessionStore["Service"]["verify"] = Effect.fn("SessionStore.verify")( |
| @@ -623,22 +667,37 @@ export const make = Effect.gen(function* () { · 12 unmodified lines |
| 623 | 667 | Effect.mapError((cause) => new InvalidSessionTokenPayloadError({ cause })), |
| 624 | 668 | ); |
| 625 | 669 | |
| 626 | | − const now = yield* Clock.currentTimeMillis; |
| 627 | | − if (claims.exp <= now) { |
| 628 | | − return yield* new SessionTokenExpiredError({}); |
| 670 | + const observedAt = yield* DateTime.now; |
| 671 | + const expiresAt = DateTime.make(claims.exp); |
| 672 | + if (Option.isNone(expiresAt)) { |
| 673 | + return yield* new InvalidSessionExpirationClaimError({ |
| 674 | + sessionId: claims.sid, |
| 675 | + expirationClaim: claims.exp, |
| 676 | + }); |
| 677 | + } |
| 678 | + if (claims.exp <= observedAt.epochMilliseconds) { |
| 679 | + return yield* new SessionTokenExpiredError({ |
| 680 | + sessionId: claims.sid, |
| 681 | + expiresAt: expiresAt.value, |
| 682 | + observedAt, |
| 683 | + }); |
| 629 | 684 | } |
| 630 | 685 | |
| 631 | | − const row = yield* authSessions.getById({ sessionId: claims.sid }); |
| 686 | + const row = yield* authSessions |
| 687 | + .getById({ sessionId: claims.sid }) |
| 688 | + .pipe( |
| 689 | + Effect.mapError( |
| 690 | + (cause) => new SessionCredentialVerificationError({ sessionId: claims.sid, cause }), |
| 691 | + ), |
| 692 | + ); |
| 632 | 693 | if (Option.isNone(row)) { |
| 633 | | − return yield* new UnknownSessionTokenError({}); |
| 694 | + return yield* new UnknownSessionTokenError({ sessionId: claims.sid }); |
| 634 | 695 | } |
| 635 | 696 | if (row.value.revokedAt !== null) { |
| 636 | | − return yield* new SessionTokenRevokedError({}); |
| 637 | | − } |
| 638 | | − |
| 639 | | − const expiresAt = DateTime.make(claims.exp); |
| 640 | | − if (Option.isNone(expiresAt)) { |
| 641 | | − return yield* new InvalidSessionExpirationClaimError({}); |
| 697 | + return yield* new SessionTokenRevokedError({ |
| 698 | + sessionId: claims.sid, |
| 699 | + revokedAt: row.value.revokedAt, |
| 700 | + }); |
| 642 | 701 | } |
| 643 | 702 | |
| 644 | 703 | return { |
| @@ -652,95 +711,111 @@ export const make = Effect.gen(function* () { · 7 unmodified lines |
| 652 | 711 | ...(claims.jkt ? { proofKeyThumbprint: claims.jkt } : {}), |
| 653 | 712 | } satisfies VerifiedSession; |
| 654 | 713 | }, |
| 655 | | − Effect.mapError((cause) => |
| 656 | | − isSessionCredentialInvalidError(cause) |
| 657 | | − ? cause |
| 658 | | − : new SessionCredentialVerificationError({ cause }), |
| 659 | | − ), |
| 660 | 714 | ); |
| 661 | 715 | |
| 662 | 716 | const encodeWsClaims = Schema.encodeEffect(Schema.fromJsonString(WebSocketClaims)); |
| 663 | 717 | const issueWebSocketToken: SessionStore["Service"]["issueWebSocketToken"] = Effect.fn( |
| 664 | 718 | "SessionStore.issueWebSocketToken", |
| 665 | | − )( |
| 666 | | − function* (sessionId, input) { |
| 667 | | − const issuedAt = yield* DateTime.now; |
| 668 | | − const expiresAt = DateTime.add(issuedAt, { |
| 669 | | − milliseconds: Duration.toMillis(input?.ttl ?? DEFAULT_WEBSOCKET_TOKEN_TTL), |
| 670 | | − }); |
| 671 | | − const claims: WebSocketClaims = { |
| 672 | | − v: 1, |
| 673 | | − kind: "websocket", |
| 674 | | − sid: sessionId, |
| 675 | | − iat: issuedAt.epochMilliseconds, |
| 676 | | − exp: expiresAt.epochMilliseconds, |
| 677 | | − }; |
| 678 | | − const encodedPayload = yield* encodeWsClaims(claims).pipe( |
| 679 | | − Effect.map(base64UrlEncode), |
| 680 | | − Effect.mapError( |
| 681 | | − (cause) => |
| 682 | | − new SessionClaimsEncodingError({ operation: "encode_websocket_claims", cause }), |
| 683 | | − ), |
| 684 | | − ); |
| 685 | | − const signature = signPayload(encodedPayload, signingSecret); |
| 686 | | − return { |
| 687 | | − token: `${encodedPayload}.${signature}`, |
| 688 | | − expiresAt, |
| 689 | | − }; |
| 690 | | − }, |
| 691 | | − Effect.mapError((cause) => new WebSocketTokenIssueError({ cause })), |
| 692 | | − ); |
| 719 | + )(function* (sessionId, input) { |
| 720 | + const issuedAt = yield* DateTime.now; |
| 721 | + const expiresAt = DateTime.add(issuedAt, { |
| 722 | + milliseconds: Duration.toMillis(input?.ttl ?? DEFAULT_WEBSOCKET_TOKEN_TTL), |
| 723 | + }); |
| 724 | + const claims: WebSocketClaims = { |
| 725 | + v: 1, |
| 726 | + kind: "websocket", |
| 727 | + sid: sessionId, |
| 728 | + iat: issuedAt.epochMilliseconds, |
| 729 | + exp: expiresAt.epochMilliseconds, |
| 730 | + }; |
| 731 | + const encodedPayload = yield* encodeWsClaims(claims).pipe( |
| 732 | + Effect.map(base64UrlEncode), |
| 733 | + Effect.mapError( |
| 734 | + (cause) => |
| 735 | + new WebSocketTokenIssueError({ |
| 736 | + sessionId, |
| 737 | + cause: new SessionClaimsEncodingError({ |
| 738 | + sessionId, |
| 739 | + operation: "encode_websocket_claims", |
| 740 | + cause, |
| 741 | + }), |
| 742 | + }), |
| 743 | + ), |
| 744 | + ); |
| 745 | + const signature = signPayload(encodedPayload, signingSecret); |
| 746 | + return { |
| 747 | + token: `${encodedPayload}.${signature}`, |
| 748 | + expiresAt, |
| 749 | + }; |
| 750 | + }); |
| 693 | 751 | |
| 694 | 752 | const verifyWebSocketToken: SessionStore["Service"]["verifyWebSocketToken"] = Effect.fn( |
| 695 | 753 | "SessionStore.verifyWebSocketToken", |
| 696 | | − )( |
| 697 | | − function* (token) { |
| 698 | | − const [encodedPayload, signature] = token.split("."); |
| 699 | | − if (!encodedPayload || !signature) { |
| 700 | | − return yield* new MalformedWebSocketTokenError({}); |
| 701 | | − } |
| 702 | | − |
| 703 | | − const expectedSignature = signPayload(encodedPayload, signingSecret); |
| 704 | | − if (!timingSafeEqualBase64Url(signature, expectedSignature)) { |
| 705 | | − return yield* new InvalidWebSocketTokenSignatureError({}); |
| 706 | | − } |
| 754 | + )(function* (token) { |
| 755 | + const [encodedPayload, signature] = token.split("."); |
| 756 | + if (!encodedPayload || !signature) { |
| 757 | + return yield* new MalformedWebSocketTokenError({}); |
| 758 | + } |
| 707 | 759 | |
| 708 | | − const claims = yield* decodeWebSocketClaims(base64UrlDecodeUtf8(encodedPayload)).pipe( |
| 709 | | − Effect.mapError((cause) => new InvalidWebSocketTokenPayloadError({ cause })), |
| 710 | | − ); |
| 760 | + const expectedSignature = signPayload(encodedPayload, signingSecret); |
| 761 | + if (!timingSafeEqualBase64Url(signature, expectedSignature)) { |
| 762 | + return yield* new InvalidWebSocketTokenSignatureError({}); |
| 763 | + } |
| 711 | 764 | |
| 712 | | − const now = yield* Clock.currentTimeMillis; |
| 713 | | − if (claims.exp <= now) { |
| 714 | | − return yield* new WebSocketTokenExpiredError({}); |
| 715 | | − } |
| 765 | + const claims = yield* decodeWebSocketClaims(base64UrlDecodeUtf8(encodedPayload)).pipe( |
| 766 | + Effect.mapError((cause) => new InvalidWebSocketTokenPayloadError({ cause })), |
| 767 | + ); |
| 716 | 768 | |
| 717 | | − const row = yield* authSessions.getById({ sessionId: claims.sid }); |
| 718 | | − if (Option.isNone(row)) { |
| 719 | | − return yield* new UnknownWebSocketSessionError({}); |
| 720 | | − } |
| 721 | | − if (row.value.expiresAt.epochMilliseconds <= now) { |
| 722 | | − return yield* new WebSocketSessionExpiredError({}); |
| 723 | | − } |
| 724 | | − if (row.value.revokedAt !== null) { |
| 725 | | − return yield* new WebSocketSessionRevokedError({}); |
| 726 | | − } |
| 769 | + const observedAt = yield* DateTime.now; |
| 770 | + const expiresAt = DateTime.make(claims.exp); |
| 771 | + if (Option.isNone(expiresAt)) { |
| 772 | + return yield* new InvalidSessionExpirationClaimError({ |
| 773 | + sessionId: claims.sid, |
| 774 | + expirationClaim: claims.exp, |
| 775 | + }); |
| 776 | + } |
| 777 | + if (claims.exp <= observedAt.epochMilliseconds) { |
| 778 | + return yield* new WebSocketTokenExpiredError({ |
| 779 | + sessionId: claims.sid, |
| 780 | + expiresAt: expiresAt.value, |
| 781 | + observedAt, |
| 782 | + }); |
| 783 | + } |
| 727 | 784 | |
| 728 | | − return { |
| 729 | | − sessionId: row.value.sessionId, |
| 730 | | − token, |
| 731 | | − method: row.value.method, |
| 732 | | − client: toClientMetadata(row.value.client), |
| 785 | + const row = yield* authSessions |
| 786 | + .getById({ sessionId: claims.sid }) |
| 787 | + .pipe( |
| 788 | + Effect.mapError( |
| 789 | + (cause) => new WebSocketTokenVerificationError({ sessionId: claims.sid, cause }), |
| 790 | + ), |
| 791 | + ); |
| 792 | + if (Option.isNone(row)) { |
| 793 | + return yield* new UnknownWebSocketSessionError({ sessionId: claims.sid }); |
| 794 | + } |
| 795 | + if (row.value.expiresAt.epochMilliseconds <= observedAt.epochMilliseconds) { |
| 796 | + return yield* new WebSocketSessionExpiredError({ |
| 797 | + sessionId: claims.sid, |
| 733 | 798 | expiresAt: row.value.expiresAt, |
| 734 | | − subject: row.value.subject, |
| 735 | | − scopes: row.value.scopes, |
| 736 | | − } satisfies VerifiedSession; |
| 737 | | − }, |
| 738 | | − Effect.mapError((cause) => |
| 739 | | − isSessionCredentialInvalidError(cause) |
| 740 | | − ? cause |
| 741 | | − : new WebSocketTokenVerificationError({ cause }), |
| 742 | | − ), |
| 743 | | − ); |
| 799 | + observedAt, |
| 800 | + }); |
| 801 | + } |
| 802 | + if (row.value.revokedAt !== null) { |
| 803 | + return yield* new WebSocketSessionRevokedError({ |
| 804 | + sessionId: claims.sid, |
| 805 | + revokedAt: row.value.revokedAt, |
| 806 | + }); |
| 807 | + } |
| 808 | + |
| 809 | + return { |
| 810 | + sessionId: row.value.sessionId, |
| 811 | + token, |
| 812 | + method: row.value.method, |
| 813 | + client: toClientMetadata(row.value.client), |
| 814 | + expiresAt: row.value.expiresAt, |
| 815 | + subject: row.value.subject, |
| 816 | + scopes: row.value.scopes, |
| 817 | + } satisfies VerifiedSession; |
| 818 | + }); |
| 744 | 819 | |
| 745 | 820 | const listActive: SessionStore["Service"]["listActive"] = Effect.fn("SessionStore.listActive")( |
| 746 | 821 | function* () { |
| @@ -768,10 +843,12 @@ export const make = Effect.gen(function* () { · 21 unmodified lines |
| 768 | 843 | const revoke: SessionStore["Service"]["revoke"] = Effect.fn("SessionStore.revoke")( |
| 769 | 844 | function* (sessionId) { |
| 770 | 845 | const revokedAt = yield* DateTime.now; |
| 771 | | − const revoked = yield* authSessions.revoke({ |
| 772 | | − sessionId, |
| 773 | | − revokedAt, |
| 774 | | − }); |
| 846 | + const revoked = yield* authSessions |
| 847 | + .revoke({ |
| 848 | + sessionId, |
| 849 | + revokedAt, |
| 850 | + }) |
| 851 | + .pipe(Effect.mapError((cause) => new SessionRevocationError({ sessionId, cause }))); |
| 775 | 852 | if (revoked) { |
| 776 | 853 | yield* Ref.update(connectedSessionsRef, (current) => { |
| 777 | 854 | const next = new Map(current); |
| @@ -782,39 +859,41 @@ export const make = Effect.gen(function* () { · 4 unmodified lines |
| 782 | 859 | } |
| 783 | 860 | return revoked; |
| 784 | 861 | }, |
| 785 | | − Effect.mapError((cause) => new SessionRevocationError({ cause })), |
| 786 | 862 | ); |
| 787 | 863 | |
| 788 | 864 | const revokeAllExcept: SessionStore["Service"]["revokeAllExcept"] = Effect.fn( |
| 789 | 865 | "SessionStore.revokeAllExcept", |
| 790 | | − )( |
| 791 | | − function* (sessionId) { |
| 792 | | − const revokedAt = yield* DateTime.now; |
| 793 | | − const revokedSessionIds = yield* authSessions.revokeAllExcept({ |
| 866 | + )(function* (sessionId) { |
| 867 | + const revokedAt = yield* DateTime.now; |
| 868 | + const revokedSessionIds = yield* authSessions |
| 869 | + .revokeAllExcept({ |
| 794 | 870 | currentSessionId: sessionId, |
| 795 | 871 | revokedAt, |
| 872 | + }) |
| 873 | + .pipe( |
| 874 | + Effect.mapError( |
| 875 | + (cause) => new OtherSessionsRevocationError({ currentSessionId: sessionId, cause }), |
| 876 | + ), |
| 877 | + ); |
| 878 | + if (revokedSessionIds.length > 0) { |
| 879 | + yield* Ref.update(connectedSessionsRef, (current) => { |
| 880 | + const next = new Map(current); |
| 881 | + for (const revokedSessionId of revokedSessionIds) { |
| 882 | + next.delete(revokedSessionId); |
| 883 | + } |
| 884 | + return next; |
| 796 | 885 | }); |
| 797 | | − if (revokedSessionIds.length > 0) { |
| 798 | | − yield* Ref.update(connectedSessionsRef, (current) => { |
| 799 | | − const next = new Map(current); |
| 800 | | − for (const revokedSessionId of revokedSessionIds) { |
| 801 | | − next.delete(revokedSessionId); |
| 802 | | − } |
| 803 | | − return next; |
| 804 | | − }); |
| 805 | | − yield* Effect.forEach( |
| 806 | | − revokedSessionIds, |
| 807 | | − (revokedSessionId) => emitRemoved(revokedSessionId), |
| 808 | | − { |
| 809 | | − concurrency: "unbounded", |
| 810 | | − discard: true, |
| 811 | | − }, |
| 812 | | − ); |
| 813 | | − } |
| 814 | | − return revokedSessionIds.length; |
| 815 | | − }, |
| 816 | | − Effect.mapError((cause) => new OtherSessionsRevocationError({ cause })), |
| 817 | | − ); |
| 886 | + yield* Effect.forEach( |
| 887 | + revokedSessionIds, |
| 888 | + (revokedSessionId) => emitRemoved(revokedSessionId), |
| 889 | + { |
| 890 | + concurrency: "unbounded", |
| 891 | + discard: true, |
| 892 | + }, |
| 893 | + ); |
| 894 | + } |
| 895 | + return revokedSessionIds.length; |
| 896 | + }); |
| 818 | 897 | |
| 819 | 898 | return SessionStore.of({ |
| 820 | 899 | cookieName, |