Created
March 18, 2025 13:17
-
-
Save dilame/8ac7e6be16b8bf38afb46449780db9c9 to your computer and use it in GitHub Desktop.
TS pagination cursor class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto' | |
import process from 'node:process' | |
import { CustomError } from 'ts-custom-error' | |
import { z } from 'zod' | |
export const environment = z | |
.object({ | |
PAGINATION_SECRET_KEY: z.string().min(1) | |
}) | |
.parse(process.env) | |
export class FeedCursorParsingError extends CustomError {} | |
export class FeedCursor<T extends z.ZodTypeAny> { | |
private readonly key: Buffer | |
private readonly algorithm = 'chacha20-poly1305' | |
private readonly nonceLength = 12 | |
private readonly authTagLength = 16 | |
constructor(private readonly schema: T) { | |
this.key = Buffer.alloc(32, environment.PAGINATION_SECRET_KEY) | |
} | |
encrypt(cursor: z.infer<T>): string { | |
const nonce = randomBytes(this.nonceLength) | |
const cipher = createCipheriv(this.algorithm, this.key, nonce, { authTagLength: this.authTagLength }) | |
const serializedData = Buffer.from(JSON.stringify(cursor)) | |
const ciphertext = Buffer.concat([cipher.update(serializedData), cipher.final()]) | |
const authTag = cipher.getAuthTag() | |
return Buffer.concat([nonce, ciphertext, authTag]).toString('base64') | |
} | |
decrypt(cursor: string): z.infer<T> | FeedCursorParsingError { | |
const encryptedBuffer = Buffer.from(cursor, 'base64') | |
const nonce = encryptedBuffer.slice(0, this.nonceLength) | |
const authTag = encryptedBuffer.slice(-16) | |
const ciphertext = encryptedBuffer.slice(this.nonceLength, -16) | |
const decipher = createDecipheriv(this.algorithm, this.key, nonce, { authTagLength: this.authTagLength }) | |
decipher.setAuthTag(authTag) | |
const decryptedData = Buffer.concat([decipher.update(ciphertext), decipher.final()]) | |
try { | |
return this.schema.parse(JSON.parse(decryptedData.toString())) | |
} catch (cause: unknown) { | |
return new FeedCursorParsingError('', { cause }) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment