ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: move util classes to hono api

byarielm.fyi e5dd8261 c305845e

verified
+300
+139
packages/api/src/utils/encryption.utils.ts
··· 1 + import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; 2 + import { ApiError } from "../errors"; 3 + 4 + /** 5 + * Token Encryption Service 6 + * Encrypts sensitive OAuth tokens at rest using AES-256-GCM 7 + */ 8 + 9 + function getEncryptionKey(): Buffer { 10 + const key = process.env.TOKEN_ENCRYPTION_KEY; 11 + 12 + if (!key) { 13 + throw new ApiError( 14 + "Encryption key not configured", 15 + 500, 16 + "TOKEN_ENCRYPTION_KEY environment variable is required", 17 + ); 18 + } 19 + 20 + // Expect 64-char hex string (32 bytes) 21 + if (key.length !== 64) { 22 + throw new ApiError( 23 + "Invalid encryption key", 24 + 500, 25 + "TOKEN_ENCRYPTION_KEY must be 64 hex characters (32 bytes)", 26 + ); 27 + } 28 + 29 + return Buffer.from(key, "hex"); 30 + } 31 + 32 + interface EncryptedPayload { 33 + iv: string; 34 + data: string; 35 + tag: string; 36 + } 37 + 38 + /** 39 + * Encrypt sensitive data using AES-256-GCM 40 + * @param data - Data to encrypt (will be JSON stringified) 41 + * @returns Encrypted payload as JSON string 42 + */ 43 + export function encryptToken(data: any): string { 44 + try { 45 + const key = getEncryptionKey(); 46 + const iv = randomBytes(16); 47 + 48 + const cipher = createCipheriv("aes-256-gcm", key, iv); 49 + 50 + const jsonData = JSON.stringify(data); 51 + const encrypted = Buffer.concat([ 52 + cipher.update(jsonData, "utf8"), 53 + cipher.final(), 54 + ]); 55 + 56 + const authTag = cipher.getAuthTag(); 57 + 58 + const payload: EncryptedPayload = { 59 + iv: iv.toString("hex"), 60 + data: encrypted.toString("hex"), 61 + tag: authTag.toString("hex"), 62 + }; 63 + 64 + return JSON.stringify(payload); 65 + } catch (error) { 66 + console.error("Token encryption failed:", error); 67 + throw new ApiError( 68 + "Failed to encrypt token", 69 + 500, 70 + error instanceof Error ? error.message : "Unknown encryption error", 71 + ); 72 + } 73 + } 74 + 75 + /** 76 + * Decrypt sensitive data 77 + * @param encrypted - Encrypted payload as JSON string 78 + * @returns Decrypted data 79 + */ 80 + export function decryptToken(encrypted: string): any { 81 + try { 82 + const key = getEncryptionKey(); 83 + const payload: EncryptedPayload = JSON.parse(encrypted); 84 + 85 + const decipher = createDecipheriv( 86 + "aes-256-gcm", 87 + key, 88 + Buffer.from(payload.iv, "hex"), 89 + ); 90 + 91 + decipher.setAuthTag(Buffer.from(payload.tag, "hex")); 92 + 93 + const decrypted = Buffer.concat([ 94 + decipher.update(Buffer.from(payload.data, "hex")), 95 + decipher.final(), 96 + ]); 97 + 98 + return JSON.parse(decrypted.toString("utf8")); 99 + } catch (error) { 100 + console.error("Token decryption failed:", error); 101 + throw new ApiError( 102 + "Failed to decrypt token", 103 + 500, 104 + error instanceof Error ? error.message : "Unknown decryption error", 105 + ); 106 + } 107 + } 108 + 109 + /** 110 + * Generate a new encryption key (for initial setup) 111 + * Run this once and store in environment variables 112 + */ 113 + export function generateEncryptionKey(): string { 114 + return randomBytes(32).toString("hex"); 115 + } 116 + 117 + /** 118 + * Check if encryption is properly configured 119 + * Returns false in development if key is missing (with warning) 120 + */ 121 + export function isEncryptionConfigured(): boolean { 122 + const key = process.env.TOKEN_ENCRYPTION_KEY; 123 + 124 + if (!key) { 125 + if (process.env.NODE_ENV === "production") { 126 + throw new ApiError( 127 + "Encryption key not configured in production", 128 + 500, 129 + "TOKEN_ENCRYPTION_KEY is required in production", 130 + ); 131 + } 132 + console.warn( 133 + "⚠️ TOKEN_ENCRYPTION_KEY not set - tokens will NOT be encrypted", 134 + ); 135 + return false; 136 + } 137 + 138 + return true; 139 + }
+4
packages/api/src/utils/index.ts
··· 1 + export * from "./response.utils"; 2 + export * from "./string.utils"; 3 + export * from "./encryption.utils"; 4 + export * from "./validation.utils";
+74
packages/api/src/utils/response.utils.ts
··· 1 + /** 2 + * Response utilities for Hono API 3 + * Provides consistent JSON response formatting 4 + */ 5 + 6 + export interface ApiResponse<T = any> { 7 + success: boolean; 8 + data?: T; 9 + error?: string; 10 + details?: string; 11 + } 12 + 13 + /** 14 + * Get CORS headers based on request origin 15 + * Supports credentialed requests from extensions and localhost 16 + */ 17 + export function getCorsHeaders(origin?: string): Record<string, string> { 18 + // Allow all origins for non-credentialed requests (backward compatibility) 19 + if (!origin) { 20 + return { 21 + "Access-Control-Allow-Origin": "*", 22 + }; 23 + } 24 + 25 + // Check if origin is allowed for credentialed requests 26 + const allowedOrigins = [ 27 + "http://localhost:8888", 28 + "http://127.0.0.1:8888", 29 + "http://localhost:5173", 30 + "http://127.0.0.1:5173", 31 + "https://atlast.byarielm.fyi", 32 + ]; 33 + 34 + const isExtension = 35 + origin.startsWith("chrome-extension://") || 36 + origin.startsWith("moz-extension://"); 37 + const isAllowedOrigin = allowedOrigins.includes(origin); 38 + 39 + if (isExtension || isAllowedOrigin) { 40 + return { 41 + "Access-Control-Allow-Origin": origin, 42 + "Access-Control-Allow-Credentials": "true", 43 + }; 44 + } 45 + 46 + // Default to wildcard for unknown origins 47 + return { 48 + "Access-Control-Allow-Origin": "*", 49 + }; 50 + } 51 + 52 + /** 53 + * Create a success response 54 + */ 55 + export function createSuccessResponse<T>(data: T): ApiResponse<T> { 56 + return { 57 + success: true, 58 + data, 59 + }; 60 + } 61 + 62 + /** 63 + * Create an error response 64 + */ 65 + export function createErrorResponse( 66 + error: string, 67 + details?: string, 68 + ): ApiResponse { 69 + return { 70 + success: false, 71 + error, 72 + details, 73 + }; 74 + }
+9
packages/api/src/utils/string.utils.ts
··· 1 + /** 2 + * Normalize a string for comparison purposes 3 + * 4 + * @param str - The string to normalize 5 + * @returns Normalized lowercase string 6 + **/ 7 + export function normalize(str: string): string { 8 + return str.toLowerCase().replace(/[._-]/g, ""); 9 + }
+74
packages/api/src/utils/validation.utils.ts
··· 1 + import { z } from "zod"; 2 + import { ValidationError } from "../errors"; 3 + 4 + /** 5 + * Validation utility schemas using Zod 6 + * Provides type-safe validation with clear error messages 7 + */ 8 + 9 + /** 10 + * Generic array validation schema factory 11 + * @param itemSchema - Zod schema for array items 12 + * @param maxLength - Maximum array length 13 + * @param fieldName - Name of field for error messages 14 + */ 15 + export function createArraySchema<T extends z.ZodTypeAny>( 16 + itemSchema: T, 17 + maxLength: number, 18 + fieldName: string = "items", 19 + ) { 20 + return z 21 + .array(itemSchema) 22 + .min(1, `${fieldName} array is required and must not be empty`) 23 + .max(maxLength, `Maximum ${maxLength} ${fieldName} per batch`); 24 + } 25 + 26 + /** 27 + * Common validation schemas 28 + */ 29 + export const ValidationSchemas = { 30 + // DIDs array (max 100) 31 + didsArray: createArraySchema(z.string(), 100, "DIDs"), 32 + 33 + // Usernames array (max 50) 34 + usernamesArray: createArraySchema(z.string(), 50, "usernames"), 35 + 36 + // Generic string array with custom max 37 + stringArray: (maxLength: number, fieldName: string = "items") => 38 + createArraySchema(z.string(), maxLength, fieldName), 39 + }; 40 + 41 + /** 42 + * Validates input against a Zod schema and throws ValidationError on failure 43 + * @param schema - Zod schema to validate against 44 + * @param data - Data to validate 45 + * @returns Parsed and validated data 46 + * @throws ValidationError if validation fails 47 + */ 48 + export function validateInput<T>(schema: z.ZodSchema<T>, data: unknown): T { 49 + const result = schema.safeParse(data); 50 + 51 + if (!result.success) { 52 + // Extract first error message for cleaner API responses 53 + const firstError = result.error.issues[0]; 54 + const message = firstError.message; 55 + throw new ValidationError(message); 56 + } 57 + 58 + return result.data; 59 + } 60 + 61 + /** 62 + * Parses request body and validates array input 63 + * Common pattern: JSON.parse(body) -> extract array -> validate 64 + */ 65 + export function validateArrayInput<T>( 66 + body: string | null, 67 + fieldName: string, 68 + schema: z.ZodArray<any>, 69 + ): T[] { 70 + const parsed = JSON.parse(body || "{}"); 71 + const data = parsed[fieldName]; 72 + 73 + return validateInput(schema, data); 74 + }