/** * Thin wrapper around `@shopify/shopify-app-session-storage-prisma` that * encrypts `accessToken` / `refreshToken` at rest using field-level AES-256-GCM. * * Tokens are encrypted before being handed to the underlying storage and * decrypted after they are loaded back out. `decryptField` is backward * compatible, so any legacy plaintext tokens already in the DB keep working. */ import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma"; import type { Session } from "@shopify/shopify-api"; import type { PrismaClient } from "@prisma/client"; import { decryptField, encryptField } from "../crypto/fieldCrypto.server"; type SessionTokenFields = { accessToken?: string; refreshToken?: string; }; function encryptTokens(session: Session): Session { const s = session as Session & SessionTokenFields; if (s.accessToken) s.accessToken = encryptField(s.accessToken); if (s.refreshToken) s.refreshToken = encryptField(s.refreshToken); return session; } function decryptTokens(session: Session): Session { const s = session as Session & SessionTokenFields; if (s.accessToken) s.accessToken = decryptField(s.accessToken); if (s.refreshToken) s.refreshToken = decryptField(s.refreshToken); return session; } /** * Clones a Session so we never mutate the caller's instance when encrypting * for storage. The Prisma session storage only reads plain properties, so a * shallow structured copy via the Session class is sufficient. */ function cloneSession(session: Session): Session { return Object.assign( Object.create(Object.getPrototypeOf(session)), session, ) as Session; } export class EncryptedPrismaSessionStorage extends PrismaSessionStorage { async storeSession(session: Session): Promise { return super.storeSession(encryptTokens(cloneSession(session))); } async loadSession(id: string): Promise { const session = await super.loadSession(id); return session ? decryptTokens(session) : session; } async findSessionsByShop(shop: string): Promise { const sessions = await super.findSessionsByShop(shop); return sessions.map((s) => decryptTokens(s)); } }