From 6bf57766feb8321f860baf300140563cd9539053 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 14 Dec 2025 20:36:24 -0800 Subject: Init --- src/token/index.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/token/index.ts (limited to 'src/token/index.ts') diff --git a/src/token/index.ts b/src/token/index.ts new file mode 100644 index 0000000..7251714 --- /dev/null +++ b/src/token/index.ts @@ -0,0 +1,76 @@ +import { createHmac, randomBytes } from 'crypto'; +import { Either, type IEither } from '@emprespresso/pengueno'; + +export interface TokenPayload { + routeName: string; + timestamp: number; +} + +export class TokenSigner { + private readonly secret: string; + private readonly ttlSeconds: number; + + constructor(secret?: string, ttlSeconds: number = 30) { + this.secret = secret || randomBytes(32).toString('hex'); + this.ttlSeconds = ttlSeconds; + } + + generate(routeName: string): string { + const timestamp = Date.now(); + const payload = JSON.stringify({ routeName, timestamp }); + const signature = this.sign(payload); + const token = Buffer.from(`${payload}.${signature}`).toString('base64url'); + return token; + } + + validate(token: string, expectedRoute: string): IEither { + try { + const decoded = Buffer.from(token, 'base64url').toString('utf-8'); + const lastDotIndex = decoded.lastIndexOf('.'); + + if (lastDotIndex === -1) { + return Either.left(new Error('Invalid token format')); + } + + const payload = decoded.substring(0, lastDotIndex); + const signature = decoded.substring(lastDotIndex + 1); + + // Verify signature + const expectedSignature = this.sign(payload); + if (signature !== expectedSignature) { + return Either.left(new Error('Invalid token signature')); + } + + // Parse payload + const parsed: TokenPayload = JSON.parse(payload); + + // Check route name + if (parsed.routeName !== expectedRoute) { + return Either.left(new Error('Token route mismatch')); + } + + // Check expiration + const now = Date.now(); + const age = (now - parsed.timestamp) / 1000; + if (age > this.ttlSeconds) { + return Either.left(new Error('Token expired')); + } + + if (age < 0) { + return Either.left(new Error('Token from future')); + } + + return Either.right(parsed); + } catch (err) { + return Either.left(err instanceof Error ? err : new Error(String(err))); + } + } + + private sign(payload: string): string { + return createHmac('sha256', this.secret).update(payload).digest('hex'); + } + + getSecret(): string { + return this.secret; + } +} -- cgit v1.2.3-70-g09d2