@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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { startRuntime } from "./runtime.js";
2
+ const runtime = startRuntime();
3
+ for (const signal of ["SIGINT", "SIGTERM"]) {
4
+ process.on(signal, () => {
5
+ runtime.shutdown();
6
+ process.exit(0);
7
+ });
8
+ }
@@ -0,0 +1,30 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
2
+ function deriveKey(secret) {
3
+ return scryptSync(secret, "vidfarm", 32);
4
+ }
5
+ export function hashSecret(value) {
6
+ return createHash("sha256").update(value).digest("hex");
7
+ }
8
+ export function safeEqualHash(raw, hashed) {
9
+ const left = Buffer.from(hashSecret(raw), "hex");
10
+ const right = Buffer.from(hashed, "hex");
11
+ return left.length === right.length && timingSafeEqual(left, right);
12
+ }
13
+ export function encryptString(value, secret) {
14
+ const iv = randomBytes(12);
15
+ const key = deriveKey(secret);
16
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
17
+ const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
18
+ const tag = cipher.getAuthTag();
19
+ return Buffer.concat([iv, tag, encrypted]).toString("base64");
20
+ }
21
+ export function decryptString(value, secret) {
22
+ const raw = Buffer.from(value, "base64");
23
+ const iv = raw.subarray(0, 12);
24
+ const tag = raw.subarray(12, 28);
25
+ const encrypted = raw.subarray(28);
26
+ const key = deriveKey(secret);
27
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
28
+ decipher.setAuthTag(tag);
29
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
30
+ }
@@ -0,0 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function createId(prefix) {
3
+ return `${prefix}_${randomUUID().replace(/-/g, "")}`;
4
+ }
@@ -0,0 +1,18 @@
1
+ import sharp from "sharp";
2
+ export async function normalizeToPortraitFrame(input, target = { width: 1080, height: 1920 }) {
3
+ const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
4
+ const output = await sharp(buffer, { density: 144 })
5
+ .rotate()
6
+ .resize(target.width, target.height, {
7
+ fit: "cover",
8
+ position: sharp.strategy.attention
9
+ })
10
+ .png()
11
+ .toBuffer();
12
+ return {
13
+ bytes: output,
14
+ contentType: "image/png",
15
+ width: target.width,
16
+ height: target.height
17
+ };
18
+ }
@@ -0,0 +1,14 @@
1
+ export function parseJson(value, fallback) {
2
+ if (!value) {
3
+ return fallback;
4
+ }
5
+ try {
6
+ return JSON.parse(value);
7
+ }
8
+ catch {
9
+ return fallback;
10
+ }
11
+ }
12
+ export function stringifyJson(value) {
13
+ return JSON.stringify(value ?? null);
14
+ }
@@ -0,0 +1,6 @@
1
+ export function nowIso() {
2
+ return new Date().toISOString();
3
+ }
4
+ export function addSeconds(date, seconds) {
5
+ return new Date(date.getTime() + seconds * 1000).toISOString();
6
+ }
@@ -0,0 +1,10 @@
1
+ import { demoTemplate } from "../templates/template_0000/demo-template.js";
2
+ const templates = [demoTemplate];
3
+ export const templateRegistry = {
4
+ list() {
5
+ return templates;
6
+ },
7
+ get(templateId) {
8
+ return templates.find((template) => template.id === templateId) ?? null;
9
+ }
10
+ };
@@ -0,0 +1,19 @@
1
+ import { serve } from "@hono/node-server";
2
+ import app from "./app.js";
3
+ import { config } from "./config.js";
4
+ import { Worker } from "./worker.js";
5
+ export function startRuntime() {
6
+ const worker = new Worker();
7
+ worker.start();
8
+ const server = serve({
9
+ fetch: app.fetch,
10
+ port: config.PORT
11
+ }, (info) => {
12
+ console.log(`[vidfarm] listening on http://localhost:${info.port}`);
13
+ });
14
+ const shutdown = () => {
15
+ worker.stop();
16
+ server.close();
17
+ };
18
+ return { server, worker, shutdown };
19
+ }
@@ -0,0 +1,80 @@
1
+ import { randomInt } from "node:crypto";
2
+ import { config } from "../config.js";
3
+ import { database } from "../db.js";
4
+ import { hashSecret, safeEqualHash } from "../lib/crypto.js";
5
+ import { createId } from "../lib/ids.js";
6
+ import { addSeconds, nowIso } from "../lib/time.js";
7
+ export class AuthService {
8
+ async requestOtp(email) {
9
+ const code = `${randomInt(100000, 999999)}`;
10
+ database.insertOtpChallenge({
11
+ id: createId("otp"),
12
+ email,
13
+ codeHash: hashSecret(code),
14
+ expiresAt: addSeconds(new Date(), 600)
15
+ });
16
+ if (config.RESEND_API_KEY) {
17
+ await fetch("https://api.resend.com/emails", {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Authorization: `Bearer ${config.RESEND_API_KEY}`
22
+ },
23
+ body: JSON.stringify({
24
+ from: config.RESEND_FROM_EMAIL,
25
+ to: email,
26
+ subject: "Your Vidfarm login code",
27
+ text: `Your login code is ${code}. It expires in 10 minutes.`
28
+ })
29
+ });
30
+ }
31
+ else {
32
+ console.log(`[vidfarm] OTP for ${email}: ${code}`);
33
+ }
34
+ }
35
+ verifyOtp(email, code, name) {
36
+ const challenge = database.findLatestOtpChallenge(email);
37
+ if (!challenge) {
38
+ throw new Error("No OTP challenge found for that email.");
39
+ }
40
+ if (challenge.consumed_at) {
41
+ throw new Error("OTP challenge already used.");
42
+ }
43
+ if (new Date(String(challenge.expires_at)).getTime() < Date.now()) {
44
+ throw new Error("OTP challenge expired.");
45
+ }
46
+ if (!safeEqualHash(code, String(challenge.code_hash))) {
47
+ throw new Error("Invalid OTP code.");
48
+ }
49
+ database.consumeOtpChallenge(String(challenge.id));
50
+ const customer = database.getCustomerByEmail(email) ?? database.upsertCustomer({
51
+ id: createId("cus"),
52
+ email,
53
+ name: name ?? null
54
+ });
55
+ const rawApiKey = `vf_${createId("key")}`;
56
+ database.insertApiKey({
57
+ id: createId("api"),
58
+ customerId: customer.id,
59
+ keyHash: hashSecret(rawApiKey + config.API_KEY_SALT),
60
+ label: `Issued ${nowIso()}`
61
+ });
62
+ return { customer, apiKey: rawApiKey };
63
+ }
64
+ authenticate(userId, apiKey) {
65
+ if (!userId || !apiKey) {
66
+ throw new Error("Missing vidfarm-user-id or vidfarm-api-key header.");
67
+ }
68
+ const row = database.findApiKeyByHash(hashSecret(apiKey + config.API_KEY_SALT));
69
+ if (!row || String(row.customer_id) !== userId) {
70
+ throw new Error("Invalid API credentials.");
71
+ }
72
+ database.touchApiKey(String(row.id));
73
+ return {
74
+ id: String(row.customer_id),
75
+ email: String(row.email),
76
+ name: row.name ? String(row.name) : null,
77
+ defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,16 @@
1
+ import { database } from "../db.js";
2
+ import { createId } from "../lib/ids.js";
3
+ export class BillingService {
4
+ async record(input) {
5
+ database.insertBillingEvent({
6
+ id: createId("bill"),
7
+ customerId: input.customerId,
8
+ jobId: input.jobId,
9
+ templateId: input.templateId,
10
+ type: input.type,
11
+ costUsd: input.costUsd,
12
+ chargeUsd: input.chargeUsd,
13
+ metadata: input.metadata
14
+ });
15
+ }
16
+ }
@@ -0,0 +1,97 @@
1
+ import { config } from "../config.js";
2
+ import { database } from "../db.js";
3
+ import { createId } from "../lib/ids.js";
4
+ import { addSeconds, nowIso } from "../lib/time.js";
5
+ export class JobsService {
6
+ createRootJob(input) {
7
+ return database.createJob({
8
+ id: createId("job"),
9
+ templateId: input.templateId,
10
+ operationName: input.operationName,
11
+ workflowName: input.workflowName,
12
+ tracer: input.tracer,
13
+ status: "queued",
14
+ customerId: input.customer.id,
15
+ payload: {
16
+ ...input.payload,
17
+ _providerHint: input.providerHint ?? null
18
+ },
19
+ progress: 0,
20
+ webhookUrl: input.webhookUrl ?? input.customer.defaultWebhookUrl ?? null,
21
+ parentJobId: null,
22
+ priority: 100,
23
+ attemptCount: 0,
24
+ maxAttempts: 6,
25
+ runAfter: nowIso()
26
+ });
27
+ }
28
+ async enqueueChildJob(input) {
29
+ return database.createJob({
30
+ id: createId("job"),
31
+ templateId: input.templateId,
32
+ operationName: input.operationName,
33
+ workflowName: input.workflowName,
34
+ tracer: input.tracer,
35
+ status: "queued",
36
+ customerId: input.customerId,
37
+ payload: {
38
+ ...input.payload,
39
+ _providerHint: input.providerHint ?? null
40
+ },
41
+ progress: 0,
42
+ webhookUrl: null,
43
+ parentJobId: input.parentJobId,
44
+ priority: 110,
45
+ attemptCount: 0,
46
+ maxAttempts: 6,
47
+ runAfter: nowIso()
48
+ });
49
+ }
50
+ getJob(jobId) {
51
+ return database.getJob(jobId);
52
+ }
53
+ listJobs(customerId, templateId) {
54
+ return database.listJobsForCustomer(customerId, templateId);
55
+ }
56
+ listLogs(jobId, since, limit) {
57
+ return database.getJobEvents(jobId, since, limit);
58
+ }
59
+ cancelJob(jobId) {
60
+ database.updateJobStatus({
61
+ id: jobId,
62
+ status: "cancelled",
63
+ completedAt: nowIso(),
64
+ error: { message: "Cancelled by user request." }
65
+ });
66
+ }
67
+ markRunning(jobId) {
68
+ database.markJobRunning(jobId);
69
+ }
70
+ succeedJob(jobId, result, progress = 1) {
71
+ database.updateJobStatus({
72
+ id: jobId,
73
+ status: "succeeded",
74
+ progress,
75
+ result,
76
+ completedAt: nowIso()
77
+ });
78
+ }
79
+ failJob(jobId, error) {
80
+ database.updateJobStatus({
81
+ id: jobId,
82
+ status: "failed",
83
+ error,
84
+ completedAt: nowIso()
85
+ });
86
+ }
87
+ requeueJob(job, delaySeconds = config.DEFAULT_JOB_DELAY_SECONDS, error) {
88
+ const nextStatus = job.attemptCount + 1 >= job.maxAttempts ? "failed" : "queued";
89
+ database.updateJobStatus({
90
+ id: job.id,
91
+ status: nextStatus,
92
+ error,
93
+ runAfter: addSeconds(new Date(), delaySeconds),
94
+ completedAt: nextStatus === "failed" ? nowIso() : null
95
+ });
96
+ }
97
+ }