@mevdragon/vidfarm-devcli 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/.env.example +11 -4
- package/PLATFORM_SPEC.md +142 -2
- package/README.md +165 -16
- package/SKILL.developer.md +577 -0
- package/dist/infra/cdk/bin/vidfarm-prod.js +59 -0
- package/dist/infra/cdk/lib/vidfarm-prod-stack.js +212 -0
- package/dist/src/account-pages.js +578 -0
- package/dist/src/app.js +887 -66
- package/dist/src/cli.js +284 -5
- package/dist/src/config.js +24 -4
- package/dist/src/db.js +427 -18
- package/dist/src/dev-app.js +59 -12
- package/dist/src/homepage.js +441 -0
- package/dist/src/index.js +12 -7
- package/dist/src/lib/crypto.js +14 -0
- package/dist/src/lib/template-dna.js +542 -0
- package/dist/src/lib/template-style-options.js +49 -0
- package/dist/src/registry.js +54 -7
- package/dist/src/runtime.js +3 -1
- package/dist/src/services/auth.js +69 -5
- package/dist/src/services/jobs.js +23 -4
- package/dist/src/services/providers.js +74 -12
- package/dist/src/services/storage.js +52 -18
- package/dist/src/services/template-certification.js +160 -0
- package/dist/src/services/template-loader.js +37 -0
- package/dist/src/services/template-sources.js +135 -0
- package/dist/src/worker.js +19 -7
- package/dist/templates/template_0000/src/lib/images.js +242 -0
- package/dist/templates/template_0000/src/remotion/Root.js +33 -0
- package/dist/templates/template_0000/src/sdk.js +3 -0
- package/dist/templates/template_0000/src/style-options.js +51 -0
- package/dist/templates/template_0000/src/template-dna.js +9 -0
- package/dist/templates/template_0000/src/template.js +1217 -0
- package/package.json +9 -1
- package/templates/template_0000/README.md +121 -0
- package/templates/template_0000/SKILL.md +193 -0
- package/templates/template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/template_0000/composition.json +11 -0
- package/templates/template_0000/package-lock.json +5137 -0
- package/templates/template_0000/package.json +30 -0
- package/templates/template_0000/research/preview/.gitkeep +1 -0
- package/templates/template_0000/research/source_notes.md +7 -0
- package/templates/template_0000/scripts/create-site.mjs +27 -0
- package/templates/template_0000/scripts/render-cloud.mjs +72 -0
- package/templates/template_0000/src/lib/images.ts +284 -0
- package/templates/template_0000/src/remotion/Root.js +33 -0
- package/templates/template_0000/src/remotion/Root.tsx +75 -0
- package/templates/template_0000/src/remotion/index.tsx +4 -0
- package/templates/template_0000/src/sdk.ts +122 -0
- package/templates/template_0000/src/style-options.js +51 -0
- package/templates/template_0000/src/style-options.ts +60 -0
- package/templates/template_0000/src/template-dna.ts +15 -0
- package/templates/template_0000/src/template.ts +1747 -0
- package/templates/template_0000/template.config.json +26 -0
- package/templates/template_0000/tsconfig.json +19 -0
- package/dist/templates/template_0000/demo-template.js +0 -196
- package/dist/templates/template_0000/remotion/Root.js +0 -66
- /package/dist/templates/template_0000/{remotion → src/remotion}/index.js +0 -0
package/dist/src/app.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { renderLoginPage, renderPricingPage, renderSettingsPage } from "./account-pages.js";
|
|
5
6
|
import { config } from "./config.js";
|
|
6
7
|
import { database } from "./db.js";
|
|
7
8
|
import { renderDevApp } from "./dev-app.js";
|
|
8
|
-
import {
|
|
9
|
+
import { renderHomepage } from "./homepage.js";
|
|
10
|
+
import { decryptString, encryptString, hashSecret } from "./lib/crypto.js";
|
|
9
11
|
import { createId } from "./lib/ids.js";
|
|
10
12
|
import { templateRegistry } from "./registry.js";
|
|
11
13
|
import { AuthService } from "./services/auth.js";
|
|
12
14
|
import { JobsService } from "./services/jobs.js";
|
|
13
15
|
import { StorageService } from "./services/storage.js";
|
|
16
|
+
import { TemplateSourceService } from "./services/template-sources.js";
|
|
14
17
|
const auth = new AuthService();
|
|
15
18
|
const jobs = new JobsService();
|
|
16
19
|
const storage = new StorageService();
|
|
20
|
+
const templateSources = new TemplateSourceService();
|
|
21
|
+
const API_PREFIX = "/api/v1";
|
|
22
|
+
const USER_PREFIX = `${API_PREFIX}/user`;
|
|
23
|
+
const TEMPLATES_PREFIX = `${API_PREFIX}/templates`;
|
|
24
|
+
const SESSION_COOKIE = "vidfarm_session";
|
|
17
25
|
const app = new Hono();
|
|
18
26
|
const otpRequestSchema = z.object({
|
|
19
27
|
email: z.string().email()
|
|
@@ -23,42 +31,533 @@ const otpVerifySchema = z.object({
|
|
|
23
31
|
code: z.string().min(6).max(6),
|
|
24
32
|
name: z.string().optional()
|
|
25
33
|
});
|
|
34
|
+
const passwordLoginSchema = z.object({
|
|
35
|
+
email: z.string().email(),
|
|
36
|
+
password: z.string().min(1)
|
|
37
|
+
});
|
|
38
|
+
const adminWhitelistSchema = z.object({
|
|
39
|
+
emails: z.array(z.string().email()).min(1)
|
|
40
|
+
});
|
|
41
|
+
const adminCreateUserSchema = z.object({
|
|
42
|
+
email: z.string().email(),
|
|
43
|
+
password: z.string().min(8),
|
|
44
|
+
name: z.string().optional()
|
|
45
|
+
});
|
|
26
46
|
const providerKeySchema = z.object({
|
|
27
47
|
provider: z.enum(["openai", "gemini", "openrouter", "perplexity"]),
|
|
28
48
|
label: z.string().optional(),
|
|
29
49
|
secret: z.string().min(8),
|
|
30
50
|
weight: z.number().int().min(1).max(100).default(1)
|
|
31
51
|
});
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const settingsProviderKeyFormSchema = z.object({
|
|
53
|
+
provider: z.enum(["openai", "gemini", "openrouter", "perplexity"]),
|
|
54
|
+
label: z.string().trim().optional(),
|
|
55
|
+
secret: z.string().min(8),
|
|
56
|
+
weight: z.coerce.number().int().min(1).max(100).default(1)
|
|
57
|
+
});
|
|
58
|
+
const settingsProfileFormSchema = z.object({
|
|
59
|
+
about: z.string().max(10000).optional(),
|
|
60
|
+
groupchat_url: z.union([z.literal(""), z.string().url()]).optional(),
|
|
61
|
+
flockposter_api_key: z.string().max(1000).optional()
|
|
35
62
|
});
|
|
63
|
+
const listJobsQuerySchema = z.object({
|
|
64
|
+
tracer: z.string().min(1).optional(),
|
|
65
|
+
start_time: z.string().min(1).optional(),
|
|
66
|
+
end_time: z.string().min(1).optional(),
|
|
67
|
+
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
68
|
+
template_id: z.string().min(1).optional()
|
|
69
|
+
});
|
|
70
|
+
function getLoginMode(value) {
|
|
71
|
+
return value === "password" ? "password" : "otp";
|
|
72
|
+
}
|
|
73
|
+
const allowedAttachmentExtensions = new Set([
|
|
74
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg",
|
|
75
|
+
".mp4", ".mov", ".webm", ".m4v",
|
|
76
|
+
".mp3", ".wav", ".m4a", ".aac", ".ogg",
|
|
77
|
+
".pdf", ".md", ".txt"
|
|
78
|
+
]);
|
|
36
79
|
app.use("*", async (c, next) => {
|
|
37
80
|
c.header("x-powered-by", "vidfarm");
|
|
81
|
+
await templateRegistry.ensureInitialized();
|
|
38
82
|
await next();
|
|
39
83
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
84
|
+
function renderConsole(c) {
|
|
85
|
+
return c.html(renderDevApp({
|
|
86
|
+
environment: config.NODE_ENV,
|
|
87
|
+
templates: templateRegistry.list().map((template) => ({
|
|
88
|
+
id: template.id,
|
|
89
|
+
slug_id: template.slugId,
|
|
90
|
+
version: template.version,
|
|
91
|
+
description: template.about.description,
|
|
92
|
+
operations: Object.entries(template.operations).map(([name, operation]) => ({
|
|
93
|
+
name,
|
|
94
|
+
description: operation.description,
|
|
95
|
+
workflow: operation.workflow,
|
|
96
|
+
providerHint: operation.providerHint ?? null
|
|
97
|
+
}))
|
|
53
98
|
}))
|
|
54
|
-
}))
|
|
55
|
-
}
|
|
56
|
-
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
function parseCookieHeader(value) {
|
|
102
|
+
const cookies = new Map();
|
|
103
|
+
if (!value) {
|
|
104
|
+
return cookies;
|
|
105
|
+
}
|
|
106
|
+
for (const part of value.split(";")) {
|
|
107
|
+
const trimmed = part.trim();
|
|
108
|
+
if (!trimmed) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const separator = trimmed.indexOf("=");
|
|
112
|
+
if (separator < 0) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
cookies.set(trimmed.slice(0, separator), decodeURIComponent(trimmed.slice(separator + 1)));
|
|
116
|
+
}
|
|
117
|
+
return cookies;
|
|
118
|
+
}
|
|
119
|
+
function appendCookie(c, value) {
|
|
120
|
+
c.header("set-cookie", value, { append: true });
|
|
121
|
+
}
|
|
122
|
+
function setBrowserSession(c, customer) {
|
|
123
|
+
const payload = encryptString(JSON.stringify({
|
|
124
|
+
customerId: customer.id,
|
|
125
|
+
email: customer.email,
|
|
126
|
+
issuedAt: new Date().toISOString()
|
|
127
|
+
}), config.ENCRYPTION_SECRET);
|
|
128
|
+
appendCookie(c, `${SESSION_COOKIE}=${encodeURIComponent(payload)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000${config.isProduction ? "; Secure" : ""}`);
|
|
129
|
+
}
|
|
130
|
+
function clearBrowserSession(c) {
|
|
131
|
+
appendCookie(c, `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${config.isProduction ? "; Secure" : ""}`);
|
|
132
|
+
}
|
|
133
|
+
function getBrowserCustomer(c) {
|
|
134
|
+
try {
|
|
135
|
+
const cookies = parseCookieHeader(c.req.header("cookie"));
|
|
136
|
+
const raw = cookies.get(SESSION_COOKIE);
|
|
137
|
+
if (!raw) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const session = JSON.parse(decryptString(raw, config.ENCRYPTION_SECRET));
|
|
141
|
+
if (!session.customerId) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return database.getCustomerById(session.customerId);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function requireBrowserCustomer(c) {
|
|
151
|
+
const customer = getBrowserCustomer(c);
|
|
152
|
+
if (!customer?.isPaidPlan) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return customer;
|
|
156
|
+
}
|
|
157
|
+
function redirect(c, location, status = 302) {
|
|
158
|
+
return c.redirect(location, status);
|
|
159
|
+
}
|
|
160
|
+
function redirectToSettings(c, input) {
|
|
161
|
+
const params = new URLSearchParams();
|
|
162
|
+
if (input?.notice) {
|
|
163
|
+
params.set("notice", input.notice);
|
|
164
|
+
}
|
|
165
|
+
if (input?.error) {
|
|
166
|
+
params.set("error", input.error);
|
|
167
|
+
}
|
|
168
|
+
const suffix = params.size ? `?${params.toString()}` : "";
|
|
169
|
+
return redirect(c, `/settings${suffix}`, 303);
|
|
170
|
+
}
|
|
171
|
+
function sanitizeFileName(value) {
|
|
172
|
+
const normalized = path.basename(value).replace(/[^\w.-]+/g, "_");
|
|
173
|
+
return normalized.length ? normalized : "upload.bin";
|
|
174
|
+
}
|
|
175
|
+
function isAllowedAttachment(fileName, contentType) {
|
|
176
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
177
|
+
if (allowedAttachmentExtensions.has(extension)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return contentType.startsWith("image/")
|
|
181
|
+
|| contentType.startsWith("video/")
|
|
182
|
+
|| contentType.startsWith("audio/")
|
|
183
|
+
|| contentType === "application/pdf"
|
|
184
|
+
|| contentType === "text/plain"
|
|
185
|
+
|| contentType === "text/markdown";
|
|
186
|
+
}
|
|
187
|
+
function isFormFile(value) {
|
|
188
|
+
return Boolean(value
|
|
189
|
+
&& typeof value === "object"
|
|
190
|
+
&& "name" in value
|
|
191
|
+
&& "arrayBuffer" in value);
|
|
192
|
+
}
|
|
193
|
+
function getVisibleApiKey(customerId) {
|
|
194
|
+
const existing = database.getLatestApiKeyForCustomer(customerId);
|
|
195
|
+
const rawValue = existing?.raw_value ? String(existing.raw_value) : null;
|
|
196
|
+
if (rawValue) {
|
|
197
|
+
return rawValue;
|
|
198
|
+
}
|
|
199
|
+
const apiKey = `vf_${createId("key")}`;
|
|
200
|
+
database.insertApiKey({
|
|
201
|
+
id: createId("api"),
|
|
202
|
+
customerId,
|
|
203
|
+
keyHash: hashSecret(apiKey + config.API_KEY_SALT),
|
|
204
|
+
rawValue: apiKey,
|
|
205
|
+
label: "Settings API key"
|
|
206
|
+
});
|
|
207
|
+
return apiKey;
|
|
208
|
+
}
|
|
209
|
+
function readStoredSecret(value) {
|
|
210
|
+
try {
|
|
211
|
+
return decryptString(value, config.ENCRYPTION_SECRET);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function getReleaseTimestamp(release) {
|
|
218
|
+
return release.activatedAt ?? release.updatedAt ?? release.createdAt;
|
|
219
|
+
}
|
|
220
|
+
function getApprovedHomepageTemplates(c) {
|
|
221
|
+
const approvedReleaseByTemplateId = new Map();
|
|
222
|
+
for (const release of database.listTemplateReleases()) {
|
|
223
|
+
if (release.status !== "active" && release.status !== "certified") {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const current = approvedReleaseByTemplateId.get(release.templateId);
|
|
227
|
+
if (!current || getReleaseTimestamp(release) > getReleaseTimestamp(current)) {
|
|
228
|
+
approvedReleaseByTemplateId.set(release.templateId, release);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return templateRegistry.list()
|
|
232
|
+
.map((template) => {
|
|
233
|
+
const release = approvedReleaseByTemplateId.get(template.id);
|
|
234
|
+
const approvedAt = release?.activatedAt
|
|
235
|
+
?? release?.updatedAt
|
|
236
|
+
?? release?.createdAt
|
|
237
|
+
?? (template.skillPath
|
|
238
|
+
? (() => {
|
|
239
|
+
try {
|
|
240
|
+
return statSync(template.skillPath).mtime.toISOString();
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
})()
|
|
246
|
+
: null);
|
|
247
|
+
const skillContent = template.skillPath
|
|
248
|
+
? (() => {
|
|
249
|
+
try {
|
|
250
|
+
return readFileSync(template.skillPath, "utf8");
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
})()
|
|
256
|
+
: "";
|
|
257
|
+
return {
|
|
258
|
+
title: template.about.title,
|
|
259
|
+
templateId: template.id,
|
|
260
|
+
slugId: template.slugId,
|
|
261
|
+
viralDna: template.about.viral_dna,
|
|
262
|
+
visualDna: template.about.visual_dna,
|
|
263
|
+
previewUrl: template.about.preview_media[0]
|
|
264
|
+
? resolveHomepagePreviewUrl(c, template.id, template.about.preview_media[0])
|
|
265
|
+
: null,
|
|
266
|
+
skillContent,
|
|
267
|
+
approvedAt
|
|
268
|
+
};
|
|
269
|
+
})
|
|
270
|
+
.sort((a, b) => {
|
|
271
|
+
const aTime = a.approvedAt ? Date.parse(a.approvedAt) : 0;
|
|
272
|
+
const bTime = b.approvedAt ? Date.parse(b.approvedAt) : 0;
|
|
273
|
+
return bTime - aTime || a.title.localeCompare(b.title);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function renderApprovedHomepage(c) {
|
|
277
|
+
return c.html(renderHomepage({
|
|
278
|
+
templates: getApprovedHomepageTemplates(c),
|
|
279
|
+
account: {
|
|
280
|
+
isLoggedIn: Boolean(getBrowserCustomer(c))
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
app.get("/", (c) => renderApprovedHomepage(c));
|
|
285
|
+
app.get("/health", (c) => c.json({ ok: true, time: new Date().toISOString() }));
|
|
286
|
+
app.get("/dev", (c) => renderConsole(c));
|
|
287
|
+
app.get("/template-media", async (c) => {
|
|
288
|
+
const key = c.req.query("key") ?? "";
|
|
289
|
+
return serveStorageKeyAsset(c, key);
|
|
290
|
+
});
|
|
291
|
+
app.get(API_PREFIX, (c) => c.json({ name: "vidfarm", version: "0.1.0", environment: config.NODE_ENV }));
|
|
292
|
+
app.get("/login", (c) => {
|
|
293
|
+
const customer = getBrowserCustomer(c);
|
|
294
|
+
if (customer?.isPaidPlan) {
|
|
295
|
+
return redirect(c, "/settings");
|
|
296
|
+
}
|
|
297
|
+
return c.html(renderLoginPage({ mode: getLoginMode(c.req.query("mode")) }));
|
|
298
|
+
});
|
|
299
|
+
app.post("/login/password", async (c) => {
|
|
300
|
+
const mode = getLoginMode(c.req.query("mode"));
|
|
301
|
+
const parsed = passwordLoginSchema.safeParse(await parseFormBody(c));
|
|
302
|
+
if (!parsed.success) {
|
|
303
|
+
return c.html(renderLoginPage({ mode, error: "Enter a valid email and password." }), 400);
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const result = auth.authenticateWithPassword(parsed.data.email, parsed.data.password);
|
|
307
|
+
if (result.status === "pricing") {
|
|
308
|
+
return c.html(renderPricingPage({ email: result.customer.email }));
|
|
309
|
+
}
|
|
310
|
+
setBrowserSession(c, result.customer);
|
|
311
|
+
return redirect(c, "/settings");
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return c.html(renderLoginPage({
|
|
315
|
+
mode,
|
|
316
|
+
email: parsed.data.email,
|
|
317
|
+
error: error instanceof Error ? error.message : "Unable to login."
|
|
318
|
+
}), 401);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
app.post("/login/otp/request", async (c) => {
|
|
322
|
+
const mode = getLoginMode(c.req.query("mode"));
|
|
323
|
+
const parsed = otpRequestSchema.safeParse(await parseFormBody(c));
|
|
324
|
+
if (!parsed.success) {
|
|
325
|
+
return c.html(renderLoginPage({ mode, error: "Enter a valid email address." }), 400);
|
|
326
|
+
}
|
|
327
|
+
await auth.requestOtp(parsed.data.email);
|
|
328
|
+
return c.html(renderLoginPage({
|
|
329
|
+
mode,
|
|
330
|
+
email: parsed.data.email,
|
|
331
|
+
otpSent: true,
|
|
332
|
+
message: "OTP sent. Enter the code to continue."
|
|
333
|
+
}));
|
|
334
|
+
});
|
|
335
|
+
app.post("/login/otp/verify", async (c) => {
|
|
336
|
+
const mode = getLoginMode(c.req.query("mode"));
|
|
337
|
+
const parsed = otpVerifySchema.safeParse(await parseFormBody(c));
|
|
338
|
+
if (!parsed.success) {
|
|
339
|
+
return c.html(renderLoginPage({ mode, error: "Enter the email and 6-digit OTP." }), 400);
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const result = auth.verifyOtpForBrowserLogin(parsed.data.email, parsed.data.code, parsed.data.name);
|
|
343
|
+
if (result.status === "pricing") {
|
|
344
|
+
clearBrowserSession(c);
|
|
345
|
+
return c.html(renderPricingPage({ email: result.customer.email }));
|
|
346
|
+
}
|
|
347
|
+
setBrowserSession(c, result.customer);
|
|
348
|
+
return redirect(c, "/settings");
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return c.html(renderLoginPage({
|
|
352
|
+
mode,
|
|
353
|
+
email: parsed.data.email,
|
|
354
|
+
otpSent: true,
|
|
355
|
+
error: error instanceof Error ? error.message : "Unable to verify OTP."
|
|
356
|
+
}), 401);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
app.get("/settings", (c) => {
|
|
360
|
+
const customer = requireBrowserCustomer(c);
|
|
361
|
+
if (!customer) {
|
|
362
|
+
return redirect(c, "/login");
|
|
363
|
+
}
|
|
364
|
+
const directorSkill = (() => {
|
|
365
|
+
try {
|
|
366
|
+
return readFileSync(path.resolve("SKILL.user.md"), "utf8");
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
const developerSkill = customer.isDeveloper
|
|
373
|
+
? (() => {
|
|
374
|
+
try {
|
|
375
|
+
return readFileSync(path.resolve("SKILL.developer.md"), "utf8");
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
})()
|
|
381
|
+
: null;
|
|
382
|
+
return c.html(renderSettingsPage({
|
|
383
|
+
notice: c.req.query("notice") ?? null,
|
|
384
|
+
error: c.req.query("error") ?? null,
|
|
385
|
+
email: customer.email,
|
|
386
|
+
isPaidPlan: customer.isPaidPlan,
|
|
387
|
+
isDeveloper: customer.isDeveloper,
|
|
388
|
+
about: customer.about,
|
|
389
|
+
groupchatUrl: customer.groupchatUrl,
|
|
390
|
+
flockposterApiKey: customer.flockposterApiKey,
|
|
391
|
+
vidfarmApiKey: getVisibleApiKey(customer.id),
|
|
392
|
+
directorSkill,
|
|
393
|
+
developerSkill,
|
|
394
|
+
providerKeys: database.listProviderKeysWithSecrets(customer.id).map((entry) => ({
|
|
395
|
+
id: String(entry.id),
|
|
396
|
+
provider: String(entry.provider),
|
|
397
|
+
label: entry.label ? String(entry.label) : null,
|
|
398
|
+
secret: readStoredSecret(String(entry.secret)),
|
|
399
|
+
status: String(entry.status),
|
|
400
|
+
weight: Number(entry.weight),
|
|
401
|
+
created_at: String(entry.created_at),
|
|
402
|
+
last_used_at: entry.last_used_at ? String(entry.last_used_at) : null
|
|
403
|
+
})),
|
|
404
|
+
attachments: database.listUserAttachments(customer.id)
|
|
405
|
+
}));
|
|
406
|
+
});
|
|
407
|
+
app.post("/settings/profile", async (c) => {
|
|
408
|
+
const customer = requireBrowserCustomer(c);
|
|
409
|
+
if (!customer) {
|
|
410
|
+
return redirect(c, "/login");
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const body = settingsProfileFormSchema.parse(await parseFormBody(c));
|
|
414
|
+
database.updateCustomerProfile({
|
|
415
|
+
customerId: customer.id,
|
|
416
|
+
about: body.about?.trim() || null,
|
|
417
|
+
groupchatUrl: body.groupchat_url?.trim() || null,
|
|
418
|
+
flockposterApiKey: body.flockposter_api_key?.trim() || null
|
|
419
|
+
});
|
|
420
|
+
return redirectToSettings(c, { notice: "Profile updated." });
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
return redirectToSettings(c, {
|
|
424
|
+
error: error instanceof Error ? error.message : "Unable to update profile."
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
app.post("/settings/provider-keys", async (c) => {
|
|
429
|
+
const customer = requireBrowserCustomer(c);
|
|
430
|
+
if (!customer) {
|
|
431
|
+
return redirect(c, "/login");
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const body = settingsProviderKeyFormSchema.parse(await parseFormBody(c));
|
|
435
|
+
database.createProviderKey({
|
|
436
|
+
id: createId("pkey"),
|
|
437
|
+
customerId: customer.id,
|
|
438
|
+
provider: body.provider,
|
|
439
|
+
label: body.label?.trim() || null,
|
|
440
|
+
encryptedSecret: body.secret,
|
|
441
|
+
weight: body.weight
|
|
442
|
+
});
|
|
443
|
+
return redirectToSettings(c, { notice: "Provider key added." });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
return redirectToSettings(c, {
|
|
447
|
+
error: error instanceof Error ? error.message : "Unable to add provider key."
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
app.post("/settings/provider-keys/:keyId/delete", (c) => {
|
|
452
|
+
const customer = requireBrowserCustomer(c);
|
|
453
|
+
if (!customer) {
|
|
454
|
+
return redirect(c, "/login");
|
|
455
|
+
}
|
|
456
|
+
database.deleteProviderKey(customer.id, c.req.param("keyId"));
|
|
457
|
+
return redirectToSettings(c, { notice: "Provider key removed." });
|
|
458
|
+
});
|
|
459
|
+
app.post("/settings/attachments", async (c) => {
|
|
460
|
+
const customer = requireBrowserCustomer(c);
|
|
461
|
+
if (!customer) {
|
|
462
|
+
return redirect(c, "/login");
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const formData = await c.req.formData();
|
|
466
|
+
const files = formData.getAll("files").filter(isFormFile);
|
|
467
|
+
if (!files.length) {
|
|
468
|
+
return redirectToSettings(c, { error: "Select at least one file to upload." });
|
|
469
|
+
}
|
|
470
|
+
for (const file of files) {
|
|
471
|
+
const fileName = sanitizeFileName(file.name || "upload.bin");
|
|
472
|
+
const contentType = file.type || "application/octet-stream";
|
|
473
|
+
if (!isAllowedAttachment(fileName, contentType)) {
|
|
474
|
+
return redirectToSettings(c, { error: `Unsupported attachment type: ${fileName}` });
|
|
475
|
+
}
|
|
476
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
477
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
478
|
+
const attachmentId = createId("att");
|
|
479
|
+
const storageKey = `user/${customer.id}/attachments/${attachmentId}/${fileName}`;
|
|
480
|
+
const stored = await storage.putBuffer(storageKey, buffer, contentType, { publicRead: true });
|
|
481
|
+
database.createUserAttachment({
|
|
482
|
+
id: attachmentId,
|
|
483
|
+
customerId: customer.id,
|
|
484
|
+
fileName,
|
|
485
|
+
contentType,
|
|
486
|
+
sizeBytes: buffer.byteLength,
|
|
487
|
+
storageKey: stored.key,
|
|
488
|
+
publicUrl: stored.url
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return redirectToSettings(c, { notice: "Attachment upload complete." });
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
return redirectToSettings(c, {
|
|
495
|
+
error: error instanceof Error ? error.message : "Unable to upload attachments."
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
app.post("/settings/attachments/:attachmentId/delete", async (c) => {
|
|
500
|
+
const customer = requireBrowserCustomer(c);
|
|
501
|
+
if (!customer) {
|
|
502
|
+
return redirect(c, "/login");
|
|
503
|
+
}
|
|
504
|
+
const attachment = database.getUserAttachment(customer.id, c.req.param("attachmentId"));
|
|
505
|
+
if (!attachment) {
|
|
506
|
+
return redirectToSettings(c, { error: "Attachment not found." });
|
|
507
|
+
}
|
|
508
|
+
await storage.deleteObject(attachment.storageKey).catch(() => undefined);
|
|
509
|
+
database.deleteUserAttachment(customer.id, attachment.id);
|
|
510
|
+
return redirectToSettings(c, { notice: "Attachment removed." });
|
|
511
|
+
});
|
|
512
|
+
app.post("/logout", (c) => {
|
|
513
|
+
clearBrowserSession(c);
|
|
514
|
+
return redirect(c, "/");
|
|
515
|
+
});
|
|
516
|
+
app.post(`${API_PREFIX}/admin/auth/whitelist`, async (c) => {
|
|
517
|
+
try {
|
|
518
|
+
requireSuperagency(c);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
522
|
+
}
|
|
523
|
+
const parsed = adminWhitelistSchema.parse(await c.req.json());
|
|
524
|
+
const customers = parsed.emails.map((email) => {
|
|
525
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
526
|
+
const existing = database.getCustomerByEmail(normalizedEmail);
|
|
527
|
+
return database.upsertCustomer({
|
|
528
|
+
id: existing?.id ?? createId("cus"),
|
|
529
|
+
email: normalizedEmail,
|
|
530
|
+
name: existing?.name ?? null,
|
|
531
|
+
defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
|
|
532
|
+
isDeveloper: existing?.isDeveloper
|
|
533
|
+
|| config.adminEmails.includes(normalizedEmail)
|
|
534
|
+
|| config.developerEmails.includes(normalizedEmail),
|
|
535
|
+
isPaidPlan: true
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
return c.json({ customers });
|
|
539
|
+
});
|
|
540
|
+
app.post(`${API_PREFIX}/admin/auth/users`, async (c) => {
|
|
541
|
+
try {
|
|
542
|
+
requireSuperagency(c);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
546
|
+
}
|
|
547
|
+
const parsed = adminCreateUserSchema.parse(await c.req.json());
|
|
548
|
+
const customer = auth.createPasswordUser({
|
|
549
|
+
email: parsed.email,
|
|
550
|
+
password: parsed.password,
|
|
551
|
+
name: parsed.name ?? null
|
|
552
|
+
});
|
|
553
|
+
return c.json({ customer }, 201);
|
|
554
|
+
});
|
|
555
|
+
app.post(`${USER_PREFIX}/request-otp`, async (c) => {
|
|
57
556
|
const body = otpRequestSchema.parse(await c.req.json());
|
|
58
557
|
await auth.requestOtp(body.email);
|
|
59
558
|
return c.json({ ok: true });
|
|
60
559
|
});
|
|
61
|
-
app.post(
|
|
560
|
+
app.post(`${USER_PREFIX}/verify-otp`, async (c) => {
|
|
62
561
|
const body = otpVerifySchema.parse(await c.req.json());
|
|
63
562
|
return c.json(auth.verifyOtp(body.email, body.code, body.name));
|
|
64
563
|
});
|
|
@@ -75,13 +574,185 @@ const requireAuth = async (c, next) => {
|
|
|
75
574
|
return c.json({ error: error instanceof Error ? error.message : "Unauthorized" }, 401);
|
|
76
575
|
}
|
|
77
576
|
};
|
|
78
|
-
app.use(
|
|
79
|
-
app.use(
|
|
80
|
-
app.
|
|
81
|
-
|
|
577
|
+
app.use(`${USER_PREFIX}/me`, requireAuth);
|
|
578
|
+
app.use(`${USER_PREFIX}/me/*`, requireAuth);
|
|
579
|
+
app.use(`${TEMPLATES_PREFIX}/*`, requireAuth);
|
|
580
|
+
function requireAdmin(c) {
|
|
581
|
+
const customer = requireCustomer(c);
|
|
582
|
+
if (!config.adminEmails.includes(customer.email.toLowerCase())) {
|
|
583
|
+
throw new Error("Admin access required.");
|
|
584
|
+
}
|
|
585
|
+
return customer;
|
|
586
|
+
}
|
|
587
|
+
function requireDeveloper(c) {
|
|
588
|
+
const customer = requireCustomer(c);
|
|
589
|
+
if (!customer.isDeveloper && !config.adminEmails.includes(customer.email.toLowerCase())) {
|
|
590
|
+
throw new Error("Developer access required.");
|
|
591
|
+
}
|
|
592
|
+
return customer;
|
|
593
|
+
}
|
|
594
|
+
function requireSuperagency(c) {
|
|
595
|
+
const provided = c.req.header("x-superagency-key");
|
|
596
|
+
if (!config.SUPERAGENCY_KEY || provided !== config.SUPERAGENCY_KEY) {
|
|
597
|
+
throw new Error("Superagency access required.");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function parseFormBody(c) {
|
|
601
|
+
const body = await c.req.parseBody();
|
|
602
|
+
const normalized = {};
|
|
603
|
+
for (const [key, value] of Object.entries(body)) {
|
|
604
|
+
normalized[key] = typeof value === "string" ? value : "";
|
|
605
|
+
}
|
|
606
|
+
return normalized;
|
|
607
|
+
}
|
|
608
|
+
function buildAbsoluteUrl(c, pathname) {
|
|
609
|
+
return new URL(pathname, config.PUBLIC_BASE_URL || c.req.url).toString();
|
|
610
|
+
}
|
|
611
|
+
function resolveTemplateAboutStorageKey(templateId, entry) {
|
|
612
|
+
const normalizedEntry = entry.replace(/^\/+/, "");
|
|
613
|
+
if (!/^https?:\/\//i.test(entry)) {
|
|
614
|
+
const templateAboutPrefix = `templates/${templateId}/about/`;
|
|
615
|
+
if (normalizedEntry.startsWith(templateAboutPrefix)) {
|
|
616
|
+
return normalizedEntry;
|
|
617
|
+
}
|
|
618
|
+
return normalizedEntry.startsWith("about/")
|
|
619
|
+
? `templates/${templateId}/${normalizedEntry}`
|
|
620
|
+
: `templates/${templateId}/about/${normalizedEntry}`;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const url = new URL(entry);
|
|
624
|
+
const pathname = url.pathname.replace(/^\/+/, "");
|
|
625
|
+
if (pathname.startsWith(`templates/${templateId}/about/`)) {
|
|
626
|
+
return pathname;
|
|
627
|
+
}
|
|
628
|
+
if (config.AWS_S3_BUCKET && pathname.startsWith(`${config.AWS_S3_BUCKET}/`)) {
|
|
629
|
+
const withoutBucket = pathname.slice(config.AWS_S3_BUCKET.length + 1);
|
|
630
|
+
if (withoutBucket.startsWith(`templates/${templateId}/about/`)) {
|
|
631
|
+
return withoutBucket;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const embeddedMatch = pathname.match(/(templates\/[^/]+\/about\/.+)$/);
|
|
635
|
+
if (embeddedMatch) {
|
|
636
|
+
return embeddedMatch[1];
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
function resolveHomepagePreviewUrl(c, templateId, entry) {
|
|
645
|
+
const storageKey = resolveTemplateAboutStorageKey(templateId, entry);
|
|
646
|
+
if (!storageKey) {
|
|
647
|
+
return entry;
|
|
648
|
+
}
|
|
649
|
+
return buildAbsoluteUrl(c, `/template-media?key=${encodeURIComponent(storageKey)}`);
|
|
650
|
+
}
|
|
651
|
+
async function serveTemplateAboutAsset(c, templateId, assetPath) {
|
|
652
|
+
const template = templateRegistry.get(templateId);
|
|
653
|
+
if (!template) {
|
|
654
|
+
return c.json({ error: "Template not found" }, 404);
|
|
655
|
+
}
|
|
656
|
+
const normalizedAssetPath = assetPath.replace(/^\/+/, "");
|
|
657
|
+
if (!normalizedAssetPath) {
|
|
658
|
+
return c.json({ error: "Template about asset path is required" }, 400);
|
|
659
|
+
}
|
|
660
|
+
const key = `templates/${template.id}/about/${normalizedAssetPath}`;
|
|
661
|
+
const readUrl = await storage.getReadUrl(key);
|
|
662
|
+
if (config.STORAGE_DRIVER === "s3" && readUrl) {
|
|
663
|
+
return c.redirect(readUrl, 302);
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const object = storage.readLocalObject(key);
|
|
667
|
+
return new Response(object.body, {
|
|
668
|
+
headers: {
|
|
669
|
+
"content-type": object.contentType
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return c.json({ error: "Template about asset not found" }, 404);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function serveStorageKeyAsset(c, key) {
|
|
678
|
+
const normalizedKey = key.replace(/^\/+/, "");
|
|
679
|
+
if (!normalizedKey) {
|
|
680
|
+
return c.json({ error: "Storage key is required" }, 400);
|
|
681
|
+
}
|
|
682
|
+
const readUrl = await storage.getReadUrl(normalizedKey);
|
|
683
|
+
if (config.STORAGE_DRIVER === "s3" && readUrl) {
|
|
684
|
+
return c.redirect(readUrl, 302);
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const object = storage.readLocalObject(normalizedKey);
|
|
688
|
+
return new Response(object.body, {
|
|
689
|
+
headers: {
|
|
690
|
+
"content-type": object.contentType
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return c.json({ error: "Asset not found" }, 404);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function serializeJob(job) {
|
|
699
|
+
return {
|
|
700
|
+
job_id: job.id,
|
|
701
|
+
template_id: job.templateId,
|
|
702
|
+
operation_name: job.operationName,
|
|
703
|
+
workflow_name: job.workflowName,
|
|
704
|
+
tracer: job.tracer,
|
|
705
|
+
status: job.status,
|
|
706
|
+
progress: job.progress,
|
|
707
|
+
result: job.result,
|
|
708
|
+
error: job.error,
|
|
709
|
+
parent_job_id: job.parentJobId,
|
|
710
|
+
created_at: job.createdAt,
|
|
711
|
+
updated_at: job.updatedAt,
|
|
712
|
+
started_at: job.startedAt,
|
|
713
|
+
completed_at: job.completedAt
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function serializeTemplate(c, template) {
|
|
717
|
+
return {
|
|
82
718
|
id: template.id,
|
|
719
|
+
slug_id: template.slugId,
|
|
83
720
|
version: template.version,
|
|
84
|
-
|
|
721
|
+
title: template.about.title,
|
|
722
|
+
description: template.about.description,
|
|
723
|
+
skill_url: buildAbsoluteUrl(c, `${TEMPLATES_PREFIX}/${template.id}/skill`),
|
|
724
|
+
operations: Object.entries(template.operations).map(([name, operation]) => ({
|
|
725
|
+
name,
|
|
726
|
+
description: operation.description,
|
|
727
|
+
providerHint: operation.providerHint ?? null
|
|
728
|
+
}))
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function serializeTemplateAbout(c, template) {
|
|
732
|
+
return {
|
|
733
|
+
...serializeTemplate(c, template),
|
|
734
|
+
viral_dna: template.about.viral_dna,
|
|
735
|
+
visual_dna: template.about.visual_dna,
|
|
736
|
+
preview_media: template.about.preview_media.map((entry) => resolveTemplateAboutMediaUrl(c, template.id, entry)),
|
|
737
|
+
link_to_original: template.about.link_to_original
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function resolveTemplateAboutMediaUrl(c, templateId, entry) {
|
|
741
|
+
if (/^https?:\/\//i.test(entry)) {
|
|
742
|
+
return entry;
|
|
743
|
+
}
|
|
744
|
+
const normalizedEntry = entry.replace(/^\/+/, "");
|
|
745
|
+
const templateAboutPrefix = `templates/${templateId}/about/`;
|
|
746
|
+
const aboutPath = normalizedEntry.startsWith(templateAboutPrefix)
|
|
747
|
+
? `about/${normalizedEntry.slice(templateAboutPrefix.length)}`
|
|
748
|
+
: normalizedEntry.startsWith("about/")
|
|
749
|
+
? normalizedEntry
|
|
750
|
+
: `about/${normalizedEntry}`;
|
|
751
|
+
return buildAbsoluteUrl(c, `${TEMPLATES_PREFIX}/${templateId}/${aboutPath}`);
|
|
752
|
+
}
|
|
753
|
+
app.get(TEMPLATES_PREFIX, (c) => c.json({
|
|
754
|
+
templates: templateRegistry.list().map((template) => ({
|
|
755
|
+
...serializeTemplate(c, template),
|
|
85
756
|
operations: Object.entries(template.operations).map(([name, operation]) => ({
|
|
86
757
|
name,
|
|
87
758
|
description: operation.description,
|
|
@@ -90,23 +761,35 @@ app.get("/templates", (c) => c.json({
|
|
|
90
761
|
}))
|
|
91
762
|
}))
|
|
92
763
|
}));
|
|
93
|
-
app.get(
|
|
764
|
+
app.get(`${TEMPLATES_PREFIX}/:templateId`, (c) => {
|
|
94
765
|
const template = templateRegistry.get(c.req.param("templateId"));
|
|
95
766
|
if (!template) {
|
|
96
767
|
return c.json({ error: "Template not found" }, 404);
|
|
97
768
|
}
|
|
98
|
-
return c.json(
|
|
99
|
-
id: template.id,
|
|
100
|
-
version: template.version,
|
|
101
|
-
description: template.description,
|
|
102
|
-
operations: Object.entries(template.operations).map(([name, operation]) => ({
|
|
103
|
-
name,
|
|
104
|
-
description: operation.description,
|
|
105
|
-
providerHint: operation.providerHint ?? null
|
|
106
|
-
}))
|
|
107
|
-
});
|
|
769
|
+
return c.json(serializeTemplateAbout(c, template));
|
|
108
770
|
});
|
|
109
|
-
app.
|
|
771
|
+
app.get(`${TEMPLATES_PREFIX}/:templateId/skill`, (c) => {
|
|
772
|
+
const template = templateRegistry.get(c.req.param("templateId"));
|
|
773
|
+
if (!template) {
|
|
774
|
+
return c.json({ error: "Template not found" }, 404);
|
|
775
|
+
}
|
|
776
|
+
if (!template.skillPath) {
|
|
777
|
+
return c.json({ error: "Template skill file is not configured" }, 404);
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
return c.body(readFileSync(template.skillPath, "utf8"), 200, {
|
|
781
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
return c.json({ error: error instanceof Error ? error.message : "Skill file could not be read" }, 500);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
app.get(`${TEMPLATES_PREFIX}/:templateId/about/*`, async (c) => {
|
|
789
|
+
const assetPath = c.req.param("*") ?? "";
|
|
790
|
+
return serveTemplateAboutAsset(c, c.req.param("templateId"), assetPath);
|
|
791
|
+
});
|
|
792
|
+
app.post(`${TEMPLATES_PREFIX}/:templateId/config`, async (c) => {
|
|
110
793
|
const customer = requireCustomer(c);
|
|
111
794
|
const template = templateRegistry.get(c.req.param("templateId"));
|
|
112
795
|
if (!template) {
|
|
@@ -122,7 +805,7 @@ app.post("/templates/:templateId/config", async (c) => {
|
|
|
122
805
|
});
|
|
123
806
|
return c.json({ ok: true, template_id: template.id, config: parsed });
|
|
124
807
|
});
|
|
125
|
-
app.post(
|
|
808
|
+
app.post(`${TEMPLATES_PREFIX}/:templateId/operations/:operationName`, async (c) => {
|
|
126
809
|
const customer = requireCustomer(c);
|
|
127
810
|
const template = templateRegistry.get(c.req.param("templateId"));
|
|
128
811
|
if (!template) {
|
|
@@ -150,45 +833,98 @@ app.post("/templates/:templateId/operations/:operationName", async (c) => {
|
|
|
150
833
|
});
|
|
151
834
|
return c.json({ job_id: job.id, tracer: job.tracer, status: job.status }, 202);
|
|
152
835
|
});
|
|
153
|
-
app.get(
|
|
836
|
+
app.get(`${TEMPLATES_PREFIX}/:templateId/jobs`, (c) => {
|
|
154
837
|
const customer = requireCustomer(c);
|
|
838
|
+
const template = templateRegistry.get(c.req.param("templateId"));
|
|
839
|
+
if (!template) {
|
|
840
|
+
return c.json({ error: "Template not found" }, 404);
|
|
841
|
+
}
|
|
842
|
+
const query = listJobsQuerySchema.parse({
|
|
843
|
+
tracer: c.req.query("tracer"),
|
|
844
|
+
start_time: c.req.query("start_time"),
|
|
845
|
+
end_time: c.req.query("end_time"),
|
|
846
|
+
limit: c.req.query("limit")
|
|
847
|
+
});
|
|
848
|
+
const listedJobs = jobs.listJobs({
|
|
849
|
+
customerId: customer.id,
|
|
850
|
+
templateId: template.id,
|
|
851
|
+
tracer: query.tracer,
|
|
852
|
+
startTime: query.start_time,
|
|
853
|
+
endTime: query.end_time,
|
|
854
|
+
limit: query.limit
|
|
855
|
+
});
|
|
856
|
+
return c.json({ template_id: template.id, jobs: listedJobs.map(serializeJob) });
|
|
857
|
+
});
|
|
858
|
+
app.get(`${TEMPLATES_PREFIX}/:templateId/jobs/:jobId`, (c) => {
|
|
859
|
+
const customer = requireCustomer(c);
|
|
860
|
+
const template = templateRegistry.get(c.req.param("templateId"));
|
|
861
|
+
if (!template) {
|
|
862
|
+
return c.json({ error: "Template not found" }, 404);
|
|
863
|
+
}
|
|
155
864
|
const job = jobs.getJob(c.req.param("jobId"));
|
|
156
|
-
if (!job || job.customerId !== customer.id || job.templateId !==
|
|
865
|
+
if (!job || job.customerId !== customer.id || job.templateId !== template.id) {
|
|
157
866
|
return c.json({ error: "Job not found" }, 404);
|
|
158
867
|
}
|
|
159
|
-
return c.json(
|
|
160
|
-
job_id: job.id,
|
|
161
|
-
tracer: job.tracer,
|
|
162
|
-
status: job.status,
|
|
163
|
-
progress: job.progress,
|
|
164
|
-
result: job.result,
|
|
165
|
-
error: job.error,
|
|
166
|
-
created_at: job.createdAt,
|
|
167
|
-
updated_at: job.updatedAt
|
|
168
|
-
});
|
|
868
|
+
return c.json(serializeJob(job));
|
|
169
869
|
});
|
|
170
|
-
app.get(
|
|
870
|
+
app.get(`${TEMPLATES_PREFIX}/:templateId/jobs/:jobId/logs`, (c) => {
|
|
171
871
|
const customer = requireCustomer(c);
|
|
872
|
+
const template = templateRegistry.get(c.req.param("templateId"));
|
|
873
|
+
if (!template) {
|
|
874
|
+
return c.json({ error: "Template not found" }, 404);
|
|
875
|
+
}
|
|
172
876
|
const job = jobs.getJob(c.req.param("jobId"));
|
|
173
|
-
if (!job || job.customerId !== customer.id || job.templateId !==
|
|
877
|
+
if (!job || job.customerId !== customer.id || job.templateId !== template.id) {
|
|
174
878
|
return c.json({ error: "Job not found" }, 404);
|
|
175
879
|
}
|
|
176
|
-
const
|
|
880
|
+
const query = listJobsQuerySchema.parse({
|
|
881
|
+
start_time: c.req.query("start_time"),
|
|
882
|
+
end_time: c.req.query("end_time"),
|
|
883
|
+
limit: c.req.query("limit")
|
|
884
|
+
});
|
|
885
|
+
const logs = jobs.listLogs({
|
|
886
|
+
jobId: job.id,
|
|
887
|
+
startTime: query.start_time,
|
|
888
|
+
endTime: query.end_time,
|
|
889
|
+
limit: query.limit
|
|
890
|
+
});
|
|
177
891
|
return c.json({ job_id: job.id, tracer: job.tracer, logs });
|
|
178
892
|
});
|
|
179
|
-
app.post(
|
|
893
|
+
app.post(`${TEMPLATES_PREFIX}/:templateId/jobs/:jobId/cancel`, (c) => {
|
|
180
894
|
const customer = requireCustomer(c);
|
|
895
|
+
const template = templateRegistry.get(c.req.param("templateId"));
|
|
896
|
+
if (!template) {
|
|
897
|
+
return c.json({ error: "Template not found" }, 404);
|
|
898
|
+
}
|
|
181
899
|
const job = jobs.getJob(c.req.param("jobId"));
|
|
182
|
-
if (!job || job.customerId !== customer.id || job.templateId !==
|
|
900
|
+
if (!job || job.customerId !== customer.id || job.templateId !== template.id) {
|
|
183
901
|
return c.json({ error: "Job not found" }, 404);
|
|
184
902
|
}
|
|
185
903
|
jobs.cancelJob(job.id);
|
|
186
904
|
return c.json({ ok: true, job_id: job.id, status: "cancelled" });
|
|
187
905
|
});
|
|
188
|
-
app.get(
|
|
189
|
-
app.get(
|
|
190
|
-
|
|
191
|
-
|
|
906
|
+
app.get(`${USER_PREFIX}/me`, (c) => c.json({ customer: requireCustomer(c) }));
|
|
907
|
+
app.get(`${USER_PREFIX}/me/jobs`, (c) => {
|
|
908
|
+
const query = listJobsQuerySchema.parse({
|
|
909
|
+
template_id: c.req.query("template_id"),
|
|
910
|
+
tracer: c.req.query("tracer"),
|
|
911
|
+
start_time: c.req.query("start_time"),
|
|
912
|
+
end_time: c.req.query("end_time"),
|
|
913
|
+
limit: c.req.query("limit")
|
|
914
|
+
});
|
|
915
|
+
return c.json({
|
|
916
|
+
jobs: jobs.listJobs({
|
|
917
|
+
customerId: requireCustomer(c).id,
|
|
918
|
+
templateId: query.template_id,
|
|
919
|
+
tracer: query.tracer,
|
|
920
|
+
startTime: query.start_time,
|
|
921
|
+
endTime: query.end_time,
|
|
922
|
+
limit: query.limit
|
|
923
|
+
}).map(serializeJob)
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
app.get(`${USER_PREFIX}/me/provider-keys`, (c) => c.json({ provider_keys: database.listProviderKeys(requireCustomer(c).id) }));
|
|
927
|
+
app.post(`${USER_PREFIX}/me/provider-keys`, async (c) => {
|
|
192
928
|
const customer = requireCustomer(c);
|
|
193
929
|
const body = providerKeySchema.parse(await c.req.json());
|
|
194
930
|
database.createProviderKey({
|
|
@@ -196,15 +932,100 @@ app.post("/me/provider-keys", async (c) => {
|
|
|
196
932
|
customerId: customer.id,
|
|
197
933
|
provider: body.provider,
|
|
198
934
|
label: body.label ?? null,
|
|
199
|
-
encryptedSecret:
|
|
935
|
+
encryptedSecret: body.secret,
|
|
200
936
|
weight: body.weight
|
|
201
937
|
});
|
|
202
938
|
return c.json({ ok: true }, 201);
|
|
203
939
|
});
|
|
204
|
-
app.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
940
|
+
app.get(`${TEMPLATES_PREFIX}/sources`, (c) => {
|
|
941
|
+
try {
|
|
942
|
+
requireAdmin(c);
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
946
|
+
}
|
|
947
|
+
return c.json({ sources: templateSources.listSources() });
|
|
948
|
+
});
|
|
949
|
+
app.post(`${TEMPLATES_PREFIX}/sources`, async (c) => {
|
|
950
|
+
try {
|
|
951
|
+
requireDeveloper(c);
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const body = z.object({
|
|
958
|
+
template_id: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, "template_id must be a UUIDv4."),
|
|
959
|
+
slug_id: z.string().min(3).regex(/^[a-z0-9_]+$/i, "slug_id must contain only letters, numbers, and underscores."),
|
|
960
|
+
repo_url: z.string().url(),
|
|
961
|
+
branch: z.string().min(1).default("production"),
|
|
962
|
+
template_module_path: z.string().min(1),
|
|
963
|
+
skill_path: z.string().min(1).default("SKILL.md"),
|
|
964
|
+
install_command: z.string().min(1).default("npm install"),
|
|
965
|
+
build_command: z.string().min(1).default("npm run build")
|
|
966
|
+
}).parse(await c.req.json());
|
|
967
|
+
const source = templateSources.registerSource({
|
|
968
|
+
templateId: body.template_id,
|
|
969
|
+
slugId: body.slug_id,
|
|
970
|
+
repoUrl: body.repo_url,
|
|
971
|
+
branch: body.branch,
|
|
972
|
+
templateModulePath: body.template_module_path,
|
|
973
|
+
skillPath: body.skill_path,
|
|
974
|
+
installCommand: body.install_command,
|
|
975
|
+
buildCommand: body.build_command
|
|
976
|
+
});
|
|
977
|
+
return c.json({ source }, 201);
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
const message = error instanceof Error ? error.message : "Unable to register template source.";
|
|
981
|
+
const status = /already exists/i.test(message) ? 409 : 400;
|
|
982
|
+
return c.json({ error: message }, status);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
app.get(`${TEMPLATES_PREFIX}/releases`, (c) => {
|
|
986
|
+
try {
|
|
987
|
+
requireAdmin(c);
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
991
|
+
}
|
|
992
|
+
return c.json({ releases: templateSources.listReleases(c.req.query("template_id") || undefined) });
|
|
993
|
+
});
|
|
994
|
+
app.post(`${TEMPLATES_PREFIX}/sources/:sourceId/import`, async (c) => {
|
|
995
|
+
try {
|
|
996
|
+
requireAdmin(c);
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
1000
|
+
}
|
|
1001
|
+
const body = z.object({
|
|
1002
|
+
commit_sha: z.string().min(7).optional()
|
|
1003
|
+
}).parse(await c.req.json().catch(() => ({})));
|
|
1004
|
+
const release = await templateSources.importRelease({
|
|
1005
|
+
sourceId: c.req.param("sourceId"),
|
|
1006
|
+
commitSha: body.commit_sha ?? null
|
|
1007
|
+
});
|
|
1008
|
+
return c.json({ release }, 201);
|
|
1009
|
+
});
|
|
1010
|
+
app.post(`${TEMPLATES_PREFIX}/releases/:releaseId/activate`, async (c) => {
|
|
1011
|
+
try {
|
|
1012
|
+
requireAdmin(c);
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
return c.json({ error: error instanceof Error ? error.message : "Forbidden" }, 403);
|
|
1016
|
+
}
|
|
1017
|
+
const { release, template } = await templateSources.activateRelease({
|
|
1018
|
+
releaseId: c.req.param("releaseId")
|
|
1019
|
+
});
|
|
1020
|
+
templateRegistry.registerRuntimeTemplate(template);
|
|
1021
|
+
return c.json({
|
|
1022
|
+
release,
|
|
1023
|
+
template: {
|
|
1024
|
+
id: template.id,
|
|
1025
|
+
version: template.version,
|
|
1026
|
+
description: template.about.description
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
208
1029
|
});
|
|
209
1030
|
app.get("/storage/:key", (c) => {
|
|
210
1031
|
const key = decodeURIComponent(c.req.param("key"));
|