@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/dist/index.js ADDED
@@ -0,0 +1,934 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/utils/rate-limit.ts
34
+ var rate_limit_exports = {};
35
+ __export(rate_limit_exports, {
36
+ checkRateLimit: () => checkRateLimit,
37
+ cleanupRateLimitStore: () => cleanupRateLimitStore,
38
+ incrementRateLimit: () => incrementRateLimit,
39
+ resetRateLimit: () => resetRateLimit
40
+ });
41
+ function checkRateLimit(identifier) {
42
+ const now = Date.now();
43
+ const entry = rateLimitStore.get(identifier);
44
+ if (!entry || now > entry.resetTime) {
45
+ rateLimitStore.set(identifier, {
46
+ count: 0,
47
+ resetTime: now + WINDOW_MS
48
+ });
49
+ return {
50
+ isLimited: false,
51
+ remaining: MAX_ATTEMPTS,
52
+ resetTime: now + WINDOW_MS
53
+ };
54
+ }
55
+ if (entry.count >= MAX_ATTEMPTS) {
56
+ return {
57
+ isLimited: true,
58
+ remaining: 0,
59
+ resetTime: entry.resetTime
60
+ };
61
+ }
62
+ return {
63
+ isLimited: false,
64
+ remaining: MAX_ATTEMPTS - entry.count,
65
+ resetTime: entry.resetTime
66
+ };
67
+ }
68
+ function incrementRateLimit(identifier) {
69
+ const now = Date.now();
70
+ const entry = rateLimitStore.get(identifier);
71
+ if (!entry || now > entry.resetTime) {
72
+ rateLimitStore.set(identifier, {
73
+ count: 1,
74
+ resetTime: now + WINDOW_MS
75
+ });
76
+ } else {
77
+ entry.count++;
78
+ }
79
+ }
80
+ function resetRateLimit(identifier) {
81
+ rateLimitStore.delete(identifier);
82
+ }
83
+ function cleanupRateLimitStore() {
84
+ const now = Date.now();
85
+ for (const [identifier, entry] of rateLimitStore.entries()) {
86
+ if (now > entry.resetTime) {
87
+ rateLimitStore.delete(identifier);
88
+ }
89
+ }
90
+ }
91
+ var rateLimitStore, MAX_ATTEMPTS, WINDOW_MS;
92
+ var init_rate_limit = __esm({
93
+ "src/utils/rate-limit.ts"() {
94
+ "use strict";
95
+ rateLimitStore = /* @__PURE__ */ new Map();
96
+ MAX_ATTEMPTS = 5;
97
+ WINDOW_MS = 15 * 60 * 1e3;
98
+ if (typeof setInterval !== "undefined") {
99
+ setInterval(cleanupRateLimitStore, 5 * 60 * 1e3);
100
+ }
101
+ }
102
+ });
103
+
104
+ // src/index.ts
105
+ var index_exports = {};
106
+ __export(index_exports, {
107
+ GitHubStorage: () => GitHubStorage,
108
+ LocalStorage: () => LocalStorage,
109
+ MAX_FILE_SIZE: () => MAX_FILE_SIZE,
110
+ MAX_PASSWORD_LENGTH: () => MAX_PASSWORD_LENGTH,
111
+ MAX_USERNAME_LENGTH: () => MAX_USERNAME_LENGTH,
112
+ checkRateLimit: () => checkRateLimit,
113
+ clearSessionCookie: () => clearSessionCookie,
114
+ createAuthMiddleware: () => createAuthMiddleware,
115
+ createContentApiHandlers: () => createContentApiHandlers,
116
+ createListApiHandlers: () => createListApiHandlers,
117
+ createSession: () => createSession,
118
+ deleteMarkdownContent: () => deleteMarkdownContent,
119
+ getCollectionItem: () => getCollectionItem,
120
+ getCollectionItems: () => getCollectionItems,
121
+ getCollections: () => getCollections,
122
+ getMarkdownContent: () => getMarkdownContent,
123
+ getSessionFromCookies: () => getSessionFromCookies,
124
+ getStorageProvider: () => getStorageProvider,
125
+ handleDeleteContent: () => handleDeleteContent,
126
+ handleGetContent: () => handleGetContent,
127
+ handleListFiles: () => handleListFiles,
128
+ handleLogin: () => handleLogin,
129
+ handleLogout: () => handleLogout,
130
+ handleSaveContent: () => handleSaveContent,
131
+ handleUpload: () => handleUpload,
132
+ incrementRateLimit: () => incrementRateLimit,
133
+ initCMS: () => initCMS,
134
+ listDirectory: () => listDirectory,
135
+ listMarkdownFiles: () => listMarkdownFiles,
136
+ markdownFileExists: () => markdownFileExists,
137
+ parseMarkdown: () => parseMarkdown,
138
+ resetRateLimit: () => resetRateLimit,
139
+ sanitizeFrontmatter: () => sanitizeFrontmatter,
140
+ saveMarkdownContent: () => saveMarkdownContent,
141
+ setSessionCookie: () => setSessionCookie,
142
+ stringifyMarkdown: () => stringifyMarkdown,
143
+ uploadFile: () => uploadFile,
144
+ validateCredentials: () => validateCredentials,
145
+ validateFilePath: () => validateFilePath,
146
+ validateFileSize: () => validateFileSize,
147
+ validatePassword: () => validatePassword,
148
+ validateUsername: () => validateUsername,
149
+ verifySession: () => verifySession
150
+ });
151
+ module.exports = __toCommonJS(index_exports);
152
+
153
+ // src/content/markdown.ts
154
+ var import_gray_matter = __toESM(require("gray-matter"));
155
+
156
+ // src/content/storage.ts
157
+ var import_promises = __toESM(require("fs/promises"));
158
+ var import_path = __toESM(require("path"));
159
+ var import_rest = require("@octokit/rest");
160
+ var LocalStorage = class {
161
+ constructor(baseDir = "public/content") {
162
+ this.baseDir = baseDir;
163
+ }
164
+ getFullPath(filePath) {
165
+ return import_path.default.join(process.cwd(), this.baseDir, filePath);
166
+ }
167
+ async readFile(filePath) {
168
+ const fullPath = this.getFullPath(filePath);
169
+ return import_promises.default.readFile(fullPath, "utf-8");
170
+ }
171
+ async writeFile(filePath, content) {
172
+ const fullPath = this.getFullPath(filePath);
173
+ await import_promises.default.mkdir(import_path.default.dirname(fullPath), { recursive: true });
174
+ await import_promises.default.writeFile(fullPath, content, "utf-8");
175
+ }
176
+ async deleteFile(filePath) {
177
+ const fullPath = this.getFullPath(filePath);
178
+ await import_promises.default.unlink(fullPath);
179
+ }
180
+ async listFiles(directory) {
181
+ const fullPath = this.getFullPath(directory);
182
+ try {
183
+ const entries = await import_promises.default.readdir(fullPath, { withFileTypes: true });
184
+ return entries.map((entry) => ({
185
+ path: import_path.default.join(directory, entry.name),
186
+ name: entry.name,
187
+ type: entry.isDirectory() ? "directory" : "file"
188
+ })).filter(
189
+ (entry) => entry.type === "directory" || entry.name.endsWith(".md")
190
+ );
191
+ } catch (error) {
192
+ return [];
193
+ }
194
+ }
195
+ async exists(filePath) {
196
+ try {
197
+ const fullPath = this.getFullPath(filePath);
198
+ await import_promises.default.access(fullPath);
199
+ return true;
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+ async uploadFile(filePath, buffer) {
205
+ const uploadPath = import_path.default.join(process.cwd(), "public", "uploads", filePath);
206
+ await import_promises.default.mkdir(import_path.default.dirname(uploadPath), { recursive: true });
207
+ await import_promises.default.writeFile(uploadPath, buffer);
208
+ return `/uploads/${filePath}`;
209
+ }
210
+ };
211
+ var GitHubStorage = class {
212
+ constructor() {
213
+ const token = process.env.GITHUB_TOKEN;
214
+ const repoInfo = process.env.GITHUB_REPO;
215
+ if (!token || !repoInfo) {
216
+ throw new Error(
217
+ "GITHUB_TOKEN and GITHUB_REPO must be set for production"
218
+ );
219
+ }
220
+ const [owner, repo] = repoInfo.split("/");
221
+ if (!owner || !repo) {
222
+ throw new Error('GITHUB_REPO must be in format "owner/repo"');
223
+ }
224
+ this.octokit = new import_rest.Octokit({ auth: token });
225
+ this.owner = owner;
226
+ this.repo = repo;
227
+ this.branch = process.env.GITHUB_BRANCH || "main";
228
+ this.baseDir = "public/content";
229
+ }
230
+ getGitHubPath(filePath) {
231
+ return `${this.baseDir}/${filePath}`;
232
+ }
233
+ async readFile(filePath) {
234
+ const { data } = await this.octokit.repos.getContent({
235
+ owner: this.owner,
236
+ repo: this.repo,
237
+ path: this.getGitHubPath(filePath),
238
+ ref: this.branch
239
+ });
240
+ if ("content" in data) {
241
+ return Buffer.from(data.content, "base64").toString("utf-8");
242
+ }
243
+ throw new Error("File not found");
244
+ }
245
+ async writeFile(filePath, content) {
246
+ const githubPath = this.getGitHubPath(filePath);
247
+ let sha;
248
+ try {
249
+ const { data } = await this.octokit.repos.getContent({
250
+ owner: this.owner,
251
+ repo: this.repo,
252
+ path: githubPath,
253
+ ref: this.branch
254
+ });
255
+ if ("sha" in data) {
256
+ sha = data.sha;
257
+ }
258
+ } catch (error) {
259
+ }
260
+ await this.octokit.repos.createOrUpdateFileContents({
261
+ owner: this.owner,
262
+ repo: this.repo,
263
+ path: githubPath,
264
+ message: `Update ${filePath}`,
265
+ content: Buffer.from(content).toString("base64"),
266
+ branch: this.branch,
267
+ sha
268
+ });
269
+ }
270
+ async deleteFile(filePath) {
271
+ const githubPath = this.getGitHubPath(filePath);
272
+ const { data } = await this.octokit.repos.getContent({
273
+ owner: this.owner,
274
+ repo: this.repo,
275
+ path: githubPath,
276
+ ref: this.branch
277
+ });
278
+ if ("sha" in data) {
279
+ await this.octokit.repos.deleteFile({
280
+ owner: this.owner,
281
+ repo: this.repo,
282
+ path: githubPath,
283
+ message: `Delete ${filePath}`,
284
+ sha: data.sha,
285
+ branch: this.branch
286
+ });
287
+ }
288
+ }
289
+ async listFiles(directory) {
290
+ const githubPath = this.getGitHubPath(directory);
291
+ try {
292
+ const { data } = await this.octokit.repos.getContent({
293
+ owner: this.owner,
294
+ repo: this.repo,
295
+ path: githubPath,
296
+ ref: this.branch
297
+ });
298
+ if (Array.isArray(data)) {
299
+ return data.map((item) => ({
300
+ path: import_path.default.join(directory, item.name),
301
+ name: item.name,
302
+ type: item.type === "dir" ? "directory" : "file"
303
+ })).filter(
304
+ (entry) => entry.type === "directory" || entry.name.endsWith(".md")
305
+ );
306
+ }
307
+ } catch (error) {
308
+ }
309
+ return [];
310
+ }
311
+ async exists(filePath) {
312
+ try {
313
+ await this.octokit.repos.getContent({
314
+ owner: this.owner,
315
+ repo: this.repo,
316
+ path: this.getGitHubPath(filePath),
317
+ ref: this.branch
318
+ });
319
+ return true;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+ async uploadFile(filePath, buffer) {
325
+ const uploadPath = `public/uploads/${filePath}`;
326
+ await this.octokit.repos.createOrUpdateFileContents({
327
+ owner: this.owner,
328
+ repo: this.repo,
329
+ path: uploadPath,
330
+ message: `Upload file: ${filePath}`,
331
+ content: buffer.toString("base64"),
332
+ branch: this.branch
333
+ });
334
+ return `/uploads/${filePath}`;
335
+ }
336
+ };
337
+ function getStorageProvider() {
338
+ if (process.env.NODE_ENV === "production" && process.env.GITHUB_TOKEN) {
339
+ return new GitHubStorage();
340
+ }
341
+ return new LocalStorage();
342
+ }
343
+
344
+ // src/utils/validation.ts
345
+ var import_path2 = __toESM(require("path"));
346
+ var MAX_USERNAME_LENGTH = 100;
347
+ var MAX_PASSWORD_LENGTH = 1e3;
348
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
349
+ function validateFilePath(filePath) {
350
+ if (!filePath || typeof filePath !== "string") {
351
+ throw new Error("Invalid file path");
352
+ }
353
+ if (filePath.includes("\0")) {
354
+ throw new Error("Invalid file path: null byte detected");
355
+ }
356
+ if (filePath.includes("..")) {
357
+ throw new Error("Invalid file path: directory traversal detected");
358
+ }
359
+ const normalized = import_path2.default.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, "");
360
+ if (normalized.startsWith("/") || normalized.startsWith("\\")) {
361
+ throw new Error("Invalid file path: directory traversal detected");
362
+ }
363
+ const dangerousPatterns = [
364
+ /\.\./,
365
+ /^[/\\]/,
366
+ /[<>:"|?*]/,
367
+ /\x00/,
368
+ /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
369
+ // Windows reserved names
370
+ ];
371
+ for (const pattern of dangerousPatterns) {
372
+ if (pattern.test(normalized)) {
373
+ throw new Error("Invalid file path: contains dangerous characters");
374
+ }
375
+ }
376
+ return normalized;
377
+ }
378
+ function validateUsername(username) {
379
+ if (!username || typeof username !== "string") {
380
+ throw new Error("Username is required");
381
+ }
382
+ if (username.length > MAX_USERNAME_LENGTH) {
383
+ throw new Error(
384
+ `Username must be ${MAX_USERNAME_LENGTH} characters or less`
385
+ );
386
+ }
387
+ if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
388
+ throw new Error(
389
+ "Username must contain only letters, numbers, underscores, and hyphens"
390
+ );
391
+ }
392
+ return true;
393
+ }
394
+ function validatePassword(password) {
395
+ if (!password || typeof password !== "string") {
396
+ throw new Error("Password is required");
397
+ }
398
+ if (password.length > MAX_PASSWORD_LENGTH) {
399
+ throw new Error(
400
+ `Password must be ${MAX_PASSWORD_LENGTH} characters or less`
401
+ );
402
+ }
403
+ return true;
404
+ }
405
+ function validateFileSize(size) {
406
+ if (size > MAX_FILE_SIZE) {
407
+ throw new Error(
408
+ `File size exceeds maximum allowed size of ${MAX_FILE_SIZE / 1024 / 1024}MB`
409
+ );
410
+ }
411
+ return true;
412
+ }
413
+ function sanitizeFrontmatter(data) {
414
+ const sanitized = {};
415
+ for (const [key, value] of Object.entries(data)) {
416
+ if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
417
+ continue;
418
+ }
419
+ if (typeof value === "string") {
420
+ sanitized[key] = value.replace(/\x00/g, "");
421
+ } else if (typeof value === "number" || typeof value === "boolean" || value === null) {
422
+ sanitized[key] = value;
423
+ } else if (Array.isArray(value)) {
424
+ sanitized[key] = value.map(
425
+ (item) => typeof item === "string" ? item.replace(/\x00/g, "") : item
426
+ );
427
+ } else if (typeof value === "object") {
428
+ sanitized[key] = sanitizeFrontmatter(value);
429
+ }
430
+ }
431
+ return sanitized;
432
+ }
433
+
434
+ // src/content/markdown.ts
435
+ function parseMarkdown(rawContent) {
436
+ const { data, content } = (0, import_gray_matter.default)(rawContent);
437
+ return { data, content };
438
+ }
439
+ function stringifyMarkdown(data, content) {
440
+ return import_gray_matter.default.stringify(content, data);
441
+ }
442
+ async function getMarkdownContent(filePath) {
443
+ const validatedPath = validateFilePath(filePath);
444
+ const storage = getStorageProvider();
445
+ const rawContent = await storage.readFile(validatedPath);
446
+ const size = Buffer.byteLength(rawContent, "utf-8");
447
+ if (size > MAX_FILE_SIZE) {
448
+ throw new Error(
449
+ `File size (${size} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`
450
+ );
451
+ }
452
+ return parseMarkdown(rawContent);
453
+ }
454
+ async function saveMarkdownContent(filePath, data, content) {
455
+ const validatedPath = validateFilePath(filePath);
456
+ const sanitizedData = sanitizeFrontmatter(data);
457
+ const markdown = stringifyMarkdown(sanitizedData, content);
458
+ const size = Buffer.byteLength(markdown, "utf-8");
459
+ if (size > MAX_FILE_SIZE) {
460
+ throw new Error(
461
+ `Content size (${size} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`
462
+ );
463
+ }
464
+ const storage = getStorageProvider();
465
+ await storage.writeFile(validatedPath, markdown);
466
+ }
467
+ async function deleteMarkdownContent(filePath) {
468
+ const validatedPath = validateFilePath(filePath);
469
+ const storage = getStorageProvider();
470
+ await storage.deleteFile(validatedPath);
471
+ }
472
+ async function listMarkdownFiles(directory = "") {
473
+ const validatedDir = directory ? validateFilePath(directory) : "";
474
+ const storage = getStorageProvider();
475
+ const entries = await storage.listFiles(validatedDir);
476
+ return entries.filter((entry) => entry.type === "file").map((entry) => entry.path);
477
+ }
478
+ async function markdownFileExists(filePath) {
479
+ const validatedPath = validateFilePath(filePath);
480
+ const storage = getStorageProvider();
481
+ return storage.exists(validatedPath);
482
+ }
483
+
484
+ // src/content/directory.ts
485
+ async function listDirectory(directory = "") {
486
+ const storage = getStorageProvider();
487
+ return storage.listFiles(directory);
488
+ }
489
+
490
+ // src/content/collection.ts
491
+ async function getCollectionItems(folderName) {
492
+ const storage = getStorageProvider();
493
+ const entries = await storage.listFiles(folderName);
494
+ const mdFiles = entries.filter(
495
+ (entry) => entry.type === "file" && entry.path.endsWith(".md")
496
+ );
497
+ const items = await Promise.all(
498
+ mdFiles.map(async (file) => {
499
+ const rawContent = await storage.readFile(file.path);
500
+ const { data, content } = parseMarkdown(rawContent);
501
+ const slug = file.path.split("/").pop().replace(/\.md$/, "");
502
+ return {
503
+ content,
504
+ data,
505
+ slug
506
+ };
507
+ })
508
+ );
509
+ return items;
510
+ }
511
+ async function getCollectionItem(folderName, slug) {
512
+ const storage = getStorageProvider();
513
+ const filePath = `${folderName}/${slug}.md`;
514
+ try {
515
+ const exists = await storage.exists(filePath);
516
+ if (!exists) {
517
+ return null;
518
+ }
519
+ const rawContent = await storage.readFile(filePath);
520
+ const { data, content } = parseMarkdown(rawContent);
521
+ return {
522
+ content,
523
+ data,
524
+ slug
525
+ };
526
+ } catch (error) {
527
+ return null;
528
+ }
529
+ }
530
+ async function getCollections(baseDir = "") {
531
+ const storage = getStorageProvider();
532
+ const entries = await storage.listFiles(baseDir);
533
+ return entries.filter((entry) => entry.type === "directory").map((entry) => entry.path.split("/").pop()).filter(Boolean);
534
+ }
535
+
536
+ // src/content/upload.ts
537
+ async function uploadFile(fileName, buffer) {
538
+ const storage = getStorageProvider();
539
+ const timestamp = Date.now();
540
+ const ext = fileName.split(".").pop();
541
+ const nameWithoutExt = fileName.replace(`.${ext}`, "");
542
+ const uniqueFileName = `${nameWithoutExt}-${timestamp}.${ext}`;
543
+ return storage.uploadFile(uniqueFileName, buffer);
544
+ }
545
+
546
+ // src/auth/session.ts
547
+ var import_jose = require("jose");
548
+ var import_headers = require("next/headers");
549
+ var SESSION_COOKIE_NAME = "cms-session";
550
+ var SESSION_DURATION = 60 * 60 * 24 * 7;
551
+ function getSecretKey() {
552
+ const secret = process.env.CMS_SESSION_SECRET;
553
+ if (!secret || secret.length < 32) {
554
+ throw new Error("CMS_SESSION_SECRET must be at least 32 characters");
555
+ }
556
+ return new TextEncoder().encode(secret);
557
+ }
558
+ async function createSession(username) {
559
+ const payload = {
560
+ username,
561
+ exp: Math.floor(Date.now() / 1e3) + SESSION_DURATION
562
+ };
563
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setExpirationTime("7d").sign(getSecretKey());
564
+ return token;
565
+ }
566
+ async function verifySession(token) {
567
+ try {
568
+ const { payload } = await (0, import_jose.jwtVerify)(token, getSecretKey());
569
+ if (typeof payload.username === "string" && typeof payload.exp === "number") {
570
+ return payload;
571
+ }
572
+ return null;
573
+ } catch (error) {
574
+ return null;
575
+ }
576
+ }
577
+ async function getSessionFromCookies() {
578
+ const cookieStore = await (0, import_headers.cookies)();
579
+ const token = cookieStore.get(SESSION_COOKIE_NAME)?.value;
580
+ if (!token) {
581
+ return null;
582
+ }
583
+ return verifySession(token);
584
+ }
585
+ async function setSessionCookie(token) {
586
+ const cookieStore = await (0, import_headers.cookies)();
587
+ cookieStore.set(SESSION_COOKIE_NAME, token, {
588
+ httpOnly: true,
589
+ secure: process.env.NODE_ENV === "production",
590
+ sameSite: "lax",
591
+ maxAge: SESSION_DURATION,
592
+ path: "/"
593
+ });
594
+ }
595
+ async function clearSessionCookie() {
596
+ const cookieStore = await (0, import_headers.cookies)();
597
+ cookieStore.delete(SESSION_COOKIE_NAME);
598
+ }
599
+
600
+ // src/auth/login.ts
601
+ var import_crypto = __toESM(require("crypto"));
602
+ function validateCredentials(username, password) {
603
+ try {
604
+ validateUsername(username);
605
+ validatePassword(password);
606
+ } catch (error) {
607
+ return false;
608
+ }
609
+ const envUsername = process.env.CMS_ADMIN_USERNAME;
610
+ const envPassword = process.env.CMS_ADMIN_PASSWORD;
611
+ if (!envUsername || !envPassword) {
612
+ throw new Error(
613
+ "CMS_ADMIN_USERNAME and CMS_ADMIN_PASSWORD must be set in environment variables"
614
+ );
615
+ }
616
+ const usernameMatch = timingSafeEqual(username, envUsername);
617
+ const passwordMatch = timingSafeEqual(password, envPassword);
618
+ return usernameMatch && passwordMatch;
619
+ }
620
+ function timingSafeEqual(a, b) {
621
+ try {
622
+ const bufferA = Buffer.from(a, "utf-8");
623
+ const bufferB = Buffer.from(b, "utf-8");
624
+ if (bufferA.length !== bufferB.length) {
625
+ const dummyBuffer = Buffer.alloc(bufferA.length);
626
+ import_crypto.default.timingSafeEqual(bufferA, dummyBuffer);
627
+ return false;
628
+ }
629
+ return import_crypto.default.timingSafeEqual(bufferA, bufferB);
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+
635
+ // src/api/handlers.ts
636
+ var import_server = require("next/server");
637
+ async function handleLogin(request) {
638
+ try {
639
+ const ip = request.headers.get("x-forwarded-for")?.split(",")[0] || request.headers.get("x-real-ip") || "unknown";
640
+ const { checkRateLimit: checkRateLimit2, incrementRateLimit: incrementRateLimit2, resetRateLimit: resetRateLimit2 } = await Promise.resolve().then(() => (init_rate_limit(), rate_limit_exports));
641
+ const rateLimit = checkRateLimit2(ip);
642
+ if (rateLimit.isLimited) {
643
+ const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1e3);
644
+ return import_server.NextResponse.json(
645
+ {
646
+ error: "Too many login attempts. Please try again later.",
647
+ retryAfter
648
+ },
649
+ {
650
+ status: 429,
651
+ headers: {
652
+ "Retry-After": retryAfter.toString()
653
+ }
654
+ }
655
+ );
656
+ }
657
+ const body = await request.json();
658
+ const { username, password } = body;
659
+ if (!username || !password) {
660
+ return import_server.NextResponse.json(
661
+ { error: "Username and password are required" },
662
+ { status: 400 }
663
+ );
664
+ }
665
+ const isValid = validateCredentials(username, password);
666
+ if (!isValid) {
667
+ incrementRateLimit2(ip);
668
+ return import_server.NextResponse.json(
669
+ { error: "Invalid credentials" },
670
+ { status: 401 }
671
+ );
672
+ }
673
+ resetRateLimit2(ip);
674
+ const token = await createSession(username);
675
+ const response = import_server.NextResponse.json({ success: true });
676
+ response.cookies.set("cms-session", token, {
677
+ httpOnly: true,
678
+ secure: process.env.NODE_ENV === "production",
679
+ sameSite: "lax",
680
+ maxAge: 60 * 60 * 24 * 7,
681
+ // 7 days
682
+ path: "/"
683
+ });
684
+ return response;
685
+ } catch (error) {
686
+ console.error("Login error:", error);
687
+ return import_server.NextResponse.json(
688
+ { error: "Authentication service temporarily unavailable" },
689
+ { status: 500 }
690
+ );
691
+ }
692
+ }
693
+ async function handleLogout() {
694
+ const response = import_server.NextResponse.json({ success: true });
695
+ response.cookies.delete("cms-session");
696
+ return response;
697
+ }
698
+ async function handleGetContent(_request, filePath) {
699
+ try {
700
+ const session = await getSessionFromCookies();
701
+ if (!session) {
702
+ return import_server.NextResponse.json({ error: "Unauthorized" }, { status: 401 });
703
+ }
704
+ const content = await getMarkdownContent(filePath);
705
+ return import_server.NextResponse.json(content);
706
+ } catch (error) {
707
+ console.error("Get content error:", error);
708
+ return import_server.NextResponse.json(
709
+ { error: "Failed to read content" },
710
+ { status: 500 }
711
+ );
712
+ }
713
+ }
714
+ async function handleSaveContent(request, filePath) {
715
+ try {
716
+ const session = await getSessionFromCookies();
717
+ if (!session) {
718
+ return import_server.NextResponse.json({ error: "Unauthorized" }, { status: 401 });
719
+ }
720
+ const body = await request.json();
721
+ const { data, content } = body;
722
+ await saveMarkdownContent(filePath, data || {}, content || "");
723
+ return import_server.NextResponse.json({ success: true });
724
+ } catch (error) {
725
+ console.error("Save content error:", error);
726
+ return import_server.NextResponse.json(
727
+ { error: "Failed to save content" },
728
+ { status: 500 }
729
+ );
730
+ }
731
+ }
732
+ async function handleDeleteContent(_request, filePath) {
733
+ try {
734
+ const session = await getSessionFromCookies();
735
+ if (!session) {
736
+ return import_server.NextResponse.json({ error: "Unauthorized" }, { status: 401 });
737
+ }
738
+ await deleteMarkdownContent(filePath);
739
+ return import_server.NextResponse.json({ success: true });
740
+ } catch (error) {
741
+ console.error("Delete content error:", error);
742
+ return import_server.NextResponse.json(
743
+ { error: "Failed to delete content" },
744
+ { status: 500 }
745
+ );
746
+ }
747
+ }
748
+ async function handleListFiles(directory = "") {
749
+ try {
750
+ const session = await getSessionFromCookies();
751
+ if (!session) {
752
+ return import_server.NextResponse.json({ error: "Unauthorized" }, { status: 401 });
753
+ }
754
+ const entries = await listDirectory(directory);
755
+ return import_server.NextResponse.json({ entries });
756
+ } catch (error) {
757
+ console.error("List files error:", error);
758
+ return import_server.NextResponse.json(
759
+ { error: "Failed to list files" },
760
+ { status: 500 }
761
+ );
762
+ }
763
+ }
764
+ function createContentApiHandlers() {
765
+ return {
766
+ async GET(request, { params }) {
767
+ const filePath = params.path.join("/");
768
+ return handleGetContent(request, filePath);
769
+ },
770
+ async POST(request, { params }) {
771
+ const filePath = params.path.join("/");
772
+ return handleSaveContent(request, filePath);
773
+ },
774
+ async DELETE(request, { params }) {
775
+ const filePath = params.path.join("/");
776
+ return handleDeleteContent(request, filePath);
777
+ }
778
+ };
779
+ }
780
+ function createListApiHandlers() {
781
+ return {
782
+ async GET(_request, { params }) {
783
+ const directory = params?.path?.join("/") || "";
784
+ return handleListFiles(directory);
785
+ }
786
+ };
787
+ }
788
+
789
+ // src/api/upload.ts
790
+ var import_server2 = require("next/server");
791
+ async function handleUpload(request) {
792
+ try {
793
+ const session = await getSessionFromCookies();
794
+ if (!session) {
795
+ return import_server2.NextResponse.json({ error: "Unauthorized" }, { status: 401 });
796
+ }
797
+ const formData = await request.formData();
798
+ const file = formData.get("file");
799
+ if (!file) {
800
+ return import_server2.NextResponse.json({ error: "No file provided" }, { status: 400 });
801
+ }
802
+ const bytes = await file.arrayBuffer();
803
+ const buffer = Buffer.from(bytes);
804
+ const publicUrl = await uploadFile(file.name, buffer);
805
+ return import_server2.NextResponse.json({
806
+ success: true,
807
+ url: publicUrl,
808
+ filename: file.name
809
+ });
810
+ } catch (error) {
811
+ console.error("Upload error:", error);
812
+ return import_server2.NextResponse.json(
813
+ { error: "Failed to upload file" },
814
+ { status: 500 }
815
+ );
816
+ }
817
+ }
818
+
819
+ // src/middleware/auth.ts
820
+ var import_server3 = require("next/server");
821
+ function createAuthMiddleware(options = {}) {
822
+ const loginPath = options.loginPath || "/admin/login";
823
+ const protectedPaths = options.protectedPaths || ["/admin"];
824
+ return async function middleware(request) {
825
+ const { pathname } = request.nextUrl;
826
+ const isProtected = protectedPaths.some(
827
+ (path4) => pathname.startsWith(path4)
828
+ );
829
+ if (!isProtected) {
830
+ return import_server3.NextResponse.next();
831
+ }
832
+ if (pathname === loginPath) {
833
+ return import_server3.NextResponse.next();
834
+ }
835
+ const session = await getSessionFromCookies();
836
+ if (!session) {
837
+ const url = request.nextUrl.clone();
838
+ url.pathname = loginPath;
839
+ url.searchParams.set("from", pathname);
840
+ return import_server3.NextResponse.redirect(url);
841
+ }
842
+ return import_server3.NextResponse.next();
843
+ };
844
+ }
845
+
846
+ // src/init/setup.ts
847
+ var import_promises2 = __toESM(require("fs/promises"));
848
+ var import_path3 = __toESM(require("path"));
849
+ async function initCMS(config = {}) {
850
+ const contentDir = config.contentDir || "public/content";
851
+ const fullPath = import_path3.default.join(process.cwd(), contentDir);
852
+ await import_promises2.default.mkdir(fullPath, { recursive: true });
853
+ const exampleContent = `---
854
+ title: Example Post
855
+ description: This is an example markdown file
856
+ date: ${(/* @__PURE__ */ new Date()).toISOString()}
857
+ ---
858
+
859
+ # Welcome to your CMS!
860
+
861
+ This is an example markdown file. You can edit or delete it from the admin dashboard.
862
+
863
+ ## Features
864
+
865
+ - File-based content storage
866
+ - Markdown with frontmatter
867
+ - GitHub integration for production
868
+ - Simple authentication
869
+ - No database required
870
+ `;
871
+ const examplePath = import_path3.default.join(fullPath, "example.md");
872
+ await import_promises2.default.writeFile(examplePath, exampleContent, "utf-8");
873
+ console.log("\u2705 CMS initialized successfully!");
874
+ console.log(`\u{1F4C1} Content directory: ${contentDir}`);
875
+ console.log(`\u{1F4DD} Example file created: ${contentDir}/example.md`);
876
+ console.log("");
877
+ console.log("Next steps:");
878
+ console.log("1. Set up environment variables in .env.local:");
879
+ console.log(" - CMS_ADMIN_USERNAME");
880
+ console.log(" - CMS_ADMIN_PASSWORD");
881
+ console.log(" - CMS_SESSION_SECRET (min 32 characters)");
882
+ console.log("2. For production: GITHUB_TOKEN, GITHUB_REPO, GITHUB_BRANCH");
883
+ console.log("3. Import and use the CMS utilities in your Next.js app");
884
+ }
885
+
886
+ // src/index.ts
887
+ init_rate_limit();
888
+ // Annotate the CommonJS export names for ESM import in node:
889
+ 0 && (module.exports = {
890
+ GitHubStorage,
891
+ LocalStorage,
892
+ MAX_FILE_SIZE,
893
+ MAX_PASSWORD_LENGTH,
894
+ MAX_USERNAME_LENGTH,
895
+ checkRateLimit,
896
+ clearSessionCookie,
897
+ createAuthMiddleware,
898
+ createContentApiHandlers,
899
+ createListApiHandlers,
900
+ createSession,
901
+ deleteMarkdownContent,
902
+ getCollectionItem,
903
+ getCollectionItems,
904
+ getCollections,
905
+ getMarkdownContent,
906
+ getSessionFromCookies,
907
+ getStorageProvider,
908
+ handleDeleteContent,
909
+ handleGetContent,
910
+ handleListFiles,
911
+ handleLogin,
912
+ handleLogout,
913
+ handleSaveContent,
914
+ handleUpload,
915
+ incrementRateLimit,
916
+ initCMS,
917
+ listDirectory,
918
+ listMarkdownFiles,
919
+ markdownFileExists,
920
+ parseMarkdown,
921
+ resetRateLimit,
922
+ sanitizeFrontmatter,
923
+ saveMarkdownContent,
924
+ setSessionCookie,
925
+ stringifyMarkdown,
926
+ uploadFile,
927
+ validateCredentials,
928
+ validateFilePath,
929
+ validateFileSize,
930
+ validatePassword,
931
+ validateUsername,
932
+ verifySession
933
+ });
934
+ //# sourceMappingURL=index.js.map