@mevdragon/vidfarm-devcli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Vidfarm
2
+
3
+ Vidfarm is a single-container Node.js control plane for async video template pipelines. It follows the architecture in [video-pipeline-architecture-draft.md](/Users/localghost/Projects/OfficeX/OfficeX/ZoomGTM/vidfarm/video-pipeline-architecture-draft.md) and [PLATFORM_SPEC.md](/Users/localghost/Projects/OfficeX/OfficeX/ZoomGTM/vidfarm/PLATFORM_SPEC.md): Hono API, SQLite-backed jobs and provider-key leasing, S3-compatible storage, and an in-process worker suitable for Docker on EC2.
4
+
5
+ ## What Is Included
6
+
7
+ - Async template API under `/templates/:templateId/*`
8
+ - Email OTP login with API key issuance
9
+ - Customer-scoped provider key vault with encryption at rest
10
+ - SQLite-backed job queue and provider-key lease coordination
11
+ - Structured job logs, artifacts, billing events, and webhook delivery queue
12
+ - Local storage mode plus S3-compatible mode for AWS
13
+ - Remotion adapter seam and a sample `demo-template` slideshow pipeline
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ npm install
19
+ cp .env.example .env
20
+ npm run dev
21
+ ```
22
+
23
+ The service starts on `http://localhost:3000` by default and creates SQLite plus local artifact storage under `./data`.
24
+
25
+ ## Template Developer CLI
26
+
27
+ For local template development, use the bundled CLI instead of manually wiring auth, provider keys, SQLite, and Remotion:
28
+
29
+ ```bash
30
+ npm install
31
+ npx @officexapp/vidfarm-devcli dev --port 3310 --reset
32
+ ```
33
+
34
+ What it does:
35
+
36
+ - starts the same REST API and in-process worker as production
37
+ - provisions a local SQLite database automatically
38
+ - uses local filesystem storage as an S3 stand-in
39
+ - seeds a dev customer and API key
40
+ - seeds provider keys from local `.env` variables like `OPENAI_API_KEY`, `GEMINI_API_KEY`, and `OPENROUTER_API_KEY`
41
+ - falls back to mock provider keys plus mocked AI responses when no env keys are present
42
+ - renders Remotion compositions locally instead of using Lambda
43
+
44
+ Useful commands:
45
+
46
+ ```bash
47
+ npx @officexapp/vidfarm-devcli dev --port 3310 --reset
48
+ npx @officexapp/vidfarm-devcli session
49
+ ```
50
+
51
+ After install, the package also exposes `vidfarm` and `vidfarm-devcli` as local binaries.
52
+
53
+ `vidfarm session` prints reusable auth headers and a sample `curl` request for the local REST API.
54
+
55
+ ## Environment
56
+
57
+ Primary variables:
58
+
59
+ - `PORT`
60
+ - `VIDFARM_DB_PATH`
61
+ - `VIDFARM_DATA_DIR`
62
+ - `ENCRYPTION_SECRET`
63
+ - `API_KEY_SALT`
64
+ - `STORAGE_DRIVER=local|s3`
65
+ - `AWS_REGION`
66
+ - `AWS_S3_BUCKET`
67
+ - `PUBLIC_BASE_URL`
68
+ - `RESEND_API_KEY`
69
+ - `WEBHOOK_SECRET`
70
+ - `MOCK_PROVIDER_RESPONSES=true|false`
71
+
72
+ Remotion-specific variables:
73
+
74
+ - `REMOTION_REGION`
75
+ - `REMOTION_FUNCTION_NAME`
76
+ - `REMOTION_BUCKET_NAME`
77
+ - `REMOTION_SITE_NAME`
78
+ - `REMOTION_SERVE_URL`
79
+ - `REMOTION_COMPOSITION_ID`
80
+ - `REMOTION_AWS_ACCESS_KEY_ID`
81
+ - `REMOTION_AWS_SECRET_ACCESS_KEY`
82
+
83
+ When `MOCK_PROVIDER_RESPONSES=true`, AI provider calls use the real key-leasing flow but return mocked content. This makes local end-to-end testing possible without burning customer API calls.
84
+
85
+ When using `npx @officexapp/vidfarm-devcli dev`, the CLI automatically chooses local-friendly defaults:
86
+
87
+ - `STORAGE_DRIVER=local`
88
+ - `REMOTION_MODE=local`
89
+ - a local SQLite path under `.vidfarm/local`
90
+ - `MOCK_PROVIDER_RESPONSES=false` if real provider env keys are present, otherwise `true`
91
+
92
+ ## Remotion AWS
93
+
94
+ For shared Remotion AWS infra, use [AWS_REMOTION_HANDOFF.md](/Users/localghost/Projects/OfficeX/OfficeX/ZoomGTM/vidfarm/AWS_REMOTION_HANDOFF.md) as the operating contract.
95
+
96
+ The key points verified on `2026-05-16` are:
97
+
98
+ - region: `us-east-1`
99
+ - shared Lambda function: `remotion-render-4-0-355-mem2048mb-disk2048mb-180sec`
100
+ - shared bucket: `remotionlambda-useast1-ujg7c0h43q`
101
+ - Remotion package version to pin: `4.0.355`
102
+
103
+ This repo also has separate Remotion AWS runner credentials available in local production env config via:
104
+
105
+ - `REMOTION_AWS_ACCESS_KEY_ID`
106
+ - `REMOTION_AWS_SECRET_ACCESS_KEY`
107
+
108
+ Use those for Remotion site publish/render work rather than assuming the generic app AWS credentials are the right identity.
109
+
110
+ Recommended repo-level config for real Remotion work:
111
+
112
+ ```env
113
+ REMOTION_REGION=us-east-1
114
+ REMOTION_FUNCTION_NAME=remotion-render-4-0-355-mem2048mb-disk2048mb-180sec
115
+ REMOTION_BUCKET_NAME=remotionlambda-useast1-ujg7c0h43q
116
+ REMOTION_SITE_NAME=your-site-name
117
+ REMOTION_SERVE_URL=https://remotionlambda-useast1-ujg7c0h43q.s3.us-east-1.amazonaws.com/sites/your-site-name/index.html
118
+ REMOTION_COMPOSITION_ID=YourComposition
119
+ ```
120
+
121
+ If you wire real Lambda rendering into `src/services/remotion.ts`, preserve the current platform contract and make the service read these env vars rather than hard-coding one-off values.
122
+
123
+ ## API Flow
124
+
125
+ 1. `POST /auth/request-otp`
126
+ 2. `POST /auth/verify-otp`
127
+ 3. `POST /me/provider-keys`
128
+ 4. `POST /templates/demo-template/config`
129
+ 5. `POST /templates/demo-template/operations/generate`
130
+ 6. Poll `GET /templates/demo-template/jobs/:jobId`
131
+ 7. Stream timeline data from `GET /templates/demo-template/jobs/:jobId/logs`
132
+
133
+ ## Docker
134
+
135
+ ```bash
136
+ docker compose up --build
137
+ ```
138
+
139
+ For AWS/EC2, mount `/app/data` onto durable storage if SQLite is local, or switch artifacts to S3 with:
140
+
141
+ ```env
142
+ STORAGE_DRIVER=s3
143
+ AWS_REGION=us-east-1
144
+ AWS_S3_BUCKET=your-vidfarm-bucket
145
+ PUBLIC_BASE_URL=https://your-domain.example
146
+ ```
147
+
148
+ ## Notes
149
+
150
+ - `demo-template` is the canonical sample internal template. Add more templates by registering them in [src/registry.ts](/Users/localghost/Projects/OfficeX/OfficeX/ZoomGTM/vidfarm/src/registry.ts).
151
+ - Remotion is still stubbed behind [src/services/remotion.ts](/Users/localghost/Projects/OfficeX/OfficeX/ZoomGTM/vidfarm/src/services/remotion.ts). When upgrading it to real Lambda submission, follow [AWS_REMOTION_HANDOFF.md](/Users/localghost/Projects/OfficeX/OfficeX/ZoomGTM/vidfarm/AWS_REMOTION_HANDOFF.md) and use the Remotion-specific AWS credentials already available in local production env config.
@@ -0,0 +1,224 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Hono } from "hono";
4
+ import { z } from "zod";
5
+ import { config } from "./config.js";
6
+ import { database } from "./db.js";
7
+ import { renderDevApp } from "./dev-app.js";
8
+ import { encryptString } from "./lib/crypto.js";
9
+ import { createId } from "./lib/ids.js";
10
+ import { templateRegistry } from "./registry.js";
11
+ import { AuthService } from "./services/auth.js";
12
+ import { JobsService } from "./services/jobs.js";
13
+ import { StorageService } from "./services/storage.js";
14
+ const auth = new AuthService();
15
+ const jobs = new JobsService();
16
+ const storage = new StorageService();
17
+ const app = new Hono();
18
+ const otpRequestSchema = z.object({
19
+ email: z.string().email()
20
+ });
21
+ const otpVerifySchema = z.object({
22
+ email: z.string().email(),
23
+ code: z.string().min(6).max(6),
24
+ name: z.string().optional()
25
+ });
26
+ const providerKeySchema = z.object({
27
+ provider: z.enum(["openai", "gemini", "openrouter", "perplexity"]),
28
+ label: z.string().optional(),
29
+ secret: z.string().min(8),
30
+ weight: z.number().int().min(1).max(100).default(1)
31
+ });
32
+ const workspacePresignSchema = z.object({
33
+ path: z.string().min(1),
34
+ contentType: z.string().min(3).default("application/octet-stream")
35
+ });
36
+ app.use("*", async (c, next) => {
37
+ c.header("x-powered-by", "vidfarm");
38
+ await next();
39
+ });
40
+ app.get("/", (c) => c.json({ name: "vidfarm", version: "0.1.0", environment: config.NODE_ENV }));
41
+ app.get("/health", (c) => c.json({ ok: true, time: new Date().toISOString() }));
42
+ app.get("/dev", (c) => c.html(renderDevApp({
43
+ environment: config.NODE_ENV,
44
+ templates: templateRegistry.list().map((template) => ({
45
+ id: template.id,
46
+ version: template.version,
47
+ description: template.description,
48
+ operations: Object.entries(template.operations).map(([name, operation]) => ({
49
+ name,
50
+ description: operation.description,
51
+ workflow: operation.workflow,
52
+ providerHint: operation.providerHint ?? null
53
+ }))
54
+ }))
55
+ })));
56
+ app.post("/auth/request-otp", async (c) => {
57
+ const body = otpRequestSchema.parse(await c.req.json());
58
+ await auth.requestOtp(body.email);
59
+ return c.json({ ok: true });
60
+ });
61
+ app.post("/auth/verify-otp", async (c) => {
62
+ const body = otpVerifySchema.parse(await c.req.json());
63
+ return c.json(auth.verifyOtp(body.email, body.code, body.name));
64
+ });
65
+ function requireCustomer(c) {
66
+ return c.get("customer");
67
+ }
68
+ const requireAuth = async (c, next) => {
69
+ try {
70
+ const customer = auth.authenticate(c.req.header("vidfarm-user-id"), c.req.header("vidfarm-api-key"));
71
+ c.set("customer", customer);
72
+ await next();
73
+ }
74
+ catch (error) {
75
+ return c.json({ error: error instanceof Error ? error.message : "Unauthorized" }, 401);
76
+ }
77
+ };
78
+ app.use("/me/*", requireAuth);
79
+ app.use("/templates/*", requireAuth);
80
+ app.get("/templates", (c) => c.json({
81
+ templates: templateRegistry.list().map((template) => ({
82
+ id: template.id,
83
+ version: template.version,
84
+ description: template.description,
85
+ operations: Object.entries(template.operations).map(([name, operation]) => ({
86
+ name,
87
+ description: operation.description,
88
+ workflow: operation.workflow,
89
+ providerHint: operation.providerHint ?? null
90
+ }))
91
+ }))
92
+ }));
93
+ app.get("/templates/:templateId", (c) => {
94
+ const template = templateRegistry.get(c.req.param("templateId"));
95
+ if (!template) {
96
+ return c.json({ error: "Template not found" }, 404);
97
+ }
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
+ });
108
+ });
109
+ app.post("/templates/:templateId/config", async (c) => {
110
+ const customer = requireCustomer(c);
111
+ const template = templateRegistry.get(c.req.param("templateId"));
112
+ if (!template) {
113
+ return c.json({ error: "Template not found" }, 404);
114
+ }
115
+ const body = z.object({ config: z.record(z.string(), z.unknown()) }).parse(await c.req.json());
116
+ const parsed = template.configSchema.parse(body.config);
117
+ database.upsertTemplateConfig({
118
+ id: createId("cfg"),
119
+ customerId: customer.id,
120
+ templateId: template.id,
121
+ config: parsed
122
+ });
123
+ return c.json({ ok: true, template_id: template.id, config: parsed });
124
+ });
125
+ app.post("/templates/:templateId/operations/:operationName", async (c) => {
126
+ const customer = requireCustomer(c);
127
+ const template = templateRegistry.get(c.req.param("templateId"));
128
+ if (!template) {
129
+ return c.json({ error: "Template not found" }, 404);
130
+ }
131
+ const operation = template.operations[c.req.param("operationName")];
132
+ if (!operation) {
133
+ return c.json({ error: "Operation not found" }, 404);
134
+ }
135
+ const body = z.object({
136
+ tracer: z.string().min(3),
137
+ payload: z.record(z.string(), z.unknown()),
138
+ webhook_url: z.string().url().optional()
139
+ }).parse(await c.req.json());
140
+ const payload = operation.inputSchema.parse(body.payload);
141
+ const job = jobs.createRootJob({
142
+ templateId: template.id,
143
+ operationName: c.req.param("operationName"),
144
+ workflowName: operation.workflow,
145
+ tracer: body.tracer,
146
+ payload,
147
+ webhookUrl: body.webhook_url,
148
+ customer,
149
+ providerHint: operation.providerHint
150
+ });
151
+ return c.json({ job_id: job.id, tracer: job.tracer, status: job.status }, 202);
152
+ });
153
+ app.get("/templates/:templateId/jobs/:jobId", (c) => {
154
+ const customer = requireCustomer(c);
155
+ const job = jobs.getJob(c.req.param("jobId"));
156
+ if (!job || job.customerId !== customer.id || job.templateId !== c.req.param("templateId")) {
157
+ return c.json({ error: "Job not found" }, 404);
158
+ }
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
+ });
169
+ });
170
+ app.get("/templates/:templateId/jobs/:jobId/logs", (c) => {
171
+ const customer = requireCustomer(c);
172
+ const job = jobs.getJob(c.req.param("jobId"));
173
+ if (!job || job.customerId !== customer.id || job.templateId !== c.req.param("templateId")) {
174
+ return c.json({ error: "Job not found" }, 404);
175
+ }
176
+ const logs = jobs.listLogs(job.id, c.req.query("logs_from") || undefined, Number(c.req.query("limit") ?? 100));
177
+ return c.json({ job_id: job.id, tracer: job.tracer, logs });
178
+ });
179
+ app.post("/templates/:templateId/jobs/:jobId/cancel", (c) => {
180
+ const customer = requireCustomer(c);
181
+ const job = jobs.getJob(c.req.param("jobId"));
182
+ if (!job || job.customerId !== customer.id || job.templateId !== c.req.param("templateId")) {
183
+ return c.json({ error: "Job not found" }, 404);
184
+ }
185
+ jobs.cancelJob(job.id);
186
+ return c.json({ ok: true, job_id: job.id, status: "cancelled" });
187
+ });
188
+ app.get("/me", (c) => c.json({ customer: requireCustomer(c) }));
189
+ app.get("/me/jobs", (c) => c.json({ jobs: jobs.listJobs(requireCustomer(c).id, c.req.query("template_id") || undefined) }));
190
+ app.get("/me/provider-keys", (c) => c.json({ provider_keys: database.listProviderKeys(requireCustomer(c).id) }));
191
+ app.post("/me/provider-keys", async (c) => {
192
+ const customer = requireCustomer(c);
193
+ const body = providerKeySchema.parse(await c.req.json());
194
+ database.createProviderKey({
195
+ id: createId("pkey"),
196
+ customerId: customer.id,
197
+ provider: body.provider,
198
+ label: body.label ?? null,
199
+ encryptedSecret: encryptString(body.secret, config.ENCRYPTION_SECRET),
200
+ weight: body.weight
201
+ });
202
+ return c.json({ ok: true }, 201);
203
+ });
204
+ app.post("/me/workspace/presign", async (c) => {
205
+ const customer = requireCustomer(c);
206
+ const body = workspacePresignSchema.parse(await c.req.json());
207
+ return c.json(await storage.createPresignedWorkspaceUpload(customer.id, body.path, body.contentType));
208
+ });
209
+ app.get("/storage/:key", (c) => {
210
+ const key = decodeURIComponent(c.req.param("key"));
211
+ try {
212
+ const object = storage.readLocalObject(key);
213
+ return new Response(object.body, {
214
+ headers: {
215
+ "content-type": object.contentType
216
+ }
217
+ });
218
+ }
219
+ catch {
220
+ return c.json({ error: "Artifact not found" }, 404);
221
+ }
222
+ });
223
+ app.get("/docs/spec", (c) => c.text(readFileSync(path.resolve("PLATFORM_SPEC.md"), "utf8")));
224
+ export default app;
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ import dotenv from "dotenv";
6
+ void main().catch((error) => {
7
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
8
+ process.exit(1);
9
+ });
10
+ async function main() {
11
+ const command = process.argv[2] ?? "dev";
12
+ if (command === "session") {
13
+ await runSessionCommand(process.argv.slice(3));
14
+ return;
15
+ }
16
+ if (command === "dev") {
17
+ await runDevCommand(process.argv.slice(3));
18
+ return;
19
+ }
20
+ throw new Error(`Unknown command: ${command}`);
21
+ }
22
+ async function runDevCommand(argv) {
23
+ const parsed = parseArgs({
24
+ args: argv,
25
+ options: {
26
+ port: { type: "string", default: "3000" },
27
+ "data-dir": { type: "string", default: ".vidfarm/local" },
28
+ "env-file": { type: "string", default: ".env" },
29
+ email: { type: "string", default: "dev@vidfarm.local" },
30
+ name: { type: "string", default: "Vidfarm Local Dev" },
31
+ "api-key": { type: "string" },
32
+ reset: { type: "boolean", default: false },
33
+ "mock-ai": { type: "boolean" }
34
+ }
35
+ });
36
+ const port = Number(parsed.values.port);
37
+ const root = process.cwd();
38
+ const dataDir = path.resolve(root, parsed.values["data-dir"]);
39
+ const dbPath = path.join(dataDir, "vidfarm.sqlite");
40
+ const sessionPath = path.join(dataDir, "session.json");
41
+ dotenv.config({
42
+ path: path.resolve(root, parsed.values["env-file"]),
43
+ override: false
44
+ });
45
+ if (parsed.values.reset) {
46
+ rmSync(dataDir, { recursive: true, force: true });
47
+ }
48
+ mkdirSync(dataDir, { recursive: true });
49
+ const hasProviderEnvKey = Boolean(process.env.OPENAI_API_KEY ||
50
+ process.env.GEMINI_API_KEY ||
51
+ process.env.OPENROUTER_API_KEY ||
52
+ process.env.PERPLEXITY_API_KEY);
53
+ process.env.NODE_ENV = process.env.NODE_ENV || "development";
54
+ process.env.PORT = String(port);
55
+ process.env.VIDFARM_DATA_DIR = dataDir;
56
+ process.env.VIDFARM_DB_PATH = dbPath;
57
+ process.env.STORAGE_DRIVER = "local";
58
+ process.env.PUBLIC_BASE_URL = `http://127.0.0.1:${port}`;
59
+ process.env.REMOTION_MODE = "local";
60
+ process.env.MOCK_PROVIDER_RESPONSES =
61
+ parsed.values["mock-ai"] ? "true" : process.env.MOCK_PROVIDER_RESPONSES ?? (hasProviderEnvKey ? "false" : "true");
62
+ const [{ startRuntime }, { database }, { config }, { createId }, crypto] = await Promise.all([
63
+ import("./runtime.js"),
64
+ import("./db.js"),
65
+ import("./config.js"),
66
+ import("./lib/ids.js"),
67
+ import("./lib/crypto.js")
68
+ ]);
69
+ const customer = database.getCustomerByEmail(parsed.values.email) ?? database.upsertCustomer({
70
+ id: createId("cus"),
71
+ email: parsed.values.email,
72
+ name: parsed.values.name
73
+ });
74
+ const rawApiKey = parsed.values["api-key"] ?? `vf_local_${Math.random().toString(36).slice(2, 12)}`;
75
+ database.insertApiKey({
76
+ id: createId("api"),
77
+ customerId: customer.id,
78
+ keyHash: crypto.hashSecret(rawApiKey + config.API_KEY_SALT),
79
+ label: "Local CLI session"
80
+ });
81
+ seedEnvProviderKeys({
82
+ customerId: customer.id,
83
+ database,
84
+ createId,
85
+ encryptString: crypto.encryptString,
86
+ encryptionSecret: config.ENCRYPTION_SECRET,
87
+ allowMockPlaceholders: config.mockProviders
88
+ });
89
+ writeFileSync(sessionPath, JSON.stringify({
90
+ baseUrl: process.env.PUBLIC_BASE_URL,
91
+ customerId: customer.id,
92
+ apiKey: rawApiKey,
93
+ email: customer.email,
94
+ dbPath,
95
+ dataDir
96
+ }, null, 2));
97
+ printStartupBanner({
98
+ baseUrl: process.env.PUBLIC_BASE_URL,
99
+ customerId: customer.id,
100
+ apiKey: rawApiKey,
101
+ email: customer.email,
102
+ dataDir,
103
+ dbPath,
104
+ sessionPath,
105
+ mockProviders: config.mockProviders
106
+ });
107
+ const runtime = startRuntime();
108
+ for (const signal of ["SIGINT", "SIGTERM"]) {
109
+ process.on(signal, () => {
110
+ runtime.shutdown();
111
+ process.exit(0);
112
+ });
113
+ }
114
+ }
115
+ async function runSessionCommand(argv) {
116
+ const parsed = parseArgs({
117
+ args: argv,
118
+ options: {
119
+ "data-dir": { type: "string", default: ".vidfarm/local" }
120
+ }
121
+ });
122
+ const sessionPath = path.resolve(process.cwd(), parsed.values["data-dir"], "session.json");
123
+ const session = JSON.parse(readFileSync(sessionPath, "utf8"));
124
+ console.log(JSON.stringify({
125
+ base_url: session.baseUrl,
126
+ email: session.email,
127
+ headers: {
128
+ "vidfarm-user-id": session.customerId,
129
+ "vidfarm-api-key": session.apiKey,
130
+ "content-type": "application/json"
131
+ },
132
+ curl_example: [
133
+ `curl -X POST ${session.baseUrl}/templates/demo-template/operations/generate \\`,
134
+ ` -H 'vidfarm-user-id: ${session.customerId}' \\`,
135
+ ` -H 'vidfarm-api-key: ${session.apiKey}' \\`,
136
+ " -H 'content-type: application/json' \\",
137
+ " -d '{\"tracer\":\"local-test\",\"payload\":{\"slides\":[[\"a cinematic founder at a desk\",\"Exact text on slide one\"]],\"secondsPerSlide\":4}}'"
138
+ ].join("\n")
139
+ }, null, 2));
140
+ }
141
+ function seedEnvProviderKeys(input) {
142
+ const entries = [
143
+ { provider: "openai", secret: process.env.OPENAI_API_KEY ?? "", placeholder: "mock-openai-key" },
144
+ { provider: "gemini", secret: process.env.GEMINI_API_KEY ?? "", placeholder: "mock-gemini-key" },
145
+ { provider: "openrouter", secret: process.env.OPENROUTER_API_KEY ?? "", placeholder: "mock-openrouter-key" },
146
+ { provider: "perplexity", secret: process.env.PERPLEXITY_API_KEY ?? "", placeholder: "mock-perplexity-key" }
147
+ ];
148
+ const existing = input.database.listProviderKeys(input.customerId);
149
+ for (const entry of entries) {
150
+ const secret = entry.secret || (input.allowMockPlaceholders ? entry.placeholder : "");
151
+ if (!secret) {
152
+ continue;
153
+ }
154
+ const alreadySeeded = existing.some((row) => row.provider === entry.provider && row.label === "Local CLI env key");
155
+ if (alreadySeeded) {
156
+ continue;
157
+ }
158
+ input.database.createProviderKey({
159
+ id: input.createId("pkey"),
160
+ customerId: input.customerId,
161
+ provider: entry.provider,
162
+ label: "Local CLI env key",
163
+ encryptedSecret: input.encryptString(secret, input.encryptionSecret),
164
+ weight: 1
165
+ });
166
+ }
167
+ }
168
+ function printStartupBanner(input) {
169
+ console.log("");
170
+ console.log("Vidfarm local dev runtime");
171
+ console.log(`Base URL: ${input.baseUrl}`);
172
+ console.log(`Dev user: ${input.email}`);
173
+ console.log(`vidfarm-user-id: ${input.customerId}`);
174
+ console.log(`vidfarm-api-key: ${input.apiKey}`);
175
+ console.log(`Session file: ${input.sessionPath}`);
176
+ console.log(`Data dir: ${input.dataDir}`);
177
+ console.log(`SQLite: ${input.dbPath}`);
178
+ console.log(`AI mode: ${input.mockProviders ? "mock provider responses" : "real provider env keys"}`);
179
+ console.log("");
180
+ console.log("REST API:");
181
+ console.log(` ${input.baseUrl}/health`);
182
+ console.log(` ${input.baseUrl}/dev`);
183
+ console.log("");
184
+ console.log("Get reusable auth headers:");
185
+ console.log(" vidfarm session");
186
+ console.log("");
187
+ }
@@ -0,0 +1,52 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import dotenv from "dotenv";
4
+ import { z } from "zod";
5
+ dotenv.config();
6
+ const schema = z.object({
7
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
8
+ PORT: z.coerce.number().default(3000),
9
+ VIDFARM_DB_PATH: z.string().default("./data/vidfarm.sqlite"),
10
+ VIDFARM_DATA_DIR: z.string().default("./data"),
11
+ ENCRYPTION_SECRET: z.string().default("development-encryption-secret-change-me"),
12
+ API_KEY_SALT: z.string().default("development-api-key-salt"),
13
+ WORKER_POLL_MS: z.coerce.number().default(1500),
14
+ WORKER_BATCH_SIZE: z.coerce.number().default(4),
15
+ WEBHOOK_SECRET: z.string().default("development-webhook-secret"),
16
+ DEFAULT_JOB_DELAY_SECONDS: z.coerce.number().default(20),
17
+ STORAGE_DRIVER: z.enum(["local", "s3"]).default("local"),
18
+ AWS_REGION: z.string().default("us-east-1"),
19
+ AWS_S3_BUCKET: z.string().default(""),
20
+ AWS_S3_ENDPOINT: z.string().optional(),
21
+ AWS_ACCESS_KEY_ID: z.string().optional(),
22
+ AWS_SECRET_ACCESS_KEY: z.string().optional(),
23
+ PUBLIC_BASE_URL: z.string().optional(),
24
+ RESEND_API_KEY: z.string().optional(),
25
+ RESEND_FROM_EMAIL: z.string().default("noreply@example.com"),
26
+ OPENAI_API_KEY: z.string().optional(),
27
+ OPENROUTER_API_KEY: z.string().optional(),
28
+ GEMINI_API_KEY: z.string().optional(),
29
+ PERPLEXITY_API_KEY: z.string().optional(),
30
+ REMOTION_REGION: z.string().default("us-east-1"),
31
+ REMOTION_BUCKET_NAME: z.string().optional(),
32
+ REMOTION_SITE_NAME: z.string().optional(),
33
+ REMOTION_FUNCTION_NAME: z.string().optional(),
34
+ REMOTION_SERVE_URL: z.string().optional(),
35
+ REMOTION_COMPOSITION_ID: z.string().default("demo-template"),
36
+ REMOTION_AWS_ACCESS_KEY_ID: z.string().optional(),
37
+ REMOTION_AWS_SECRET_ACCESS_KEY: z.string().optional(),
38
+ REMOTION_MODE: z.enum(["auto", "mock", "local", "lambda"]).default("auto"),
39
+ MOCK_PROVIDER_RESPONSES: z.string().default("true")
40
+ });
41
+ const parsed = schema.parse(process.env);
42
+ const dataDir = path.resolve(parsed.VIDFARM_DATA_DIR);
43
+ mkdirSync(dataDir, { recursive: true });
44
+ const publicBaseUrl = parsed.PUBLIC_BASE_URL || `http://localhost:${parsed.PORT}`;
45
+ export const config = {
46
+ ...parsed,
47
+ VIDFARM_DB_PATH: path.resolve(parsed.VIDFARM_DB_PATH),
48
+ VIDFARM_DATA_DIR: dataDir,
49
+ PUBLIC_BASE_URL: publicBaseUrl,
50
+ isProduction: parsed.NODE_ENV === "production",
51
+ mockProviders: parsed.MOCK_PROVIDER_RESPONSES !== "false"
52
+ };