@lovelybunch/api 1.0.69-alpha.12 → 1.0.69-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/jobs/job-scheduler.js +12 -1
- package/dist/lib/jobs/job-store.d.ts +1 -0
- package/dist/lib/jobs/job-store.js +150 -28
- package/dist/lib/storage/file-storage.js +16 -7
- package/dist/routes/api/v1/config/index.js +2 -1
- package/dist/routes/api/v1/config/route.d.ts +11 -0
- package/dist/routes/api/v1/config/route.js +104 -0
- package/dist/routes/api/v1/resources/[id]/route.js +11 -7
- package/dist/routes/api/v1/resources/generate/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/generate/index.js +5 -0
- package/dist/routes/api/v1/resources/generate/route.d.ts +19 -0
- package/dist/routes/api/v1/resources/generate/route.js +205 -0
- package/dist/routes/api/v1/resources/index.js +2 -0
- package/dist/routes/api/v1/version/index.d.ts +3 -0
- package/dist/routes/api/v1/version/index.js +5 -0
- package/dist/routes/api/v1/version/route.d.ts +32 -0
- package/dist/routes/api/v1/version/route.js +61 -0
- package/dist/server-with-static.js +2 -0
- package/dist/server.js +2 -0
- package/package.json +5 -4
- package/static/assets/{index-cHsdSr23.js → index-DGl_L0ie.js} +270 -215
- package/static/assets/index-uyccX96d.css +33 -0
- package/static/index.html +2 -2
- package/static/assets/index-CDMfOGVc.css +0 -33
|
@@ -26,7 +26,18 @@ export class JobScheduler {
|
|
|
26
26
|
return;
|
|
27
27
|
const jobs = await this.store.listJobs();
|
|
28
28
|
for (const job of jobs) {
|
|
29
|
-
|
|
29
|
+
// Skip corrupted jobs (jobs with _error field)
|
|
30
|
+
if (job._error) {
|
|
31
|
+
console.warn(`Skipping corrupted job ${job.id}: ${job._error}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await this.register(job);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error(`Failed to register job ${job.id}:`, error);
|
|
39
|
+
// Continue with other jobs even if one fails
|
|
40
|
+
}
|
|
30
41
|
}
|
|
31
42
|
this.initialized = true;
|
|
32
43
|
}
|
|
@@ -8,6 +8,7 @@ export declare class JobStore {
|
|
|
8
8
|
private getJobFilePath;
|
|
9
9
|
listJobs(): Promise<ScheduledJob[]>;
|
|
10
10
|
getJob(id: string): Promise<ScheduledJob | null>;
|
|
11
|
+
private createErrorJob;
|
|
11
12
|
saveJob(job: ScheduledJob, bodyContent?: string): Promise<void>;
|
|
12
13
|
deleteJob(id: string): Promise<boolean>;
|
|
13
14
|
appendRun(jobId: string, run: ScheduledJobRun): Promise<ScheduledJob>;
|
|
@@ -90,15 +90,53 @@ export class JobStore {
|
|
|
90
90
|
try {
|
|
91
91
|
const filePath = await this.getJobFilePath(id);
|
|
92
92
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
93
|
+
// Handle empty files
|
|
94
|
+
if (!content || content.trim().length === 0) {
|
|
95
|
+
return this.createErrorJob(id, 'Job file is empty');
|
|
96
|
+
}
|
|
93
97
|
const { data, content: body } = matter(content);
|
|
98
|
+
// Validate that we have at least an id field
|
|
99
|
+
if (!data || typeof data !== 'object' || !data.id) {
|
|
100
|
+
return this.createErrorJob(id, 'Job file is missing required id field');
|
|
101
|
+
}
|
|
94
102
|
return this.fromFrontmatter(data, body);
|
|
95
103
|
}
|
|
96
104
|
catch (error) {
|
|
97
105
|
if (error?.code === 'ENOENT')
|
|
98
106
|
return null;
|
|
99
|
-
|
|
107
|
+
// Handle parsing errors (e.g., invalid YAML, corrupted frontmatter)
|
|
108
|
+
const errorMessage = error?.message || 'Unknown error parsing job file';
|
|
109
|
+
return this.createErrorJob(id, errorMessage);
|
|
100
110
|
}
|
|
101
111
|
}
|
|
112
|
+
createErrorJob(id, errorMessage) {
|
|
113
|
+
const now = new Date();
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
name: id,
|
|
117
|
+
description: undefined,
|
|
118
|
+
prompt: '',
|
|
119
|
+
model: 'anthropic/claude-sonnet-4',
|
|
120
|
+
status: 'paused',
|
|
121
|
+
schedule: {
|
|
122
|
+
type: 'interval',
|
|
123
|
+
hours: 6,
|
|
124
|
+
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
125
|
+
},
|
|
126
|
+
metadata: {
|
|
127
|
+
createdAt: now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
lastRunAt: undefined,
|
|
130
|
+
nextRunAt: undefined,
|
|
131
|
+
},
|
|
132
|
+
runs: [],
|
|
133
|
+
tags: [],
|
|
134
|
+
contextPaths: [],
|
|
135
|
+
// Store error in a way that won't break serialization
|
|
136
|
+
// We'll use a special tag to mark error jobs
|
|
137
|
+
_error: errorMessage
|
|
138
|
+
};
|
|
139
|
+
}
|
|
102
140
|
async saveJob(job, bodyContent = '') {
|
|
103
141
|
const filePath = await this.getJobFilePath(job.id);
|
|
104
142
|
const normalizedJob = {
|
|
@@ -138,34 +176,118 @@ export class JobStore {
|
|
|
138
176
|
if (!data?.id) {
|
|
139
177
|
throw new Error('Scheduled job is missing required id field');
|
|
140
178
|
}
|
|
179
|
+
// Validate and sanitize fields to prevent issues with corrupted data
|
|
141
180
|
const createdAt = toDate(data.metadata?.createdAt) ?? new Date();
|
|
142
181
|
const updatedAt = toDate(data.metadata?.updatedAt) ?? createdAt;
|
|
182
|
+
// Validate schedule structure
|
|
183
|
+
let schedule;
|
|
184
|
+
if (data.schedule && typeof data.schedule === 'object') {
|
|
185
|
+
if (data.schedule.type === 'cron') {
|
|
186
|
+
const cronSchedule = data.schedule;
|
|
187
|
+
if (typeof cronSchedule.expression === 'string' && cronSchedule.expression.trim()) {
|
|
188
|
+
schedule = {
|
|
189
|
+
type: 'cron',
|
|
190
|
+
expression: cronSchedule.expression.trim(),
|
|
191
|
+
timezone: typeof cronSchedule.timezone === 'string' ? cronSchedule.timezone : undefined,
|
|
192
|
+
description: typeof cronSchedule.description === 'string' ? cronSchedule.description : undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Invalid cron schedule, fall back to default
|
|
197
|
+
schedule = {
|
|
198
|
+
type: 'interval',
|
|
199
|
+
hours: 6,
|
|
200
|
+
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const intervalSchedule = data.schedule;
|
|
206
|
+
const hours = typeof intervalSchedule.hours === 'number' && intervalSchedule.hours >= 1
|
|
207
|
+
? intervalSchedule.hours
|
|
208
|
+
: 6;
|
|
209
|
+
const daysOfWeek = Array.isArray(intervalSchedule.daysOfWeek)
|
|
210
|
+
? intervalSchedule.daysOfWeek.filter((day) => typeof day === 'string' && ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].includes(day.toLowerCase()))
|
|
211
|
+
: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
|
|
212
|
+
schedule = {
|
|
213
|
+
type: 'interval',
|
|
214
|
+
hours,
|
|
215
|
+
daysOfWeek: daysOfWeek.length > 0 ? daysOfWeek : ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
|
|
216
|
+
anchorHour: typeof intervalSchedule.anchorHour === 'number' && intervalSchedule.anchorHour >= 0 && intervalSchedule.anchorHour <= 23
|
|
217
|
+
? intervalSchedule.anchorHour
|
|
218
|
+
: undefined,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
schedule = {
|
|
224
|
+
type: 'interval',
|
|
225
|
+
hours: 6,
|
|
226
|
+
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Validate and sanitize runs
|
|
143
230
|
const runs = Array.isArray(data.runs)
|
|
144
|
-
? data.runs
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
231
|
+
? data.runs
|
|
232
|
+
.filter((run) => run && typeof run === 'object')
|
|
233
|
+
.map((run) => {
|
|
234
|
+
const startedAt = toDate(run.startedAt);
|
|
235
|
+
if (!startedAt) {
|
|
236
|
+
// Skip runs with invalid dates
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
id: typeof run.id === 'string' ? run.id : `run-${randomUUID()}`,
|
|
241
|
+
jobId: data.id,
|
|
242
|
+
trigger: (run.trigger === 'manual' || run.trigger === 'scheduled') ? run.trigger : 'manual',
|
|
243
|
+
status: (['pending', 'running', 'succeeded', 'failed'].includes(run.status)) ? run.status : 'pending',
|
|
244
|
+
startedAt,
|
|
245
|
+
finishedAt: toDate(run.finishedAt),
|
|
246
|
+
outputPath: typeof run.outputPath === 'string' ? run.outputPath : undefined,
|
|
247
|
+
summary: typeof run.summary === 'string' ? run.summary : undefined,
|
|
248
|
+
error: typeof run.error === 'string' ? run.error : undefined,
|
|
249
|
+
cliCommand: typeof run.cliCommand === 'string' ? run.cliCommand : undefined,
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
.filter((run) => run !== null)
|
|
253
|
+
: [];
|
|
254
|
+
// Validate and sanitize string fields
|
|
255
|
+
const name = typeof data.name === 'string' && data.name.trim()
|
|
256
|
+
? data.name.trim().slice(0, 500) // Limit length
|
|
257
|
+
: data.id;
|
|
258
|
+
const description = typeof data.description === 'string'
|
|
259
|
+
? data.description.slice(0, 1000) // Limit length
|
|
260
|
+
: undefined;
|
|
261
|
+
const prompt = typeof data.prompt === 'string'
|
|
262
|
+
? data.prompt.trim() || body.trim()
|
|
263
|
+
: body.trim();
|
|
264
|
+
const model = typeof data.model === 'string' && data.model.trim()
|
|
265
|
+
? data.model.trim()
|
|
266
|
+
: 'anthropic/claude-sonnet-4';
|
|
267
|
+
const status = (data.status === 'active' || data.status === 'paused')
|
|
268
|
+
? data.status
|
|
269
|
+
: 'paused';
|
|
270
|
+
// Validate arrays
|
|
271
|
+
const tags = Array.isArray(data.tags)
|
|
272
|
+
? data.tags.filter((tag) => typeof tag === 'string').slice(0, 50)
|
|
273
|
+
: [];
|
|
274
|
+
const contextPaths = Array.isArray(data.contextPaths)
|
|
275
|
+
? data.contextPaths.filter((path) => typeof path === 'string').slice(0, 100)
|
|
156
276
|
: [];
|
|
277
|
+
const agentIds = Array.isArray(data.agentIds)
|
|
278
|
+
? data.agentIds.filter((id) => typeof id === 'string').slice(0, 50)
|
|
279
|
+
: undefined;
|
|
280
|
+
const mcpServers = Array.isArray(data.mcpServers)
|
|
281
|
+
? data.mcpServers.filter((server) => typeof server === 'string').slice(0, 50)
|
|
282
|
+
: undefined;
|
|
157
283
|
return {
|
|
158
284
|
id: data.id,
|
|
159
|
-
name
|
|
160
|
-
description
|
|
161
|
-
prompt
|
|
162
|
-
model
|
|
163
|
-
status
|
|
164
|
-
schedule
|
|
165
|
-
type: 'interval',
|
|
166
|
-
hours: 6,
|
|
167
|
-
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
168
|
-
},
|
|
285
|
+
name,
|
|
286
|
+
description,
|
|
287
|
+
prompt,
|
|
288
|
+
model,
|
|
289
|
+
status,
|
|
290
|
+
schedule,
|
|
169
291
|
metadata: {
|
|
170
292
|
createdAt,
|
|
171
293
|
updatedAt,
|
|
@@ -173,11 +295,11 @@ export class JobStore {
|
|
|
173
295
|
nextRunAt: toDate(data.metadata?.nextRunAt),
|
|
174
296
|
},
|
|
175
297
|
runs,
|
|
176
|
-
tags
|
|
177
|
-
contextPaths
|
|
178
|
-
agentId: data.agentId,
|
|
179
|
-
agentIds
|
|
180
|
-
mcpServers
|
|
298
|
+
tags,
|
|
299
|
+
contextPaths,
|
|
300
|
+
agentId: typeof data.agentId === 'string' ? data.agentId : undefined,
|
|
301
|
+
agentIds,
|
|
302
|
+
mcpServers,
|
|
181
303
|
};
|
|
182
304
|
}
|
|
183
305
|
toFrontmatter(job) {
|
|
@@ -122,19 +122,28 @@ export class FileStorageAdapter {
|
|
|
122
122
|
// Never allow id to be updated to prevent overwriting other proposals
|
|
123
123
|
const { id: _, ...safeUpdates } = updates;
|
|
124
124
|
// Handle comments separately as they're stored at root level in file format
|
|
125
|
-
|
|
125
|
+
// Comments can come from either root level or metadata.comments
|
|
126
|
+
let commentsToStore = existing.metadata.comments || [];
|
|
127
|
+
if (safeUpdates.comments) {
|
|
128
|
+
// If comments are provided at root level, use them
|
|
129
|
+
commentsToStore = safeUpdates.comments;
|
|
130
|
+
}
|
|
131
|
+
else if (safeUpdates.metadata?.comments) {
|
|
132
|
+
// If comments are provided in metadata, use them
|
|
133
|
+
commentsToStore = safeUpdates.metadata.comments;
|
|
134
|
+
}
|
|
135
|
+
const updated = {
|
|
126
136
|
...existing,
|
|
127
137
|
...safeUpdates,
|
|
128
138
|
metadata: {
|
|
129
139
|
...existing.metadata,
|
|
130
140
|
...safeUpdates.metadata,
|
|
131
|
-
updatedAt: new Date()
|
|
132
|
-
|
|
141
|
+
updatedAt: new Date(),
|
|
142
|
+
comments: commentsToStore
|
|
143
|
+
},
|
|
144
|
+
// Store comments at the root level for the file format
|
|
145
|
+
comments: commentsToStore
|
|
133
146
|
};
|
|
134
|
-
// If comments are being updated, store them at the root level for the file format
|
|
135
|
-
if (safeUpdates.comments) {
|
|
136
|
-
updated.comments = safeUpdates.comments;
|
|
137
|
-
}
|
|
138
147
|
await this.createCP(updated);
|
|
139
148
|
}
|
|
140
149
|
async deleteCP(id) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { GET, PUT, TEST } from './route.js';
|
|
2
|
+
import { GET, PUT, TEST, EXPORT } from './route.js';
|
|
3
3
|
const config = new Hono();
|
|
4
4
|
config.get('/', GET);
|
|
5
5
|
config.put('/', PUT);
|
|
6
6
|
config.post('/test', TEST);
|
|
7
|
+
config.post('/export-to-env', EXPORT);
|
|
7
8
|
export default config;
|
|
@@ -30,3 +30,14 @@ export declare function TEST(c: Context): Promise<(Response & import("hono").Typ
|
|
|
30
30
|
success: false;
|
|
31
31
|
message: string;
|
|
32
32
|
}, 501, "json">)>;
|
|
33
|
+
export declare function EXPORT(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
34
|
+
success: false;
|
|
35
|
+
message: string;
|
|
36
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
37
|
+
success: false;
|
|
38
|
+
message: string;
|
|
39
|
+
}, 500, "json">) | (Response & import("hono").TypedResponse<{
|
|
40
|
+
success: true;
|
|
41
|
+
message: string;
|
|
42
|
+
envPath: string;
|
|
43
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
|
|
@@ -240,3 +240,107 @@ export async function TEST(c) {
|
|
|
240
240
|
return c.json({ success: false, message: 'Invalid request body' }, 400);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
+
// Provider to environment variable mapping
|
|
244
|
+
const PROVIDER_ENV_VARS = {
|
|
245
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
246
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
247
|
+
openai: 'OPENAI_API_KEY',
|
|
248
|
+
gemini: 'GEMINI_API_KEY',
|
|
249
|
+
factorydroid: 'FACTORY_DROID_API_KEY',
|
|
250
|
+
bedrock: 'AWS_BEDROCK_API_KEY',
|
|
251
|
+
baseten: 'BASETEN_API_KEY',
|
|
252
|
+
fireworks: 'FIREWORKS_API_KEY',
|
|
253
|
+
deepinfra: 'DEEPINFRA_API_KEY'
|
|
254
|
+
};
|
|
255
|
+
// Helper to find workspace root (where .env should be written)
|
|
256
|
+
async function findWorkspaceRoot() {
|
|
257
|
+
// Try to find .nut directory first
|
|
258
|
+
let currentDir = process.cwd();
|
|
259
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
260
|
+
const nutPath = path.join(currentDir, '.nut');
|
|
261
|
+
try {
|
|
262
|
+
await fs.access(nutPath);
|
|
263
|
+
// Found .nut directory, return parent as workspace root
|
|
264
|
+
return currentDir;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
currentDir = path.dirname(currentDir);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Fallback to cwd if no .nut found
|
|
271
|
+
return process.cwd();
|
|
272
|
+
}
|
|
273
|
+
// POST /api/v1/config/export-to-env
|
|
274
|
+
// Body: { provider: string }
|
|
275
|
+
// Exports the saved API key for a provider to a .env file in workspace root
|
|
276
|
+
export async function EXPORT(c) {
|
|
277
|
+
try {
|
|
278
|
+
const body = await c.req.json();
|
|
279
|
+
const provider = (body?.provider || '').toString();
|
|
280
|
+
if (!provider) {
|
|
281
|
+
return c.json({ success: false, message: 'Missing provider' }, 400);
|
|
282
|
+
}
|
|
283
|
+
const envVarName = PROVIDER_ENV_VARS[provider];
|
|
284
|
+
if (!envVarName) {
|
|
285
|
+
return c.json({ success: false, message: `Unknown provider: ${provider}` }, 400);
|
|
286
|
+
}
|
|
287
|
+
// Load the API key from global config
|
|
288
|
+
const configPath = await getGlobalConfigPath();
|
|
289
|
+
let config = { apiKeys: {}, defaults: {} };
|
|
290
|
+
try {
|
|
291
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
292
|
+
config = JSON.parse(content);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Config doesn't exist yet
|
|
296
|
+
}
|
|
297
|
+
const apiKey = config.apiKeys?.[provider];
|
|
298
|
+
if (!apiKey) {
|
|
299
|
+
return c.json({ success: false, message: `No API key configured for ${provider}` }, 400);
|
|
300
|
+
}
|
|
301
|
+
// Find workspace root
|
|
302
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
303
|
+
if (!workspaceRoot) {
|
|
304
|
+
return c.json({ success: false, message: 'Could not find workspace root' }, 500);
|
|
305
|
+
}
|
|
306
|
+
const envPath = path.join(workspaceRoot, '.env');
|
|
307
|
+
// Read existing .env file if it exists
|
|
308
|
+
let envContent = '';
|
|
309
|
+
try {
|
|
310
|
+
envContent = await fs.readFile(envPath, 'utf-8');
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// File doesn't exist, will create new
|
|
314
|
+
}
|
|
315
|
+
// Parse existing env vars
|
|
316
|
+
const envLines = envContent.split('\n');
|
|
317
|
+
let found = false;
|
|
318
|
+
const updatedLines = envLines.map(line => {
|
|
319
|
+
const trimmed = line.trim();
|
|
320
|
+
if (trimmed.startsWith(envVarName + '=') || trimmed.startsWith(envVarName + ' =')) {
|
|
321
|
+
found = true;
|
|
322
|
+
return `${envVarName}=${apiKey}`;
|
|
323
|
+
}
|
|
324
|
+
return line;
|
|
325
|
+
});
|
|
326
|
+
// If not found, append to the end
|
|
327
|
+
if (!found) {
|
|
328
|
+
// Add a newline if the file doesn't end with one
|
|
329
|
+
if (updatedLines.length > 0 && updatedLines[updatedLines.length - 1] !== '') {
|
|
330
|
+
updatedLines.push('');
|
|
331
|
+
}
|
|
332
|
+
updatedLines.push(`${envVarName}=${apiKey}`);
|
|
333
|
+
}
|
|
334
|
+
// Write back to .env file
|
|
335
|
+
await fs.writeFile(envPath, updatedLines.join('\n'), 'utf-8');
|
|
336
|
+
return c.json({
|
|
337
|
+
success: true,
|
|
338
|
+
message: `${envVarName} saved to .env file`,
|
|
339
|
+
envPath
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.error('Error exporting to env:', error);
|
|
344
|
+
return c.json({ success: false, message: error instanceof Error ? error.message : 'Failed to export to env' }, 500);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -52,10 +52,11 @@ export async function GET(c) {
|
|
|
52
52
|
}
|
|
53
53
|
const url = new URL(c.req.url);
|
|
54
54
|
const download = url.searchParams.get('download');
|
|
55
|
+
// Return the actual file (either for download or inline display)
|
|
56
|
+
const filePath = path.join(FILES_DIR, resource.path);
|
|
57
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
55
58
|
if (download === 'true') {
|
|
56
|
-
//
|
|
57
|
-
const filePath = path.join(FILES_DIR, resource.path);
|
|
58
|
-
const fileBuffer = await fs.readFile(filePath);
|
|
59
|
+
// Force download with Content-Disposition: attachment
|
|
59
60
|
return new Response(fileBuffer, {
|
|
60
61
|
headers: {
|
|
61
62
|
'Content-Type': resource.type,
|
|
@@ -64,10 +65,13 @@ export async function GET(c) {
|
|
|
64
65
|
}
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
|
-
//
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// Serve file inline (for preview in iframe, img, video, etc.)
|
|
69
|
+
return new Response(fileBuffer, {
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': resource.type,
|
|
72
|
+
'Content-Length': resource.size.toString(),
|
|
73
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
74
|
+
}
|
|
71
75
|
});
|
|
72
76
|
}
|
|
73
77
|
catch (error) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
export declare function POST(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
3
|
+
success: false;
|
|
4
|
+
error: {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
9
|
+
success: true;
|
|
10
|
+
data: {
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
};
|
|
13
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: any;
|
|
18
|
+
};
|
|
19
|
+
}, 500, "json">)>;
|