@fydemy/cms 1.0.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2024-12-06
9
+
10
+ ### Added
11
+
12
+ - Initial stable release of @fydemy/cms
13
+ - File-based CMS for Next.js with markdown support
14
+ - GitHub integration for production deployments
15
+ - Simple authentication with username/password from environment variables
16
+ - TypeScript support with full type definitions
17
+ - Local and GitHub storage providers
18
+ - API handlers for content management (CRUD operations)
19
+ - Authentication middleware for protecting admin routes
20
+ - Session management with JWT
21
+ - File upload functionality
22
+ - Collection and directory listing utilities
23
+
24
+ ### Security
25
+
26
+ - Timing-safe password comparison using `crypto.timingSafeEqual` to prevent timing attacks
27
+ - Rate limiting on login endpoint (5 attempts per 15 minutes)
28
+ - Input validation and sanitization for all user inputs
29
+ - Path validation to prevent directory traversal attacks
30
+ - File size limits (10MB maximum)
31
+ - Frontmatter sanitization to prevent injection attacks
32
+ - Secure session cookies with httpOnly, sameSite, and secure flags
33
+ - Length limits on username (100 chars) and password (1000 chars) inputs
34
+ - Generic error messages to prevent username enumeration
35
+
36
+ ### Documentation
37
+
38
+ - Comprehensive README with installation and usage instructions
39
+ - JSDoc documentation for all public APIs
40
+ - Security policy and vulnerability reporting guidelines
41
+ - MIT License
42
+
43
+ ## [0.1.0] - 2024-12-05
44
+
45
+ ### Added
46
+
47
+ - Initial development release
48
+ - Basic file-based CMS functionality
49
+ - Simple authentication
50
+ - Markdown file operations
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Fydemy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,431 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ interface MarkdownData {
4
+ content: string;
5
+ data: Record<string, any>;
6
+ }
7
+ /**
8
+ * Parse markdown content with frontmatter
9
+ * @param rawContent - Raw markdown content to parse
10
+ * @returns Parsed markdown data with frontmatter and content
11
+ */
12
+ declare function parseMarkdown(rawContent: string): MarkdownData;
13
+ /**
14
+ * Convert data and content back to markdown with frontmatter
15
+ */
16
+ declare function stringifyMarkdown(data: Record<string, any>, content: string): string;
17
+ /**
18
+ * Read and parse a markdown file
19
+ * @param filePath - Path to the markdown file
20
+ * @returns Parsed markdown data
21
+ * @throws Error if file path is invalid or file is too large
22
+ */
23
+ declare function getMarkdownContent(filePath: string): Promise<MarkdownData>;
24
+ /**
25
+ * Write markdown file with frontmatter
26
+ * @param filePath - Path to the markdown file
27
+ * @param data - Frontmatter data object
28
+ * @param content - Markdown content
29
+ * @throws Error if file path is invalid or content is too large
30
+ */
31
+ declare function saveMarkdownContent(filePath: string, data: Record<string, any>, content: string): Promise<void>;
32
+ /**
33
+ * Delete a markdown file
34
+ * @param filePath - Path to the markdown file
35
+ * @throws Error if file path is invalid
36
+ */
37
+ declare function deleteMarkdownContent(filePath: string): Promise<void>;
38
+ /**
39
+ * List all markdown files in a directory
40
+ * @param directory - Directory path (optional, defaults to root)
41
+ * @returns Array of file paths
42
+ * @throws Error if directory path is invalid
43
+ */
44
+ declare function listMarkdownFiles(directory?: string): Promise<string[]>;
45
+ /**
46
+ * Check if a markdown file exists
47
+ * @param filePath - Path to the markdown file
48
+ * @returns True if file exists, false otherwise
49
+ * @throws Error if file path is invalid
50
+ */
51
+ declare function markdownFileExists(filePath: string): Promise<boolean>;
52
+
53
+ /**
54
+ * Represents a file or directory entry
55
+ */
56
+ interface FileEntry {
57
+ /** Relative path from base content directory */
58
+ path: string;
59
+ /** File or directory name */
60
+ name: string;
61
+ /** Type of entry */
62
+ type: "file" | "directory";
63
+ }
64
+ /**
65
+ * Interface for file storage operations
66
+ */
67
+ interface StorageProvider {
68
+ /** Read file content as string */
69
+ readFile(filePath: string): Promise<string>;
70
+ /** Write content to file, creating parent directories if needed */
71
+ writeFile(filePath: string, content: string): Promise<void>;
72
+ /** Delete a file */
73
+ deleteFile(filePath: string): Promise<void>;
74
+ /** List files in a directory */
75
+ listFiles(directory: string): Promise<FileEntry[]>;
76
+ /** Check if a file exists */
77
+ exists(filePath: string): Promise<boolean>;
78
+ /** Upload a binary file */
79
+ uploadFile(filePath: string, buffer: Buffer): Promise<string>;
80
+ }
81
+ /**
82
+ * Local file system storage (for development)
83
+ */
84
+ declare class LocalStorage implements StorageProvider {
85
+ private baseDir;
86
+ constructor(baseDir?: string);
87
+ private getFullPath;
88
+ readFile(filePath: string): Promise<string>;
89
+ writeFile(filePath: string, content: string): Promise<void>;
90
+ deleteFile(filePath: string): Promise<void>;
91
+ listFiles(directory: string): Promise<FileEntry[]>;
92
+ exists(filePath: string): Promise<boolean>;
93
+ uploadFile(filePath: string, buffer: Buffer): Promise<string>;
94
+ }
95
+ /**
96
+ * GitHub storage (for production)
97
+ */
98
+ declare class GitHubStorage implements StorageProvider {
99
+ private octokit;
100
+ private owner;
101
+ private repo;
102
+ private branch;
103
+ private baseDir;
104
+ constructor();
105
+ private getGitHubPath;
106
+ readFile(filePath: string): Promise<string>;
107
+ writeFile(filePath: string, content: string): Promise<void>;
108
+ deleteFile(filePath: string): Promise<void>;
109
+ listFiles(directory: string): Promise<FileEntry[]>;
110
+ exists(filePath: string): Promise<boolean>;
111
+ uploadFile(filePath: string, buffer: Buffer): Promise<string>;
112
+ }
113
+ /**
114
+ * Get the appropriate storage provider based on environment
115
+ */
116
+ declare function getStorageProvider(): StorageProvider;
117
+
118
+ /**
119
+ * List all files and directories in a directory
120
+ * @param directory - Relative path to the directory (default: root)
121
+ * @returns Array of file entries (files and directories)
122
+ */
123
+ declare function listDirectory(directory?: string): Promise<FileEntry[]>;
124
+
125
+ /**
126
+ * Base interface for collection items with flexible frontmatter
127
+ */
128
+ interface CollectionItem<T = Record<string, any>> {
129
+ /** The markdown content body */
130
+ content: string;
131
+ /** Frontmatter data with dynamic fields */
132
+ data: T;
133
+ /** File slug (filename without extension) */
134
+ slug: string;
135
+ }
136
+ /**
137
+ * Fetch all markdown files from a specific folder/collection
138
+ * @param folderName - The folder name under public/content (e.g., "blog", "pages")
139
+ * @returns Array of collection items with parsed frontmatter and content
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * // Fetch all blog posts
144
+ * const posts = await getCollectionItems("blog");
145
+ * posts.forEach(post => {
146
+ * console.log(post.data.title); // Access any frontmatter field
147
+ * console.log(post.slug); // Filename without .md
148
+ * });
149
+ *
150
+ * // With type inference for known fields
151
+ * interface BlogPost {
152
+ * title: string;
153
+ * date: string;
154
+ * author?: string;
155
+ * [key: string]: any; // Allow any other fields
156
+ * }
157
+ * const typedPosts = await getCollectionItems<BlogPost>("blog");
158
+ * ```
159
+ */
160
+ declare function getCollectionItems<T = Record<string, any>>(folderName: string): Promise<CollectionItem<T>[]>;
161
+ /**
162
+ * Fetch a single item from a collection by slug
163
+ * @param folderName - The folder name under public/content
164
+ * @param slug - The filename without .md extension
165
+ * @returns Single collection item or null if not found
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * // Fetch a specific blog post
170
+ * const post = await getCollectionItem("blog", "my-first-post");
171
+ * if (post) {
172
+ * console.log(post.data.title);
173
+ * }
174
+ * ```
175
+ */
176
+ declare function getCollectionItem<T = Record<string, any>>(folderName: string, slug: string): Promise<CollectionItem<T> | null>;
177
+ /**
178
+ * Get all unique folder names (collections) in the content directory
179
+ * @param baseDir - Base directory to scan (default: "")
180
+ * @returns Array of folder names
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const collections = await getCollections();
185
+ * // Returns: ["blog", "pages", "docs", ...]
186
+ * ```
187
+ */
188
+ declare function getCollections(baseDir?: string): Promise<string[]>;
189
+
190
+ /**
191
+ * Upload a file and return its public URL
192
+ * @param fileName - Original filename
193
+ * @param buffer - File content buffer
194
+ * @returns Public URL path to the uploaded file
195
+ */
196
+ declare function uploadFile(fileName: string, buffer: Buffer): Promise<string>;
197
+
198
+ /**
199
+ * Session payload structure for JWT
200
+ */
201
+ interface SessionPayload {
202
+ /** Username of the authenticated user */
203
+ username: string;
204
+ /** Expiration timestamp (Unix time) */
205
+ exp: number;
206
+ }
207
+ /**
208
+ * Create a new session token for the given username
209
+ * @param username - The username to create a session for
210
+ * @returns Signed JWT string
211
+ */
212
+ declare function createSession(username: string): Promise<string>;
213
+ /**
214
+ * Verify and decode a session token
215
+ * @param token - The JWT token to verify
216
+ * @returns Decoded payload if valid, null otherwise
217
+ */
218
+ declare function verifySession(token: string): Promise<SessionPayload | null>;
219
+ /**
220
+ * Get session from Next.js request cookies
221
+ */
222
+ declare function getSessionFromCookies(): Promise<SessionPayload | null>;
223
+ /**
224
+ * Set session cookie
225
+ */
226
+ declare function setSessionCookie(token: string): Promise<void>;
227
+ /**
228
+ * Clear session cookie
229
+ */
230
+ declare function clearSessionCookie(): Promise<void>;
231
+
232
+ /**
233
+ * Validate username and password against environment variables using timing-safe comparison
234
+ * @param username - Username to validate
235
+ * @param password - Password to validate
236
+ * @returns True if credentials are valid, false otherwise
237
+ * @throws Error if environment variables are not configured
238
+ */
239
+ declare function validateCredentials(username: string, password: string): boolean;
240
+
241
+ /**
242
+ * Login endpoint with rate limiting
243
+ */
244
+ declare function handleLogin(request: NextRequest): Promise<NextResponse<{
245
+ error: string;
246
+ }> | NextResponse<{
247
+ success: boolean;
248
+ }>>;
249
+ /**
250
+ * Logout endpoint
251
+ */
252
+ declare function handleLogout(): Promise<NextResponse<{
253
+ success: boolean;
254
+ }>>;
255
+ /**
256
+ * Get content endpoint
257
+ */
258
+ declare function handleGetContent(_request: NextRequest, filePath: string): Promise<NextResponse<{
259
+ error: string;
260
+ }> | NextResponse<MarkdownData>>;
261
+ /**
262
+ * Save content endpoint
263
+ */
264
+ declare function handleSaveContent(request: NextRequest, filePath: string): Promise<NextResponse<{
265
+ error: string;
266
+ }> | NextResponse<{
267
+ success: boolean;
268
+ }>>;
269
+ /**
270
+ * Delete content endpoint
271
+ */
272
+ declare function handleDeleteContent(_request: NextRequest, filePath: string): Promise<NextResponse<{
273
+ error: string;
274
+ }> | NextResponse<{
275
+ success: boolean;
276
+ }>>;
277
+ /**
278
+ * List files endpoint
279
+ */
280
+ declare function handleListFiles(directory?: string): Promise<NextResponse<{
281
+ error: string;
282
+ }> | NextResponse<{
283
+ entries: FileEntry[];
284
+ }>>;
285
+ /**
286
+ * Create API route handlers for Next.js App Router
287
+ */
288
+ declare function createContentApiHandlers(): {
289
+ GET(request: NextRequest, { params }: {
290
+ params: {
291
+ path: string[];
292
+ };
293
+ }): Promise<NextResponse<{
294
+ error: string;
295
+ }> | NextResponse<MarkdownData>>;
296
+ POST(request: NextRequest, { params }: {
297
+ params: {
298
+ path: string[];
299
+ };
300
+ }): Promise<NextResponse<{
301
+ error: string;
302
+ }> | NextResponse<{
303
+ success: boolean;
304
+ }>>;
305
+ DELETE(request: NextRequest, { params }: {
306
+ params: {
307
+ path: string[];
308
+ };
309
+ }): Promise<NextResponse<{
310
+ error: string;
311
+ }> | NextResponse<{
312
+ success: boolean;
313
+ }>>;
314
+ };
315
+ /**
316
+ * Create list API handlers
317
+ */
318
+ declare function createListApiHandlers(): {
319
+ GET(_request: NextRequest, { params }: {
320
+ params: {
321
+ path?: string[];
322
+ };
323
+ }): Promise<NextResponse<{
324
+ error: string;
325
+ }> | NextResponse<{
326
+ entries: FileEntry[];
327
+ }>>;
328
+ };
329
+
330
+ /**
331
+ * Handle file upload
332
+ */
333
+ declare function handleUpload(request: NextRequest): Promise<NextResponse<{
334
+ error: string;
335
+ }> | NextResponse<{
336
+ success: boolean;
337
+ url: string;
338
+ filename: string;
339
+ }>>;
340
+
341
+ /**
342
+ * Create middleware to protect admin routes
343
+ */
344
+ declare function createAuthMiddleware(options?: {
345
+ loginPath?: string;
346
+ protectedPaths?: string[];
347
+ }): (request: NextRequest) => Promise<NextResponse<unknown>>;
348
+
349
+ interface InitCMSConfig {
350
+ /** Directory where content is stored (default: "public/content") */
351
+ contentDir?: string;
352
+ }
353
+ /**
354
+ * Initialize CMS in a Next.js project
355
+ * Creates the content directory and an example markdown file.
356
+ * @param config - Configuration options
357
+ */
358
+ declare function initCMS(config?: InitCMSConfig): Promise<void>;
359
+
360
+ /**
361
+ * Maximum username length (prevents DoS)
362
+ */
363
+ declare const MAX_USERNAME_LENGTH = 100;
364
+ /**
365
+ * Maximum password length (prevents DoS)
366
+ */
367
+ declare const MAX_PASSWORD_LENGTH = 1000;
368
+ /**
369
+ * Maximum file size in bytes (10MB)
370
+ */
371
+ declare const MAX_FILE_SIZE: number;
372
+ /**
373
+ * Validate and sanitize file path to prevent directory traversal
374
+ * @param filePath - The file path to validate
375
+ * @returns Sanitized file path
376
+ * @throws Error if path is invalid or attempts directory traversal
377
+ */
378
+ declare function validateFilePath(filePath: string): string;
379
+ /**
380
+ * Validate username input
381
+ * @param username - The username to validate
382
+ * @returns True if valid
383
+ * @throws Error if invalid
384
+ */
385
+ declare function validateUsername(username: string): boolean;
386
+ /**
387
+ * Validate password input
388
+ * @param password - The password to validate
389
+ * @returns True if valid
390
+ * @throws Error if invalid
391
+ */
392
+ declare function validatePassword(password: string): boolean;
393
+ /**
394
+ * Validate file size
395
+ * @param size - File size in bytes
396
+ * @returns True if valid
397
+ * @throws Error if too large
398
+ */
399
+ declare function validateFileSize(size: number): boolean;
400
+ /**
401
+ * Sanitize frontmatter data to prevent injection
402
+ * @param data - The data object to sanitize
403
+ * @returns Sanitized data
404
+ */
405
+ declare function sanitizeFrontmatter(data: Record<string, any>): Record<string, any>;
406
+
407
+ /**
408
+ * Simple in-memory rate limiter for login attempts
409
+ */
410
+ /**
411
+ * Check if IP/identifier is rate limited
412
+ * @param identifier - IP address or other unique identifier
413
+ * @returns Object with isLimited and remaining attempts
414
+ */
415
+ declare function checkRateLimit(identifier: string): {
416
+ isLimited: boolean;
417
+ remaining: number;
418
+ resetTime: number;
419
+ };
420
+ /**
421
+ * Increment rate limit counter for identifier
422
+ * @param identifier - IP address or other unique identifier
423
+ */
424
+ declare function incrementRateLimit(identifier: string): void;
425
+ /**
426
+ * Reset rate limit for identifier (use after successful login)
427
+ * @param identifier - IP address or other unique identifier
428
+ */
429
+ declare function resetRateLimit(identifier: string): void;
430
+
431
+ export { type CollectionItem, type FileEntry, GitHubStorage, type InitCMSConfig, LocalStorage, MAX_FILE_SIZE, MAX_PASSWORD_LENGTH, MAX_USERNAME_LENGTH, type MarkdownData, type StorageProvider, checkRateLimit, clearSessionCookie, createAuthMiddleware, createContentApiHandlers, createListApiHandlers, createSession, deleteMarkdownContent, getCollectionItem, getCollectionItems, getCollections, getMarkdownContent, getSessionFromCookies, getStorageProvider, handleDeleteContent, handleGetContent, handleListFiles, handleLogin, handleLogout, handleSaveContent, handleUpload, incrementRateLimit, initCMS, listDirectory, listMarkdownFiles, markdownFileExists, parseMarkdown, resetRateLimit, sanitizeFrontmatter, saveMarkdownContent, setSessionCookie, stringifyMarkdown, uploadFile, validateCredentials, validateFilePath, validateFileSize, validatePassword, validateUsername, verifySession };