@xexr/blog-platform 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/app/blog/[slug]/page.d.ts +68 -0
  3. package/dist/app/blog/[slug]/page.tsx +155 -113
  4. package/dist/components/ArticleReadingProgress.tsx +208 -0
  5. package/dist/components/ArticleTopActions.tsx +179 -0
  6. package/dist/components/AuthorByline.tsx +569 -0
  7. package/dist/components/ExpandableArticleImage.tsx +79 -0
  8. package/dist/components/Pagination.tsx +236 -0
  9. package/dist/components/TopicArchivePages.tsx +204 -0
  10. package/dist/components/ui/badge.tsx +36 -0
  11. package/dist/components/ui/button.tsx +59 -0
  12. package/dist/components/ui/card.tsx +92 -0
  13. package/dist/components/ui/pagination.tsx +127 -0
  14. package/dist/components/ui/separator.tsx +29 -0
  15. package/dist/index.d.mts +59 -69
  16. package/dist/index.d.ts +59 -69
  17. package/dist/index.js +83 -8
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +83 -8
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/lib/article-ui.ts +158 -0
  22. package/dist/lib/auth.ts +354 -0
  23. package/dist/lib/cors.ts +243 -0
  24. package/dist/lib/database.ts +277 -0
  25. package/dist/lib/init.ts +186 -0
  26. package/dist/lib/mdx.ts +73 -0
  27. package/dist/lib/metadata.ts +558 -0
  28. package/dist/lib/ratelimit.ts +224 -0
  29. package/dist/lib/security-headers.ts +322 -0
  30. package/dist/lib/seo.ts +295 -0
  31. package/dist/lib/sitemap.ts +719 -0
  32. package/dist/lib/storage/drizzle.ts +400 -0
  33. package/dist/lib/structured-data.ts +608 -0
  34. package/dist/lib/topics.ts +56 -0
  35. package/dist/lib/urls.ts +29 -0
  36. package/dist/lib/utils.ts +6 -0
  37. package/dist/lib/version.ts +154 -0
  38. package/dist/types/contracts.ts +256 -0
  39. package/dist/types/index.d.mts +18 -1
  40. package/dist/types/index.d.ts +18 -1
  41. package/dist/types/index.ts +16 -0
  42. package/package.json +14 -9
@@ -0,0 +1,243 @@
1
+ /**
2
+ * CORS configuration for blog platform API endpoints
3
+ *
4
+ * This provides centralized CORS handling with security best practices
5
+ * for cross-origin requests from Bloggy core applications.
6
+ */
7
+
8
+ import type { NextRequest } from "next/server";
9
+ import { NextResponse } from "next/server";
10
+ import { getAPISecurityHeaders, type SecurityConfig } from "./security-headers";
11
+
12
+ /**
13
+ * CORS configuration interface
14
+ */
15
+ export interface CorsConfig {
16
+ /** Allowed origins (default: allow configured domains only) */
17
+ allowedOrigins?: string[];
18
+
19
+ /** Allow all origins with wildcard (default: false for security) */
20
+ allowWildcard?: boolean;
21
+
22
+ /** Allow credentials to be sent (default: false) */
23
+ allowCredentials?: boolean;
24
+
25
+ /** Cache preflight requests for this many seconds (default: 86400 = 24 hours) */
26
+ maxAge?: number;
27
+
28
+ /** Security headers configuration */
29
+ securityConfig?: SecurityConfig;
30
+ }
31
+
32
+ /**
33
+ * Default CORS configuration for blog platform
34
+ */
35
+ const DEFAULT_CORS_CONFIG: Required<CorsConfig> = {
36
+ allowedOrigins: [
37
+ "http://localhost:3000", // Development Bloggy core
38
+ "https://localhost:3000", // Development Bloggy core (HTTPS)
39
+ "http://127.0.0.1:3000", // Alternative localhost
40
+ "https://127.0.0.1:3000", // Alternative localhost (HTTPS)
41
+ ],
42
+ allowWildcard: false, // Security: don't allow all origins by default
43
+ allowCredentials: false, // Security: don't allow credentials by default
44
+ maxAge: 86400, // Cache preflight for 24 hours
45
+ securityConfig: {}, // Default security configuration
46
+ };
47
+
48
+ /**
49
+ * Get CORS configuration from environment or defaults
50
+ */
51
+ function getCorsConfig(): Required<CorsConfig> {
52
+ const envOrigins = process.env.BLOG_PLATFORM_ALLOWED_ORIGINS;
53
+ const allowWildcard = process.env.BLOG_PLATFORM_ALLOW_WILDCARD === "true";
54
+ const allowCredentials = process.env.BLOG_PLATFORM_ALLOW_CREDENTIALS === "true";
55
+
56
+ return {
57
+ ...DEFAULT_CORS_CONFIG,
58
+ allowedOrigins: envOrigins ? envOrigins.split(",").map(origin => origin.trim()) : DEFAULT_CORS_CONFIG.allowedOrigins,
59
+ allowWildcard,
60
+ allowCredentials,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Check if an origin is allowed based on CORS configuration
66
+ */
67
+ function isOriginAllowed(origin: string | null, config: Required<CorsConfig>): boolean {
68
+ if (!origin) {
69
+ return false;
70
+ }
71
+
72
+ // If wildcard is enabled, allow all origins
73
+ if (config.allowWildcard) {
74
+ return true;
75
+ }
76
+
77
+ // Check against allowed origins list
78
+ return config.allowedOrigins.includes(origin);
79
+ }
80
+
81
+ /**
82
+ * Get CORS headers for a given request and configuration
83
+ */
84
+ function getCorsHeaders(request: NextRequest, config: Required<CorsConfig>): Record<string, string> {
85
+ const origin = request.headers.get("origin");
86
+ const headers: Record<string, string> = {};
87
+
88
+ // Set allowed origin
89
+ if (config.allowWildcard) {
90
+ headers["Access-Control-Allow-Origin"] = "*";
91
+ } else if (origin && isOriginAllowed(origin, config)) {
92
+ headers["Access-Control-Allow-Origin"] = origin;
93
+ }
94
+
95
+ // Set credentials if allowed (only when not using wildcard)
96
+ if (config.allowCredentials && !config.allowWildcard) {
97
+ headers["Access-Control-Allow-Credentials"] = "true";
98
+ }
99
+
100
+ // Always include Vary header when not using wildcard
101
+ if (!config.allowWildcard) {
102
+ headers.Vary = "Origin";
103
+ }
104
+
105
+ return headers;
106
+ }
107
+
108
+ /**
109
+ * Get preflight CORS headers for OPTIONS requests
110
+ */
111
+ function getPreflightHeaders(
112
+ request: NextRequest,
113
+ config: Required<CorsConfig>,
114
+ allowedMethods: string[],
115
+ allowedHeaders: string[]
116
+ ): Record<string, string> {
117
+ const corsHeaders = getCorsHeaders(request, config);
118
+
119
+ return {
120
+ ...corsHeaders,
121
+ "Access-Control-Allow-Methods": allowedMethods.join(", "),
122
+ "Access-Control-Allow-Headers": allowedHeaders.join(", "),
123
+ "Access-Control-Max-Age": config.maxAge.toString(),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Standard allowed headers for blog platform API endpoints
129
+ */
130
+ export const STANDARD_ALLOWED_HEADERS = [
131
+ "Content-Type",
132
+ "X-Bloggy-Version",
133
+ "X-Idempotency-Key",
134
+ "Authorization",
135
+ "X-Requested-With",
136
+ ];
137
+
138
+ /**
139
+ * Create CORS-enabled response for API endpoints
140
+ */
141
+ export function createCorsResponse<T>(
142
+ request: NextRequest,
143
+ data: T,
144
+ options: {
145
+ status?: number;
146
+ headers?: Record<string, string>;
147
+ corsConfig?: CorsConfig;
148
+ } = {}
149
+ ): NextResponse<T> {
150
+ const { status = 200, headers = {}, corsConfig = {} } = options;
151
+ const config = { ...getCorsConfig(), ...corsConfig };
152
+
153
+ const corsHeaders = getCorsHeaders(request, config);
154
+ const securityHeaders = getAPISecurityHeaders(request, config.securityConfig);
155
+
156
+ const responseHeaders = {
157
+ "Content-Type": "application/json",
158
+ ...corsHeaders,
159
+ ...securityHeaders,
160
+ ...headers,
161
+ };
162
+
163
+ return NextResponse.json(data, {
164
+ status,
165
+ headers: responseHeaders,
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Handle CORS preflight OPTIONS requests
171
+ */
172
+ export function handleCorsOptions(
173
+ request: NextRequest,
174
+ allowedMethods: string[],
175
+ options: {
176
+ allowedHeaders?: string[];
177
+ corsConfig?: CorsConfig;
178
+ } = {}
179
+ ): NextResponse {
180
+ const { allowedHeaders = STANDARD_ALLOWED_HEADERS, corsConfig = {} } = options;
181
+ const config = { ...getCorsConfig(), ...corsConfig };
182
+
183
+ const origin = request.headers.get("origin");
184
+
185
+ // Check if origin is allowed
186
+ if (origin && !config.allowWildcard && !isOriginAllowed(origin, config)) {
187
+ const securityHeaders = getAPISecurityHeaders(request, config.securityConfig);
188
+ return new NextResponse(null, {
189
+ status: 403,
190
+ headers: {
191
+ "Content-Type": "text/plain",
192
+ ...securityHeaders,
193
+ },
194
+ });
195
+ }
196
+
197
+ const preflightHeaders = getPreflightHeaders(
198
+ request,
199
+ config,
200
+ [...allowedMethods, "OPTIONS"],
201
+ allowedHeaders
202
+ );
203
+
204
+ const securityHeaders = getAPISecurityHeaders(request, config.securityConfig);
205
+
206
+ return new NextResponse(null, {
207
+ status: 200,
208
+ headers: {
209
+ ...preflightHeaders,
210
+ ...securityHeaders,
211
+ },
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Middleware to add CORS headers to any response
217
+ */
218
+ export function addCorsHeaders(
219
+ request: NextRequest,
220
+ response: NextResponse,
221
+ corsConfig: CorsConfig = {}
222
+ ): NextResponse {
223
+ const config = { ...getCorsConfig(), ...corsConfig };
224
+ const corsHeaders = getCorsHeaders(request, config);
225
+ const securityHeaders = getAPISecurityHeaders(request, config.securityConfig);
226
+
227
+ // Add CORS and security headers to existing response
228
+ Object.entries({ ...corsHeaders, ...securityHeaders }).forEach(([key, value]) => {
229
+ response.headers.set(key, value);
230
+ });
231
+
232
+ return response;
233
+ }
234
+
235
+ /**
236
+ * Check if request origin is allowed (utility for manual CORS checking)
237
+ */
238
+ export function checkOriginAllowed(request: NextRequest, corsConfig: CorsConfig = {}): boolean {
239
+ const config = { ...getCorsConfig(), ...corsConfig };
240
+ const origin = request.headers.get("origin");
241
+
242
+ return isOriginAllowed(origin, config);
243
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Database interface for blog platform storage
3
+ *
4
+ * This provides a simple interface for storing published articles
5
+ * without coupling to any specific database implementation.
6
+ * Customers can implement this interface with their preferred database.
7
+ */
8
+
9
+ import type { PublishedArticle, PublishResult } from "@/types/contracts";
10
+
11
+ export interface BlogPlatformStorage {
12
+ /**
13
+ * Store a published article
14
+ */
15
+ saveArticle(
16
+ article: PublishedArticle,
17
+ idempotencyKey: string,
18
+ ): Promise<PublishResult>;
19
+
20
+ /**
21
+ * Get an article by slug
22
+ */
23
+ getArticle(slug: string): Promise<PublishedArticle | null>;
24
+
25
+ /**
26
+ * List all published articles
27
+ */
28
+ listArticles(options?: {
29
+ limit?: number;
30
+ offset?: number;
31
+ sortBy?: "publishedAt" | "updatedAt";
32
+ order?: "asc" | "desc";
33
+ }): Promise<{
34
+ articles: PublishedArticle[];
35
+ total: number;
36
+ }>;
37
+
38
+ /**
39
+ * Remove an article by slug
40
+ */
41
+ removeArticle(slug: string, idempotencyKey: string): Promise<PublishResult>;
42
+
43
+ /**
44
+ * Check if an operation was already performed (idempotency)
45
+ */
46
+ checkIdempotency(idempotencyKey: string): Promise<PublishResult | null>;
47
+
48
+ /**
49
+ * Store the result of an operation for idempotency
50
+ */
51
+ storeOperationResult(
52
+ idempotencyKey: string,
53
+ result: PublishResult,
54
+ ): Promise<void>;
55
+ }
56
+
57
+ /**
58
+ * Default in-memory storage implementation for development/testing
59
+ * IMPORTANT: This is not suitable for production use!
60
+ */
61
+ export class InMemoryStorage implements BlogPlatformStorage {
62
+ private articles = new Map<string, PublishedArticle>();
63
+ private operations = new Map<string, PublishResult>();
64
+
65
+ async saveArticle(
66
+ article: PublishedArticle,
67
+ idempotencyKey: string,
68
+ ): Promise<PublishResult> {
69
+ // Check for existing idempotency key
70
+ const existingResult = await this.checkIdempotency(idempotencyKey);
71
+
72
+ // CRITICAL FIX: Allow republish after unpublish
73
+ // If we found a cached operation result AND it was successful,
74
+ // check if the article still exists. If it was removed (unpublished),
75
+ // proceed with the save to allow republishing.
76
+ if (existingResult) {
77
+ if (existingResult.success) {
78
+ // Check if article still exists by finding it in the articles map
79
+ const articleStillExists = Array.from(this.articles.values()).some(
80
+ (a) => a.id === article.id,
81
+ );
82
+
83
+ // If article was removed, allow republish by not returning cached result
84
+ if (!articleStillExists) {
85
+ // Article was unpublished - proceed with save
86
+ } else {
87
+ // Article still exists - this is a true duplicate
88
+ return existingResult;
89
+ }
90
+ } else {
91
+ // Failed operations are always returned as-is
92
+ return existingResult;
93
+ }
94
+ }
95
+
96
+ // Validate slug uniqueness
97
+ if (this.articles.has(article.slug)) {
98
+ const result: PublishResult = {
99
+ success: false,
100
+ error: {
101
+ code: "PUB003",
102
+ message: "This URL is already in use. Please choose another.",
103
+ },
104
+ idempotencyKey,
105
+ };
106
+ await this.storeOperationResult(idempotencyKey, result);
107
+ return result;
108
+ }
109
+
110
+ // Store the article
111
+ this.articles.set(article.slug, article);
112
+
113
+ const result: PublishResult = {
114
+ success: true,
115
+ url: `/${article.slug}`,
116
+ platformPostId: article.id,
117
+ publishedAt: article.publishedAt,
118
+ idempotencyKey,
119
+ };
120
+
121
+ await this.storeOperationResult(idempotencyKey, result);
122
+ return result;
123
+ }
124
+
125
+ async getArticle(slug: string): Promise<PublishedArticle | null> {
126
+ return this.articles.get(slug) ?? null;
127
+ }
128
+
129
+ async listArticles(
130
+ options: {
131
+ limit?: number;
132
+ offset?: number;
133
+ sortBy?: "publishedAt" | "updatedAt";
134
+ order?: "asc" | "desc";
135
+ } = {},
136
+ ): Promise<{ articles: PublishedArticle[]; total: number }> {
137
+ const {
138
+ limit = 10,
139
+ offset = 0,
140
+ sortBy = "publishedAt",
141
+ order = "desc",
142
+ } = options;
143
+
144
+ const allArticles = Array.from(this.articles.values());
145
+
146
+ // Sort articles
147
+ allArticles.sort((a, b) => {
148
+ const aValue = new Date(a[sortBy]).getTime();
149
+ const bValue = new Date(b[sortBy]).getTime();
150
+ return order === "desc" ? bValue - aValue : aValue - bValue;
151
+ });
152
+
153
+ // Apply pagination
154
+ const articles = allArticles.slice(offset, offset + limit);
155
+
156
+ return {
157
+ articles,
158
+ total: allArticles.length,
159
+ };
160
+ }
161
+
162
+ async removeArticle(
163
+ slug: string,
164
+ idempotencyKey: string,
165
+ ): Promise<PublishResult> {
166
+ // Check for existing idempotency key
167
+ const existingResult = await this.checkIdempotency(idempotencyKey);
168
+
169
+ // CRITICAL FIX: Allow re-unpublish after republish
170
+ // If we found a cached operation result AND it was successful,
171
+ // check if the article NOW exists. If it was republished,
172
+ // proceed with the removal to allow re-unpublishing.
173
+ if (existingResult) {
174
+ if (existingResult.success) {
175
+ // Check if article exists NOW (might have been republished)
176
+ const articleExists = this.articles.has(slug);
177
+
178
+ // If article EXISTS, it was republished - allow re-unpublish
179
+ if (articleExists) {
180
+ // Article was republished - proceed with removal
181
+ } else {
182
+ // Article still removed - this is a true duplicate
183
+ return existingResult;
184
+ }
185
+ } else {
186
+ // Failed operations are always returned as-is
187
+ return existingResult;
188
+ }
189
+ }
190
+
191
+ // Check if article exists
192
+ if (!this.articles.has(slug)) {
193
+ const result: PublishResult = {
194
+ success: false,
195
+ error: {
196
+ code: "PUB007",
197
+ message: "Article not found",
198
+ },
199
+ idempotencyKey,
200
+ };
201
+ await this.storeOperationResult(idempotencyKey, result);
202
+ return result;
203
+ }
204
+
205
+ // Remove the article
206
+ this.articles.delete(slug);
207
+
208
+ const result: PublishResult = {
209
+ success: true,
210
+ idempotencyKey,
211
+ };
212
+
213
+ await this.storeOperationResult(idempotencyKey, result);
214
+ return result;
215
+ }
216
+
217
+ async checkIdempotency(
218
+ idempotencyKey: string,
219
+ ): Promise<PublishResult | null> {
220
+ return this.operations.get(idempotencyKey) ?? null;
221
+ }
222
+
223
+ async storeOperationResult(
224
+ idempotencyKey: string,
225
+ result: PublishResult,
226
+ ): Promise<void> {
227
+ this.operations.set(idempotencyKey, result);
228
+ }
229
+ }
230
+
231
+ type GlobalWithStorage = typeof globalThis & {
232
+ __BLOG_PLATFORM_STORAGE__?: BlogPlatformStorage;
233
+ __BLOG_PLATFORM_STORAGE_WARNED__?: boolean;
234
+ };
235
+
236
+ function warnAboutEphemeralStorage(): void {
237
+ const globalRef = globalThis as GlobalWithStorage;
238
+ if (globalRef.__BLOG_PLATFORM_STORAGE_WARNED__) {
239
+ return;
240
+ }
241
+
242
+ globalRef.__BLOG_PLATFORM_STORAGE_WARNED__ = true;
243
+
244
+ const message =
245
+ "Blog platform: No storage provider configured. Falling back to in-memory storage (data is not persisted).";
246
+ if (process.env.NODE_ENV === "production") {
247
+ console.error(
248
+ `${message} Call initBlogPlatform({ storage: /* your persistent adapter */ }) to avoid data loss.`,
249
+ );
250
+ } else {
251
+ console.warn(message);
252
+ }
253
+ }
254
+
255
+ function getGlobalStorage(): BlogPlatformStorage {
256
+ const globalRef = globalThis as GlobalWithStorage;
257
+ if (!globalRef.__BLOG_PLATFORM_STORAGE__) {
258
+ warnAboutEphemeralStorage();
259
+ globalRef.__BLOG_PLATFORM_STORAGE__ = new InMemoryStorage();
260
+ }
261
+ const storage = globalRef.__BLOG_PLATFORM_STORAGE__;
262
+ if (!storage) {
263
+ // This should be unreachable because we set it above,
264
+ // but the guard satisfies strict type checking without assertions.
265
+ throw new Error("Blog platform storage not initialized");
266
+ }
267
+ return storage;
268
+ }
269
+
270
+ export function getStorage(): BlogPlatformStorage {
271
+ return getGlobalStorage();
272
+ }
273
+
274
+ export function setStorage(storage: BlogPlatformStorage): void {
275
+ const globalRef = globalThis as GlobalWithStorage;
276
+ globalRef.__BLOG_PLATFORM_STORAGE__ = storage;
277
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Initialization utilities for the blog platform package
3
+ *
4
+ * This module provides convenient setup functions for customers
5
+ * to configure authentication, storage, and other platform features.
6
+ */
7
+
8
+ import {
9
+ setAuthConfig,
10
+ type AuthConfig,
11
+ NoAuth,
12
+ EnvAuth,
13
+ } from "./auth";
14
+ import { setStorage, type BlogPlatformStorage } from "./database";
15
+ import {
16
+ autoConfigureRateLimit,
17
+ configureRateLimit,
18
+ type RateLimitConfig,
19
+ } from "./ratelimit";
20
+
21
+ declare global {
22
+ // Using `var` here intentionally to declare a global on Node's globalThis
23
+ // across module boundaries. This is not a directive; remove stale disable.
24
+ var __BLOG_PLATFORM_INITIALISED__: boolean | undefined;
25
+ }
26
+
27
+ /**
28
+ * Configuration options for blog platform initialization
29
+ */
30
+ export interface BlogPlatformConfig {
31
+ /** Authentication configuration (REQUIRED) */
32
+ auth: AuthConfig;
33
+
34
+ /** Custom storage implementation */
35
+ storage?: BlogPlatformStorage;
36
+
37
+ /** Custom rate limiting configuration */
38
+ rateLimit?: RateLimitConfig;
39
+
40
+ /** Whether to automatically configure rate limiting from environment */
41
+ autoRateLimit?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Initialize the blog platform with custom configuration
46
+ *
47
+ * This should be called once during your app startup, typically in:
48
+ * - Next.js: middleware.ts or layout.tsx
49
+ * - Express: app startup
50
+ * - Other frameworks: main entry point
51
+ *
52
+ * @param config - Configuration options (auth is REQUIRED)
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Environment-based API key authentication
57
+ * initBlogPlatform({
58
+ * auth: new EnvAuth(), // Reads from BLOG_PLATFORM_API_KEYS
59
+ * });
60
+ *
61
+ * // Custom environment variable
62
+ * initBlogPlatform({
63
+ * auth: new EnvAuth("MY_BLOG_API_KEYS"),
64
+ * });
65
+ *
66
+ * // Custom authentication provider
67
+ * initBlogPlatform({
68
+ * auth: new CustomAuthProvider(),
69
+ * storage: new PostgresStorage(connectionString),
70
+ * rateLimit: {
71
+ * redisUrl: "https://...",
72
+ * redisToken: "...",
73
+ * requestsPerMinute: 10
74
+ * }
75
+ * });
76
+ *
77
+ * // Disable rate limiting for testing
78
+ * initBlogPlatform({
79
+ * auth: new NoAuth(), // Development only!
80
+ * autoRateLimit: false
81
+ * });
82
+ * ```
83
+ */
84
+ export function initBlogPlatform(config: BlogPlatformConfig): void {
85
+ const {
86
+ auth,
87
+ storage,
88
+ rateLimit,
89
+ autoRateLimit = true,
90
+ } = config;
91
+
92
+ // Validate required configuration
93
+ if (!auth) {
94
+ throw new Error(
95
+ "Blog platform initialization failed: 'auth' is required.\n" +
96
+ "Example: initBlogPlatform({ auth: new EnvAuth() })\n" +
97
+ "See https://github.com/xexr/blog-platform#authentication for more details."
98
+ );
99
+ }
100
+
101
+ // Set up authentication (REQUIRED)
102
+ console.log("Blog platform: Setting up authentication");
103
+ setAuthConfig(auth);
104
+
105
+ // Set up rate limiting
106
+ if (rateLimit) {
107
+ console.log("Blog platform: Using custom rate limiting configuration");
108
+ configureRateLimit(rateLimit);
109
+ } else if (autoRateLimit) {
110
+ console.log(
111
+ "Blog platform: Auto-configuring rate limiting from environment",
112
+ );
113
+ autoConfigureRateLimit();
114
+ } else {
115
+ console.log("Blog platform: Rate limiting disabled");
116
+ }
117
+
118
+ // Set up storage
119
+ if (storage) {
120
+ console.log("Blog platform: Using custom storage provider");
121
+ setStorage(storage);
122
+ } else {
123
+ console.log(
124
+ "Blog platform: Using default in-memory storage (not suitable for production)",
125
+ );
126
+ }
127
+
128
+ console.log("Blog platform initialized successfully");
129
+ globalThis.__BLOG_PLATFORM_INITIALISED__ = true;
130
+ }
131
+
132
+ /**
133
+ * Initialize the blog platform only once per process.
134
+ * Safe to call from multiple modules (e.g., layout + API routes).
135
+ *
136
+ * @param config - Configuration options (auth is REQUIRED)
137
+ */
138
+ export function initBlogPlatformOnce(config: BlogPlatformConfig): void {
139
+ if (globalThis.__BLOG_PLATFORM_INITIALISED__) {
140
+ console.log("Blog platform: Already initialized, skipping");
141
+ return;
142
+ }
143
+ console.log("Blog platform: Initializing blog platform once");
144
+ initBlogPlatform(config);
145
+ }
146
+
147
+ /**
148
+ * Quick setup for development/testing
149
+ * WARNING: Disables authentication - never use in production!
150
+ * Uses NoAuth (accepts all requests), in-memory storage, and disables rate limiting
151
+ */
152
+ export function initBlogPlatformDev(): void {
153
+ initBlogPlatform({
154
+ auth: new NoAuth(),
155
+ autoRateLimit: false
156
+ });
157
+ console.log(
158
+ "Blog platform: Development mode enabled (no auth, in-memory storage, no rate limiting)",
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Production setup with environment-based API keys and rate limiting
164
+ * Requires BLOG_PLATFORM_API_KEYS (or custom env var), UPSTASH_REDIS_REST_URL,
165
+ * and UPSTASH_REDIS_REST_TOKEN environment variables
166
+ *
167
+ * @param apiKeysEnvVar - Optional custom environment variable name for API keys (default: BLOG_PLATFORM_API_KEYS)
168
+ */
169
+ export function initBlogPlatformProd(apiKeysEnvVar?: string): void {
170
+ const envVar = apiKeysEnvVar ?? "BLOG_PLATFORM_API_KEYS";
171
+
172
+ if (!process.env[envVar]) {
173
+ throw new Error(
174
+ `Missing required environment variable: ${envVar}. ` +
175
+ "Set this to a comma-separated list of API keys for blog platform authentication.",
176
+ );
177
+ }
178
+
179
+ initBlogPlatform({
180
+ auth: new EnvAuth(envVar),
181
+ autoRateLimit: true,
182
+ });
183
+ console.log(
184
+ "Blog platform: Production mode enabled (environment-based auth, rate limiting)",
185
+ );
186
+ }