diff --git a/lib/README.md b/lib/README.md index e2e8034..00398ee 100644 --- a/lib/README.md +++ b/lib/README.md @@ -6,10 +6,40 @@ This directory contains utility functions, business logic, and service modules. ``` lib/ +├── config.ts # Configuration management +├── constants.ts # Application constants and limits +├── feature-flags.ts # Feature flag system ├── utils.ts # General utility functions (cn, etc.) +├── config.test.ts # Configuration tests +├── constants.test.ts # Constants tests └── [future modules will be added here] ``` +## Implemented Modules + +### Configuration (`config.ts`) + +- Type-safe access to Cloudflare environment variables +- Feature flags based on available services +- Environment detection (development/production) +- Application URL helpers + +### Constants (`constants.ts`) + +- File size limits (500KB/file, 5MB/gist, 20 files) +- Expiry durations for gists +- HTTP status codes +- Validation helpers for files, PINs, and expiry options +- Binary format constants +- Cache control headers + +### Feature Flags (`feature-flags.ts`) + +- Advanced feature flag system with gradual rollout +- Percentage-based feature rollouts +- User-specific feature targeting +- Runtime feature toggling without deployments + ## Planned Modules (to be implemented) ### Core Modules @@ -24,7 +54,6 @@ lib/ - `logger.ts` - Structured logging utility - `errors.ts` - Custom error classes and handling - `validation.ts` - Input validation helpers -- `constants.ts` - Application constants and limits ## Conventions diff --git a/lib/config.test.ts b/lib/config.test.ts new file mode 100644 index 0000000..f27a0fe --- /dev/null +++ b/lib/config.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + getConfig, + isFeatureEnabled, + isDevelopment, + isProduction, +} from "./config"; +import type { Env } from "@/types"; + +// Mock the Cloudflare context +vi.mock("@opennextjs/cloudflare", () => ({ + getCloudflareContext: vi.fn(), +})); + +describe("Configuration Management", () => { + const mockEnv: Env = { + GHOSTPASTE_BUCKET: {} as R2Bucket, + NEXT_PUBLIC_APP_URL: "https://ghostpaste.dev", + ENVIRONMENT: "production", + }; + + beforeEach(async () => { + vi.clearAllMocks(); + const { getCloudflareContext } = vi.mocked( + await import("@opennextjs/cloudflare") + ); + getCloudflareContext.mockResolvedValue({ + env: mockEnv, + ctx: {} as ExecutionContext, + cf: {}, + }); + }); + + describe("getConfig", () => { + it("should return configuration with all required fields", async () => { + const config = await getConfig(); + + expect(config).toMatchObject({ + appUrl: "https://ghostpaste.dev", + environment: "production", + r2Bucket: mockEnv.GHOSTPASTE_BUCKET, + rateLimitKV: undefined, + analytics: undefined, + }); + }); + + it("should enable core features by default", async () => { + const config = await getConfig(); + + expect(config.features.rateLimit).toBe(false); // Not yet enabled + expect(config.features.analytics).toBe(false); // Not yet enabled + expect(config.features.oneTimeView).toBe(true); + expect(config.features.editPin).toBe(true); + expect(config.features.expiry).toBe(true); + }); + }); + + describe("Feature checks", () => { + it("should correctly identify enabled features", async () => { + const config = await getConfig(); + + expect(isFeatureEnabled(config, "rateLimit")).toBe(false); + expect(isFeatureEnabled(config, "analytics")).toBe(false); + expect(isFeatureEnabled(config, "oneTimeView")).toBe(true); + }); + }); + + describe("Environment checks", () => { + it("should correctly identify production environment", async () => { + const config = await getConfig(); + + expect(isDevelopment(config)).toBe(false); + expect(isProduction(config)).toBe(true); + }); + + it("should correctly identify development environment", async () => { + const { getCloudflareContext } = vi.mocked( + await import("@opennextjs/cloudflare") + ); + getCloudflareContext.mockResolvedValue({ + env: { ...mockEnv, ENVIRONMENT: "development" }, + ctx: {} as ExecutionContext, + cf: {}, + }); + + const config = await getConfig(); + + expect(isDevelopment(config)).toBe(true); + expect(isProduction(config)).toBe(false); + }); + }); +}); diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..0ed0f2a --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,120 @@ +/** + * Configuration management for GhostPaste + * Provides type-safe access to environment variables and settings + */ + +import { getCloudflareContext } from "@opennextjs/cloudflare"; + +/** + * Application configuration interface + */ +export interface AppConfig { + // Application + appUrl: string; + environment: "development" | "production"; + + // Storage + r2Bucket: R2Bucket; + + // Optional services + rateLimitKV?: KVNamespace; + analytics?: AnalyticsEngineDataset; + + // Feature flags + features: { + rateLimit: boolean; + analytics: boolean; + oneTimeView: boolean; + editPin: boolean; + expiry: boolean; + }; +} + +/** + * Get the current environment + */ +function getEnvironment(): "development" | "production" { + if (process.env.NODE_ENV === "development") { + return "development"; + } + // In Cloudflare Workers, we check the URL + if (typeof globalThis !== "undefined" && "location" in globalThis) { + const hostname = globalThis.location?.hostname || ""; + if (hostname.includes("localhost") || hostname.includes("127.0.0.1")) { + return "development"; + } + } + return "production"; +} + +/** + * Get application configuration from Cloudflare environment + * This function must be called within a request context + */ +export async function getConfig(): Promise { + const { env } = await getCloudflareContext(); + + const environment = env.ENVIRONMENT || getEnvironment(); + + return { + // Application + appUrl: env.NEXT_PUBLIC_APP_URL || "https://ghostpaste.dev", + environment, + + // Storage + r2Bucket: env.GHOSTPASTE_BUCKET, + + // Optional services - will be undefined until added to wrangler.toml + rateLimitKV: undefined, // env.RATE_LIMIT_KV when enabled + analytics: undefined, // env.ANALYTICS when enabled + + // Feature flags - can be toggled based on environment + features: { + rateLimit: false, // Will be !!env.RATE_LIMIT_KV when enabled + analytics: false, // Will be !!env.ANALYTICS when enabled + oneTimeView: true, // Always enabled + editPin: true, // Always enabled + expiry: true, // Always enabled + }, + }; +} + +/** + * Type guard to check if a feature is enabled + */ +export function isFeatureEnabled( + config: AppConfig, + feature: keyof AppConfig["features"] +): boolean { + return config.features[feature]; +} + +/** + * Get the base URL for the application + */ +export function getBaseUrl(config: AppConfig): string { + return config.appUrl; +} + +/** + * Get the API URL for the application + */ +export function getApiUrl(config: AppConfig, path: string): string { + const baseUrl = getBaseUrl(config); + const cleanPath = path.startsWith("/") ? path : `/${path}`; + return `${baseUrl}/api${cleanPath}`; +} + +/** + * Check if running in development + */ +export function isDevelopment(config: AppConfig): boolean { + return config.environment === "development"; +} + +/** + * Check if running in production + */ +export function isProduction(config: AppConfig): boolean { + return config.environment === "production"; +} diff --git a/lib/constants.test.ts b/lib/constants.test.ts new file mode 100644 index 0000000..0aa51c9 --- /dev/null +++ b/lib/constants.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from "vitest"; +import { + FILE_LIMITS, + GIST_LIMITS, + EXPIRY_DURATIONS, + isValidFileSize, + isValidTotalSize, + isValidFileCount, + isValidFilename, + isValidPin, + isValidExpiryOption, + getExpiryDate, + isExpired, +} from "./constants"; + +describe("Constants and Validation", () => { + describe("File validation", () => { + it("should validate file sizes correctly", () => { + expect(isValidFileSize(100)).toBe(true); + expect(isValidFileSize(FILE_LIMITS.MAX_FILE_SIZE)).toBe(true); + expect(isValidFileSize(FILE_LIMITS.MAX_FILE_SIZE + 1)).toBe(false); + expect(isValidFileSize(0)).toBe(false); + expect(isValidFileSize(-1)).toBe(false); + }); + + it("should validate total sizes correctly", () => { + expect(isValidTotalSize(1000)).toBe(true); + expect(isValidTotalSize(FILE_LIMITS.MAX_TOTAL_SIZE)).toBe(true); + expect(isValidTotalSize(FILE_LIMITS.MAX_TOTAL_SIZE + 1)).toBe(false); + expect(isValidTotalSize(0)).toBe(false); + }); + + it("should validate file counts correctly", () => { + expect(isValidFileCount(1)).toBe(true); + expect(isValidFileCount(FILE_LIMITS.MAX_FILE_COUNT)).toBe(true); + expect(isValidFileCount(FILE_LIMITS.MAX_FILE_COUNT + 1)).toBe(false); + expect(isValidFileCount(0)).toBe(false); + }); + + it("should validate filenames correctly", () => { + expect(isValidFilename("test.js")).toBe(true); + expect(isValidFilename("a".repeat(FILE_LIMITS.MAX_FILENAME_LENGTH))).toBe( + true + ); + expect( + isValidFilename("a".repeat(FILE_LIMITS.MAX_FILENAME_LENGTH + 1)) + ).toBe(false); + expect(isValidFilename("")).toBe(false); + expect(isValidFilename("test/file.js")).toBe(false); + expect(isValidFilename("test\\file.js")).toBe(false); + }); + }); + + describe("PIN validation", () => { + it("should validate PINs correctly", () => { + expect(isValidPin("1234")).toBe(true); + expect(isValidPin("a".repeat(GIST_LIMITS.MIN_PIN_LENGTH))).toBe(true); + expect(isValidPin("a".repeat(GIST_LIMITS.MAX_PIN_LENGTH))).toBe(true); + expect(isValidPin("123")).toBe(false); + expect(isValidPin("a".repeat(GIST_LIMITS.MAX_PIN_LENGTH + 1))).toBe( + false + ); + }); + }); + + describe("Expiry validation", () => { + it("should validate expiry options correctly", () => { + expect(isValidExpiryOption("never")).toBe(true); + expect(isValidExpiryOption("1hour")).toBe(true); + expect(isValidExpiryOption("24hours")).toBe(true); + expect(isValidExpiryOption("7days")).toBe(true); + expect(isValidExpiryOption("30days")).toBe(true); + expect(isValidExpiryOption("invalid")).toBe(false); + }); + + it("should calculate expiry dates correctly", () => { + const now = Date.now(); + + expect(getExpiryDate("never")).toBe(null); + + const oneHour = getExpiryDate("1hour"); + expect(oneHour).toBeInstanceOf(Date); + expect(oneHour!.getTime() - now).toBeCloseTo( + EXPIRY_DURATIONS["1hour"]!, + -3 + ); + + const sevenDays = getExpiryDate("7days"); + expect(sevenDays).toBeInstanceOf(Date); + expect(sevenDays!.getTime() - now).toBeCloseTo( + EXPIRY_DURATIONS["7days"]!, + -3 + ); + }); + + it("should check expiry correctly", () => { + const future = new Date(Date.now() + 1000).toISOString(); + const past = new Date(Date.now() - 1000).toISOString(); + + expect(isExpired(null)).toBe(false); + expect(isExpired(undefined)).toBe(false); + expect(isExpired(future)).toBe(false); + expect(isExpired(past)).toBe(true); + }); + }); + + describe("Constants values", () => { + it("should have correct file limits", () => { + expect(FILE_LIMITS.MAX_FILE_SIZE).toBe(500 * 1024); + expect(FILE_LIMITS.MAX_TOTAL_SIZE).toBe(5 * 1024 * 1024); + expect(FILE_LIMITS.MAX_FILE_COUNT).toBe(20); + }); + + it("should have correct expiry durations", () => { + expect(EXPIRY_DURATIONS.never).toBe(null); + expect(EXPIRY_DURATIONS["1hour"]).toBe(60 * 60 * 1000); + expect(EXPIRY_DURATIONS["24hours"]).toBe(24 * 60 * 60 * 1000); + }); + }); +}); diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 0000000..5736072 --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1,188 @@ +/** + * Application constants and limits for GhostPaste + */ + +import { + DEFAULT_SIZE_LIMITS, + MAGIC_NUMBER, + BINARY_FORMAT_VERSION, + DEFAULT_R2_CONFIG, +} from "@/types"; + +/** + * File size limits + * Re-exported from types for convenience + */ +export const FILE_LIMITS = { + MAX_FILE_SIZE: DEFAULT_SIZE_LIMITS.maxFileSize, + MAX_TOTAL_SIZE: DEFAULT_SIZE_LIMITS.maxTotalSize, + MAX_FILE_COUNT: DEFAULT_SIZE_LIMITS.maxFileCount, + MAX_FILENAME_LENGTH: DEFAULT_SIZE_LIMITS.maxFilenameLength, + MAX_LANGUAGE_LENGTH: DEFAULT_SIZE_LIMITS.maxLanguageLength, +} as const; + +/** + * Gist metadata limits + */ +export const GIST_LIMITS = { + MAX_DESCRIPTION_LENGTH: 1000, // Maximum description length + MIN_PIN_LENGTH: 4, // Minimum PIN length + MAX_PIN_LENGTH: 20, // Maximum PIN length +} as const; + +/** + * Expiry options and their durations in milliseconds + */ +export const EXPIRY_DURATIONS = { + never: null, + "1hour": 60 * 60 * 1000, + "24hours": 24 * 60 * 60 * 1000, + "7days": 7 * 24 * 60 * 60 * 1000, + "30days": 30 * 24 * 60 * 60 * 1000, +} as const; + +export type ExpiryOption = keyof typeof EXPIRY_DURATIONS; + +/** + * Default editor preferences + */ +export const DEFAULT_EDITOR_PREFERENCES = { + indentMode: "spaces" as const, + indentSize: 2, + wrapMode: "soft" as const, + theme: "auto" as const, +}; + +/** + * Supported indent sizes + */ +export const INDENT_SIZES = [2, 4, 8] as const; + +/** + * Encryption constants + */ +export const ENCRYPTION = { + ALGORITHM: "AES-GCM", + KEY_LENGTH: 256, // bits + IV_LENGTH: 12, // bytes + SALT_LENGTH: 16, // bytes + ITERATIONS: 100000, // PBKDF2 iterations + TAG_LENGTH: 16, // bytes +} as const; + +/** + * Binary format constants + * Using values from types/binary.ts + */ +export const BINARY_FORMAT = { + MAGIC_NUMBER, + VERSION: BINARY_FORMAT_VERSION, + HEADER_SIZE: 11, // bytes (4 + 1 + 2 + 4) +} as const; + +/** + * R2 storage prefixes + * Using values from types/env.ts + */ +export const STORAGE_PREFIXES = { + GISTS: DEFAULT_R2_CONFIG.gistPrefix, + BLOBS: DEFAULT_R2_CONFIG.blobPrefix, +} as const; + +/** + * HTTP status codes + */ +export const HTTP_STATUS = { + OK: 200, + CREATED: 201, + NO_CONTENT: 204, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + PAYLOAD_TOO_LARGE: 413, + UNPROCESSABLE_ENTITY: 422, + TOO_MANY_REQUESTS: 429, + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, +} as const; + +/** + * Rate limiting defaults + */ +export const RATE_LIMITS = { + REQUESTS_PER_MINUTE: 60, + REQUESTS_PER_HOUR: 300, + CREATE_PER_HOUR: 10, +} as const; + +/** + * Cache control headers + */ +export const CACHE_CONTROL = { + NO_STORE: "no-store", + PRIVATE_NO_CACHE: "private, no-cache", + PUBLIC_IMMUTABLE: "public, max-age=31536000, immutable", + PUBLIC_SHORT: "public, max-age=300", +} as const; + +/** + * Content types + */ +export const CONTENT_TYPES = { + JSON: "application/json", + OCTET_STREAM: "application/octet-stream", + TEXT_PLAIN: "text/plain", + TEXT_HTML: "text/html", +} as const; + +/** + * Validation helpers + */ +export function isValidFileSize(size: number): boolean { + return size > 0 && size <= FILE_LIMITS.MAX_FILE_SIZE; +} + +export function isValidTotalSize(size: number): boolean { + return size > 0 && size <= FILE_LIMITS.MAX_TOTAL_SIZE; +} + +export function isValidFileCount(count: number): boolean { + return count > 0 && count <= FILE_LIMITS.MAX_FILE_COUNT; +} + +export function isValidFilename(filename: string): boolean { + return ( + filename.length > 0 && + filename.length <= FILE_LIMITS.MAX_FILENAME_LENGTH && + !filename.includes("/") && + !filename.includes("\\") + ); +} + +export function isValidPin(pin: string): boolean { + return ( + pin.length >= GIST_LIMITS.MIN_PIN_LENGTH && + pin.length <= GIST_LIMITS.MAX_PIN_LENGTH + ); +} + +export function isValidExpiryOption(expiry: string): expiry is ExpiryOption { + return expiry in EXPIRY_DURATIONS; +} + +export function getExpiryDate(option: ExpiryOption): Date | null { + const duration = EXPIRY_DURATIONS[option]; + if (duration === null) { + return null; + } + return new Date(Date.now() + duration); +} + +export function isExpired(expiresAt: string | null | undefined): boolean { + if (!expiresAt) { + return false; + } + return new Date(expiresAt) <= new Date(); +} diff --git a/lib/feature-flags.ts b/lib/feature-flags.ts new file mode 100644 index 0000000..523d81f --- /dev/null +++ b/lib/feature-flags.ts @@ -0,0 +1,187 @@ +/** + * Feature flags system for GhostPaste + * Allows toggling features without code changes + */ + +import { getCloudflareContext } from "@opennextjs/cloudflare"; +import type { Env } from "@/types"; + +/** + * Feature flag names + */ +export enum Feature { + // Core features + ONE_TIME_VIEW = "one_time_view", + EDIT_PIN = "edit_pin", + EXPIRY = "expiry", + + // Service features + RATE_LIMIT = "rate_limit", + ANALYTICS = "analytics", + + // UI features + DARK_MODE = "dark_mode", + SYNTAX_HIGHLIGHTING = "syntax_highlighting", + MARKDOWN_PREVIEW = "markdown_preview", + + // Experimental features + COLLABORATIVE_EDITING = "collaborative_editing", + VERSION_HISTORY = "version_history", + CUSTOM_THEMES = "custom_themes", +} + +/** + * Feature flag configuration + */ +interface FeatureConfig { + enabled: boolean; + rolloutPercentage?: number; // 0-100 + allowedUsers?: string[]; // User IDs for gradual rollout + minVersion?: string; // Minimum app version +} + +/** + * Default feature flag configuration + */ +const DEFAULT_FEATURES: Record = { + // Core features - always enabled + [Feature.ONE_TIME_VIEW]: { enabled: true }, + [Feature.EDIT_PIN]: { enabled: true }, + [Feature.EXPIRY]: { enabled: true }, + + // Service features - depend on bindings + [Feature.RATE_LIMIT]: { enabled: false }, // Enabled if KV is available + [Feature.ANALYTICS]: { enabled: false }, // Enabled if Analytics is available + + // UI features - enabled by default + [Feature.DARK_MODE]: { enabled: true }, + [Feature.SYNTAX_HIGHLIGHTING]: { enabled: true }, + [Feature.MARKDOWN_PREVIEW]: { enabled: true }, + + // Experimental features - disabled by default + [Feature.COLLABORATIVE_EDITING]: { enabled: false, rolloutPercentage: 0 }, + [Feature.VERSION_HISTORY]: { enabled: false, rolloutPercentage: 0 }, + [Feature.CUSTOM_THEMES]: { enabled: false, rolloutPercentage: 0 }, +}; + +/** + * Feature flags manager + */ +export class FeatureFlags { + private features: Record; + private env: Env; + + constructor(env: Env) { + this.env = env; + this.features = this.initializeFeatures(); + } + + /** + * Initialize features based on environment + */ + private initializeFeatures(): Record { + const features = { ...DEFAULT_FEATURES }; + + // Enable service features based on bindings + // These will be enabled when the bindings are added to wrangler.toml + // For now, check if the properties exist on the env object + if ("RATE_LIMIT_KV" in this.env && this.env.RATE_LIMIT_KV) { + features[Feature.RATE_LIMIT].enabled = true; + } + + if ("ANALYTICS" in this.env && this.env.ANALYTICS) { + features[Feature.ANALYTICS].enabled = true; + } + + // Override features from environment variables if available + // In production, these could come from a KV store or external service + + return features; + } + + /** + * Check if a feature is enabled + */ + isEnabled(feature: Feature, userId?: string): boolean { + const config = this.features[feature]; + + if (!config || !config.enabled) { + return false; + } + + // Check user-specific rollout + if (config.allowedUsers && userId) { + return config.allowedUsers.includes(userId); + } + + // Check percentage rollout + if ( + config.rolloutPercentage !== undefined && + config.rolloutPercentage < 100 + ) { + // Simple hash-based rollout + const hash = this.hashString(userId || "anonymous"); + const percentage = (hash % 100) + 1; + return percentage <= config.rolloutPercentage; + } + + return true; + } + + /** + * Get all enabled features + */ + getEnabledFeatures(): Feature[] { + return Object.entries(this.features) + .filter(([_, config]) => config.enabled) + .map(([feature]) => feature as Feature); + } + + /** + * Get feature configuration + */ + getFeatureConfig(feature: Feature): FeatureConfig | undefined { + return this.features[feature]; + } + + /** + * Update feature configuration (for testing/development) + */ + setFeatureConfig(feature: Feature, config: Partial): void { + if (this.features[feature]) { + this.features[feature] = { ...this.features[feature], ...config }; + } + } + + /** + * Simple hash function for rollout + */ + private hashString(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash); + } +} + +/** + * Get feature flags instance + * Must be called within a request context + */ +export async function getFeatureFlags(): Promise { + const { env } = await getCloudflareContext(); + return new FeatureFlags(env); +} + +/** + * React hook-friendly feature check + * Returns a function that can be used in components + */ +export async function createFeatureChecker() { + const flags = await getFeatureFlags(); + return (feature: Feature, userId?: string) => + flags.isEnabled(feature, userId); +} diff --git a/types/env.ts b/types/env.ts index 7ae5426..5374667 100644 --- a/types/env.ts +++ b/types/env.ts @@ -3,22 +3,16 @@ */ import type { - R2Bucket, KVNamespace, AnalyticsEngineDataset, ExecutionContext, } from "@cloudflare/workers-types"; /** - * Cloudflare Workers environment bindings + * Extended environment interface for future features + * This represents the full environment once all features are enabled */ -export interface Env { - // R2 bucket binding - GHOSTPASTE_BUCKET: R2Bucket; - - // Environment variables - ENVIRONMENT?: "development" | "production"; - +export interface ExtendedEnv extends CloudflareEnv { // Optional rate limiting (Cloudflare Workers KV) RATE_LIMIT_KV?: KVNamespace; @@ -26,6 +20,12 @@ export interface Env { ANALYTICS?: AnalyticsEngineDataset; } +/** + * Type alias for the current environment + * Uses CloudflareEnv which is auto-generated from wrangler.toml + */ +export type Env = CloudflareEnv; + /** * Request context for Cloudflare Workers */ diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index b604596..d591be8 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,9 +1,13 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv` (hash: de33e4e6d3a08e93da2613a2d70f104c) +// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv` (hash: 8938a748588cd634218f61ad1e5d0b7b) // Runtime types generated with workerd@1.20250525.0 2024-12-01 nodejs_compat declare namespace Cloudflare { interface Env { - NEXT_PUBLIC_APP_URL: "https://ghostpaste.dev"; + NEXT_PUBLIC_APP_URL: + | "https://ghostpaste.dev" + | "http://localhost:3000" + | "https://staging.ghostpaste.dev"; + ENVIRONMENT: "production" | "development"; GHOSTPASTE_BUCKET: R2Bucket; } } diff --git a/wrangler.toml b/wrangler.toml index 30c4f4c..755dae3 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -20,7 +20,27 @@ bucket_name = "ghostpaste-bucket" # Environment variables [vars] NEXT_PUBLIC_APP_URL = "https://ghostpaste.dev" +ENVIRONMENT = "production" + +# KV namespace for rate limiting (optional) +# Uncomment when ready to enable rate limiting +# [[kv_namespaces]] +# binding = "RATE_LIMIT_KV" +# id = "YOUR_KV_NAMESPACE_ID" + +# Analytics Engine (optional) +# Uncomment when ready to enable analytics +# [[analytics_engine_datasets]] +# binding = "ANALYTICS" # Scheduled worker for expiry cleanup (will be added later) # [[triggers]] -# crons = ["0 * * * *"] # Every hour \ No newline at end of file +# crons = ["0 * * * *"] # Every hour + +# Development environment overrides +[env.development] +vars = { ENVIRONMENT = "development", NEXT_PUBLIC_APP_URL = "http://localhost:3000" } + +# Staging environment (optional) +[env.staging] +vars = { ENVIRONMENT = "production", NEXT_PUBLIC_APP_URL = "https://staging.ghostpaste.dev" } \ No newline at end of file