Skip to content

Instantly share code, notes, and snippets.

@dilame
Created March 18, 2025 13:17
Show Gist options
  • Save dilame/8ac7e6be16b8bf38afb46449780db9c9 to your computer and use it in GitHub Desktop.
Save dilame/8ac7e6be16b8bf38afb46449780db9c9 to your computer and use it in GitHub Desktop.
TS pagination cursor class
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