@natesena/blog-lib 0.1.0 → 0.2.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/README.md +52 -22
- package/dist/server/index.d.ts +59 -1
- package/dist/server/index.js +45 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,55 +178,85 @@ gcs: {
|
|
|
178
178
|
|
|
179
179
|
## 4. Add auth
|
|
180
180
|
|
|
181
|
-
blog-lib
|
|
181
|
+
blog-lib ships pre-built adapters for the most popular Next.js auth providers. One line of setup, zero glue code.
|
|
182
182
|
|
|
183
|
-
###
|
|
183
|
+
### NextAuth.js v5 (Auth.js)
|
|
184
184
|
|
|
185
|
-
```
|
|
186
|
-
|
|
185
|
+
```typescript
|
|
186
|
+
import { configureBlogAuthWithNextAuth } from '@natesena/blog-lib/server';
|
|
187
|
+
import { auth } from '@/auth'; // your Auth.js config
|
|
188
|
+
|
|
189
|
+
configureBlogAuthWithNextAuth(auth);
|
|
187
190
|
```
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
import { checkApiKeyAuth } from '@natesena/blog-lib/server';
|
|
192
|
+
### Clerk
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const isAuthorized = await checkApiKeyAuth(); // reads x-api-key header
|
|
195
|
-
if (!isAuthorized) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
196
|
-
// ...
|
|
197
|
-
}
|
|
194
|
+
```bash
|
|
195
|
+
npm install @clerk/nextjs # if not already installed
|
|
198
196
|
```
|
|
199
197
|
|
|
200
|
-
|
|
198
|
+
```typescript
|
|
199
|
+
import { configureBlogAuthWithClerk } from '@natesena/blog-lib/server';
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
CMS_ADMIN_EMAILS=admin@example.com,editor@example.com
|
|
201
|
+
configureBlogAuthWithClerk();
|
|
204
202
|
```
|
|
205
203
|
|
|
204
|
+
### Supabase Auth
|
|
205
|
+
|
|
206
206
|
```typescript
|
|
207
|
-
import {
|
|
207
|
+
import { configureBlogAuthWithSupabase } from '@natesena/blog-lib/server';
|
|
208
|
+
import { createSupabaseServerClient } from '@/lib/supabase-server';
|
|
208
209
|
|
|
209
|
-
|
|
210
|
-
const hasAccess = canAccessCMS({ id: user.id, email: user.email }); // true/false
|
|
210
|
+
configureBlogAuthWithSupabase(() => createSupabaseServerClient());
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
-
###
|
|
213
|
+
### Custom (any provider)
|
|
214
214
|
|
|
215
|
-
If
|
|
215
|
+
If your auth provider isn't listed above, use `configureBlogAuth` directly:
|
|
216
216
|
|
|
217
217
|
```typescript
|
|
218
218
|
import { configureBlogAuth } from '@natesena/blog-lib/server';
|
|
219
219
|
|
|
220
220
|
configureBlogAuth({
|
|
221
221
|
getCurrentUser: async () => {
|
|
222
|
-
const session = await
|
|
222
|
+
const session = await yourAuthProvider.getSession();
|
|
223
223
|
if (!session?.user) return null;
|
|
224
224
|
return { id: session.user.id, email: session.user.email };
|
|
225
225
|
},
|
|
226
226
|
});
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
### Email-based CMS permissions
|
|
230
|
+
|
|
231
|
+
Control who can access CMS routes with an allowlist:
|
|
232
|
+
|
|
233
|
+
```env
|
|
234
|
+
CMS_ADMIN_EMAILS=admin@example.com,editor@example.com
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { canAccessCMS } from '@natesena/blog-lib/server';
|
|
239
|
+
|
|
240
|
+
const hasAccess = canAccessCMS({ id: user.id, email: user.email }); // true/false
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### API key auth (headless CMS)
|
|
244
|
+
|
|
245
|
+
For API-only setups without user sessions:
|
|
246
|
+
|
|
247
|
+
```env
|
|
248
|
+
CMS_API_KEY=your-secret-key
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { checkApiKeyAuth } from '@natesena/blog-lib/server';
|
|
253
|
+
|
|
254
|
+
export async function GET() {
|
|
255
|
+
const isAuthorized = await checkApiKeyAuth(); // reads x-api-key header
|
|
256
|
+
if (!isAuthorized) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
257
|
+
// ...
|
|
258
|
+
}
|
|
259
|
+
```
|
|
230
260
|
|
|
231
261
|
> **No auth?** Skip this section. The SDK methods have no auth built in — they're just database operations. Add auth at whatever layer makes sense for your app.
|
|
232
262
|
|
package/dist/server/index.d.ts
CHANGED
|
@@ -33,6 +33,64 @@ interface BlogAuthConfig {
|
|
|
33
33
|
*/
|
|
34
34
|
declare function configureBlogAuth(config: BlogAuthConfig): void;
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* NextAuth.js v5 (Auth.js) Adapter
|
|
38
|
+
*
|
|
39
|
+
* One-liner auth setup for NextAuth users:
|
|
40
|
+
* import { configureBlogAuthWithNextAuth } from '@natesena/blog-lib/server';
|
|
41
|
+
* import { auth } from '@/auth';
|
|
42
|
+
* configureBlogAuthWithNextAuth(auth);
|
|
43
|
+
*
|
|
44
|
+
* Ref: Integration plan Phase 0, Step 0.1
|
|
45
|
+
*/
|
|
46
|
+
type NextAuthSession = {
|
|
47
|
+
user?: {
|
|
48
|
+
id?: string;
|
|
49
|
+
email?: string | null;
|
|
50
|
+
} | null;
|
|
51
|
+
} | null;
|
|
52
|
+
declare function configureBlogAuthWithNextAuth(authFunction: () => Promise<NextAuthSession>): void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clerk Auth Adapter
|
|
56
|
+
*
|
|
57
|
+
* One-liner auth setup for Clerk users:
|
|
58
|
+
* import { configureBlogAuthWithClerk } from '@natesena/blog-lib/server';
|
|
59
|
+
* configureBlogAuthWithClerk();
|
|
60
|
+
*
|
|
61
|
+
* Requires @clerk/nextjs to be installed. Lazy-imports it so the package
|
|
62
|
+
* is not required unless this adapter is used.
|
|
63
|
+
*
|
|
64
|
+
* Ref: Integration plan Phase 0, Step 0.1
|
|
65
|
+
*/
|
|
66
|
+
declare function configureBlogAuthWithClerk(): void;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Supabase Auth Adapter
|
|
70
|
+
*
|
|
71
|
+
* One-liner auth setup for Supabase users:
|
|
72
|
+
* import { configureBlogAuthWithSupabase } from '@natesena/blog-lib/server';
|
|
73
|
+
* configureBlogAuthWithSupabase(() => createSupabaseServerClient());
|
|
74
|
+
*
|
|
75
|
+
* Accepts a factory function that returns a Supabase client with auth
|
|
76
|
+
* capabilities. This avoids importing @supabase/ssr directly.
|
|
77
|
+
*
|
|
78
|
+
* Ref: Integration plan Phase 0, Step 0.1
|
|
79
|
+
*/
|
|
80
|
+
interface SupabaseAuthClient {
|
|
81
|
+
auth: {
|
|
82
|
+
getUser: () => Promise<{
|
|
83
|
+
data: {
|
|
84
|
+
user: {
|
|
85
|
+
id: string;
|
|
86
|
+
email?: string;
|
|
87
|
+
} | null;
|
|
88
|
+
};
|
|
89
|
+
}>;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
declare function configureBlogAuthWithSupabase(createClient: () => SupabaseAuthClient): void;
|
|
93
|
+
|
|
36
94
|
/**
|
|
37
95
|
* Auth Permission Utilities
|
|
38
96
|
*
|
|
@@ -128,4 +186,4 @@ declare function confirmImageUpload(destinationFilename: string): Promise<{
|
|
|
128
186
|
isUploadConfirmed: boolean;
|
|
129
187
|
}>;
|
|
130
188
|
|
|
131
|
-
export { type AuthUser, type BlogAuthConfig, type BlogAuthUser, type InitiateUploadInput, type InitiateUploadResult, canAccessCMS, checkApiKeyAuth, configureBlogAuth, confirmImageUpload, initiateImageUpload, requireCMSAuth, verifyApiKey };
|
|
189
|
+
export { type AuthUser, type BlogAuthConfig, type BlogAuthUser, type InitiateUploadInput, type InitiateUploadResult, canAccessCMS, checkApiKeyAuth, configureBlogAuth, configureBlogAuthWithClerk, configureBlogAuthWithNextAuth, configureBlogAuthWithSupabase, confirmImageUpload, initiateImageUpload, requireCMSAuth, verifyApiKey };
|
package/dist/server/index.js
CHANGED
|
@@ -17,6 +17,48 @@ function isBlogAuthConfigured() {
|
|
|
17
17
|
return blogAuthConfig !== null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// src/server/auth/adapters/nextauth.ts
|
|
21
|
+
function configureBlogAuthWithNextAuth(authFunction) {
|
|
22
|
+
configureBlogAuth({
|
|
23
|
+
getCurrentUser: async () => {
|
|
24
|
+
var _a;
|
|
25
|
+
const session = await authFunction();
|
|
26
|
+
if (!((_a = session == null ? void 0 : session.user) == null ? void 0 : _a.email)) return null;
|
|
27
|
+
return {
|
|
28
|
+
id: session.user.id || session.user.email,
|
|
29
|
+
email: session.user.email
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/server/auth/adapters/clerk.ts
|
|
36
|
+
function configureBlogAuthWithClerk() {
|
|
37
|
+
configureBlogAuth({
|
|
38
|
+
getCurrentUser: async () => {
|
|
39
|
+
var _a;
|
|
40
|
+
const { currentUser } = await import("@clerk/nextjs/server");
|
|
41
|
+
const clerkUser = await currentUser();
|
|
42
|
+
if (!clerkUser) return null;
|
|
43
|
+
const clerkUserPrimaryEmail = (_a = clerkUser.emailAddresses[0]) == null ? void 0 : _a.emailAddress;
|
|
44
|
+
if (!clerkUserPrimaryEmail) return null;
|
|
45
|
+
return { id: clerkUser.id, email: clerkUserPrimaryEmail };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/server/auth/adapters/supabase.ts
|
|
51
|
+
function configureBlogAuthWithSupabase(createClient) {
|
|
52
|
+
configureBlogAuth({
|
|
53
|
+
getCurrentUser: async () => {
|
|
54
|
+
const supabase = createClient();
|
|
55
|
+
const { data: { user: supabaseUser } } = await supabase.auth.getUser();
|
|
56
|
+
if (!(supabaseUser == null ? void 0 : supabaseUser.email)) return null;
|
|
57
|
+
return { id: supabaseUser.id, email: supabaseUser.email };
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
20
62
|
// src/server/auth/permissions.ts
|
|
21
63
|
function canAccessCMS(user) {
|
|
22
64
|
const adminEmailsCsv = process.env.CMS_ADMIN_EMAILS;
|
|
@@ -124,6 +166,9 @@ export {
|
|
|
124
166
|
canAccessCMS,
|
|
125
167
|
checkApiKeyAuth,
|
|
126
168
|
configureBlogAuth,
|
|
169
|
+
configureBlogAuthWithClerk,
|
|
170
|
+
configureBlogAuthWithNextAuth,
|
|
171
|
+
configureBlogAuthWithSupabase,
|
|
127
172
|
confirmImageUpload,
|
|
128
173
|
initiateImageUpload,
|
|
129
174
|
requireCMSAuth,
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/auth/config.ts","../../src/server/auth/permissions.ts","../../src/server/auth/api-key.ts","../../src/server/auth/middleware.ts","../../src/server/actions/upload.ts"],"sourcesContent":["/**\n * Pluggable Auth Configuration\n *\n * Allows consumers to inject their own auth provider into blog-lib.\n * Upload actions and other protected operations call getCurrentUser()\n * and reject requests if no user is returned.\n *\n * @example\n * import { configureBlogAuth } from '@nate/blog-lib/server';\n *\n * configureBlogAuth({\n * getCurrentUser: async () => {\n * const session = await auth(); // your auth provider\n * return session?.user ?? null;\n * },\n * });\n *\n * Ref: Plan Phase 3, Step 3.2\n */\n\nexport interface BlogAuthUser {\n id: string;\n email?: string;\n}\n\nexport interface BlogAuthConfig {\n /**\n * Return the currently authenticated user, or null if unauthenticated.\n * Called by upload actions and other protected operations.\n */\n getCurrentUser: () => Promise<BlogAuthUser | null>;\n}\n\nlet blogAuthConfig: BlogAuthConfig | null = null;\n\n/**\n * Configure blog-lib's auth integration. Call once during app initialization.\n */\nexport function configureBlogAuth(config: BlogAuthConfig): void {\n blogAuthConfig = config;\n}\n\n/**\n * Get the currently authenticated user using the configured auth provider.\n * Returns null if no auth provider is configured or no user is authenticated.\n *\n * @internal Used by upload actions and other protected operations.\n */\nexport async function getBlogAuthCurrentUser(): Promise<BlogAuthUser | null> {\n if (!blogAuthConfig) {\n return null;\n }\n return blogAuthConfig.getCurrentUser();\n}\n\n/**\n * Check if blog auth has been configured.\n */\nexport function isBlogAuthConfigured(): boolean {\n return blogAuthConfig !== null;\n}\n","/**\n * Auth Permission Utilities\n *\n * Pluggable permission checks for CMS access.\n * Works with any auth system — Clerk, Auth0, NextAuth.js, or custom.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 308-324)\n * Ref: docs/epic-blog-lib/04-auth-guide.md\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.7)\n */\n\nexport interface AuthUser {\n id: string;\n email: string;\n isAdmin?: boolean;\n}\n\n/**\n * Check if a user has permission to access the CMS.\n *\n * Uses CMS_ADMIN_EMAILS env var (comma-separated) to restrict access.\n * If no env var is set, all authenticated users are allowed.\n */\nexport function canAccessCMS(user: AuthUser): boolean {\n const adminEmailsCsv = process.env.CMS_ADMIN_EMAILS;\n\n if (adminEmailsCsv) {\n const allowedAdminEmails = adminEmailsCsv.split(',').map((email) => email.trim().toLowerCase());\n const normalizedUserEmail = user.email.trim().toLowerCase();\n return allowedAdminEmails.includes(normalizedUserEmail);\n }\n\n // If isAdmin is explicitly set, use it\n if (user.isAdmin !== undefined) {\n return user.isAdmin;\n }\n\n // Default deny — CMS_ADMIN_EMAILS must be set or isAdmin must be true\n // Ref: Plan Phase 3, Step 3.3\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n '[blog-lib] canAccessCMS: No CMS_ADMIN_EMAILS env var set and user.isAdmin is undefined. ' +\n 'Set CMS_ADMIN_EMAILS=email@example.com or pass isAdmin: true to grant CMS access.',\n );\n }\n return false;\n}\n","/**\n * Simple API Key Auth\n *\n * For headless CMS access or simple auth without a full auth provider.\n * Set CMS_API_KEY env var and send it in the x-api-key header.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 646-671)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.7)\n */\n\nimport { headers } from 'next/headers';\n\n/**\n * Verify an API key against the CMS_API_KEY environment variable.\n * Returns false if CMS_API_KEY is not configured.\n */\nexport function verifyApiKey(providedApiKey?: string | null): boolean {\n const configuredApiKey = process.env.CMS_API_KEY;\n\n if (!configuredApiKey) {\n return false; // API key auth not configured\n }\n\n return providedApiKey === configuredApiKey;\n}\n\n/**\n * Check the incoming request's x-api-key header against CMS_API_KEY.\n * Use in server components or API routes.\n */\nexport async function checkApiKeyAuth(): Promise<boolean> {\n const requestHeaders = await headers();\n const providedApiKey = requestHeaders.get('x-api-key');\n return verifyApiKey(providedApiKey);\n}\n","/**\n * Auth Middleware Helper\n *\n * Convenience function to enforce auth in server components.\n * Redirects unauthenticated users and checks CMS permissions.\n *\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.7)\n */\n\nimport { redirect } from 'next/navigation';\nimport { canAccessCMS } from './permissions';\nimport type { AuthUser } from './permissions';\n\n/**\n * Require authenticated CMS access. Redirects to signInUrl if no user,\n * throws Error if user lacks CMS permission.\n *\n * @example\n * const user = await getYourAuthUser();\n * await requireCMSAuth(user, '/sign-in');\n */\nexport async function requireCMSAuth(\n user: AuthUser | null,\n signInRedirectUrl: string = '/sign-in',\n): Promise<void> {\n if (!user) {\n redirect(signInRedirectUrl);\n }\n\n if (!canAccessCMS(user)) {\n throw new Error('Unauthorized: insufficient CMS permissions');\n }\n}\n","/**\n * Image Upload Server Actions\n *\n * Server-side actions for initiating and confirming image uploads to GCS.\n * Frontend calls these, then uploads directly to GCS using the presigned URL.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 927-991)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.10)\n */\n\n'use server';\n\nimport { GCSStorage } from '../../sdk/storage/gcs';\nimport { getBlogAuthCurrentUser, isBlogAuthConfigured } from '../auth/config';\n\nconst ALLOWED_IMAGE_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n];\n\nconst MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB\n\nexport interface InitiateUploadInput {\n fileName: string;\n fileType: string;\n fileSizeBytes: number;\n folder?: string;\n}\n\nexport interface InitiateUploadResult {\n uploadUrl: string;\n publicUrl: string;\n destinationFilename: string;\n}\n\n/**\n * Validate the file and generate a presigned GCS upload URL.\n * The client then uploads directly to GCS using this URL.\n */\nexport async function initiateImageUpload(\n input: InitiateUploadInput,\n): Promise<InitiateUploadResult> {\n // Auth check — require authenticated user if auth is configured\n // Ref: Plan Phase 3, Step 3.2\n if (isBlogAuthConfigured()) {\n const authenticatedUser = await getBlogAuthCurrentUser();\n if (!authenticatedUser) {\n throw new Error('Authentication required. Configure auth with configureBlogAuth().');\n }\n }\n\n // Validate file type\n if (!ALLOWED_IMAGE_MIME_TYPES.includes(input.fileType)) {\n throw new Error(\n `Invalid file type \"${input.fileType}\". Allowed: ${ALLOWED_IMAGE_MIME_TYPES.join(', ')}`,\n );\n }\n\n // Validate file size\n if (input.fileSizeBytes > MAX_FILE_SIZE_BYTES) {\n throw new Error(\n `File too large (${Math.round(input.fileSizeBytes / 1024 / 1024)}MB). Maximum: 10MB.`,\n );\n }\n\n // Generate unique destination filename\n const timestamp = Date.now();\n const randomSuffix = Math.random().toString(36).substring(2, 8);\n const fileExtension = input.fileName.split('.').pop() || 'jpg';\n const destinationFilename = input.folder\n ? `${input.folder}/${timestamp}-${randomSuffix}.${fileExtension}`\n : `${timestamp}-${randomSuffix}.${fileExtension}`;\n\n // Initialize GCS and generate presigned URL\n const gcsStorage = new GCSStorage({\n projectId: process.env.GCP_PROJECT_ID!,\n bucket: process.env.GCS_BUCKET!,\n keyFile: process.env.GCP_KEYFILE,\n });\n\n const { uploadUrl, publicUrl } = await gcsStorage.getPresignedUploadUrl(\n destinationFilename,\n input.fileType,\n );\n\n return { uploadUrl, publicUrl, destinationFilename };\n}\n\n/**\n * Confirm that a file was successfully uploaded to GCS.\n * Call after the client finishes the direct upload.\n */\nexport async function confirmImageUpload(\n destinationFilename: string,\n): Promise<{ isUploadConfirmed: boolean }> {\n // Auth check — require authenticated user if auth is configured\n if (isBlogAuthConfigured()) {\n const authenticatedUser = await getBlogAuthCurrentUser();\n if (!authenticatedUser) {\n throw new Error('Authentication required. Configure auth with configureBlogAuth().');\n }\n }\n\n const gcsStorage = new GCSStorage({\n projectId: process.env.GCP_PROJECT_ID!,\n bucket: process.env.GCS_BUCKET!,\n keyFile: process.env.GCP_KEYFILE,\n });\n\n const isUploadConfirmed = await gcsStorage.exists(destinationFilename);\n if (!isUploadConfirmed) {\n throw new Error('File not found in storage after upload');\n }\n\n return { isUploadConfirmed };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/auth/config.ts","../../src/server/auth/adapters/nextauth.ts","../../src/server/auth/adapters/clerk.ts","../../src/server/auth/adapters/supabase.ts","../../src/server/auth/permissions.ts","../../src/server/auth/api-key.ts","../../src/server/auth/middleware.ts","../../src/server/actions/upload.ts"],"sourcesContent":["/**\n * Pluggable Auth Configuration\n *\n * Allows consumers to inject their own auth provider into blog-lib.\n * Upload actions and other protected operations call getCurrentUser()\n * and reject requests if no user is returned.\n *\n * @example\n * import { configureBlogAuth } from '@nate/blog-lib/server';\n *\n * configureBlogAuth({\n * getCurrentUser: async () => {\n * const session = await auth(); // your auth provider\n * return session?.user ?? null;\n * },\n * });\n *\n * Ref: Plan Phase 3, Step 3.2\n */\n\nexport interface BlogAuthUser {\n id: string;\n email?: string;\n}\n\nexport interface BlogAuthConfig {\n /**\n * Return the currently authenticated user, or null if unauthenticated.\n * Called by upload actions and other protected operations.\n */\n getCurrentUser: () => Promise<BlogAuthUser | null>;\n}\n\nlet blogAuthConfig: BlogAuthConfig | null = null;\n\n/**\n * Configure blog-lib's auth integration. Call once during app initialization.\n */\nexport function configureBlogAuth(config: BlogAuthConfig): void {\n blogAuthConfig = config;\n}\n\n/**\n * Get the currently authenticated user using the configured auth provider.\n * Returns null if no auth provider is configured or no user is authenticated.\n *\n * @internal Used by upload actions and other protected operations.\n */\nexport async function getBlogAuthCurrentUser(): Promise<BlogAuthUser | null> {\n if (!blogAuthConfig) {\n return null;\n }\n return blogAuthConfig.getCurrentUser();\n}\n\n/**\n * Check if blog auth has been configured.\n */\nexport function isBlogAuthConfigured(): boolean {\n return blogAuthConfig !== null;\n}\n","/**\n * NextAuth.js v5 (Auth.js) Adapter\n *\n * One-liner auth setup for NextAuth users:\n * import { configureBlogAuthWithNextAuth } from '@natesena/blog-lib/server';\n * import { auth } from '@/auth';\n * configureBlogAuthWithNextAuth(auth);\n *\n * Ref: Integration plan Phase 0, Step 0.1\n */\n\nimport { configureBlogAuth } from '../config';\n\ntype NextAuthSession = {\n user?: { id?: string; email?: string | null } | null;\n} | null;\n\nexport function configureBlogAuthWithNextAuth(\n authFunction: () => Promise<NextAuthSession>,\n): void {\n configureBlogAuth({\n getCurrentUser: async () => {\n const session = await authFunction();\n if (!session?.user?.email) return null;\n return {\n id: session.user.id || session.user.email,\n email: session.user.email,\n };\n },\n });\n}\n","/**\n * Clerk Auth Adapter\n *\n * One-liner auth setup for Clerk users:\n * import { configureBlogAuthWithClerk } from '@natesena/blog-lib/server';\n * configureBlogAuthWithClerk();\n *\n * Requires @clerk/nextjs to be installed. Lazy-imports it so the package\n * is not required unless this adapter is used.\n *\n * Ref: Integration plan Phase 0, Step 0.1\n */\n\nimport { configureBlogAuth } from '../config';\n\nexport function configureBlogAuthWithClerk(): void {\n configureBlogAuth({\n getCurrentUser: async () => {\n // @ts-expect-error — @clerk/nextjs is an optional peer dep, lazy-loaded at runtime\n const { currentUser } = await import('@clerk/nextjs/server');\n const clerkUser = await currentUser();\n if (!clerkUser) return null;\n const clerkUserPrimaryEmail =\n clerkUser.emailAddresses[0]?.emailAddress;\n if (!clerkUserPrimaryEmail) return null;\n return { id: clerkUser.id, email: clerkUserPrimaryEmail };\n },\n });\n}\n","/**\n * Supabase Auth Adapter\n *\n * One-liner auth setup for Supabase users:\n * import { configureBlogAuthWithSupabase } from '@natesena/blog-lib/server';\n * configureBlogAuthWithSupabase(() => createSupabaseServerClient());\n *\n * Accepts a factory function that returns a Supabase client with auth\n * capabilities. This avoids importing @supabase/ssr directly.\n *\n * Ref: Integration plan Phase 0, Step 0.1\n */\n\nimport { configureBlogAuth } from '../config';\n\ninterface SupabaseAuthClient {\n auth: {\n getUser: () => Promise<{\n data: { user: { id: string; email?: string } | null };\n }>;\n };\n}\n\nexport function configureBlogAuthWithSupabase(\n createClient: () => SupabaseAuthClient,\n): void {\n configureBlogAuth({\n getCurrentUser: async () => {\n const supabase = createClient();\n const { data: { user: supabaseUser } } = await supabase.auth.getUser();\n if (!supabaseUser?.email) return null;\n return { id: supabaseUser.id, email: supabaseUser.email };\n },\n });\n}\n","/**\n * Auth Permission Utilities\n *\n * Pluggable permission checks for CMS access.\n * Works with any auth system — Clerk, Auth0, NextAuth.js, or custom.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 308-324)\n * Ref: docs/epic-blog-lib/04-auth-guide.md\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.7)\n */\n\nexport interface AuthUser {\n id: string;\n email: string;\n isAdmin?: boolean;\n}\n\n/**\n * Check if a user has permission to access the CMS.\n *\n * Uses CMS_ADMIN_EMAILS env var (comma-separated) to restrict access.\n * If no env var is set, all authenticated users are allowed.\n */\nexport function canAccessCMS(user: AuthUser): boolean {\n const adminEmailsCsv = process.env.CMS_ADMIN_EMAILS;\n\n if (adminEmailsCsv) {\n const allowedAdminEmails = adminEmailsCsv.split(',').map((email) => email.trim().toLowerCase());\n const normalizedUserEmail = user.email.trim().toLowerCase();\n return allowedAdminEmails.includes(normalizedUserEmail);\n }\n\n // If isAdmin is explicitly set, use it\n if (user.isAdmin !== undefined) {\n return user.isAdmin;\n }\n\n // Default deny — CMS_ADMIN_EMAILS must be set or isAdmin must be true\n // Ref: Plan Phase 3, Step 3.3\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n '[blog-lib] canAccessCMS: No CMS_ADMIN_EMAILS env var set and user.isAdmin is undefined. ' +\n 'Set CMS_ADMIN_EMAILS=email@example.com or pass isAdmin: true to grant CMS access.',\n );\n }\n return false;\n}\n","/**\n * Simple API Key Auth\n *\n * For headless CMS access or simple auth without a full auth provider.\n * Set CMS_API_KEY env var and send it in the x-api-key header.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 646-671)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.7)\n */\n\nimport { headers } from 'next/headers';\n\n/**\n * Verify an API key against the CMS_API_KEY environment variable.\n * Returns false if CMS_API_KEY is not configured.\n */\nexport function verifyApiKey(providedApiKey?: string | null): boolean {\n const configuredApiKey = process.env.CMS_API_KEY;\n\n if (!configuredApiKey) {\n return false; // API key auth not configured\n }\n\n return providedApiKey === configuredApiKey;\n}\n\n/**\n * Check the incoming request's x-api-key header against CMS_API_KEY.\n * Use in server components or API routes.\n */\nexport async function checkApiKeyAuth(): Promise<boolean> {\n const requestHeaders = await headers();\n const providedApiKey = requestHeaders.get('x-api-key');\n return verifyApiKey(providedApiKey);\n}\n","/**\n * Auth Middleware Helper\n *\n * Convenience function to enforce auth in server components.\n * Redirects unauthenticated users and checks CMS permissions.\n *\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.7)\n */\n\nimport { redirect } from 'next/navigation';\nimport { canAccessCMS } from './permissions';\nimport type { AuthUser } from './permissions';\n\n/**\n * Require authenticated CMS access. Redirects to signInUrl if no user,\n * throws Error if user lacks CMS permission.\n *\n * @example\n * const user = await getYourAuthUser();\n * await requireCMSAuth(user, '/sign-in');\n */\nexport async function requireCMSAuth(\n user: AuthUser | null,\n signInRedirectUrl: string = '/sign-in',\n): Promise<void> {\n if (!user) {\n redirect(signInRedirectUrl);\n }\n\n if (!canAccessCMS(user)) {\n throw new Error('Unauthorized: insufficient CMS permissions');\n }\n}\n","/**\n * Image Upload Server Actions\n *\n * Server-side actions for initiating and confirming image uploads to GCS.\n * Frontend calls these, then uploads directly to GCS using the presigned URL.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 927-991)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.10)\n */\n\n'use server';\n\nimport { GCSStorage } from '../../sdk/storage/gcs';\nimport { getBlogAuthCurrentUser, isBlogAuthConfigured } from '../auth/config';\n\nconst ALLOWED_IMAGE_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n];\n\nconst MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB\n\nexport interface InitiateUploadInput {\n fileName: string;\n fileType: string;\n fileSizeBytes: number;\n folder?: string;\n}\n\nexport interface InitiateUploadResult {\n uploadUrl: string;\n publicUrl: string;\n destinationFilename: string;\n}\n\n/**\n * Validate the file and generate a presigned GCS upload URL.\n * The client then uploads directly to GCS using this URL.\n */\nexport async function initiateImageUpload(\n input: InitiateUploadInput,\n): Promise<InitiateUploadResult> {\n // Auth check — require authenticated user if auth is configured\n // Ref: Plan Phase 3, Step 3.2\n if (isBlogAuthConfigured()) {\n const authenticatedUser = await getBlogAuthCurrentUser();\n if (!authenticatedUser) {\n throw new Error('Authentication required. Configure auth with configureBlogAuth().');\n }\n }\n\n // Validate file type\n if (!ALLOWED_IMAGE_MIME_TYPES.includes(input.fileType)) {\n throw new Error(\n `Invalid file type \"${input.fileType}\". Allowed: ${ALLOWED_IMAGE_MIME_TYPES.join(', ')}`,\n );\n }\n\n // Validate file size\n if (input.fileSizeBytes > MAX_FILE_SIZE_BYTES) {\n throw new Error(\n `File too large (${Math.round(input.fileSizeBytes / 1024 / 1024)}MB). Maximum: 10MB.`,\n );\n }\n\n // Generate unique destination filename\n const timestamp = Date.now();\n const randomSuffix = Math.random().toString(36).substring(2, 8);\n const fileExtension = input.fileName.split('.').pop() || 'jpg';\n const destinationFilename = input.folder\n ? `${input.folder}/${timestamp}-${randomSuffix}.${fileExtension}`\n : `${timestamp}-${randomSuffix}.${fileExtension}`;\n\n // Initialize GCS and generate presigned URL\n const gcsStorage = new GCSStorage({\n projectId: process.env.GCP_PROJECT_ID!,\n bucket: process.env.GCS_BUCKET!,\n keyFile: process.env.GCP_KEYFILE,\n });\n\n const { uploadUrl, publicUrl } = await gcsStorage.getPresignedUploadUrl(\n destinationFilename,\n input.fileType,\n );\n\n return { uploadUrl, publicUrl, destinationFilename };\n}\n\n/**\n * Confirm that a file was successfully uploaded to GCS.\n * Call after the client finishes the direct upload.\n */\nexport async function confirmImageUpload(\n destinationFilename: string,\n): Promise<{ isUploadConfirmed: boolean }> {\n // Auth check — require authenticated user if auth is configured\n if (isBlogAuthConfigured()) {\n const authenticatedUser = await getBlogAuthCurrentUser();\n if (!authenticatedUser) {\n throw new Error('Authentication required. Configure auth with configureBlogAuth().');\n }\n }\n\n const gcsStorage = new GCSStorage({\n projectId: process.env.GCP_PROJECT_ID!,\n bucket: process.env.GCS_BUCKET!,\n keyFile: process.env.GCP_KEYFILE,\n });\n\n const isUploadConfirmed = await gcsStorage.exists(destinationFilename);\n if (!isUploadConfirmed) {\n throw new Error('File not found in storage after upload');\n }\n\n return { isUploadConfirmed };\n}\n"],"mappings":"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","names":[]}
|