Skip to content

feat: create core TypeScript interfaces and types #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions types/api.ts
Original file line number Diff line number Diff line change
@@ -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<GistOptions>;
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
}
101 changes: 101 additions & 0 deletions types/binary.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
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")
);
}
65 changes: 65 additions & 0 deletions types/env.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
customMetadata?: Record<string, string>;
}

/**
* 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/",
};
89 changes: 89 additions & 0 deletions types/errors.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
};
status: number;
}

/**
* Application error class
*/
export class AppError extends Error {
constructor(
public code: ErrorCode,
public statusCode: number,
message: string,
public details?: Record<string, any>
) {
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<string, any>
): APIError {
return {
error: {
code,
message,
details,
},
status: statusCode,
};
}
18 changes: 18 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Loading