File
Blob: src/worker/services/inbox/session-store.ts
| 1 | import { nanoid } from "nanoid"; |
| 2 | import type { SessionRecord } from "@/shared/contracts"; |
| 3 | import { decodeWebSocketTicket } from "@/worker/security"; |
| 4 | import { WEBSOCKET_TICKET_TTL_MS } from "./shared"; |
| 5 | |
| 6 | function getSessionTokenKey(token: string) { |
| 7 | return `token:${token}`; |
| 8 | } |
| 9 | |
| 10 | function getInboxTokenLookupKey(address: string) { |
| 11 | return `inbox-token:${address}`; |
| 12 | } |
| 13 | |
| 14 | async function persistSessionToken(env: Env, token: string, session: SessionRecord, expirationTtl: number) { |
| 15 | await env.SESSIONS.put(getSessionTokenKey(token), JSON.stringify(session), { |
| 16 | expirationTtl, |
| 17 | }); |
| 18 | |
| 19 | if (session.type === "user") { |
| 20 | await env.SESSIONS.put(getInboxTokenLookupKey(session.address), token, { |
| 21 | expirationTtl, |
| 22 | }); |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | export async function refreshSessionToken(env: Env, token: string, session: SessionRecord, expiresAt: Date) { |
| 27 | const ttlMs = expiresAt.getTime() - Date.now(); |
| 28 | const expirationTtl = Math.max(60, Math.ceil(ttlMs / 1000)); |
| 29 | |
| 30 | await persistSessionToken(env, token, session, expirationTtl); |
| 31 | } |
| 32 | |
| 33 | export async function createSessionToken(env: Env, session: SessionRecord, ttlMs: number) { |
| 34 | const token = `tok_${nanoid(32)}`; |
| 35 | const expirationTtl = Math.max(60, Math.ceil(ttlMs / 1000)); |
| 36 | |
| 37 | await persistSessionToken(env, token, session, expirationTtl); |
| 38 | |
| 39 | return token; |
| 40 | } |
| 41 | |
| 42 | export async function revokeInboxSessionToken(env: Env, address: string) { |
| 43 | const lookupKey = getInboxTokenLookupKey(address); |
| 44 | const token = await env.SESSIONS.get(lookupKey); |
| 45 | |
| 46 | await Promise.all([ |
| 47 | env.SESSIONS.delete(lookupKey), |
| 48 | ...(token ? [env.SESSIONS.delete(getSessionTokenKey(token))] : []), |
| 49 | ]); |
| 50 | } |
| 51 | |
| 52 | export async function revokeAdminSessionToken(env: Env, token: string) { |
| 53 | if (!token) { |
| 54 | return; |
| 55 | } |
| 56 | await env.SESSIONS.delete(getSessionTokenKey(token)); |
| 57 | } |
| 58 | |
| 59 | export async function createWebSocketTicket(env: Env, address: string, session: SessionRecord) { |
| 60 | const ticket = `wst_${nanoid(24)}`; |
| 61 | |
| 62 | await env.SESSIONS.put(`ws-ticket:${ticket}`, JSON.stringify({ address, session }), { |
| 63 | expirationTtl: Math.max(30, Math.ceil(WEBSOCKET_TICKET_TTL_MS / 1000)), |
| 64 | }); |
| 65 | |
| 66 | return ticket; |
| 67 | } |
| 68 | |
| 69 | export async function consumeWebSocketTicket(env: Env, ticket: string | null | undefined) { |
| 70 | if (!ticket) { |
| 71 | return null; |
| 72 | } |
| 73 | |
| 74 | const key = `ws-ticket:${ticket}`; |
| 75 | const raw = await env.SESSIONS.get(key); |
| 76 | if (!raw) { |
| 77 | return null; |
| 78 | } |
| 79 | |
| 80 | await env.SESSIONS.delete(key); |
| 81 | return decodeWebSocketTicket(raw); |
| 82 | } |