From c64405c5cd71d0a3d55aa719c340f3931bd3b3b6 Mon Sep 17 00:00:00 2001 From: Thanan Traiongthawon <95660+nullcoder@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:28:07 -0700 Subject: [PATCH] feat: create core TypeScript interfaces and types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GistMetadata interface with system fields and edit authentication - Add UserMetadata and FileMetadata for encrypted content - Add EncryptedData structure for secure data storage - Create comprehensive API request/response interfaces - Implement error types with standardized error codes - Define binary format types for efficient file encoding - Add Cloudflare Workers environment type definitions - Export all types from central index file All interfaces are fully documented with JSDoc comments and follow the data models defined in docs/SPEC.md. Types are edge-runtime compatible and ready for use across the application. Closes #26 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- types/api.ts | 69 +++++++++++++++++++++++++++++++++ types/binary.ts | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ types/env.ts | 65 +++++++++++++++++++++++++++++++ types/errors.ts | 89 ++++++++++++++++++++++++++++++++++++++++++ types/index.ts | 18 +++++++++ types/models.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 437 insertions(+) create mode 100644 types/api.ts create mode 100644 types/binary.ts create mode 100644 types/env.ts create mode 100644 types/errors.ts create mode 100644 types/index.ts create mode 100644 types/models.ts diff --git a/types/api.ts b/types/api.ts new file mode 100644 index 0000000..d6b4e49 --- /dev/null +++ b/types/api.ts @@ -0,0 +1,69 @@ +/** + * API request and response types for GhostPaste + */ + +import { File, GistOptions, GistMetadata } from "./models"; + +/** + * Request body for creating a new gist + */ +export interface CreateGistRequest { + files: File[]; + options?: GistOptions; +} + +/** + * Response from creating a new gist + */ +export interface CreateGistResponse { + id: string; + url: string; + expires_at?: string; +} + +/** + * Response from getting gist metadata + */ +export interface GetGistResponse { + metadata: GistMetadata; + decryption_key?: string; // Only included if requested via fragment +} + +/** + * Response from getting an encrypted blob + */ +export interface GetBlobResponse { + encrypted_data: string; // Base64 encoded encrypted blob + iv: string; // Base64 encoded initialization vector +} + +/** + * Request body for updating a gist + */ +export interface UpdateGistRequest { + files: File[]; + options?: Partial; + edit_pin: string; // Required for authentication +} + +/** + * Response from updating a gist + */ +export interface UpdateGistResponse { + success: boolean; + updated_at: string; +} + +/** + * Request headers for PIN authentication + */ +export interface AuthHeaders { + "X-Edit-Pin": string; +} + +/** + * Query parameters for API endpoints + */ +export interface GistQueryParams { + include_key?: boolean; // Include decryption key in response +} diff --git a/types/binary.ts b/types/binary.ts new file mode 100644 index 0000000..f1ab6e6 --- /dev/null +++ b/types/binary.ts @@ -0,0 +1,101 @@ +/** + * Binary format types for efficient file encoding + */ + +/** + * Binary format version for future compatibility + */ +export const BINARY_FORMAT_VERSION = 1; + +/** + * Magic number to identify binary format (4 bytes: "GPST") + */ +export const MAGIC_NUMBER = 0x47505354; // "GPST" in hex + +/** + * Binary format header structure + */ +export interface BinaryHeader { + magic: number; // 4 bytes: Magic number (0x47505354) + version: number; // 1 byte: Format version + fileCount: number; // 2 bytes: Number of files + totalSize: number; // 4 bytes: Total size of all files +} + +/** + * File entry in binary format + */ +export interface BinaryFileEntry { + nameLength: number; // 2 bytes: Length of filename + name: string; // Variable: UTF-8 encoded filename + contentLength: number; // 4 bytes: Length of file content + content: Uint8Array; // Variable: File content + languageLength: number; // 1 byte: Length of language string + language?: string; // Variable: Optional language identifier +} + +/** + * Complete binary format structure + */ +export interface BinaryFormat { + header: BinaryHeader; + files: BinaryFileEntry[]; +} + +/** + * Size limits for binary format + */ +export interface BinarySizeLimits { + maxFileSize: number; // 500KB per file + maxTotalSize: number; // 5MB total + maxFileCount: number; // 20 files maximum + maxFilenameLength: number; // 255 characters + maxLanguageLength: number; // 50 characters +} + +/** + * Default size limits + */ +export const DEFAULT_SIZE_LIMITS: BinarySizeLimits = { + maxFileSize: 500 * 1024, // 500KB + maxTotalSize: 5 * 1024 * 1024, // 5MB + maxFileCount: 20, + maxFilenameLength: 255, + maxLanguageLength: 50, +}; + +/** + * Binary encoding/decoding result + */ +export interface BinaryResult { + success: boolean; + data?: T; + error?: string; +} + +/** + * Type guards for binary format validation + */ +export function isBinaryHeader(obj: any): obj is BinaryHeader { + return ( + typeof obj === "object" && + obj !== null && + typeof obj.magic === "number" && + typeof obj.version === "number" && + typeof obj.fileCount === "number" && + typeof obj.totalSize === "number" + ); +} + +export function isBinaryFileEntry(obj: any): obj is BinaryFileEntry { + return ( + typeof obj === "object" && + obj !== null && + typeof obj.nameLength === "number" && + typeof obj.name === "string" && + typeof obj.contentLength === "number" && + obj.content instanceof Uint8Array && + typeof obj.languageLength === "number" && + (obj.language === undefined || typeof obj.language === "string") + ); +} diff --git a/types/env.ts b/types/env.ts new file mode 100644 index 0000000..7ae5426 --- /dev/null +++ b/types/env.ts @@ -0,0 +1,65 @@ +/** + * Environment types for Cloudflare Workers + */ + +import type { + R2Bucket, + KVNamespace, + AnalyticsEngineDataset, + ExecutionContext, +} from "@cloudflare/workers-types"; + +/** + * Cloudflare Workers environment bindings + */ +export interface Env { + // R2 bucket binding + GHOSTPASTE_BUCKET: R2Bucket; + + // Environment variables + ENVIRONMENT?: "development" | "production"; + + // Optional rate limiting (Cloudflare Workers KV) + RATE_LIMIT_KV?: KVNamespace; + + // Optional analytics (Cloudflare Analytics Engine) + ANALYTICS?: AnalyticsEngineDataset; +} + +/** + * Request context for Cloudflare Workers + */ +export interface RequestContext { + env: Env; + ctx: ExecutionContext; + request: Request; +} + +/** + * R2 object metadata + */ +export interface R2ObjectMetadata { + key: string; + size: number; + etag: string; + httpEtag: string; + uploaded: Date; + httpMetadata?: Record; + customMetadata?: Record; +} + +/** + * Configuration for R2 operations + */ +export interface R2Config { + gistPrefix: string; // Prefix for gist metadata objects + blobPrefix: string; // Prefix for encrypted blob objects +} + +/** + * Default R2 configuration + */ +export const DEFAULT_R2_CONFIG: R2Config = { + gistPrefix: "gists/", + blobPrefix: "blobs/", +}; diff --git a/types/errors.ts b/types/errors.ts new file mode 100644 index 0000000..0a14e4e --- /dev/null +++ b/types/errors.ts @@ -0,0 +1,89 @@ +/** + * Error types and error handling for GhostPaste + */ + +/** + * Standard error codes for the application + */ +export enum ErrorCode { + // Client errors (4xx) + BAD_REQUEST = "BAD_REQUEST", + UNAUTHORIZED = "UNAUTHORIZED", + FORBIDDEN = "FORBIDDEN", + NOT_FOUND = "NOT_FOUND", + CONFLICT = "CONFLICT", + PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE", + UNPROCESSABLE_ENTITY = "UNPROCESSABLE_ENTITY", + TOO_MANY_REQUESTS = "TOO_MANY_REQUESTS", + + // Server errors (5xx) + INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR", + SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE", + + // Application-specific errors + INVALID_ENCRYPTION_KEY = "INVALID_ENCRYPTION_KEY", + DECRYPTION_FAILED = "DECRYPTION_FAILED", + INVALID_PIN = "INVALID_PIN", + GIST_EXPIRED = "GIST_EXPIRED", + FILE_TOO_LARGE = "FILE_TOO_LARGE", + TOO_MANY_FILES = "TOO_MANY_FILES", + INVALID_BINARY_FORMAT = "INVALID_BINARY_FORMAT", + STORAGE_ERROR = "STORAGE_ERROR", +} + +/** + * Standardized API error response + */ +export interface APIError { + error: { + code: ErrorCode; + message: string; + details?: Record; + }; + status: number; +} + +/** + * Application error class + */ +export class AppError extends Error { + constructor( + public code: ErrorCode, + public statusCode: number, + message: string, + public details?: Record + ) { + super(message); + this.name = "AppError"; + } + + toAPIError(): APIError { + return { + error: { + code: this.code, + message: this.message, + details: this.details, + }, + status: this.statusCode, + }; + } +} + +/** + * Error response builder + */ +export function createErrorResponse( + code: ErrorCode, + message: string, + statusCode: number, + details?: Record +): APIError { + return { + error: { + code, + message, + details, + }, + status: statusCode, + }; +} diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..b9cadb7 --- /dev/null +++ b/types/index.ts @@ -0,0 +1,18 @@ +/** + * Central export file for all GhostPaste types + */ + +// Core data models +export * from "./models"; + +// API types +export * from "./api"; + +// Error types +export * from "./errors"; + +// Binary format types +export * from "./binary"; + +// Environment types +export * from "./env"; diff --git a/types/models.ts b/types/models.ts new file mode 100644 index 0000000..546586a --- /dev/null +++ b/types/models.ts @@ -0,0 +1,95 @@ +/** + * Core data models for GhostPaste + */ + +/** + * Metadata stored in R2 for each gist + * Mix of encrypted and unencrypted fields + */ +export interface GistMetadata { + // System fields (unencrypted) + id: string; + created_at: string; // ISO 8601 + updated_at: string; // ISO 8601 + expires_at?: string; // ISO 8601, optional expiry + version: 1; // Format version for future compatibility + + // Size information (unencrypted) + total_size: number; // Total size in bytes + blob_count: number; // Number of file blobs + + // Edit authentication (unencrypted) + edit_pin_hash?: string; // BCrypt hash of edit PIN + edit_pin_salt?: string; // Salt for PIN hashing + + // View restrictions (unencrypted) + one_time_view?: boolean; // Delete after first view + + // Editor preferences (unencrypted) + indent_mode?: "tabs" | "spaces"; + indent_size?: number; // 2, 4, 8 + wrap_mode?: "none" | "soft" | "hard"; + theme?: "light" | "dark" | "auto"; + + // Encrypted metadata + encrypted_metadata: EncryptedData; +} + +/** + * User-provided metadata (encrypted) + */ +export interface UserMetadata { + description?: string; // Optional gist description + files: FileMetadata[]; // Array of file information +} + +/** + * Metadata for individual files within a gist + */ +export interface FileMetadata { + name: string; // Original filename + size: number; // Size in bytes + language?: string; // Programming language + blob_id: string; // Reference to encrypted blob in R2 +} + +/** + * Structure for encrypted data + */ +export interface EncryptedData { + iv: string; // Base64 encoded initialization vector + data: string; // Base64 encoded ciphertext +} + +/** + * Individual file in a gist (for application use) + */ +export interface File { + name: string; + content: string; + language?: string; +} + +/** + * Options for creating a gist + */ +export interface GistOptions { + description?: string; + expiry?: "never" | "1hour" | "24hours" | "7days" | "30days"; + editPin?: string; + oneTimeView?: boolean; + indentMode?: "tabs" | "spaces"; + indentSize?: number; + wrapMode?: "none" | "soft" | "hard"; + theme?: "light" | "dark" | "auto"; +} + +/** + * Editor preferences configuration + */ +export interface EditorPreferences { + indentMode: "tabs" | "spaces"; + indentSize: number; + wrapMode: "none" | "soft" | "hard"; + theme: "light" | "dark" | "auto"; +}