-2
api.md
-2
api.md
+2
-2
lexicons/fs.json
+2
-2
lexicons/fs.json
+123
public/editor/editor.tsx
+123
public/editor/editor.tsx
···
···
1
+
import { useState, useRef } from 'react'
2
+
import { createRoot } from 'react-dom/client'
3
+
4
+
import Layout from '@public/layouts'
5
+
6
+
function Editor() {
7
+
const [uploading, setUploading] = useState(false)
8
+
const [result, setResult] = useState<any>(null)
9
+
const [error, setError] = useState<string | null>(null)
10
+
const folderInputRef = useRef<HTMLInputElement>(null)
11
+
const siteNameRef = useRef<HTMLInputElement>(null)
12
+
13
+
const handleFileUpload = async (e: React.FormEvent) => {
14
+
e.preventDefault()
15
+
setError(null)
16
+
setResult(null)
17
+
18
+
const files = folderInputRef.current?.files
19
+
const siteName = siteNameRef.current?.value
20
+
21
+
if (!files || files.length === 0) {
22
+
setError('Please select a folder to upload')
23
+
return
24
+
}
25
+
26
+
if (!siteName) {
27
+
setError('Please enter a site name')
28
+
return
29
+
}
30
+
31
+
setUploading(true)
32
+
33
+
try {
34
+
const formData = new FormData()
35
+
formData.append('siteName', siteName)
36
+
37
+
for (let i = 0; i < files.length; i++) {
38
+
formData.append('files', files[i])
39
+
}
40
+
41
+
const response = await fetch('/wisp/upload-files', {
42
+
method: 'POST',
43
+
body: formData
44
+
})
45
+
46
+
if (!response.ok) {
47
+
throw new Error(`Upload failed: ${response.statusText}`)
48
+
}
49
+
50
+
const data = await response.json()
51
+
setResult(data)
52
+
} catch (err) {
53
+
setError(err instanceof Error ? err.message : 'Upload failed')
54
+
} finally {
55
+
setUploading(false)
56
+
}
57
+
}
58
+
59
+
return (
60
+
<div className="w-full max-w-2xl mx-auto p-6">
61
+
<h1 className="text-3xl font-bold mb-6 text-center">Upload Folder</h1>
62
+
63
+
<form onSubmit={handleFileUpload} className="space-y-4">
64
+
<div>
65
+
<label htmlFor="siteName" className="block text-sm font-medium mb-2">
66
+
Site Name
67
+
</label>
68
+
<input
69
+
ref={siteNameRef}
70
+
type="text"
71
+
id="siteName"
72
+
placeholder="Enter site name"
73
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
74
+
/>
75
+
</div>
76
+
77
+
<div>
78
+
<label htmlFor="folder" className="block text-sm font-medium mb-2">
79
+
Select Folder
80
+
</label>
81
+
<input
82
+
ref={folderInputRef}
83
+
type="file"
84
+
id="folder"
85
+
{...({ webkitdirectory: '', directory: '' } as any)}
86
+
multiple
87
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
88
+
/>
89
+
</div>
90
+
91
+
<button
92
+
type="submit"
93
+
disabled={uploading}
94
+
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-md transition-colors"
95
+
>
96
+
{uploading ? 'Uploading...' : 'Upload Folder'}
97
+
</button>
98
+
</form>
99
+
100
+
{error && (
101
+
<div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-md">
102
+
{error}
103
+
</div>
104
+
)}
105
+
106
+
{result && (
107
+
<div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-md">
108
+
<h3 className="font-semibold mb-2">Upload Successful!</h3>
109
+
<p>Files uploaded: {result.fileCount}</p>
110
+
<p>Site name: {result.siteName}</p>
111
+
<p>URI: {result.uri}</p>
112
+
</div>
113
+
)}
114
+
</div>
115
+
)
116
+
}
117
+
118
+
const root = createRoot(document.getElementById('elysia')!)
119
+
root.render(
120
+
<Layout className="gap-6">
121
+
<Editor />
122
+
</Layout>
123
+
)
+12
public/editor/index.html
+12
public/editor/index.html
···
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+
<title>Elysia Static</title>
7
+
</head>
8
+
<body>
9
+
<div id="elysia"></div>
10
+
<script type="module" src="./editor.tsx"></script>
11
+
</body>
12
+
</html>
+4
-21
src/index.ts
+4
-21
src/index.ts
···
10
getOAuthClient,
11
getCurrentKeys
12
} from './lib/oauth-client'
13
14
const config: Config = {
15
domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`,
···
29
prefix: '/'
30
})
31
)
32
-
.post('/api/auth/signin', async (c) => {
33
-
try {
34
-
const { handle } = await c.request.json()
35
-
const state = crypto.randomUUID()
36
-
const url = await client.authorize(handle, { state })
37
-
return { url: url.toString() }
38
-
} catch (err) {
39
-
console.error('Signin error', err)
40
-
return { error: 'Authentication failed' }
41
-
}
42
-
})
43
-
.get('/api/auth/callback', async (c) => {
44
-
const params = new URLSearchParams(c.query)
45
-
const { session } = await client.callback(params)
46
-
if (!session) return { error: 'Authentication failed' }
47
-
48
-
const cookieSession = c.cookie
49
-
cookieSession.did.value = session.did
50
-
51
-
return c.redirect('/')
52
-
})
53
.get('/client-metadata.json', (c) => {
54
return createClientMetadata(config)
55
})
···
10
getOAuthClient,
11
getCurrentKeys
12
} from './lib/oauth-client'
13
+
import { authRoutes } from './routes/auth'
14
+
import { wispRoutes } from './routes/wisp'
15
16
const config: Config = {
17
domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`,
···
31
prefix: '/'
32
})
33
)
34
+
.use(authRoutes(client))
35
+
.use(wispRoutes(client))
36
.get('/client-metadata.json', (c) => {
37
return createClientMetadata(config)
38
})
+6
-4
src/lexicon/lexicons.ts
+6
-4
src/lexicon/lexicons.ts
+2
-2
src/lexicon/types/place/wisp/fs.ts
+2
-2
src/lexicon/types/place/wisp/fs.ts
+37
src/lib/wisp-auth.ts
+37
src/lib/wisp-auth.ts
···
···
1
+
import { Did } from "@atproto/api";
2
+
import { NodeOAuthClient } from "@atproto/oauth-client-node";
3
+
import type { OAuthSession } from "@atproto/oauth-client-node";
4
+
import { Cookie } from "elysia";
5
+
6
+
7
+
export interface AuthenticatedContext {
8
+
did: Did;
9
+
session: OAuthSession;
10
+
}
11
+
12
+
export const authenticateRequest = async (
13
+
client: NodeOAuthClient,
14
+
cookies: Record<string, Cookie<unknown>>
15
+
): Promise<AuthenticatedContext | null> => {
16
+
try {
17
+
const did = cookies.did?.value as Did;
18
+
if (!did) return null;
19
+
20
+
const session = await client.restore(did, "auto");
21
+
return session ? { did, session } : null;
22
+
} catch (err) {
23
+
console.error('Authentication error:', err);
24
+
return null;
25
+
}
26
+
};
27
+
28
+
export const requireAuth = async (
29
+
client: NodeOAuthClient,
30
+
cookies: Record<string, Cookie<unknown>>
31
+
): Promise<AuthenticatedContext> => {
32
+
const auth = await authenticateRequest(client, cookies);
33
+
if (!auth) {
34
+
throw new Error('Authentication required');
35
+
}
36
+
return auth;
37
+
};
+203
src/lib/wisp-utils.ts
+203
src/lib/wisp-utils.ts
···
···
1
+
import type { BlobRef } from "@atproto/api";
2
+
import type { Record, Directory, File, Entry } from "../lexicon/types/place/wisp/fs";
3
+
4
+
export interface UploadedFile {
5
+
name: string;
6
+
content: Buffer;
7
+
mimeType: string;
8
+
size: number;
9
+
}
10
+
11
+
export interface FileUploadResult {
12
+
hash: string;
13
+
blobRef: BlobRef;
14
+
}
15
+
16
+
export interface ProcessedDirectory {
17
+
directory: Directory;
18
+
fileCount: number;
19
+
}
20
+
21
+
/**
22
+
* Process uploaded files into a directory structure
23
+
*/
24
+
export function processUploadedFiles(files: UploadedFile[]): ProcessedDirectory {
25
+
console.log(`๐๏ธ Processing ${files.length} uploaded files`);
26
+
const entries: Entry[] = [];
27
+
let fileCount = 0;
28
+
29
+
// Group files by directory
30
+
const directoryMap = new Map<string, UploadedFile[]>();
31
+
32
+
for (const file of files) {
33
+
// Remove any base folder name from the path
34
+
const normalizedPath = file.name.replace(/^[^\/]*\//, '');
35
+
const parts = normalizedPath.split('/');
36
+
37
+
console.log(`๐ Processing file: ${file.name} -> normalized: ${normalizedPath}`);
38
+
39
+
if (parts.length === 1) {
40
+
// Root level file
41
+
console.log(`๐ Root level file: ${parts[0]}`);
42
+
entries.push({
43
+
name: parts[0],
44
+
node: {
45
+
$type: 'place.wisp.fs#file' as const,
46
+
type: 'file' as const,
47
+
blob: undefined as any // Will be filled after upload
48
+
}
49
+
});
50
+
fileCount++;
51
+
} else {
52
+
// File in subdirectory
53
+
const dirPath = parts.slice(0, -1).join('/');
54
+
console.log(`๐ Subdirectory file: ${dirPath}/${parts[parts.length - 1]}`);
55
+
if (!directoryMap.has(dirPath)) {
56
+
directoryMap.set(dirPath, []);
57
+
console.log(`โ Created directory: ${dirPath}`);
58
+
}
59
+
directoryMap.get(dirPath)!.push({
60
+
...file,
61
+
name: normalizedPath
62
+
});
63
+
}
64
+
}
65
+
66
+
// Process subdirectories
67
+
console.log(`๐ Processing ${directoryMap.size} subdirectories`);
68
+
for (const [dirPath, dirFiles] of directoryMap) {
69
+
console.log(`๐ Processing directory: ${dirPath} with ${dirFiles.length} files`);
70
+
const dirEntries: Entry[] = [];
71
+
72
+
for (const file of dirFiles) {
73
+
const fileName = file.name.split('/').pop()!;
74
+
console.log(` ๐ Adding file to directory: ${fileName}`);
75
+
dirEntries.push({
76
+
name: fileName,
77
+
node: {
78
+
$type: 'place.wisp.fs#file' as const,
79
+
type: 'file' as const,
80
+
blob: undefined as any // Will be filled after upload
81
+
}
82
+
});
83
+
fileCount++;
84
+
}
85
+
86
+
// Build nested directory structure
87
+
const pathParts = dirPath.split('/');
88
+
let currentEntries = entries;
89
+
90
+
console.log(`๐๏ธ Building nested structure for path: ${pathParts.join('/')}`);
91
+
92
+
for (let i = 0; i < pathParts.length; i++) {
93
+
const part = pathParts[i];
94
+
const isLast = i === pathParts.length - 1;
95
+
96
+
let existingEntry = currentEntries.find(e => e.name === part);
97
+
98
+
if (!existingEntry) {
99
+
const newDir = {
100
+
$type: 'place.wisp.fs#directory' as const,
101
+
type: 'directory' as const,
102
+
entries: isLast ? dirEntries : []
103
+
};
104
+
105
+
existingEntry = {
106
+
name: part,
107
+
node: newDir
108
+
};
109
+
currentEntries.push(existingEntry);
110
+
console.log(` โ Created directory entry: ${part}`);
111
+
} else if ('entries' in existingEntry.node && isLast) {
112
+
(existingEntry.node as any).entries.push(...dirEntries);
113
+
console.log(` ๐ Added files to existing directory: ${part}`);
114
+
}
115
+
116
+
if (existingEntry && 'entries' in existingEntry.node) {
117
+
currentEntries = (existingEntry.node as any).entries;
118
+
}
119
+
}
120
+
}
121
+
122
+
console.log(`โ
Directory structure completed with ${fileCount} total files`);
123
+
124
+
const result = {
125
+
directory: {
126
+
$type: 'place.wisp.fs#directory' as const,
127
+
type: 'directory' as const,
128
+
entries
129
+
},
130
+
fileCount
131
+
};
132
+
133
+
console.log('๐ Final directory structure:', JSON.stringify(result, null, 2));
134
+
return result;
135
+
}
136
+
137
+
/**
138
+
* Create the manifest record for a site
139
+
*/
140
+
export function createManifest(
141
+
siteName: string,
142
+
root: Directory,
143
+
fileCount: number
144
+
): Record {
145
+
const manifest: Record = {
146
+
$type: 'place.wisp.fs' as const,
147
+
site: siteName,
148
+
root,
149
+
fileCount,
150
+
createdAt: new Date().toISOString()
151
+
};
152
+
153
+
console.log(`๐ Created manifest for site "${siteName}" with ${fileCount} files`);
154
+
console.log('๐ Manifest structure:', JSON.stringify(manifest, null, 2));
155
+
156
+
return manifest;
157
+
}
158
+
159
+
/**
160
+
* Update file blobs in directory structure after upload
161
+
*/
162
+
export function updateFileBlobs(
163
+
directory: Directory,
164
+
uploadResults: FileUploadResult[],
165
+
filePaths: string[]
166
+
): Directory {
167
+
console.log(`๐ Updating file blobs: ${uploadResults.length} results for ${filePaths.length} paths`);
168
+
169
+
const updatedEntries = directory.entries.map(entry => {
170
+
if ('type' in entry.node && entry.node.type === 'file') {
171
+
const fileIndex = filePaths.findIndex(path => path.endsWith(entry.name));
172
+
if (fileIndex !== -1 && uploadResults[fileIndex]) {
173
+
console.log(` ๐ Updating blob for file: ${entry.name} -> ${uploadResults[fileIndex].hash}`);
174
+
return {
175
+
...entry,
176
+
node: {
177
+
$type: 'place.wisp.fs#file' as const,
178
+
type: 'file' as const,
179
+
blob: uploadResults[fileIndex].blobRef
180
+
}
181
+
};
182
+
} else {
183
+
console.warn(` โ ๏ธ Could not find upload result for file: ${entry.name}`);
184
+
}
185
+
} else if ('type' in entry.node && entry.node.type === 'directory') {
186
+
console.log(` ๐ Recursively updating directory: ${entry.name}`);
187
+
return {
188
+
...entry,
189
+
node: updateFileBlobs(entry.node as Directory, uploadResults, filePaths)
190
+
};
191
+
}
192
+
return entry;
193
+
}) as Entry[];
194
+
195
+
const result = {
196
+
$type: 'place.wisp.fs#directory' as const,
197
+
type: 'directory' as const,
198
+
entries: updatedEntries
199
+
};
200
+
201
+
console.log('โ
File blobs updated');
202
+
return result;
203
+
}
+25
src/routes/auth.ts
+25
src/routes/auth.ts
···
···
1
+
import { Elysia } from 'elysia'
2
+
import { NodeOAuthClient } from '@atproto/oauth-client-node'
3
+
4
+
export const authRoutes = (client: NodeOAuthClient) => new Elysia()
5
+
.post('/api/auth/signin', async (c) => {
6
+
try {
7
+
const { handle } = await c.request.json()
8
+
const state = crypto.randomUUID()
9
+
const url = await client.authorize(handle, { state })
10
+
return { url: url.toString() }
11
+
} catch (err) {
12
+
console.error('Signin error', err)
13
+
return { error: 'Authentication failed' }
14
+
}
15
+
})
16
+
.get('/api/auth/callback', async (c) => {
17
+
const params = new URLSearchParams(c.query)
18
+
const { session } = await client.callback(params)
19
+
if (!session) return { error: 'Authentication failed' }
20
+
21
+
const cookieSession = c.cookie
22
+
cookieSession.did.value = session.did
23
+
24
+
return c.redirect('/editor')
25
+
})
+230
src/routes/wisp.ts
+230
src/routes/wisp.ts
···
···
1
+
import { Elysia } from 'elysia'
2
+
import { requireAuth, type AuthenticatedContext } from '../lib/wisp-auth'
3
+
import { NodeOAuthClient } from '@atproto/oauth-client-node'
4
+
import { Agent } from '@atproto/api'
5
+
import {
6
+
type UploadedFile,
7
+
type FileUploadResult,
8
+
processUploadedFiles,
9
+
createManifest,
10
+
updateFileBlobs
11
+
} from '../lib/wisp-utils'
12
+
13
+
export const wispRoutes = (client: NodeOAuthClient) =>
14
+
new Elysia({ prefix: '/wisp' })
15
+
.derive(async ({ cookie }) => {
16
+
const auth = await requireAuth(client, cookie)
17
+
return { auth }
18
+
})
19
+
.post(
20
+
'/upload-files',
21
+
async ({ body, auth }) => {
22
+
const { siteName, files } = body as {
23
+
siteName: string;
24
+
files: File | File[]
25
+
};
26
+
27
+
console.log('๐ Starting upload process', { siteName, fileCount: Array.isArray(files) ? files.length : 1 });
28
+
29
+
try {
30
+
if (!files || (Array.isArray(files) ? files.length === 0 : !files)) {
31
+
console.error('โ No files provided');
32
+
throw new Error('No files provided')
33
+
}
34
+
35
+
if (!siteName) {
36
+
console.error('โ Site name is required');
37
+
throw new Error('Site name is required')
38
+
}
39
+
40
+
console.log('โ
Initial validation passed');
41
+
42
+
// Create agent with OAuth session
43
+
console.log('๐ Creating agent with OAuth session');
44
+
const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
45
+
console.log('โ
Agent created successfully');
46
+
47
+
// Convert File objects to UploadedFile format
48
+
// Elysia gives us File objects directly, handle both single file and array
49
+
const fileArray = Array.isArray(files) ? files : [files];
50
+
console.log(`๐ Processing ${fileArray.length} files`);
51
+
const uploadedFiles: UploadedFile[] = [];
52
+
53
+
// Define allowed file extensions for static site hosting
54
+
const allowedExtensions = new Set([
55
+
// HTML
56
+
'.html', '.htm',
57
+
// CSS
58
+
'.css',
59
+
// JavaScript
60
+
'.js', '.mjs', '.jsx', '.ts', '.tsx',
61
+
// Images
62
+
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.avif',
63
+
// Fonts
64
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
65
+
// Documents
66
+
'.pdf', '.txt',
67
+
// JSON (for config files, but not .map files)
68
+
'.json',
69
+
// Audio/Video
70
+
'.mp3', '.mp4', '.webm', '.ogg', '.wav',
71
+
// Other web assets
72
+
'.xml', '.rss', '.atom'
73
+
]);
74
+
75
+
// Files to explicitly exclude
76
+
const excludedFiles = new Set([
77
+
'.map', '.DS_Store', 'Thumbs.db'
78
+
]);
79
+
80
+
for (let i = 0; i < fileArray.length; i++) {
81
+
const file = fileArray[i];
82
+
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
83
+
84
+
console.log(`๐ Processing file ${i + 1}/${fileArray.length}: ${file.name} (${file.size} bytes, ${file.type})`);
85
+
86
+
// Skip excluded files
87
+
if (excludedFiles.has(fileExtension)) {
88
+
console.log(`โญ๏ธ Skipping excluded file: ${file.name}`);
89
+
continue;
90
+
}
91
+
92
+
// Skip files that aren't in allowed extensions
93
+
if (!allowedExtensions.has(fileExtension)) {
94
+
console.log(`โญ๏ธ Skipping non-web file: ${file.name} (${fileExtension})`);
95
+
continue;
96
+
}
97
+
98
+
// Skip files that are too large (limit to 100MB per file)
99
+
const maxSize = 100 * 1024 * 1024; // 100MB
100
+
if (file.size > maxSize) {
101
+
console.log(`โญ๏ธ Skipping large file: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB > 100MB limit)`);
102
+
continue;
103
+
}
104
+
105
+
console.log(`โ
Including file: ${file.name}`);
106
+
const arrayBuffer = await file.arrayBuffer();
107
+
uploadedFiles.push({
108
+
name: file.name,
109
+
content: Buffer.from(arrayBuffer),
110
+
mimeType: file.type || 'application/octet-stream',
111
+
size: file.size
112
+
});
113
+
}
114
+
115
+
// Check total size limit (300MB)
116
+
const totalSize = uploadedFiles.reduce((sum, file) => sum + file.size, 0);
117
+
const maxTotalSize = 300 * 1024 * 1024; // 300MB
118
+
119
+
console.log(`๐ Filtered to ${uploadedFiles.length} files from ${fileArray.length} total files`);
120
+
console.log(`๐ฆ Total size: ${(totalSize / 1024 / 1024).toFixed(2)}MB (limit: 300MB)`);
121
+
122
+
if (totalSize > maxTotalSize) {
123
+
throw new Error(`Total upload size ${(totalSize / 1024 / 1024).toFixed(2)}MB exceeds 300MB limit`);
124
+
}
125
+
126
+
if (uploadedFiles.length === 0) {
127
+
throw new Error('No valid web files found to upload. Allowed types: HTML, CSS, JS, images, fonts, PDFs, and other web assets.');
128
+
}
129
+
130
+
console.log('โ
File conversion completed');
131
+
132
+
// Process files into directory structure
133
+
console.log('๐๏ธ Building directory structure');
134
+
const { directory, fileCount } = processUploadedFiles(uploadedFiles);
135
+
console.log(`โ
Directory structure created with ${fileCount} files`);
136
+
137
+
// Upload files as blobs
138
+
const uploadResults: FileUploadResult[] = [];
139
+
const filePaths: string[] = [];
140
+
141
+
console.log('โฌ๏ธ Starting blob upload process');
142
+
for (let i = 0; i < uploadedFiles.length; i++) {
143
+
const file = uploadedFiles[i];
144
+
console.log(`๐ค Uploading blob ${i + 1}/${uploadedFiles.length}: ${file.name}`);
145
+
146
+
try {
147
+
console.log(`๐ Upload details:`, {
148
+
fileName: file.name,
149
+
fileSize: file.size,
150
+
mimeType: file.mimeType,
151
+
contentLength: file.content.length
152
+
});
153
+
154
+
const uploadResult = await agent.com.atproto.repo.uploadBlob(
155
+
file.content,
156
+
{
157
+
encoding: file.mimeType
158
+
}
159
+
);
160
+
161
+
console.log(`โ
Upload successful for ${file.name}:`, {
162
+
hash: uploadResult.data.blob.ref.toString(),
163
+
mimeType: uploadResult.data.blob.mimeType,
164
+
size: uploadResult.data.blob.size
165
+
});
166
+
167
+
uploadResults.push({
168
+
hash: uploadResult.data.blob.ref.toString(),
169
+
blobRef: uploadResult.data.blob
170
+
});
171
+
172
+
filePaths.push(file.name);
173
+
} catch (uploadError) {
174
+
console.error(`โ Upload failed for file ${file.name}:`, uploadError);
175
+
console.error('Upload error details:', {
176
+
fileName: file.name,
177
+
fileSize: file.size,
178
+
mimeType: file.mimeType,
179
+
error: uploadError
180
+
});
181
+
throw uploadError;
182
+
}
183
+
}
184
+
185
+
console.log('โ
All blobs uploaded successfully');
186
+
187
+
// Update directory with file blobs
188
+
console.log('๐ Updating file blobs in directory structure');
189
+
const updatedDirectory = updateFileBlobs(directory, uploadResults, filePaths);
190
+
console.log('โ
File blobs updated');
191
+
192
+
// Create manifest
193
+
console.log('๐ Creating manifest');
194
+
const manifest = createManifest(siteName, updatedDirectory, fileCount);
195
+
console.log('โ
Manifest created');
196
+
197
+
// Create the record
198
+
console.log('๐ Creating record in repo');
199
+
const record = await agent.com.atproto.repo.createRecord({
200
+
repo: auth.did,
201
+
collection: 'place.wisp.fs',
202
+
record: manifest
203
+
});
204
+
205
+
console.log('โ
Record created successfully:', {
206
+
uri: record.data.uri,
207
+
cid: record.data.cid
208
+
});
209
+
210
+
const result = {
211
+
success: true,
212
+
uri: record.data.uri,
213
+
cid: record.data.cid,
214
+
fileCount,
215
+
siteName
216
+
};
217
+
218
+
console.log('๐ Upload process completed successfully');
219
+
return result;
220
+
} catch (error) {
221
+
console.error('โ Upload error:', error);
222
+
console.error('Error details:', {
223
+
message: error instanceof Error ? error.message : 'Unknown error',
224
+
stack: error instanceof Error ? error.stack : undefined,
225
+
name: error instanceof Error ? error.name : undefined
226
+
});
227
+
throw new Error(`Failed to upload files: ${error instanceof Error ? error.message : 'Unknown error'}`);
228
+
}
229
+
}
230
+
)