next-blogpanel 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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/admin/index.cjs +3062 -0
  4. package/dist/admin/index.cjs.map +1 -0
  5. package/dist/admin/index.d.cts +51 -0
  6. package/dist/admin/index.d.ts +51 -0
  7. package/dist/admin/index.js +3029 -0
  8. package/dist/admin/index.js.map +1 -0
  9. package/dist/api/categories.cjs +82 -0
  10. package/dist/api/categories.cjs.map +1 -0
  11. package/dist/api/categories.d.cts +27 -0
  12. package/dist/api/categories.d.ts +27 -0
  13. package/dist/api/categories.js +77 -0
  14. package/dist/api/categories.js.map +1 -0
  15. package/dist/api/media-file.cjs +46 -0
  16. package/dist/api/media-file.cjs.map +1 -0
  17. package/dist/api/media-file.d.cts +7 -0
  18. package/dist/api/media-file.d.ts +7 -0
  19. package/dist/api/media-file.js +44 -0
  20. package/dist/api/media-file.js.map +1 -0
  21. package/dist/api/media.cjs +180 -0
  22. package/dist/api/media.cjs.map +1 -0
  23. package/dist/api/media.d.cts +22 -0
  24. package/dist/api/media.d.ts +22 -0
  25. package/dist/api/media.js +176 -0
  26. package/dist/api/media.js.map +1 -0
  27. package/dist/api/posts.cjs +103 -0
  28. package/dist/api/posts.cjs.map +1 -0
  29. package/dist/api/posts.d.cts +27 -0
  30. package/dist/api/posts.d.ts +27 -0
  31. package/dist/api/posts.js +98 -0
  32. package/dist/api/posts.js.map +1 -0
  33. package/dist/api/rss.cjs +25 -0
  34. package/dist/api/rss.cjs.map +1 -0
  35. package/dist/api/rss.d.cts +5 -0
  36. package/dist/api/rss.d.ts +5 -0
  37. package/dist/api/rss.js +23 -0
  38. package/dist/api/rss.js.map +1 -0
  39. package/dist/api/settings.cjs +40 -0
  40. package/dist/api/settings.cjs.map +1 -0
  41. package/dist/api/settings.d.cts +17 -0
  42. package/dist/api/settings.d.ts +17 -0
  43. package/dist/api/settings.js +37 -0
  44. package/dist/api/settings.js.map +1 -0
  45. package/dist/api/sitemap.cjs +25 -0
  46. package/dist/api/sitemap.cjs.map +1 -0
  47. package/dist/api/sitemap.d.cts +5 -0
  48. package/dist/api/sitemap.d.ts +5 -0
  49. package/dist/api/sitemap.js +23 -0
  50. package/dist/api/sitemap.js.map +1 -0
  51. package/dist/api/tokens.cjs +56 -0
  52. package/dist/api/tokens.cjs.map +1 -0
  53. package/dist/api/tokens.d.cts +22 -0
  54. package/dist/api/tokens.d.ts +22 -0
  55. package/dist/api/tokens.js +52 -0
  56. package/dist/api/tokens.js.map +1 -0
  57. package/dist/chunk-2M46MMWN.cjs +76 -0
  58. package/dist/chunk-2M46MMWN.cjs.map +1 -0
  59. package/dist/chunk-4GK22Q3R.cjs +229 -0
  60. package/dist/chunk-4GK22Q3R.cjs.map +1 -0
  61. package/dist/chunk-6PZ772NQ.js +595 -0
  62. package/dist/chunk-6PZ772NQ.js.map +1 -0
  63. package/dist/chunk-BZXPRGLN.cjs +351 -0
  64. package/dist/chunk-BZXPRGLN.cjs.map +1 -0
  65. package/dist/chunk-DAX5C6SC.cjs +86 -0
  66. package/dist/chunk-DAX5C6SC.cjs.map +1 -0
  67. package/dist/chunk-DWNMHQ4I.cjs +57 -0
  68. package/dist/chunk-DWNMHQ4I.cjs.map +1 -0
  69. package/dist/chunk-FPGVR2BX.cjs +121 -0
  70. package/dist/chunk-FPGVR2BX.cjs.map +1 -0
  71. package/dist/chunk-JBP3G4QJ.js +50 -0
  72. package/dist/chunk-JBP3G4QJ.js.map +1 -0
  73. package/dist/chunk-JDP4DIHI.js +215 -0
  74. package/dist/chunk-JDP4DIHI.js.map +1 -0
  75. package/dist/chunk-LIZOVA4W.js +118 -0
  76. package/dist/chunk-LIZOVA4W.js.map +1 -0
  77. package/dist/chunk-QLBTOCT3.js +118 -0
  78. package/dist/chunk-QLBTOCT3.js.map +1 -0
  79. package/dist/chunk-UI2KDZ5H.js +344 -0
  80. package/dist/chunk-UI2KDZ5H.js.map +1 -0
  81. package/dist/chunk-V7QPRX4A.cjs +125 -0
  82. package/dist/chunk-V7QPRX4A.cjs.map +1 -0
  83. package/dist/chunk-VEJFN65Y.js +82 -0
  84. package/dist/chunk-VEJFN65Y.js.map +1 -0
  85. package/dist/chunk-WYZFAI6O.js +74 -0
  86. package/dist/chunk-WYZFAI6O.js.map +1 -0
  87. package/dist/chunk-XLYTATRQ.cjs +637 -0
  88. package/dist/chunk-XLYTATRQ.cjs.map +1 -0
  89. package/dist/cli/index.cjs +2036 -0
  90. package/dist/components/index.cjs +647 -0
  91. package/dist/components/index.cjs.map +1 -0
  92. package/dist/components/index.d.cts +187 -0
  93. package/dist/components/index.d.ts +187 -0
  94. package/dist/components/index.js +633 -0
  95. package/dist/components/index.js.map +1 -0
  96. package/dist/db-M3KXLBUP.js +3 -0
  97. package/dist/db-M3KXLBUP.js.map +1 -0
  98. package/dist/db-TDCZNY3Q.cjs +112 -0
  99. package/dist/db-TDCZNY3Q.cjs.map +1 -0
  100. package/dist/editor/index.cjs +1126 -0
  101. package/dist/editor/index.cjs.map +1 -0
  102. package/dist/editor/index.d.cts +137 -0
  103. package/dist/editor/index.d.ts +137 -0
  104. package/dist/editor/index.js +1093 -0
  105. package/dist/editor/index.js.map +1 -0
  106. package/dist/index-BcNka7gK.d.cts +475 -0
  107. package/dist/index-DPRgaNEE.d.ts +475 -0
  108. package/dist/index.cjs +433 -0
  109. package/dist/index.cjs.map +1 -0
  110. package/dist/index.d.cts +27 -0
  111. package/dist/index.d.ts +27 -0
  112. package/dist/index.js +209 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/lib/index.cjs +193 -0
  115. package/dist/lib/index.cjs.map +1 -0
  116. package/dist/lib/index.d.cts +4 -0
  117. package/dist/lib/index.d.ts +4 -0
  118. package/dist/lib/index.js +8 -0
  119. package/dist/lib/index.js.map +1 -0
  120. package/dist/styles/admin.css +667 -0
  121. package/dist/styles/blog.css +851 -0
  122. package/dist/styles/editor.css +452 -0
  123. package/dist/styles/globals.css +270 -0
  124. package/dist/styles/prose.css +299 -0
  125. package/dist/types-CgrBuig-.d.cts +778 -0
  126. package/dist/types-CgrBuig-.d.ts +778 -0
  127. package/package.json +140 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/url.ts","../src/lib/rss.ts"],"names":["getEnvConfig","getConfig","getCollection"],"mappings":"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","file":"chunk-DAX5C6SC.cjs","sourcesContent":["/**\n * URL absolutisation for the contexts that require it.\n *\n * Deliberately free of any environment or configuration access, and free of\n * any Node built-in, so it can be imported from client components as well as\n * from server code. Callers supply the site URL; on the server that is\n * `getEnvConfig().BLOGPANEL_SITE_URL`, in a component it is a prop.\n *\n * Why absolutise at the point of consumption rather than at write time: the\n * stored URL has to survive the site moving. GridFS media URLs are persisted\n * into `nbk_media` documents and into the body of every post that embeds the\n * image, so anything baked into them is permanent — a localhost or staging\n * origin captured at upload time would follow the content to production with\n * no way back short of a database rewrite. `BLOGPANEL_SITE_URL` legitimately\n * differs between environments; the storage key does not. Keeping stored URLs\n * origin-relative and prefixing them in the handful of places that require an\n * absolute URL (Open Graph, Twitter cards, JSON-LD, RSS enclosures) keeps the\n * mutable part mutable.\n */\n\n/** True for `https://…`, `data:…`, `//cdn…` — anything already resolvable without a base. */\nexport function isAbsoluteUrl(url: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//');\n}\n\nconst warned = Symbol.for('next-blogpanel.url.warned');\n\nfunction warnOnce(context: string, url: string): void {\n const store = globalThis as unknown as Record<symbol, Set<string> | undefined>;\n const seen = (store[warned] ||= new Set<string>());\n if (seen.has(context)) return;\n seen.add(context);\n console.warn(\n `[blogpanel] BLOGPANEL_SITE_URL is not set, so ${context} was omitted rather than emitted as the relative URL \"${url}\". ` +\n `Open Graph, Twitter cards, schema.org and RSS all require absolute URLs; a relative one is not a smaller version of the right answer, it is invalid. ` +\n `Set BLOGPANEL_SITE_URL to your site's origin (no trailing slash).`\n );\n}\n\n/**\n * Returns `url` as an absolute URL, or `undefined` when it cannot be made one.\n *\n * Already-absolute inputs (R2 public URLs, `data:` URIs, protocol-relative\n * hosts) are returned untouched. Root-relative inputs (the GridFS default,\n * `/api/blog/media/file/…`) are prefixed with `siteUrl`. When `siteUrl` is\n * empty the result is `undefined` — callers must then omit the field, which\n * is the only honest option: emitting the bare relative path produces a\n * malformed `og:image`/`<enclosure url>` that consumers resolve against their\n * own origin, and emitting a half-formed absolute URL is worse still.\n *\n * Pass `context` to have the omission reported once per process.\n */\nexport function absoluteUrl(\n url: string | null | undefined,\n siteUrl: string | null | undefined,\n context?: string\n): string | undefined {\n if (!url) return undefined;\n if (isAbsoluteUrl(url)) return url;\n\n const base = (siteUrl || '').trim().replace(/\\/+$/, '');\n if (!base) {\n if (context) warnOnce(context, url);\n return undefined;\n }\n\n return url.startsWith('/') ? `${base}${url}` : `${base}/${url}`;\n}\n","import { getCollection } from './db';\nimport { getEnvConfig, getConfig } from './config';\nimport { absoluteUrl } from './url';\n\nexport async function generateRSSFeed(fullContent = false): Promise<string> {\n const env = getEnvConfig();\n const config = getConfig();\n\n // Same reasoning as generateSitemap(): RSS <link>/<atom:link> must be\n // absolute per the RSS spec, and most feed readers silently drop or\n // reject relative ones. Warn rather than throw — the blog itself still\n // renders fine without BLOGPANEL_SITE_URL set, only the feed's external\n // validity suffers.\n if (!env.BLOGPANEL_SITE_URL) {\n console.warn(\n '[blogpanel] BLOGPANEL_SITE_URL is not set — the RSS feed\\'s <link> and <atom:link> entries will be relative URLs, which most feed readers reject. Set BLOGPANEL_SITE_URL to your site\\'s real domain before deploying.'\n );\n }\n\n const posts = await getCollection('nbk_posts');\n\n const publishedPosts = await posts\n .find({ status: 'published' })\n .sort({ publishedAt: -1 })\n .limit(50)\n .toArray();\n\n const items = publishedPosts\n .map((post) => {\n const postUrl = `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`;\n const content = fullContent ? post.contentHTML : post.excerpt;\n const pubDate = post.publishedAt\n ? new Date(post.publishedAt).toUTCString()\n : new Date(post.createdAt).toUTCString();\n\n // RSS 2.0 requires <enclosure url> to be an absolute http(s) URL —\n // readers fetch it with no document base to resolve against. The GridFS\n // default produces root-relative URLs, so prefix them here; R2 URLs are\n // already absolute and pass through. Without a site URL the element is\n // dropped rather than emitted with a relative href.\n let enclosure = '';\n const enclosureUrl = absoluteUrl(\n post.coverImage?.url,\n env.BLOGPANEL_SITE_URL,\n 'an RSS <enclosure> image'\n );\n if (enclosureUrl) {\n enclosure = `\\n <enclosure url=\"${escapeXml(enclosureUrl)}\" type=\"image/jpeg\" />`;\n }\n\n return ` <item>\n <title>${escapeXml(post.title)}</title>\n <link>${escapeXml(postUrl)}</link>\n <guid isPermaLink=\"true\">${escapeXml(postUrl)}</guid>\n <description><![CDATA[${content || ''}]]></description>\n <pubDate>${pubDate}</pubDate>${enclosure}${\n post.categories?.length\n ? post.categories.map((c: string) => `\\n <category>${escapeXml(c)}</category>`).join('')\n : ''\n }\n </item>`;\n })\n .join('\\n');\n\n const buildDate = new Date().toUTCString();\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>${escapeXml(env.BLOGPANEL_SITE_NAME)} Blog</title>\n <link>${escapeXml(env.BLOGPANEL_SITE_URL)}${config.basePath}</link>\n <description>Latest posts from ${escapeXml(env.BLOGPANEL_SITE_NAME)}</description>\n <language>en</language>\n <lastBuildDate>${buildDate}</lastBuildDate>\n <atom:link href=\"${escapeXml(env.BLOGPANEL_SITE_URL)}${escapeXml(config.apiPath)}/rss.xml\" rel=\"self\" type=\"application/rss+xml\" />\n${items}\n </channel>\n</rss>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n"]}
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ var chunkXLYTATRQ_cjs = require('./chunk-XLYTATRQ.cjs');
4
+ var server = require('next/server');
5
+
6
+ function jsonSuccess(data, meta, status = 200) {
7
+ return server.NextResponse.json({ success: true, data, meta }, { status });
8
+ }
9
+ function jsonError(code, message, status = 400) {
10
+ return server.NextResponse.json(
11
+ { success: false, error: { code, message } },
12
+ { status }
13
+ );
14
+ }
15
+ async function requireAuth(request) {
16
+ const env = chunkXLYTATRQ_cjs.getEnvConfig();
17
+ const authHeader = request.headers.get("authorization");
18
+ if (!authHeader) {
19
+ return jsonError("UNAUTHORIZED", "Authorization header is required", 401);
20
+ }
21
+ const token = authHeader.replace(/^Bearer\s+/i, "");
22
+ if (token === env.BLOGPANEL_API_KEY) return null;
23
+ const { verifyApiToken } = await import('./db-TDCZNY3Q.cjs');
24
+ const dbToken = await verifyApiToken(token);
25
+ if (dbToken) return null;
26
+ return jsonError("FORBIDDEN", "Invalid API key", 403);
27
+ }
28
+ function requireMasterAuth(request) {
29
+ const env = chunkXLYTATRQ_cjs.getEnvConfig();
30
+ const authHeader = request.headers.get("authorization");
31
+ if (!authHeader) {
32
+ return jsonError("UNAUTHORIZED", "Authorization header is required", 401);
33
+ }
34
+ const token = authHeader.replace(/^Bearer\s+/i, "");
35
+ if (token !== env.BLOGPANEL_API_KEY) {
36
+ return jsonError("FORBIDDEN", "Invalid API key", 403);
37
+ }
38
+ return null;
39
+ }
40
+ function getSearchParams(request) {
41
+ const url = new URL(request.url);
42
+ return Object.fromEntries(url.searchParams.entries());
43
+ }
44
+ function parseIntParam(value, defaultValue) {
45
+ if (!value) return defaultValue;
46
+ const parsed = parseInt(value, 10);
47
+ return isNaN(parsed) ? defaultValue : parsed;
48
+ }
49
+
50
+ exports.getSearchParams = getSearchParams;
51
+ exports.jsonError = jsonError;
52
+ exports.jsonSuccess = jsonSuccess;
53
+ exports.parseIntParam = parseIntParam;
54
+ exports.requireAuth = requireAuth;
55
+ exports.requireMasterAuth = requireMasterAuth;
56
+ //# sourceMappingURL=chunk-DWNMHQ4I.cjs.map
57
+ //# sourceMappingURL=chunk-DWNMHQ4I.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/middleware.ts"],"names":["NextResponse","getEnvConfig"],"mappings":";;;;;AAIO,SAAS,WAAA,CAAY,IAAA,EAAe,IAAA,EAAgC,MAAA,GAAS,GAAA,EAAK;AACvF,EAAA,OAAOA,mBAAA,CAAa,IAAA,CAAK,EAAE,OAAA,EAAS,IAAA,EAAM,MAAM,IAAA,EAAK,EAAG,EAAE,MAAA,EAAQ,CAAA;AACpE;AAEO,SAAS,SAAA,CAAU,IAAA,EAAc,OAAA,EAAiB,MAAA,GAAS,GAAA,EAAqC;AACrG,EAAA,OAAOA,mBAAA,CAAa,IAAA;AAAA,IAClB,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,EAAE,IAAA,EAAM,SAAQ,EAAE;AAAA,IAC3C,EAAE,MAAA;AAAO,GACX;AACF;AAEA,eAAsB,YAAY,OAAA,EAAkE;AAClG,EAAA,MAAM,MAAMC,8BAAA,EAAa;AACzB,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAEtD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,SAAA,CAAU,cAAA,EAAgB,kCAAA,EAAoC,GAAG,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAGlD,EAAA,IAAI,KAAA,KAAU,GAAA,CAAI,iBAAA,EAAmB,OAAO,IAAA;AAG5C,EAAA,MAAM,EAAE,cAAA,EAAe,GAAI,MAAM,OAAO,mBAAW,CAAA;AACnD,EAAA,MAAM,OAAA,GAAU,MAAM,cAAA,CAAe,KAAK,CAAA;AAC1C,EAAA,IAAI,SAAS,OAAO,IAAA;AAEpB,EAAA,OAAO,SAAA,CAAU,WAAA,EAAa,iBAAA,EAAmB,GAAG,CAAA;AACtD;AAEO,SAAS,kBAAkB,OAAA,EAAyD;AACzF,EAAA,MAAM,MAAMA,8BAAA,EAAa;AACzB,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAEtD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,SAAA,CAAU,cAAA,EAAgB,kCAAA,EAAoC,GAAG,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAClD,EAAA,IAAI,KAAA,KAAU,IAAI,iBAAA,EAAmB;AACnC,IAAA,OAAO,SAAA,CAAU,WAAA,EAAa,iBAAA,EAAmB,GAAG,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,gBAAgB,OAAA,EAAkB;AAChD,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,OAAO,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,YAAA,CAAa,SAAS,CAAA;AACtD;AAEO,SAAS,aAAA,CAAc,OAA2B,YAAA,EAA8B;AACrF,EAAA,IAAI,CAAC,OAAO,OAAO,YAAA;AACnB,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AACjC,EAAA,OAAO,KAAA,CAAM,MAAM,CAAA,GAAI,YAAA,GAAe,MAAA;AACxC","file":"chunk-DWNMHQ4I.cjs","sourcesContent":["import { NextResponse } from 'next/server';\nimport { getEnvConfig } from '../lib/config';\nimport type { ApiErrorResponse } from '../lib/types';\n\nexport function jsonSuccess(data: unknown, meta?: Record<string, unknown>, status = 200) {\n return NextResponse.json({ success: true, data, meta }, { status });\n}\n\nexport function jsonError(code: string, message: string, status = 400): NextResponse<ApiErrorResponse> {\n return NextResponse.json(\n { success: false, error: { code, message } },\n { status }\n ) as NextResponse<ApiErrorResponse>;\n}\n\nexport async function requireAuth(request: Request): Promise<NextResponse<ApiErrorResponse> | null> {\n const env = getEnvConfig();\n const authHeader = request.headers.get('authorization');\n\n if (!authHeader) {\n return jsonError('UNAUTHORIZED', 'Authorization header is required', 401);\n }\n\n const token = authHeader.replace(/^Bearer\\s+/i, '');\n\n // Fast path: master key\n if (token === env.BLOGPANEL_API_KEY) return null;\n\n // Check DB tokens\n const { verifyApiToken } = await import('../lib/db');\n const dbToken = await verifyApiToken(token);\n if (dbToken) return null;\n\n return jsonError('FORBIDDEN', 'Invalid API key', 403);\n}\n\nexport function requireMasterAuth(request: Request): NextResponse<ApiErrorResponse> | null {\n const env = getEnvConfig();\n const authHeader = request.headers.get('authorization');\n\n if (!authHeader) {\n return jsonError('UNAUTHORIZED', 'Authorization header is required', 401);\n }\n\n const token = authHeader.replace(/^Bearer\\s+/i, '');\n if (token !== env.BLOGPANEL_API_KEY) {\n return jsonError('FORBIDDEN', 'Invalid API key', 403);\n }\n\n return null;\n}\n\nexport function getSearchParams(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n}\n\nexport function parseIntParam(value: string | undefined, defaultValue: number): number {\n if (!value) return defaultValue;\n const parsed = parseInt(value, 10);\n return isNaN(parsed) ? defaultValue : parsed;\n}\n"]}
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ var chunk4GK22Q3R_cjs = require('./chunk-4GK22Q3R.cjs');
4
+ var chunkXLYTATRQ_cjs = require('./chunk-XLYTATRQ.cjs');
5
+
6
+ // src/lib/image.ts
7
+ var RESPONSIVE_SIZES = [640, 768, 1024, 1280, 1920];
8
+ var QUALITY_STEPS = [85, 75, 65, 55];
9
+ var DIMENSION_CAPS = [null, 2560, 1920];
10
+ var ImageTooLargeError = class extends Error {
11
+ constructor(achievedBytes, limitBytes) {
12
+ super(
13
+ `Could not compress image below ${limitBytes} bytes (best achieved: ${achievedBytes})`
14
+ );
15
+ this.achievedBytes = achievedBytes;
16
+ this.limitBytes = limitBytes;
17
+ this.name = "ImageTooLargeError";
18
+ }
19
+ };
20
+ async function compressToLimit(file, limitBytes) {
21
+ const sharp = (await import('sharp')).default;
22
+ let best = Number.POSITIVE_INFINITY;
23
+ for (const cap of DIMENSION_CAPS) {
24
+ for (const quality of QUALITY_STEPS) {
25
+ let pipe = sharp(file);
26
+ if (cap !== null) {
27
+ pipe = pipe.resize(cap, null, { withoutEnlargement: true });
28
+ }
29
+ const out = await pipe.webp({ quality }).toBuffer();
30
+ if (out.length <= limitBytes) return out;
31
+ best = Math.min(best, out.length);
32
+ }
33
+ }
34
+ throw new ImageTooLargeError(best, limitBytes);
35
+ }
36
+ async function processImage(file, filename, storage) {
37
+ let sharp;
38
+ try {
39
+ sharp = (await import('sharp')).default;
40
+ } catch {
41
+ const result = await storage.upload(file, filename, getMimeType(filename));
42
+ return {
43
+ original: result,
44
+ width: 0,
45
+ height: 0,
46
+ format: getExtension(filename),
47
+ uploadedKeys: [result.key]
48
+ };
49
+ }
50
+ const image = sharp(file);
51
+ const metadata = await image.metadata();
52
+ const { maxImageSize } = chunkXLYTATRQ_cjs.getConfig().editor;
53
+ const webpBuffer = await compressToLimit(file, maxImageSize);
54
+ const webpFilename = filename.replace(/\.[^.]+$/, ".webp");
55
+ const original = await storage.upload(webpBuffer, webpFilename, "image/webp");
56
+ let derivativeKeys;
57
+ try {
58
+ derivativeKeys = await generateResponsiveSizes(file, webpFilename, storage, metadata.width || 0);
59
+ } catch (error) {
60
+ await chunk4GK22Q3R_cjs.rollbackUploads(storage, [original.key], "derivative generation failed");
61
+ throw error;
62
+ }
63
+ return {
64
+ original,
65
+ width: metadata.width || 0,
66
+ height: metadata.height || 0,
67
+ format: "webp",
68
+ uploadedKeys: [original.key, ...derivativeKeys]
69
+ };
70
+ }
71
+ async function generateResponsiveSizes(file, filename, storage, originalWidth) {
72
+ const sharp = (await import('sharp')).default;
73
+ const sizes = RESPONSIVE_SIZES.filter((s) => s < originalWidth);
74
+ const uploadSize = async (width) => {
75
+ const resized = await sharp(file).resize(width).webp({ quality: 80 }).toBuffer();
76
+ const sizedFilename = filename.replace(".webp", `-${width}w.webp`);
77
+ return storage.upload(resized, sizedFilename, "image/webp");
78
+ };
79
+ const uploadThumbnail = async () => {
80
+ const thumb = await sharp(file).resize(200, 200, { fit: "cover" }).webp({ quality: 70 }).toBuffer();
81
+ const thumbFilename = filename.replace(".webp", "-thumb.webp");
82
+ return storage.upload(thumb, thumbFilename, "image/webp");
83
+ };
84
+ const results = await Promise.allSettled([...sizes.map(uploadSize), uploadThumbnail()]);
85
+ const failures = results.filter(
86
+ (r) => r.status === "rejected"
87
+ );
88
+ if (failures.length > 0) {
89
+ const succeeded = results.filter(
90
+ (r) => r.status === "fulfilled"
91
+ );
92
+ await chunk4GK22Q3R_cjs.rollbackUploads(
93
+ storage,
94
+ succeeded.map((r) => r.value.key),
95
+ "derivative generation failed"
96
+ );
97
+ throw failures[0].reason;
98
+ }
99
+ return results.map((r) => r.value.key);
100
+ }
101
+ function getMimeType(filename) {
102
+ const ext = getExtension(filename);
103
+ const mimeTypes = {
104
+ jpg: "image/jpeg",
105
+ jpeg: "image/jpeg",
106
+ png: "image/png",
107
+ gif: "image/gif",
108
+ webp: "image/webp",
109
+ svg: "image/svg+xml",
110
+ avif: "image/avif"
111
+ };
112
+ return mimeTypes[ext] || "application/octet-stream";
113
+ }
114
+ function getExtension(filename) {
115
+ return filename.split(".").pop()?.toLowerCase() || "";
116
+ }
117
+
118
+ exports.ImageTooLargeError = ImageTooLargeError;
119
+ exports.processImage = processImage;
120
+ //# sourceMappingURL=chunk-FPGVR2BX.cjs.map
121
+ //# sourceMappingURL=chunk-FPGVR2BX.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/image.ts"],"names":["getConfig","rollbackUploads"],"mappings":"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","file":"chunk-FPGVR2BX.cjs","sourcesContent":["import type { StorageProvider, MediaUploadResult } from './storage';\nimport { rollbackUploads } from './storage';\nimport { getConfig } from './config';\n\nexport interface ProcessedImage {\n original: MediaUploadResult;\n width: number;\n height: number;\n format: string;\n /**\n * Every storage key this call wrote — the original plus every responsive\n * derivative and the thumbnail. A caller that persists a reference\n * afterward (e.g. createMedia) and then fails must roll all of these back,\n * since none of them would otherwise be reachable through the UI.\n */\n uploadedKeys: string[];\n}\n\nconst RESPONSIVE_SIZES = [640, 768, 1024, 1280, 1920];\n\nconst QUALITY_STEPS = [85, 75, 65, 55];\nconst DIMENSION_CAPS: (number | null)[] = [null, 2560, 1920];\n\nexport class ImageTooLargeError extends Error {\n constructor(\n public readonly achievedBytes: number,\n public readonly limitBytes: number\n ) {\n super(\n `Could not compress image below ${limitBytes} bytes (best achieved: ${achievedBytes})`\n );\n this.name = 'ImageTooLargeError';\n }\n}\n\n/**\n * Encodes to WebP, degrading quality then dimensions until the result fits\n * limitBytes. Returns on the first success, so a normal image costs one encode.\n * Worst case is 12 encodes before ImageTooLargeError.\n */\nexport async function compressToLimit(file: Buffer, limitBytes: number): Promise<Buffer> {\n const sharp = (await import('sharp')).default;\n let best = Number.POSITIVE_INFINITY;\n\n for (const cap of DIMENSION_CAPS) {\n for (const quality of QUALITY_STEPS) {\n let pipe = sharp(file);\n if (cap !== null) {\n pipe = pipe.resize(cap, null, { withoutEnlargement: true });\n }\n const out = await pipe.webp({ quality }).toBuffer();\n if (out.length <= limitBytes) return out;\n best = Math.min(best, out.length);\n }\n }\n\n throw new ImageTooLargeError(best, limitBytes);\n}\n\nexport async function processImage(\n file: Buffer,\n filename: string,\n storage: StorageProvider\n): Promise<ProcessedImage> {\n let sharp: typeof import('sharp');\n try {\n sharp = (await import('sharp')).default;\n } catch {\n // sharp not available — upload raw file\n const result = await storage.upload(file, filename, getMimeType(filename));\n return {\n original: result,\n width: 0,\n height: 0,\n format: getExtension(filename),\n uploadedKeys: [result.key],\n };\n }\n\n const image = sharp(file);\n const metadata = await image.metadata();\n\n const { maxImageSize } = getConfig().editor;\n const webpBuffer = await compressToLimit(file, maxImageSize);\n const webpFilename = filename.replace(/\\.[^.]+$/, '.webp');\n\n const original = await storage.upload(webpBuffer, webpFilename, 'image/webp');\n\n // Awaited, not fire-and-forget: serverless runtimes may freeze the process\n // once the response is returned, which would silently drop these writes and\n // leave the media library showing broken thumbnails.\n //\n // A consequence of awaiting: a derivative failure now surfaces here, after\n // `original` has already landed in storage but before the caller has had a\n // chance to record it anywhere. Left alone, that upload becomes an orphan —\n // it counts against quota, and nothing (not even this module) has a\n // reference to it once the exception unwinds past this function.\n //\n // We choose atomicity over partial success: on any derivative failure, undo\n // the original upload (best-effort) and rethrow, so a failed processImage()\n // call never leaves storage holding an object nothing points to. The\n // alternative — record the DB row before generating derivatives — was\n // rejected because it trades an invisible-but-harmless storage orphan for a\n // *visible*, permanently broken media row (missing responsive sizes/\n // thumbnail), which is worse: it ships silently to the media library and to\n // any post that references it, whereas atomic failure just asks the user to\n // retry the upload.\n let derivativeKeys: string[];\n try {\n derivativeKeys = await generateResponsiveSizes(file, webpFilename, storage, metadata.width || 0);\n } catch (error) {\n await rollbackUploads(storage, [original.key], 'derivative generation failed');\n throw error;\n }\n\n return {\n original,\n width: metadata.width || 0,\n height: metadata.height || 0,\n format: 'webp',\n uploadedKeys: [original.key, ...derivativeKeys],\n };\n}\n\nasync function generateResponsiveSizes(\n file: Buffer,\n filename: string,\n storage: StorageProvider,\n originalWidth: number\n): Promise<string[]> {\n const sharp = (await import('sharp')).default;\n\n const sizes = RESPONSIVE_SIZES.filter((s) => s < originalWidth);\n\n const uploadSize = async (width: number): Promise<MediaUploadResult> => {\n const resized = await sharp(file).resize(width).webp({ quality: 80 }).toBuffer();\n const sizedFilename = filename.replace('.webp', `-${width}w.webp`);\n return storage.upload(resized, sizedFilename, 'image/webp');\n };\n\n const uploadThumbnail = async (): Promise<MediaUploadResult> => {\n const thumb = await sharp(file)\n .resize(200, 200, { fit: 'cover' })\n .webp({ quality: 70 })\n .toBuffer();\n const thumbFilename = filename.replace('.webp', '-thumb.webp');\n return storage.upload(thumb, thumbFilename, 'image/webp');\n };\n\n // allSettled (not all) so every derivative attempt runs to completion before\n // we decide pass/fail — otherwise a fast rejection could resolve this\n // function's promise while sibling uploads are still in flight, and any\n // that land afterward would themselves become untracked orphans.\n const results = await Promise.allSettled([...sizes.map(uploadSize), uploadThumbnail()]);\n\n const failures = results.filter(\n (r): r is PromiseRejectedResult => r.status === 'rejected'\n );\n\n if (failures.length > 0) {\n const succeeded = results.filter(\n (r): r is PromiseFulfilledResult<MediaUploadResult> => r.status === 'fulfilled'\n );\n // Best-effort: undo whichever derivatives did land before the failure.\n await rollbackUploads(\n storage,\n succeeded.map((r) => r.value.key),\n 'derivative generation failed'\n );\n throw failures[0].reason;\n }\n\n return (results as PromiseFulfilledResult<MediaUploadResult>[]).map((r) => r.value.key);\n}\n\nfunction getMimeType(filename: string): string {\n const ext = getExtension(filename);\n const mimeTypes: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n avif: 'image/avif',\n };\n return mimeTypes[ext] || 'application/octet-stream';\n}\n\nfunction getExtension(filename: string): string {\n return filename.split('.').pop()?.toLowerCase() || '';\n}\n"]}
@@ -0,0 +1,50 @@
1
+ import { getEnvConfig } from './chunk-6PZ772NQ.js';
2
+ import { NextResponse } from 'next/server';
3
+
4
+ function jsonSuccess(data, meta, status = 200) {
5
+ return NextResponse.json({ success: true, data, meta }, { status });
6
+ }
7
+ function jsonError(code, message, status = 400) {
8
+ return NextResponse.json(
9
+ { success: false, error: { code, message } },
10
+ { status }
11
+ );
12
+ }
13
+ async function requireAuth(request) {
14
+ const env = getEnvConfig();
15
+ const authHeader = request.headers.get("authorization");
16
+ if (!authHeader) {
17
+ return jsonError("UNAUTHORIZED", "Authorization header is required", 401);
18
+ }
19
+ const token = authHeader.replace(/^Bearer\s+/i, "");
20
+ if (token === env.BLOGPANEL_API_KEY) return null;
21
+ const { verifyApiToken } = await import('./db-M3KXLBUP.js');
22
+ const dbToken = await verifyApiToken(token);
23
+ if (dbToken) return null;
24
+ return jsonError("FORBIDDEN", "Invalid API key", 403);
25
+ }
26
+ function requireMasterAuth(request) {
27
+ const env = getEnvConfig();
28
+ const authHeader = request.headers.get("authorization");
29
+ if (!authHeader) {
30
+ return jsonError("UNAUTHORIZED", "Authorization header is required", 401);
31
+ }
32
+ const token = authHeader.replace(/^Bearer\s+/i, "");
33
+ if (token !== env.BLOGPANEL_API_KEY) {
34
+ return jsonError("FORBIDDEN", "Invalid API key", 403);
35
+ }
36
+ return null;
37
+ }
38
+ function getSearchParams(request) {
39
+ const url = new URL(request.url);
40
+ return Object.fromEntries(url.searchParams.entries());
41
+ }
42
+ function parseIntParam(value, defaultValue) {
43
+ if (!value) return defaultValue;
44
+ const parsed = parseInt(value, 10);
45
+ return isNaN(parsed) ? defaultValue : parsed;
46
+ }
47
+
48
+ export { getSearchParams, jsonError, jsonSuccess, parseIntParam, requireAuth, requireMasterAuth };
49
+ //# sourceMappingURL=chunk-JBP3G4QJ.js.map
50
+ //# sourceMappingURL=chunk-JBP3G4QJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/middleware.ts"],"names":[],"mappings":";;;AAIO,SAAS,WAAA,CAAY,IAAA,EAAe,IAAA,EAAgC,MAAA,GAAS,GAAA,EAAK;AACvF,EAAA,OAAO,YAAA,CAAa,IAAA,CAAK,EAAE,OAAA,EAAS,IAAA,EAAM,MAAM,IAAA,EAAK,EAAG,EAAE,MAAA,EAAQ,CAAA;AACpE;AAEO,SAAS,SAAA,CAAU,IAAA,EAAc,OAAA,EAAiB,MAAA,GAAS,GAAA,EAAqC;AACrG,EAAA,OAAO,YAAA,CAAa,IAAA;AAAA,IAClB,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,EAAE,IAAA,EAAM,SAAQ,EAAE;AAAA,IAC3C,EAAE,MAAA;AAAO,GACX;AACF;AAEA,eAAsB,YAAY,OAAA,EAAkE;AAClG,EAAA,MAAM,MAAM,YAAA,EAAa;AACzB,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAEtD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,SAAA,CAAU,cAAA,EAAgB,kCAAA,EAAoC,GAAG,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAGlD,EAAA,IAAI,KAAA,KAAU,GAAA,CAAI,iBAAA,EAAmB,OAAO,IAAA;AAG5C,EAAA,MAAM,EAAE,cAAA,EAAe,GAAI,MAAM,OAAO,kBAAW,CAAA;AACnD,EAAA,MAAM,OAAA,GAAU,MAAM,cAAA,CAAe,KAAK,CAAA;AAC1C,EAAA,IAAI,SAAS,OAAO,IAAA;AAEpB,EAAA,OAAO,SAAA,CAAU,WAAA,EAAa,iBAAA,EAAmB,GAAG,CAAA;AACtD;AAEO,SAAS,kBAAkB,OAAA,EAAyD;AACzF,EAAA,MAAM,MAAM,YAAA,EAAa;AACzB,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AAEtD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,SAAA,CAAU,cAAA,EAAgB,kCAAA,EAAoC,GAAG,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAClD,EAAA,IAAI,KAAA,KAAU,IAAI,iBAAA,EAAmB;AACnC,IAAA,OAAO,SAAA,CAAU,WAAA,EAAa,iBAAA,EAAmB,GAAG,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,gBAAgB,OAAA,EAAkB;AAChD,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,OAAO,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,YAAA,CAAa,SAAS,CAAA;AACtD;AAEO,SAAS,aAAA,CAAc,OAA2B,YAAA,EAA8B;AACrF,EAAA,IAAI,CAAC,OAAO,OAAO,YAAA;AACnB,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AACjC,EAAA,OAAO,KAAA,CAAM,MAAM,CAAA,GAAI,YAAA,GAAe,MAAA;AACxC","file":"chunk-JBP3G4QJ.js","sourcesContent":["import { NextResponse } from 'next/server';\nimport { getEnvConfig } from '../lib/config';\nimport type { ApiErrorResponse } from '../lib/types';\n\nexport function jsonSuccess(data: unknown, meta?: Record<string, unknown>, status = 200) {\n return NextResponse.json({ success: true, data, meta }, { status });\n}\n\nexport function jsonError(code: string, message: string, status = 400): NextResponse<ApiErrorResponse> {\n return NextResponse.json(\n { success: false, error: { code, message } },\n { status }\n ) as NextResponse<ApiErrorResponse>;\n}\n\nexport async function requireAuth(request: Request): Promise<NextResponse<ApiErrorResponse> | null> {\n const env = getEnvConfig();\n const authHeader = request.headers.get('authorization');\n\n if (!authHeader) {\n return jsonError('UNAUTHORIZED', 'Authorization header is required', 401);\n }\n\n const token = authHeader.replace(/^Bearer\\s+/i, '');\n\n // Fast path: master key\n if (token === env.BLOGPANEL_API_KEY) return null;\n\n // Check DB tokens\n const { verifyApiToken } = await import('../lib/db');\n const dbToken = await verifyApiToken(token);\n if (dbToken) return null;\n\n return jsonError('FORBIDDEN', 'Invalid API key', 403);\n}\n\nexport function requireMasterAuth(request: Request): NextResponse<ApiErrorResponse> | null {\n const env = getEnvConfig();\n const authHeader = request.headers.get('authorization');\n\n if (!authHeader) {\n return jsonError('UNAUTHORIZED', 'Authorization header is required', 401);\n }\n\n const token = authHeader.replace(/^Bearer\\s+/i, '');\n if (token !== env.BLOGPANEL_API_KEY) {\n return jsonError('FORBIDDEN', 'Invalid API key', 403);\n }\n\n return null;\n}\n\nexport function getSearchParams(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n}\n\nexport function parseIntParam(value: string | undefined, defaultValue: number): number {\n if (!value) return defaultValue;\n const parsed = parseInt(value, 10);\n return isNaN(parsed) ? defaultValue : parsed;\n}\n"]}
@@ -0,0 +1,215 @@
1
+ import { getEnvConfig, getDb, requireApiPath, isR2Configured } from './chunk-6PZ772NQ.js';
2
+ import { PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';
3
+ import { GridFSBucket } from 'mongodb';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ var BUILT_IN_PROVIDER_NAMES = ["gridfs", "r2"];
7
+ var clientInstance = null;
8
+ function getS3Client() {
9
+ if (clientInstance) return clientInstance;
10
+ if (!isR2Configured()) {
11
+ throw new Error(
12
+ "Cloudflare R2 is not configured. Set BLOGPANEL_R2_ACCOUNT_ID, BLOGPANEL_R2_ACCESS_KEY, BLOGPANEL_R2_SECRET_KEY, BLOGPANEL_R2_BUCKET, and BLOGPANEL_R2_PUBLIC_URL environment variables to enable image uploads."
13
+ );
14
+ }
15
+ const env = getEnvConfig();
16
+ clientInstance = new S3Client({
17
+ region: "auto",
18
+ endpoint: `https://${env.BLOGPANEL_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
19
+ credentials: {
20
+ accessKeyId: env.BLOGPANEL_R2_ACCESS_KEY,
21
+ secretAccessKey: env.BLOGPANEL_R2_SECRET_KEY
22
+ }
23
+ });
24
+ return clientInstance;
25
+ }
26
+ function generateKey(filename) {
27
+ const now = /* @__PURE__ */ new Date();
28
+ const year = now.getFullYear();
29
+ const month = String(now.getMonth() + 1).padStart(2, "0");
30
+ const uuid = randomUUID().split("-")[0];
31
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
32
+ return `media/${year}/${month}/${uuid}-${safeName}`;
33
+ }
34
+ var R2StorageProvider = class {
35
+ constructor() {
36
+ this.name = "r2";
37
+ }
38
+ async upload(file, filename, contentType) {
39
+ const env = getEnvConfig();
40
+ const client = getS3Client();
41
+ const key = generateKey(filename);
42
+ await client.send(
43
+ new PutObjectCommand({
44
+ Bucket: env.BLOGPANEL_R2_BUCKET,
45
+ Key: key,
46
+ Body: file,
47
+ ContentType: contentType
48
+ })
49
+ );
50
+ return {
51
+ key,
52
+ url: `${env.BLOGPANEL_R2_PUBLIC_URL}/${key}`,
53
+ size: file.length,
54
+ contentType
55
+ };
56
+ }
57
+ async delete(key) {
58
+ const env = getEnvConfig();
59
+ const client = getS3Client();
60
+ await client.send(
61
+ new DeleteObjectCommand({
62
+ Bucket: env.BLOGPANEL_R2_BUCKET,
63
+ Key: key
64
+ })
65
+ );
66
+ }
67
+ async list(prefix) {
68
+ const env = getEnvConfig();
69
+ const client = getS3Client();
70
+ const response = await client.send(
71
+ new ListObjectsV2Command({
72
+ Bucket: env.BLOGPANEL_R2_BUCKET,
73
+ Prefix: prefix || "media/"
74
+ })
75
+ );
76
+ return (response.Contents || []).map((obj) => ({
77
+ key: obj.Key || "",
78
+ size: obj.Size || 0,
79
+ lastModified: obj.LastModified || /* @__PURE__ */ new Date()
80
+ }));
81
+ }
82
+ };
83
+ var GRIDFS_BUCKET = "media";
84
+ var GridFSStorageProvider = class {
85
+ constructor() {
86
+ this.name = "gridfs";
87
+ }
88
+ async bucket() {
89
+ const db = await getDb();
90
+ return new GridFSBucket(db, { bucketName: GRIDFS_BUCKET });
91
+ }
92
+ async upload(file, filename, contentType) {
93
+ const apiPath = requireApiPath("the URL for this GridFS upload");
94
+ const bucket = await this.bucket();
95
+ const key = generateKey(filename);
96
+ await new Promise((resolve, reject) => {
97
+ const stream = bucket.openUploadStream(key, { contentType });
98
+ stream.on("error", reject);
99
+ stream.on("finish", () => resolve());
100
+ stream.end(file);
101
+ });
102
+ return {
103
+ key,
104
+ url: `${apiPath}/media/file/${key}`,
105
+ size: file.length,
106
+ contentType
107
+ };
108
+ }
109
+ async delete(key) {
110
+ const bucket = await this.bucket();
111
+ const files = await bucket.find({ filename: key }).toArray();
112
+ for (const file of files) {
113
+ await bucket.delete(file._id);
114
+ }
115
+ }
116
+ async list(prefix) {
117
+ const bucket = await this.bucket();
118
+ const pattern = new RegExp("^" + (prefix || "media/").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
119
+ const files = await bucket.find({ filename: { $regex: pattern } }).toArray();
120
+ return files.map((f) => ({
121
+ key: f.filename,
122
+ size: f.length,
123
+ lastModified: f.uploadDate
124
+ }));
125
+ }
126
+ };
127
+ var REGISTRY_SLOT = /* @__PURE__ */ Symbol.for("next-blogpanel.storage.registry.v1");
128
+ function registry() {
129
+ const store = globalThis;
130
+ let current = store[REGISTRY_SLOT];
131
+ if (!current) {
132
+ current = /* @__PURE__ */ new Map();
133
+ store[REGISTRY_SLOT] = current;
134
+ }
135
+ return current;
136
+ }
137
+ var StorageProviderNotRegisteredError = class extends Error {
138
+ constructor(providerName, available) {
139
+ super(
140
+ `Next BlogPanel: no storage provider named '${providerName}' is registered in this server runtime.
141
+ Resolvable here: ${available.map((n) => `'${n}'`).join(", ")}.
142
+ This is refused rather than quietly falling back to the configured default, because every backend generates identically-shaped keys: a default asked to delete a key it does not hold finds nothing and returns cleanly, silently stranding the real object.
143
+ Fix: call registerStorageProvider(new MyProvider()) before any request is served \u2014 the generated setup module (app/next-blogpanel.setup.ts) is the place, since every scaffolded route imports it. If '${providerName}' is a typo in BLOGPANEL_STORAGE or in a stored Media.provider value, correct that instead.
144
+ See docs/storage.md \u2014 "Custom storage providers".`
145
+ );
146
+ this.name = "StorageProviderNotRegisteredError";
147
+ this.providerName = providerName;
148
+ this.available = available;
149
+ }
150
+ };
151
+ function registerStorageProvider(provider) {
152
+ if (!provider || typeof provider !== "object") {
153
+ throw new Error(
154
+ "Next BlogPanel: registerStorageProvider() expects a StorageProvider instance."
155
+ );
156
+ }
157
+ const name = provider.name;
158
+ if (typeof name !== "string" || name.trim() === "") {
159
+ throw new Error(
160
+ "Next BlogPanel: a storage provider must expose a non-empty `name`. That value is written to Media.provider on every upload and read back to route the delete, so a provider without one cannot have its objects deleted."
161
+ );
162
+ }
163
+ if (BUILT_IN_PROVIDER_NAMES.includes(name)) {
164
+ throw new Error(
165
+ `Next BlogPanel: '${name}' is reserved by a built-in storage provider and cannot be registered over. Media documents already record '${name}', so replacing it would redirect their deletes to a backend that does not hold them \u2014 which strands the objects silently. Give your provider a distinct name.`
166
+ );
167
+ }
168
+ for (const method of ["upload", "delete", "list"]) {
169
+ if (typeof provider[method] !== "function") {
170
+ throw new Error(
171
+ `Next BlogPanel: storage provider '${name}' is missing a \`${method}()\` method. upload(), delete() and list() are all required \u2014 see docs/storage.md.`
172
+ );
173
+ }
174
+ }
175
+ registry().set(name, provider);
176
+ return provider;
177
+ }
178
+ function unregisterStorageProvider(name) {
179
+ return registry().delete(name);
180
+ }
181
+ function listStorageProviderNames() {
182
+ return [...BUILT_IN_PROVIDER_NAMES, ...registry().keys()];
183
+ }
184
+ function isStorageProviderRegistered(name) {
185
+ return BUILT_IN_PROVIDER_NAMES.includes(name) || registry().has(name);
186
+ }
187
+ function getStorageProvider() {
188
+ const env = getEnvConfig();
189
+ const forced = env.BLOGPANEL_STORAGE?.trim();
190
+ if (forced) return getStorageProviderByName(forced);
191
+ return isR2Configured() ? new R2StorageProvider() : new GridFSStorageProvider();
192
+ }
193
+ function getStorageProviderByName(name) {
194
+ if (name === "gridfs") return new GridFSStorageProvider();
195
+ if (name === "r2") return new R2StorageProvider();
196
+ const registered = registry().get(name);
197
+ if (registered) return registered;
198
+ throw new StorageProviderNotRegisteredError(name, listStorageProviderNames());
199
+ }
200
+ async function rollbackUploads(storage, keys, context) {
201
+ await Promise.allSettled(
202
+ keys.map(
203
+ (key) => storage.delete(key).catch((cleanupError) => {
204
+ console.error(
205
+ `[blogpanel] Failed to clean up orphaned upload after ${context} (provider=${storage.name}, key=${key}):`,
206
+ cleanupError
207
+ );
208
+ })
209
+ )
210
+ );
211
+ }
212
+
213
+ export { BUILT_IN_PROVIDER_NAMES, GRIDFS_BUCKET, GridFSStorageProvider, R2StorageProvider, StorageProviderNotRegisteredError, generateKey, getStorageProvider, getStorageProviderByName, isStorageProviderRegistered, listStorageProviderNames, registerStorageProvider, rollbackUploads, unregisterStorageProvider };
214
+ //# sourceMappingURL=chunk-JDP4DIHI.js.map
215
+ //# sourceMappingURL=chunk-JDP4DIHI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/storage.ts"],"names":[],"mappings":"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","file":"chunk-JDP4DIHI.js","sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n} from '@aws-sdk/client-s3';\nimport { GridFSBucket } from 'mongodb';\nimport { getEnvConfig, isR2Configured, requireApiPath } from './config';\nimport { getDb } from './db';\nimport { randomUUID } from 'crypto';\n\nexport interface MediaUploadResult {\n key: string;\n url: string;\n size: number;\n contentType: string;\n}\n\nexport interface StorageObject {\n key: string;\n size: number;\n lastModified: Date;\n}\n\n/**\n * The contract every storage backend satisfies — public API, implementable\n * from outside the package.\n *\n * `name` is deliberately a free-form string rather than a union of the two\n * built-ins: it is written verbatim to `Media.provider` on every upload and\n * read back to route the delete, so a third backend that cannot state its own\n * name cannot have its objects deleted. See `registerStorageProvider()` for\n * how a name is made resolvable, and docs/storage.md for what an\n * implementation must guarantee.\n */\nexport interface StorageProvider {\n /**\n * Stable identifier for this backend, persisted to `Media.provider`.\n *\n * Must be stable across deploys — changing it strands every object already\n * recorded under the old name, because `getStorageProviderByName()` will no\n * longer resolve it. `'gridfs'` and `'r2'` are reserved for the built-ins.\n */\n readonly name: string;\n upload(file: Buffer, filename: string, contentType: string): Promise<MediaUploadResult>;\n delete(key: string): Promise<void>;\n list(prefix?: string): Promise<StorageObject[]>;\n}\n\n/**\n * Names owned by the built-in providers. Reserved: registering over them\n * would silently redirect deletes for every object already recorded under\n * that name to a different backend.\n */\nexport const BUILT_IN_PROVIDER_NAMES = ['gridfs', 'r2'] as const;\n\nlet clientInstance: S3Client | null = null;\n\nfunction getS3Client(): S3Client {\n if (clientInstance) return clientInstance;\n\n if (!isR2Configured()) {\n throw new Error(\n 'Cloudflare R2 is not configured. Set BLOGPANEL_R2_ACCOUNT_ID, BLOGPANEL_R2_ACCESS_KEY, BLOGPANEL_R2_SECRET_KEY, BLOGPANEL_R2_BUCKET, and BLOGPANEL_R2_PUBLIC_URL environment variables to enable image uploads.'\n );\n }\n\n const env = getEnvConfig();\n clientInstance = new S3Client({\n region: 'auto',\n endpoint: `https://${env.BLOGPANEL_R2_ACCOUNT_ID!}.r2.cloudflarestorage.com`,\n credentials: {\n accessKeyId: env.BLOGPANEL_R2_ACCESS_KEY!,\n secretAccessKey: env.BLOGPANEL_R2_SECRET_KEY!,\n },\n });\n\n return clientInstance;\n}\n\nexport function generateKey(filename: string): string {\n const now = new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const uuid = randomUUID().split('-')[0];\n const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase();\n return `media/${year}/${month}/${uuid}-${safeName}`;\n}\n\nexport class R2StorageProvider implements StorageProvider {\n readonly name = 'r2' as const;\n\n async upload(\n file: Buffer,\n filename: string,\n contentType: string\n ): Promise<MediaUploadResult> {\n const env = getEnvConfig();\n const client = getS3Client();\n const key = generateKey(filename);\n\n await client.send(\n new PutObjectCommand({\n Bucket: env.BLOGPANEL_R2_BUCKET,\n Key: key,\n Body: file,\n ContentType: contentType,\n })\n );\n\n return {\n key,\n url: `${env.BLOGPANEL_R2_PUBLIC_URL}/${key}`,\n size: file.length,\n contentType,\n };\n }\n\n async delete(key: string): Promise<void> {\n const env = getEnvConfig();\n const client = getS3Client();\n\n await client.send(\n new DeleteObjectCommand({\n Bucket: env.BLOGPANEL_R2_BUCKET,\n Key: key,\n })\n );\n }\n\n async list(prefix?: string): Promise<StorageObject[]> {\n const env = getEnvConfig();\n const client = getS3Client();\n\n const response = await client.send(\n new ListObjectsV2Command({\n Bucket: env.BLOGPANEL_R2_BUCKET,\n Prefix: prefix || 'media/',\n })\n );\n\n return (response.Contents || []).map((obj) => ({\n key: obj.Key || '',\n size: obj.Size || 0,\n lastModified: obj.LastModified || new Date(),\n }));\n }\n}\n\nexport const GRIDFS_BUCKET = 'media';\n\nexport class GridFSStorageProvider implements StorageProvider {\n readonly name = 'gridfs' as const;\n\n private async bucket(): Promise<GridFSBucket> {\n const db = await getDb();\n return new GridFSBucket(db, { bucketName: GRIDFS_BUCKET });\n }\n\n async upload(\n file: Buffer,\n filename: string,\n contentType: string\n ): Promise<MediaUploadResult> {\n // requireApiPath, not getConfig().apiPath: this URL is persisted into the\n // media document and into the body of every post that embeds the image,\n // so a default guessed in place of a config that never reached the server\n // would bake in a route that does not exist — and correcting the config\n // afterwards would not repair a single already-written reference. Fail\n // loudly instead, and fail *before* the bytes are written so a refused\n // upload leaves nothing behind in the bucket.\n const apiPath = requireApiPath('the URL for this GridFS upload');\n\n const bucket = await this.bucket();\n const key = generateKey(filename);\n\n await new Promise<void>((resolve, reject) => {\n const stream = bucket.openUploadStream(key, { contentType });\n stream.on('error', reject);\n stream.on('finish', () => resolve());\n stream.end(file);\n });\n\n return {\n key,\n url: `${apiPath}/media/file/${key}`,\n size: file.length,\n contentType,\n };\n }\n\n async delete(key: string): Promise<void> {\n const bucket = await this.bucket();\n const files = await bucket.find({ filename: key }).toArray();\n for (const file of files) {\n await bucket.delete(file._id);\n }\n }\n\n async list(prefix?: string): Promise<StorageObject[]> {\n const bucket = await this.bucket();\n const pattern = new RegExp('^' + (prefix || 'media/').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\n const files = await bucket.find({ filename: { $regex: pattern } }).toArray();\n\n return files.map((f) => ({\n key: f.filename,\n size: f.length,\n lastModified: f.uploadDate,\n }));\n }\n}\n\n// ============================================================\n// Custom provider registry\n// ============================================================\n\n/**\n * Where registered third-party providers live.\n *\n * Same `globalThis`-keyed-by-`Symbol.for()` treatment as `setConfig()` in\n * config.ts, and for the same reason rather than by imitation. Next.js\n * instantiates server modules per route entry: a `Map` held in module scope\n * and populated while the bundle for `/api/blog/media` was being evaluated is\n * not guaranteed to be the same `Map` the bundle serving some other route\n * reads, and in a production build with per-route bundling it usually is not.\n *\n * The consequence of getting that wrong is worse here than it is for config.\n * A config that fails to propagate produces a wrong-but-uniform apiPath; a\n * registry that fails to propagate means `getStorageProviderByName('my-cdn')`\n * resolves on the route that happened to register and throws on every other\n * one — a backend that works during upload and fails during delete, depending\n * on which route entry the request landed in. Registration must therefore be\n * process-wide, exactly like the standard Next.js MongoClient singleton, and\n * the symbol is registry-keyed so two bundled copies of this module (or two\n * versions of the package in one tree) still address the same slot.\n */\nconst REGISTRY_SLOT = Symbol.for('next-blogpanel.storage.registry.v1');\n\nfunction registry(): Map<string, StorageProvider> {\n const store = globalThis as unknown as Record<symbol, Map<string, StorageProvider> | undefined>;\n let current = store[REGISTRY_SLOT];\n if (!current) {\n current = new Map<string, StorageProvider>();\n store[REGISTRY_SLOT] = current;\n }\n return current;\n}\n\n/**\n * Thrown when a provider name cannot be resolved to a backend.\n *\n * A dedicated class rather than a string-matched `Error`, for the same reason\n * as `ConfigNotInstalledError`: `POST`/`DELETE /media` need to tell \"this\n * install never registered that provider\" apart from any other failure so\n * they can pass the actionable message through instead of flattening it to a\n * generic 500, and an `error.message.startsWith(...)` check silently stops\n * working the moment the wording changes.\n *\n * There is deliberately no fallback to the configured default in its place.\n * `generateKey()` produces identically-shaped keys on every backend, so a\n * default asked to delete a key it does not hold finds nothing and returns\n * cleanly — the object is stranded and nothing reports it. Writing to the\n * wrong backend is the same failure with the bytes going somewhere the stored\n * URL does not point.\n */\nexport class StorageProviderNotRegisteredError extends Error {\n /** The unresolvable name, for callers that want to report it structurally. */\n readonly providerName: string;\n /** Every name that *is* resolvable in this process, built-ins included. */\n readonly available: string[];\n\n constructor(providerName: string, available: string[]) {\n super(\n `Next BlogPanel: no storage provider named '${providerName}' is registered in this server runtime.\\n` +\n ` Resolvable here: ${available.map((n) => `'${n}'`).join(', ')}.\\n` +\n ` This is refused rather than quietly falling back to the configured default, because every backend generates identically-shaped keys: a default asked to delete a key it does not hold finds nothing and returns cleanly, silently stranding the real object.\\n` +\n ` Fix: call registerStorageProvider(new MyProvider()) before any request is served — the generated setup module (app/next-blogpanel.setup.ts) is the place, since every scaffolded route imports it. If '${providerName}' is a typo in BLOGPANEL_STORAGE or in a stored Media.provider value, correct that instead.\\n` +\n ` See docs/storage.md — \"Custom storage providers\".`\n );\n this.name = 'StorageProviderNotRegisteredError';\n this.providerName = providerName;\n this.available = available;\n }\n}\n\n/**\n * Makes a custom backend resolvable by name for the lifetime of this process.\n *\n * Call it before any request is served. The generated `next-blogpanel.setup`\n * module is the intended home: every scaffolded route imports it, so whichever\n * route entry a request lands on first has already run registration.\n *\n * Calling it more than once with the same name is expected and is a silent\n * last-write-wins overwrite, not an error — each per-route copy of the setup\n * module evaluates its own `new MyProvider()`, so the duplicate registrations\n * are a normal consequence of the bundling, exactly as repeated `setConfig()`\n * calls are.\n *\n * The registered value is an instance, not a factory, and is shared by every\n * concurrent request in the process — it must be safe to use that way.\n */\nexport function registerStorageProvider(provider: StorageProvider): StorageProvider {\n if (!provider || typeof provider !== 'object') {\n throw new Error(\n 'Next BlogPanel: registerStorageProvider() expects a StorageProvider instance.'\n );\n }\n\n const name = provider.name;\n if (typeof name !== 'string' || name.trim() === '') {\n throw new Error(\n 'Next BlogPanel: a storage provider must expose a non-empty `name`. That value is written to ' +\n 'Media.provider on every upload and read back to route the delete, so a provider without one ' +\n 'cannot have its objects deleted.'\n );\n }\n\n if ((BUILT_IN_PROVIDER_NAMES as readonly string[]).includes(name)) {\n throw new Error(\n `Next BlogPanel: '${name}' is reserved by a built-in storage provider and cannot be registered over. ` +\n `Media documents already record '${name}', so replacing it would redirect their deletes to a backend ` +\n `that does not hold them — which strands the objects silently. Give your provider a distinct name.`\n );\n }\n\n for (const method of ['upload', 'delete', 'list'] as const) {\n if (typeof provider[method] !== 'function') {\n throw new Error(\n `Next BlogPanel: storage provider '${name}' is missing a \\`${method}()\\` method. ` +\n `upload(), delete() and list() are all required — see docs/storage.md.`\n );\n }\n }\n\n registry().set(name, provider);\n return provider;\n}\n\n/** Removes a previously registered custom provider. Built-in names are not removable. */\nexport function unregisterStorageProvider(name: string): boolean {\n return registry().delete(name);\n}\n\n/** Every provider name resolvable in this process, built-ins first. */\nexport function listStorageProviderNames(): string[] {\n return [...BUILT_IN_PROVIDER_NAMES, ...registry().keys()];\n}\n\n/** True when `name` resolves to a backend without throwing. */\nexport function isStorageProviderRegistered(name: string): boolean {\n return (BUILT_IN_PROVIDER_NAMES as readonly string[]).includes(name) || registry().has(name);\n}\n\n/** Test-only: drops every registered custom provider. */\nexport function resetStorageProviders(): void {\n const store = globalThis as unknown as Record<symbol, unknown>;\n delete store[REGISTRY_SLOT];\n}\n\n// ============================================================\n// Resolution\n// ============================================================\n\n/**\n * Resolves the backend new uploads should go to.\n *\n * `BLOGPANEL_STORAGE` forces a specific one by name — including a registered\n * custom provider — and an unresolvable value throws rather than quietly\n * reverting to the automatic choice. Otherwise R2 is selected when all five\n * of its variables are present, and GridFS is the zero-setup default.\n */\nexport function getStorageProvider(): StorageProvider {\n const env = getEnvConfig();\n\n const forced = env.BLOGPANEL_STORAGE?.trim();\n if (forced) return getStorageProviderByName(forced);\n\n return isR2Configured() ? new R2StorageProvider() : new GridFSStorageProvider();\n}\n\n/**\n * Resolves the provider a specific object was stored under, by name — not by\n * whatever backend happens to be configured right now.\n *\n * `getStorageProvider()` answers \"what should new uploads use today?\", which\n * can diverge from \"what actually holds this particular object?\" once a\n * deployment switches backends (e.g. GridFS -> R2). `generateKey()` produces\n * identically-shaped keys on every provider, so asking the wrong one to\n * delete a key doesn't error — it just finds nothing and returns cleanly,\n * silently orphaning the real object. Callers that know which backend a\n * document was written under (`Media.provider`) must use this instead.\n *\n * The two built-in names always resolve; anything else must have been handed\n * to `registerStorageProvider()` in this process. An unknown name throws\n * `StorageProviderNotRegisteredError` — it never degrades to a default,\n * because that is the exact shape of the silent-stranding bug above.\n */\nexport function getStorageProviderByName(name: string): StorageProvider {\n if (name === 'gridfs') return new GridFSStorageProvider();\n if (name === 'r2') return new R2StorageProvider();\n\n const registered = registry().get(name);\n if (registered) return registered;\n\n throw new StorageProviderNotRegisteredError(name, listStorageProviderNames());\n}\n\n/**\n * Best-effort cleanup of storage objects after a failure elsewhere in the\n * upload pipeline (a derivative failed after the original landed, or a DB\n * write failed after every object uploaded successfully). Never throws — a\n * failed cleanup is logged with enough detail for manual recovery, never\n * allowed to mask the error that triggered the rollback.\n */\nexport async function rollbackUploads(\n storage: StorageProvider,\n keys: string[],\n context: string\n): Promise<void> {\n await Promise.allSettled(\n keys.map((key) =>\n storage.delete(key).catch((cleanupError) => {\n console.error(\n `[blogpanel] Failed to clean up orphaned upload after ${context} ` +\n `(provider=${storage.name}, key=${key}):`,\n cleanupError\n );\n })\n )\n );\n}\n"]}