| ... | ... | @@ -7,7 +7,6 @@ import { |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 = { |
| 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 |
| 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 |
| 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< |
| 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 |
| 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 |
| 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 |
| 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* () { |
| 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* () { |
| 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* () { |
| 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* () { |
| 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* () { |
| 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* () { |
| 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* () { |
| 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, |