diff --git a/.env.example b/.env.example index 6d5b4a8..31a4ae6 100644 --- a/.env.example +++ b/.env.example @@ -21,12 +21,3 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000 # Optional: bot limits # BOT_MAX_PER_USER=5 - -# Optional: app-level storage fallback. Most media storage is user-owned and can be -# configured in the app, so these are only needed if you want a shared default. -# STORAGE_ENDPOINT= -# STORAGE_REGION= -# STORAGE_BUCKET= -# STORAGE_ACCESS_KEY= -# STORAGE_SECRET_KEY= -# STORAGE_PUBLIC_BASE_URL= diff --git a/docker-compose.yml b/docker-compose.yml index 4bf5743..22441fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,14 +70,6 @@ services: # Port configuration (auto or specific port) PORT: ${PORT:-3000} - # Shared S3 storage configuration - STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-} - STORAGE_REGION: ${STORAGE_REGION:-us-east-1} - STORAGE_BUCKET: ${STORAGE_BUCKET:-} - STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-} - STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY:-} - STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL:-} - # Node environment NODE_ENV: production volumes: diff --git a/docker/.env.example b/docker/.env.example index 2a401b2..2a02910 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -43,16 +43,3 @@ PORT=auto # Maximum AI bots per user (default: 5) # BOT_MAX_PER_USER=5 - -# =========================================== -# OPTIONAL: Shared S3 Storage -# =========================================== -# These are only needed if you want app-level shared storage defaults. -# Most media storage is configured per user inside the app. - -# STORAGE_ENDPOINT= -# STORAGE_REGION=us-east-1 -# STORAGE_BUCKET= -# STORAGE_ACCESS_KEY= -# STORAGE_SECRET_KEY= -# STORAGE_PUBLIC_BASE_URL= diff --git a/docker/Dockerfile b/docker/Dockerfile index 955c510..c17b3b3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -47,6 +47,9 @@ RUN apk add --no-cache libc6-compat openssl netcat-openbsd wget WORKDIR /app +LABEL org.opencontainers.image.source="https://github.com/GnosysLabs/Synapsis" +LABEL org.opencontainers.image.description="Synapsis self-hosted social node" + # Create non-root user for security RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs diff --git a/docker/README.md b/docker/README.md index 5d99239..dfc6b20 100644 --- a/docker/README.md +++ b/docker/README.md @@ -42,15 +42,6 @@ Optional (advanced): - `NEXT_PUBLIC_NODE_DOMAIN` to override the node domain (defaults to `DOMAIN`) - `NEXT_PUBLIC_APP_URL` to override the public app URL used by background jobs (auto-derived from the node domain) - `ALLOW_LOCALHOST=1` to allow `localhost` in production containers for local testing -- Shared S3 storage env vars are available if you want app-level fallback storage - -Optional shared storage env vars: -- `STORAGE_ENDPOINT` -- `STORAGE_REGION` -- `STORAGE_BUCKET` -- `STORAGE_ACCESS_KEY` -- `STORAGE_SECRET_KEY` -- `STORAGE_PUBLIC_BASE_URL` **Port Configuration:** - `PORT=auto` (default) — Automatically finds an available port between 3000-3020 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2e585b2..caf3e00 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -63,14 +63,6 @@ services: # Admin emails ADMIN_EMAILS: ${ADMIN_EMAILS} - # S3 Storage configuration - STORAGE_ENDPOINT: ${STORAGE_ENDPOINT} - STORAGE_REGION: ${STORAGE_REGION:-us-east-1} - STORAGE_BUCKET: ${STORAGE_BUCKET} - STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY} - STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY} - STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL} - # Optional settings BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5} diff --git a/docker/install.sh b/docker/install.sh index 46453bf..9f33c73 100644 --- a/docker/install.sh +++ b/docker/install.sh @@ -122,7 +122,3 @@ echo "Next steps:" echo " 1. Review ${INSTALL_DIR}/.env" echo " 2. Start Synapsis:" echo " cd ${INSTALL_DIR} && docker compose up -d" -echo "" -echo "One-line usage examples:" -echo " curl -fsSL ${PUBLIC_INSTALL_URL} | bash" -echo " curl -fsSL ${PUBLIC_INSTALL_URL} | DOMAIN=synapsis.example.com ADMIN_EMAILS=you@example.com bash" diff --git a/src/app/api/media/upload/route.ts b/src/app/api/media/upload/route.ts index c70193e..0ee2ec5 100644 --- a/src/app/api/media/upload/route.ts +++ b/src/app/api/media/upload/route.ts @@ -102,6 +102,11 @@ export async function POST(req: NextRequest) { if (error instanceof Error && error.message === 'Authentication required') { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); } + if (error instanceof Error && error.message === 'Invalid storage password') { + return NextResponse.json({ + error: 'Incorrect password. Please try again.' + }, { status: 401 }); + } if (error instanceof Error && error.message.includes('Storage')) { return NextResponse.json({ error: error.message }, { status: 400 }); } diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts deleted file mode 100644 index bfd105f..0000000 --- a/src/app/api/uploads/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; -import { v4 as uuidv4 } from 'uuid'; - -export async function POST(req: NextRequest) { - try { - const formData = await req.formData(); - const file = formData.get('file') as File; - - if (!file) { - return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); - } - - const buffer = Buffer.from(await file.arrayBuffer()); - const filename = `${uuidv4()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '')}`; - - // S3 Configuration - const s3 = new S3Client({ - region: process.env.STORAGE_REGION || 'us-east-1', - endpoint: process.env.STORAGE_ENDPOINT, - credentials: { - accessKeyId: process.env.STORAGE_ACCESS_KEY || '', - secretAccessKey: process.env.STORAGE_SECRET_KEY || '', - }, - forcePathStyle: true, // Needed for many S3-compatible providers - }); - - const bucket = process.env.STORAGE_BUCKET || 'synapsis'; - - await s3.send(new PutObjectCommand({ - Bucket: bucket, - Key: filename, - Body: buffer, - ContentType: file.type, - ACL: 'public-read', // Depends on bucket policy, but often needed - })); - - // Construct Public URL - let url = ''; - if (process.env.STORAGE_PUBLIC_BASE_URL) { - url = `${process.env.STORAGE_PUBLIC_BASE_URL}/${filename}`; - } else if (process.env.STORAGE_ENDPOINT) { - url = `${process.env.STORAGE_ENDPOINT}/${bucket}/${filename}`; - } else { - return NextResponse.json({ error: 'Storage not configured - missing STORAGE_PUBLIC_BASE_URL or STORAGE_ENDPOINT' }, { status: 500 }); - } - - return NextResponse.json({ url }); - } catch (error) { - console.error('Upload error:', error); - return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); - } -} diff --git a/src/app/bots/[id]/edit/page.tsx b/src/app/bots/[id]/edit/page.tsx index 0574079..3f4e44b 100644 --- a/src/app/bots/[id]/edit/page.tsx +++ b/src/app/bots/[id]/edit/page.tsx @@ -12,6 +12,7 @@ import { useState, useEffect } from 'react'; import { useRouter, useParams } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeftIcon } from '@/components/Icons'; +import { UserStorageImageUpload } from '@/components/UserStorageImageUpload'; import { Bot, Sparkles, Rss, Clock, Trash2 } from 'lucide-react'; interface ContentSource { @@ -46,8 +47,7 @@ export default function EditBotPage() { const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity'); - const [uploadingAvatar, setUploadingAvatar] = useState(false); - const [uploadingBanner, setUploadingBanner] = useState(false); + const [storagePassword, setStoragePassword] = useState(''); const [formData, setFormData] = useState({ name: '', @@ -364,119 +364,37 @@ export default function EditBotPage() {

-
- -
- - {formData.avatarUrl && ( -
- Avatar preview -
- )} - {formData.avatarUrl && ( - - )} -
-

- Square image recommended (optional) -

-
+ { + setError(''); + setFormData(prev => ({ ...prev, avatarUrl })); + }} + password={storagePassword} + onPasswordChange={setStoragePassword} + previewWidth={48} + previewHeight={48} + previewBorderRadius="50%" + helperText="Square image recommended (optional)" + onError={setError} + /> -
- -
- - {formData.headerUrl && ( -
- Banner preview -
- )} - {formData.headerUrl && ( - - )} -
-

- Wide image recommended, e.g. 1500x500 (optional) -

-
+ { + setError(''); + setFormData(prev => ({ ...prev, headerUrl })); + }} + password={storagePassword} + onPasswordChange={setStoragePassword} + previewWidth={120} + previewHeight={40} + previewBorderRadius="4px" + helperText="Wide image recommended, e.g. 1500x500 (optional)" + onError={setError} + /> ); diff --git a/src/app/bots/new/page.tsx b/src/app/bots/new/page.tsx index 66a6d4f..498a44e 100644 --- a/src/app/bots/new/page.tsx +++ b/src/app/bots/new/page.tsx @@ -12,6 +12,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeftIcon } from '@/components/Icons'; +import { UserStorageImageUpload } from '@/components/UserStorageImageUpload'; import { Bot, Sparkles, Rss, Clock, Trash2 } from 'lucide-react'; interface ContentSource { @@ -39,8 +40,7 @@ export default function NewBotPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity'); - const [uploadingAvatar, setUploadingAvatar] = useState(false); - const [uploadingBanner, setUploadingBanner] = useState(false); + const [storagePassword, setStoragePassword] = useState(''); const [formData, setFormData] = useState({ name: '', @@ -183,6 +183,7 @@ export default function NewBotPage() { llmProvider: formData.llmProvider, llmModel: formData.llmModel, llmApiKey: formData.llmApiKey, + ownerPassword: storagePassword || undefined, autonomousMode: formData.autonomousMode, schedule: formData.autonomousMode ? { type: 'interval', @@ -313,119 +314,37 @@ export default function NewBotPage() {

-
- -
- - {formData.avatarUrl && ( -
- Avatar preview -
- )} - {formData.avatarUrl && ( - - )} -
-

- Square image recommended (optional) -

-
+ { + setError(''); + setFormData(prev => ({ ...prev, avatarUrl })); + }} + password={storagePassword} + onPasswordChange={setStoragePassword} + previewWidth={48} + previewHeight={48} + previewBorderRadius="50%" + helperText="Square image recommended (optional)" + onError={setError} + /> -
- -
- - {formData.headerUrl && ( -
- Banner preview -
- )} - {formData.headerUrl && ( - - )} -
-

- Wide image recommended, e.g. 1500x500 (optional) -

-
+ { + setError(''); + setFormData(prev => ({ ...prev, headerUrl })); + }} + password={storagePassword} + onPasswordChange={setStoragePassword} + previewWidth={120} + previewHeight={40} + previewBorderRadius="4px" + helperText="Wide image recommended, e.g. 1500x500 (optional)" + onError={setError} + /> ); diff --git a/src/app/u/[handle]/page.tsx b/src/app/u/[handle]/page.tsx index e493fef..ff1094d 100644 --- a/src/app/u/[handle]/page.tsx +++ b/src/app/u/[handle]/page.tsx @@ -1,13 +1,14 @@ 'use client'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons'; import { PostCard } from '@/components/PostCard'; import { User, Post } from '@/lib/types'; import AutoTextarea from '@/components/AutoTextarea'; -import { Rocket, MoreHorizontal, Mail, Camera } from 'lucide-react'; +import { UserStorageImageUpload } from '@/components/UserStorageImageUpload'; +import { Rocket, MoreHorizontal, Mail } from 'lucide-react'; import { useFormattedHandle } from '@/lib/utils/handle'; import { Bot } from 'lucide-react'; import { useAuth } from '@/lib/contexts/AuthContext'; @@ -114,68 +115,7 @@ export default function ProfilePage() { const [isSaving, setIsSaving] = useState(false); const [isBlocked, setIsBlocked] = useState(false); const [showMenu, setShowMenu] = useState(false); - - const avatarInputRef = useRef(null); - const headerInputRef = useRef(null); - - const handleUpload = async (e: React.ChangeEvent, field: 'avatarUrl' | 'headerUrl') => { - const file = e.target.files?.[0]; - if (!file) return; - - // Reset inputs so change event fires again for same file - e.target.value = ''; - - setIsSaving(true); - try { - // 1. Upload the file - const formData = new FormData(); - formData.append('file', file); - const uploadRes = await fetch('/api/uploads', { - method: 'POST', - body: formData, - }); - const uploadData = await uploadRes.json(); - - if (!uploadData.url) throw new Error('Upload failed'); - const imageUrl = uploadData.url; - - // 2. Update local form state immediately for UI feedback - setProfileForm(prev => ({ ...prev, [field]: imageUrl })); - - // 3. Auto-save the profile change - if (!isIdentityUnlocked) { - throw new Error('Session expired. Please log in again.'); - } - - // Create partial update payload - const updatePayload: any = { [field]: imageUrl }; - - // Sign the action - const signedPayload = await signUserAction('update_profile', updatePayload); - - const saveRes = await fetch('/api/auth/me', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(signedPayload), - }); - - const saveData = await saveRes.json(); - - if (!saveRes.ok) { - throw new Error(saveData.error || 'Failed to update profile'); - } - - // Update user state with the returned updated user - setUser(saveData.user); - // We do NOT exit edit mode automatically, allowing them to edit other fields - - } catch (err) { - console.error(err); - setSaveError(err instanceof Error ? err.message : 'Upload failed'); - } finally { - setIsSaving(false); - } - }; + const [storagePassword, setStoragePassword] = useState(''); useEffect(() => { setIsEditing(false); @@ -558,34 +498,8 @@ export default function ProfilePage() { ? `url(${isEditing ? profileForm.headerUrl : user.headerUrl}) center/cover` : 'linear-gradient(135deg, var(--accent-muted) 0%, var(--background-tertiary) 100%)', position: 'relative', - cursor: isEditing ? 'pointer' : 'default', - }} - onClick={() => { - if (isEditing) { - headerInputRef.current?.click(); - } }} > - {isEditing && ( -
- - handleUpload(e, 'headerUrl')} - style={{ display: 'none' }} - /> -
- )} {/* Avatar & Actions */} @@ -604,12 +518,6 @@ export default function ProfilePage() { border: '4px solid var(--background)', marginTop: '-48px', position: 'relative', - cursor: isEditing ? 'pointer' : 'default', - }} - onClick={() => { - if (isEditing) { - avatarInputRef.current?.click(); - } }} > {(isEditing ? profileForm.avatarUrl : user.avatarUrl) ? ( @@ -617,28 +525,6 @@ export default function ProfilePage() { ) : ( (user.displayName || user.handle).charAt(0).toUpperCase() )} - - {isEditing && ( -
- - handleUpload(e, 'avatarUrl')} - style={{ display: 'none' }} - /> -
- )}
@@ -852,7 +738,36 @@ export default function ProfilePage() { maxLength={100} />
- {/* File inputs removed, now click-to-upload on visual elements */} + { + setSaveError(null); + setProfileForm({ ...profileForm, avatarUrl }); + }} + password={storagePassword} + onPasswordChange={setStoragePassword} + previewWidth={48} + previewHeight={48} + previewBorderRadius="50%" + helperText="Square image recommended (optional)" + onError={(message) => setSaveError(message || null)} + /> + { + setSaveError(null); + setProfileForm({ ...profileForm, headerUrl }); + }} + password={storagePassword} + onPasswordChange={setStoragePassword} + previewWidth={120} + previewHeight={40} + previewBorderRadius="4px" + helperText="Wide image recommended, e.g. 1500x500 (optional)" + onError={(message) => setSaveError(message || null)} + /> {saveError && (
{saveError}
)} diff --git a/src/components/UserStorageImageUpload.tsx b/src/components/UserStorageImageUpload.tsx new file mode 100644 index 0000000..e2e3e6e --- /dev/null +++ b/src/components/UserStorageImageUpload.tsx @@ -0,0 +1,272 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +interface UserStorageImageUploadProps { + label: string; + value: string; + onChange: (url: string) => void; + password: string; + onPasswordChange: (password: string) => void; + helperText?: string; + previewWidth?: number; + previewHeight?: number; + previewBorderRadius?: string; + onError?: (message: string) => void; +} + +export function UserStorageImageUpload({ + label, + value, + onChange, + password, + onPasswordChange, + helperText, + previewWidth = 48, + previewHeight = 48, + previewBorderRadius = '8px', + onError, +}: UserStorageImageUploadProps) { + const inputRef = useRef(null); + const [isUploading, setIsUploading] = useState(false); + const [showPasswordPrompt, setShowPasswordPrompt] = useState(false); + const [pendingFile, setPendingFile] = useState(null); + const [passwordInput, setPasswordInput] = useState(password); + const [promptError, setPromptError] = useState(''); + + useEffect(() => { + if (!showPasswordPrompt) { + setPromptError(''); + } + }, [showPasswordPrompt]); + + useEffect(() => { + if (showPasswordPrompt && !passwordInput && password) { + setPasswordInput(password); + } + }, [showPasswordPrompt, password, passwordInput]); + + const reportError = (message: string) => { + setPromptError(message); + onError?.(message); + }; + + const resetFileInput = () => { + if (inputRef.current) { + inputRef.current.value = ''; + } + }; + + const uploadFile = async (file: File, uploadPassword: string) => { + setIsUploading(true); + setPromptError(''); + + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('password', uploadPassword); + + const res = await fetch('/api/media/upload', { + method: 'POST', + body: formData, + }); + const data = await res.json(); + + if (!res.ok || !data.url) { + const message = data.error || 'Upload failed'; + + if (res.status === 401) { + onPasswordChange(''); + setPasswordInput(''); + setPendingFile(file); + setShowPasswordPrompt(true); + setPromptError(message); + return; + } + + throw new Error(message); + } + + onPasswordChange(uploadPassword); + onChange(data.media?.url || data.url); + onError?.(''); + setPendingFile(null); + setShowPasswordPrompt(false); + setPasswordInput(uploadPassword); + } catch (error) { + reportError(error instanceof Error ? error.message : 'Upload failed'); + } finally { + setIsUploading(false); + resetFileInput(); + } + }; + + const handleFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + setPendingFile(file); + + if (password.trim()) { + await uploadFile(file, password.trim()); + return; + } + + setPasswordInput(''); + setShowPasswordPrompt(true); + }; + + const handlePasswordSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!pendingFile) { + setShowPasswordPrompt(false); + return; + } + + if (!passwordInput.trim()) { + setPromptError('Please enter your password'); + return; + } + + await uploadFile(pendingFile, passwordInput.trim()); + }; + + const handleCancel = () => { + setShowPasswordPrompt(false); + setPendingFile(null); + setPasswordInput(password); + setPromptError(''); + resetFileInput(); + }; + + return ( + <> +
+ +
+ + + {value && ( +
+ {`${label} +
+ )} + + {value && ( + + )} +
+ + {helperText && ( +

+ {helperText} +

+ )} +
+ + {showPasswordPrompt && ( +
+
event.stopPropagation()} + > +

+ Unlock storage upload +

+

+ Enter your account password to upload this image to your own storage. +

+ +
+
+ + { + setPasswordInput(event.target.value); + setPromptError(''); + }} + autoFocus + /> +
+ + {promptError && ( +
+ {promptError} +
+ )} + +
+ + +
+
+
+
+ )} + + ); +} diff --git a/src/lib/auth/avatar.ts b/src/lib/auth/avatar.ts deleted file mode 100644 index b193062..0000000 --- a/src/lib/auth/avatar.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; -import { v4 as uuidv4 } from 'uuid'; - -export async function generateAndUploadAvatar(handle: string): Promise { - try { - // 1. Fetch the avatar from DiceBear (PNG format for better compatibility) - const dicebearUrl = `https://api.dicebear.com/9.x/bottts-neutral/png?seed=${handle}`; - const response = await fetch(dicebearUrl); - - if (!response.ok) { - console.error(`Failed to fetch avatar from DiceBear: ${response.statusText}`); - return null; - } - - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // 2. Upload to S3 - const s3 = new S3Client({ - region: process.env.STORAGE_REGION || 'us-east-1', - endpoint: process.env.STORAGE_ENDPOINT, - credentials: { - accessKeyId: process.env.STORAGE_ACCESS_KEY || '', - secretAccessKey: process.env.STORAGE_SECRET_KEY || '', - }, - forcePathStyle: true, - }); - - const bucket = process.env.STORAGE_BUCKET || 'synapsis'; - // Sanitize handle for filename just in case - const safeHandle = handle.replace(/[^a-zA-Z0-9]/g, ''); - const filename = `${uuidv4()}-${safeHandle}-avatar.png`; - - await s3.send(new PutObjectCommand({ - Bucket: bucket, - Key: filename, - Body: buffer, - ContentType: 'image/png', - ACL: 'public-read', - })); - - // 3. Construct Public URL - let url = ''; - if (process.env.STORAGE_PUBLIC_BASE_URL) { - url = `${process.env.STORAGE_PUBLIC_BASE_URL}/${filename}`; - } else if (process.env.STORAGE_ENDPOINT) { - // Basic fallback construction if base url is not set but endpoint is - // This assumes path style access is okay if no custom domain - const endpoint = process.env.STORAGE_ENDPOINT.replace(/\/+$/, ''); - url = `${endpoint}/${bucket}/${filename}`; - } else { - console.warn('Storage public URL not configured properly'); - return null; - } - - return url; - - } catch (error) { - console.error('Error generating/uploading avatar:', error); - return null; - } -} - diff --git a/src/lib/storage/s3.ts b/src/lib/storage/s3.ts index 85eb3b4..13328c7 100644 --- a/src/lib/storage/s3.ts +++ b/src/lib/storage/s3.ts @@ -30,15 +30,19 @@ export function decryptS3Credentials( encryptedSecretKey: string, password: string ): { accessKeyId: string; secretAccessKey: string } { - const accessKeyId = decryptPrivateKey( - deserializeEncryptedKey(encryptedAccessKey), - password - ); - const secretAccessKey = decryptPrivateKey( - deserializeEncryptedKey(encryptedSecretKey), - password - ); - return { accessKeyId, secretAccessKey }; + try { + const accessKeyId = decryptPrivateKey( + deserializeEncryptedKey(encryptedAccessKey), + password + ); + const secretAccessKey = decryptPrivateKey( + deserializeEncryptedKey(encryptedSecretKey), + password + ); + return { accessKeyId, secretAccessKey }; + } catch { + throw new Error('Invalid storage password'); + } } /**