@mevdragon/vidfarm-devcli 0.2.7 → 0.2.9

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.
Files changed (98) hide show
  1. package/.env.example +9 -0
  2. package/GETTING_STARTED.developers.md +1 -30
  3. package/README.md +22 -5
  4. package/SKILL.developer.md +464 -12
  5. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +475 -0
  6. package/auto-create-templates/extractor-system-prompt.md +113 -0
  7. package/auto-create-templates/production-graph.schema.json +276 -0
  8. package/auto-create-templates/runbook.md +140 -0
  9. package/auto-create-templates/template-plan.schema.json +230 -0
  10. package/auto-create-templates/template-planner-prompt.md +75 -0
  11. package/dist/src/account-pages-legacy.js +9064 -0
  12. package/dist/src/account-pages.js +41 -620
  13. package/dist/src/app.js +7173 -494
  14. package/dist/src/cli.js +525 -29
  15. package/dist/src/config.js +14 -7
  16. package/dist/src/context.js +51 -35
  17. package/dist/src/db.js +1049 -55
  18. package/dist/src/dev-app-legacy.js +693 -0
  19. package/dist/src/dev-app.js +4 -904
  20. package/dist/src/domain.js +1 -1
  21. package/dist/src/editor-chat-history.js +72 -0
  22. package/dist/src/editor-chat.js +202 -0
  23. package/dist/src/frontend/flockposter-cache-store.js +116 -0
  24. package/dist/src/frontend/homepage-client.js +114 -0
  25. package/dist/src/frontend/homepage-shared.js +3 -0
  26. package/dist/src/frontend/homepage-store.js +27 -0
  27. package/dist/src/frontend/homepage-view.js +147 -0
  28. package/dist/src/frontend/page-runtime-client.js +100 -0
  29. package/dist/src/frontend/page-runtime-store.js +8 -0
  30. package/dist/src/frontend/template-editor-chat.js +2261 -0
  31. package/dist/src/homepage.js +695 -339
  32. package/dist/src/lib/template-paths.js +10 -4
  33. package/dist/src/lib/template-style-options.js +95 -15
  34. package/dist/src/page-runtime.js +1 -0
  35. package/dist/src/page-shell.js +941 -0
  36. package/dist/src/primitive-context.js +163 -0
  37. package/dist/src/primitive-registry.js +340 -0
  38. package/dist/src/primitive-sdk.js +3 -0
  39. package/dist/src/primitives/remotion/html-image.js +28 -0
  40. package/dist/src/react-page-shell.js +34 -0
  41. package/dist/src/ready-post-schedule-component.js +1514 -0
  42. package/dist/src/registry.js +44 -18
  43. package/dist/src/runtime.js +6 -1
  44. package/dist/src/services/api-call-history.js +245 -0
  45. package/dist/src/services/auth.js +27 -9
  46. package/dist/src/services/billing.js +248 -9
  47. package/dist/src/services/chat-threads.js +88 -0
  48. package/dist/src/services/job-logs.js +10 -3
  49. package/dist/src/services/jobs.js +13 -2
  50. package/dist/src/services/providers.js +944 -55
  51. package/dist/src/services/rate-limits.js +236 -0
  52. package/dist/src/services/remotion.js +143 -16
  53. package/dist/src/services/storage.js +23 -8
  54. package/dist/src/services/template-certification.js +26 -3
  55. package/dist/src/services/template-loader.js +19 -1
  56. package/dist/src/services/template-sources.js +316 -7
  57. package/dist/src/template-editor-pages.js +2309 -0
  58. package/dist/src/template-editor-shell.js +1507 -0
  59. package/dist/src/worker.js +120 -22
  60. package/package.json +30 -6
  61. package/public/assets/homepage-app.js +54 -0
  62. package/public/assets/homepage-client-app.js +54 -0
  63. package/public/assets/page-runtime-client-app.js +68 -0
  64. package/dist/templates/template_0000/src/lib/images.js +0 -202
  65. package/dist/templates/template_0000/src/remotion/Root.js +0 -33
  66. package/dist/templates/template_0000/src/remotion/index.js +0 -3
  67. package/dist/templates/template_0000/src/sdk.js +0 -3
  68. package/dist/templates/template_0000/src/style-options.js +0 -51
  69. package/dist/templates/template_0000/src/template-dna.js +0 -9
  70. package/dist/templates/template_0000/src/template.js +0 -1441
  71. package/templates/template_0000/README.md +0 -77
  72. package/templates/template_0000/SKILL.md +0 -225
  73. package/templates/template_0000/assets/Abel-Regular.ttf +0 -0
  74. package/templates/template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  75. package/templates/template_0000/assets/Montserrat[wght].ttf +0 -0
  76. package/templates/template_0000/assets/SourceCodePro[wght].ttf +0 -0
  77. package/templates/template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  78. package/templates/template_0000/assets/Yesteryear-Regular.ttf +0 -0
  79. package/templates/template_0000/composition.json +0 -11
  80. package/templates/template_0000/package.json +0 -28
  81. package/templates/template_0000/research/preview/.gitkeep +0 -1
  82. package/templates/template_0000/research/source_notes.md +0 -7
  83. package/templates/template_0000/src/lib/images.js +0 -202
  84. package/templates/template_0000/src/lib/images.ts +0 -241
  85. package/templates/template_0000/src/remotion/Root.js +0 -33
  86. package/templates/template_0000/src/remotion/Root.tsx +0 -75
  87. package/templates/template_0000/src/remotion/index.js +0 -3
  88. package/templates/template_0000/src/remotion/index.tsx +0 -4
  89. package/templates/template_0000/src/sdk.js +0 -3
  90. package/templates/template_0000/src/sdk.ts +0 -122
  91. package/templates/template_0000/src/style-options.js +0 -51
  92. package/templates/template_0000/src/style-options.ts +0 -60
  93. package/templates/template_0000/src/template-dna.js +0 -9
  94. package/templates/template_0000/src/template-dna.ts +0 -15
  95. package/templates/template_0000/src/template.js +0 -1441
  96. package/templates/template_0000/src/template.ts +0 -2042
  97. package/templates/template_0000/template.config.json +0 -21
  98. package/templates/template_0000/tsconfig.json +0 -19
@@ -1,14 +1,10 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import { database } from "./db.js";
2
- import { template0000Definition } from "../templates/template_0000/src/template.js";
3
4
  import { loadTemplateFromModule } from "./services/template-loader.js";
4
- import { TemplateCertificationService } from "./services/template-certification.js";
5
5
  class TemplateRegistry {
6
- localTemplates = new Map([[
7
- template0000Definition.id,
8
- template0000Definition
9
- ]]);
6
+ localTemplates = new Map();
10
7
  runtimeTemplates = new Map();
11
- certification = new TemplateCertificationService();
12
8
  initialized = false;
13
9
  initPromise = null;
14
10
  async ensureInitialized() {
@@ -19,22 +15,24 @@ class TemplateRegistry {
19
15
  return this.initPromise;
20
16
  }
21
17
  this.initPromise = (async () => {
22
- for (const template of this.localTemplates.values()) {
23
- const skillPath = template.skillPath;
24
- if (!skillPath) {
25
- throw new Error(`Local template ${template.id} is missing skillPath.`);
18
+ await this.loadLocalTemplates();
19
+ const visibleReleaseByTemplateId = new Map();
20
+ for (const release of database.listTemplateReleases()) {
21
+ if (release.status !== "approved" && release.status !== "active") {
22
+ continue;
26
23
  }
27
- const report = await this.certification.certify({ template, skillPath });
28
- if (!report.passed) {
29
- throw new Error(`Local template ${template.id} failed certification.`);
24
+ const current = visibleReleaseByTemplateId.get(release.templateId);
25
+ if (!current || getReleaseTimestamp(release) > getReleaseTimestamp(current)) {
26
+ visibleReleaseByTemplateId.set(release.templateId, release);
30
27
  }
31
28
  }
32
- for (const release of database.getActiveTemplateReleases()) {
29
+ for (const release of visibleReleaseByTemplateId.values()) {
33
30
  const template = await loadTemplateFromModule(release.modulePath);
34
- this.runtimeTemplates.set(release.templateId, {
31
+ const runtimeTemplate = {
35
32
  ...template,
36
33
  skillPath: release.skillPath
37
- });
34
+ };
35
+ this.runtimeTemplates.set(release.templateId, runtimeTemplate);
38
36
  }
39
37
  this.initialized = true;
40
38
  this.initPromise = null;
@@ -42,7 +40,14 @@ class TemplateRegistry {
42
40
  return this.initPromise;
43
41
  }
44
42
  list() {
45
- return [...this.localTemplates.values(), ...this.runtimeTemplates.values()].sort((a, b) => a.id.localeCompare(b.id));
43
+ const merged = new Map();
44
+ for (const template of this.localTemplates.values()) {
45
+ merged.set(template.id, template);
46
+ }
47
+ for (const template of this.runtimeTemplates.values()) {
48
+ merged.set(template.id, template);
49
+ }
50
+ return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
46
51
  }
47
52
  get(templateId) {
48
53
  return this.findTemplate(templateId, this.runtimeTemplates) ?? this.findTemplate(templateId, this.localTemplates);
@@ -50,8 +55,29 @@ class TemplateRegistry {
50
55
  registerRuntimeTemplate(template) {
51
56
  this.runtimeTemplates.set(template.id, template);
52
57
  }
58
+ async loadLocalTemplates() {
59
+ this.localTemplates.clear();
60
+ for (const modulePath of discoverLocalTemplateModulePaths(process.cwd())) {
61
+ const template = await loadTemplateFromModule(modulePath);
62
+ this.localTemplates.set(template.id, template);
63
+ }
64
+ }
53
65
  findTemplate(templateKey, source) {
54
66
  return source.get(templateKey) ?? [...source.values()].find((template) => template.slugId === templateKey) ?? null;
55
67
  }
56
68
  }
69
+ function getReleaseTimestamp(release) {
70
+ return Date.parse(release.activatedAt ?? release.updatedAt ?? release.createdAt);
71
+ }
72
+ function discoverLocalTemplateModulePaths(rootDir) {
73
+ const templatesDir = path.resolve(rootDir, "templates");
74
+ if (!existsSync(templatesDir)) {
75
+ return [];
76
+ }
77
+ return readdirSync(templatesDir, { withFileTypes: true })
78
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("vidfarm_template_"))
79
+ .map((entry) => path.join(templatesDir, entry.name, "src", "template.ts"))
80
+ .filter((modulePath) => existsSync(modulePath))
81
+ .sort((a, b) => a.localeCompare(b));
82
+ }
57
83
  export const templateRegistry = new TemplateRegistry();
@@ -2,6 +2,7 @@ import { serve } from "@hono/node-server";
2
2
  import app from "./app.js";
3
3
  import { config } from "./config.js";
4
4
  import { templateRegistry } from "./registry.js";
5
+ import { apiCallHistoryStore } from "./services/api-call-history.js";
5
6
  import { jobLogStore } from "./services/job-logs.js";
6
7
  import { Worker } from "./worker.js";
7
8
  export async function startRuntime() {
@@ -10,12 +11,16 @@ export async function startRuntime() {
10
11
  worker.start();
11
12
  const server = serve({
12
13
  fetch: app.fetch,
13
- port: config.PORT
14
+ port: config.PORT,
15
+ serverOptions: {
16
+ maxHeaderSize: 64 * 1024
17
+ }
14
18
  }, (info) => {
15
19
  console.log(`[vidfarm] listening on http://localhost:${info.port}`);
16
20
  });
17
21
  const shutdown = async () => {
18
22
  worker.stop();
23
+ await apiCallHistoryStore.shutdown();
19
24
  await jobLogStore.shutdown();
20
25
  server.close();
21
26
  };
@@ -0,0 +1,245 @@
1
+ import { StorageService } from "./storage.js";
2
+ const API_CALL_HISTORY_PREFIX = "api-call-history";
3
+ const CHUNK_INTERVAL_MS = 60_000;
4
+ const CHUNK_MAX_BYTES = 1024 * 1024;
5
+ const FLUSH_INTERVAL_MS = 5_000;
6
+ export class ApiCallHistoryStore {
7
+ storage = new StorageService();
8
+ activeChunks = new Map();
9
+ flushTimer = setInterval(() => {
10
+ void this.flushExpiredChunks();
11
+ }, FLUSH_INTERVAL_MS);
12
+ constructor() {
13
+ this.flushTimer.unref();
14
+ }
15
+ addEntry(entry) {
16
+ const normalized = {
17
+ ...entry,
18
+ createdAt: entry.createdAt ?? new Date().toISOString()
19
+ };
20
+ const minuteStartMs = floorToMinute(Date.parse(normalized.createdAt));
21
+ const activePrefix = this.activeChunkPrefix(normalized.customerId, minuteStartMs);
22
+ const current = this.findLatestChunk(activePrefix);
23
+ const entryBytes = serializedEntrySize(normalized);
24
+ const chunk = current && current.sizeBytes + entryBytes <= CHUNK_MAX_BYTES
25
+ ? current
26
+ : this.createChunk(normalized.customerId, minuteStartMs, current ? current.sequence + 1 : 0);
27
+ chunk.entries.push(normalized);
28
+ chunk.sizeBytes += entryBytes;
29
+ chunk.dirty = true;
30
+ if (chunk.sizeBytes >= CHUNK_MAX_BYTES) {
31
+ void this.persistChunk(chunk, true);
32
+ }
33
+ }
34
+ async listEntries(input) {
35
+ const persisted = await this.readPersistedEntries(input);
36
+ const buffered = this.readBufferedEntries(input);
37
+ const merged = dedupeAndSort([...persisted, ...buffered]).reverse();
38
+ const filtered = input.cursor
39
+ ? merged.filter((entry) => (Date.parse(entry.createdAt) < Date.parse(input.cursor.createdAt)
40
+ || (entry.createdAt === input.cursor.createdAt && entry.id < input.cursor.id)))
41
+ : merged;
42
+ if (input.limit === undefined) {
43
+ return filtered;
44
+ }
45
+ const limit = Math.max(1, Math.min(input.limit, 501));
46
+ return filtered.slice(0, limit);
47
+ }
48
+ async renameTracer(input) {
49
+ const fromTracer = input.fromTracer.trim();
50
+ const toTracer = input.toTracer.trim();
51
+ if (!fromTracer || !toTracer || fromTracer === toTracer) {
52
+ return { updated: 0 };
53
+ }
54
+ let updated = 0;
55
+ for (const chunk of this.activeChunks.values()) {
56
+ if (chunk.customerId !== input.customerId) {
57
+ continue;
58
+ }
59
+ let chunkChanged = false;
60
+ for (const entry of chunk.entries) {
61
+ if (entry.tracer === fromTracer && withinRange(entry.createdAt, input.startTime, input.endTime)) {
62
+ entry.tracer = toTracer;
63
+ updated += 1;
64
+ chunkChanged = true;
65
+ }
66
+ }
67
+ if (chunkChanged) {
68
+ chunk.dirty = true;
69
+ chunk.sizeBytes = chunk.entries.reduce((sum, entry) => sum + serializedEntrySize(entry), 0);
70
+ await this.persistChunk(chunk, false);
71
+ }
72
+ }
73
+ const keys = await this.listRelevantKeys(input);
74
+ await Promise.all(keys.map(async (key) => {
75
+ const content = await this.storage.readText(key);
76
+ const entries = parseChunk(content);
77
+ let changed = false;
78
+ for (const entry of entries) {
79
+ if (entry.customerId === input.customerId
80
+ && entry.tracer === fromTracer
81
+ && withinRange(entry.createdAt, input.startTime, input.endTime)) {
82
+ entry.tracer = toTracer;
83
+ updated += 1;
84
+ changed = true;
85
+ }
86
+ }
87
+ if (changed) {
88
+ const body = `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
89
+ await this.storage.putText(key, body, "application/x-ndjson; charset=utf-8");
90
+ }
91
+ }));
92
+ return { updated };
93
+ }
94
+ async shutdown() {
95
+ clearInterval(this.flushTimer);
96
+ await Promise.all([...this.activeChunks.values()].map((chunk) => this.persistChunk(chunk, true)));
97
+ }
98
+ async readPersistedEntries(input) {
99
+ const keys = await this.listRelevantKeys(input);
100
+ const entries = await Promise.all(keys.map(async (key) => parseChunk(await this.storage.readText(key))));
101
+ return entries
102
+ .flat()
103
+ .filter((entry) => entry.customerId === input.customerId)
104
+ .filter((entry) => withinRange(entry.createdAt, input.startTime, input.endTime));
105
+ }
106
+ readBufferedEntries(input) {
107
+ const buffered = [];
108
+ for (const chunk of this.activeChunks.values()) {
109
+ if (chunk.customerId !== input.customerId) {
110
+ continue;
111
+ }
112
+ for (const entry of chunk.entries.slice(chunk.lastPersistedEntryCount)) {
113
+ if (withinRange(entry.createdAt, input.startTime, input.endTime)) {
114
+ buffered.push(entry);
115
+ }
116
+ }
117
+ }
118
+ return buffered;
119
+ }
120
+ async listRelevantKeys(input) {
121
+ const minuteStarts = this.minuteStartsForQuery(input.startTime, input.endTime);
122
+ if (minuteStarts === null) {
123
+ return this.storage.listKeys(`${API_CALL_HISTORY_PREFIX}/customer_id=${input.customerId}/`);
124
+ }
125
+ const nested = await Promise.all(minuteStarts.map((minuteStartMs) => (this.storage.listKeys(this.minutePrefix(input.customerId, minuteStartMs)))));
126
+ return nested.flat();
127
+ }
128
+ minuteStartsForQuery(startTime, endTime) {
129
+ if (!startTime && !endTime) {
130
+ return null;
131
+ }
132
+ const startMs = floorToMinute(Date.parse(startTime ?? endTime ?? new Date().toISOString()));
133
+ const endMs = floorToMinute(Date.parse(endTime ?? startTime ?? new Date().toISOString()));
134
+ const minMs = Math.min(startMs, endMs);
135
+ const maxMs = Math.max(startMs, endMs);
136
+ const minuteStarts = [];
137
+ for (let cursor = minMs; cursor <= maxMs; cursor += 60_000) {
138
+ minuteStarts.push(cursor);
139
+ }
140
+ return minuteStarts;
141
+ }
142
+ async flushExpiredChunks() {
143
+ const now = Date.now();
144
+ await Promise.all([...this.activeChunks.values()].map(async (chunk) => {
145
+ const intervalElapsed = now >= chunk.minuteStartMs + CHUNK_INTERVAL_MS;
146
+ if (intervalElapsed || chunk.sizeBytes >= CHUNK_MAX_BYTES) {
147
+ await this.persistChunk(chunk, true);
148
+ return;
149
+ }
150
+ if (chunk.dirty) {
151
+ await this.persistChunk(chunk, false);
152
+ }
153
+ }));
154
+ }
155
+ createChunk(customerId, minuteStartMs, sequence) {
156
+ const chunk = {
157
+ customerId,
158
+ minuteStartMs,
159
+ sequence,
160
+ entries: [],
161
+ sizeBytes: 0,
162
+ lastPersistedEntryCount: 0,
163
+ dirty: false
164
+ };
165
+ this.activeChunks.set(this.chunkKeyForMap(customerId, minuteStartMs, sequence), chunk);
166
+ return chunk;
167
+ }
168
+ findLatestChunk(activePrefix) {
169
+ const matching = [...this.activeChunks.entries()]
170
+ .filter(([key]) => key.startsWith(activePrefix))
171
+ .map(([, chunk]) => chunk)
172
+ .sort((a, b) => a.sequence - b.sequence);
173
+ return matching.at(-1) ?? null;
174
+ }
175
+ activeChunkPrefix(customerId, minuteStartMs) {
176
+ return `${customerId}:${minuteStartMs}:`;
177
+ }
178
+ chunkKeyForMap(customerId, minuteStartMs, sequence) {
179
+ return `${this.activeChunkPrefix(customerId, minuteStartMs)}${sequence}`;
180
+ }
181
+ async persistChunk(chunk, finalize) {
182
+ if (!chunk.entries.length) {
183
+ if (finalize) {
184
+ this.activeChunks.delete(this.chunkKeyForMap(chunk.customerId, chunk.minuteStartMs, chunk.sequence));
185
+ }
186
+ return;
187
+ }
188
+ const key = this.objectKeyForChunk(chunk);
189
+ const body = `${chunk.entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
190
+ await this.storage.putText(key, body, "application/x-ndjson; charset=utf-8");
191
+ chunk.lastPersistedEntryCount = chunk.entries.length;
192
+ chunk.dirty = false;
193
+ if (finalize) {
194
+ this.activeChunks.delete(this.chunkKeyForMap(chunk.customerId, chunk.minuteStartMs, chunk.sequence));
195
+ }
196
+ }
197
+ objectKeyForChunk(chunk) {
198
+ const startTs = Math.floor(chunk.entries[0] ? Date.parse(chunk.entries[0].createdAt) / 1000 : chunk.minuteStartMs / 1000);
199
+ const endTs = Math.floor(chunk.entries.at(-1) ? Date.parse(chunk.entries.at(-1).createdAt) / 1000 : chunk.minuteStartMs / 1000);
200
+ return `${this.minutePrefix(chunk.customerId, chunk.minuteStartMs)}chunk-${startTs}-${endTs}-${String(chunk.sequence).padStart(4, "0")}.ndjson`;
201
+ }
202
+ minutePrefix(customerId, minuteStartMs) {
203
+ return `${API_CALL_HISTORY_PREFIX}/customer_id=${customerId}/minute=${Math.floor(minuteStartMs / 1000)}/`;
204
+ }
205
+ }
206
+ export const apiCallHistoryStore = new ApiCallHistoryStore();
207
+ function serializedEntrySize(entry) {
208
+ return Buffer.byteLength(`${JSON.stringify(entry)}\n`, "utf8");
209
+ }
210
+ function floorToMinute(timestampMs) {
211
+ return Math.floor(timestampMs / 60_000) * 60_000;
212
+ }
213
+ function parseChunk(content) {
214
+ if (!content) {
215
+ return [];
216
+ }
217
+ return content
218
+ .split("\n")
219
+ .map((line) => line.trim())
220
+ .filter(Boolean)
221
+ .map((line) => {
222
+ const parsed = JSON.parse(line);
223
+ return {
224
+ ...parsed,
225
+ jobId: typeof parsed.jobId === "string" && parsed.jobId.trim() ? parsed.jobId.trim() : null
226
+ };
227
+ });
228
+ }
229
+ function dedupeAndSort(entries) {
230
+ const deduped = new Map();
231
+ for (const entry of entries) {
232
+ deduped.set(entry.id, entry);
233
+ }
234
+ return [...deduped.values()].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
235
+ }
236
+ function withinRange(createdAt, startTime, endTime) {
237
+ const value = Date.parse(createdAt);
238
+ if (startTime && value < Date.parse(startTime)) {
239
+ return false;
240
+ }
241
+ if (endTime && value > Date.parse(endTime)) {
242
+ return false;
243
+ }
244
+ return true;
245
+ }
@@ -14,7 +14,7 @@ export class AuthService {
14
14
  expiresAt: addSeconds(new Date(), 600)
15
15
  });
16
16
  if (config.RESEND_API_KEY) {
17
- await fetch("https://api.resend.com/emails", {
17
+ const response = await fetch("https://api.resend.com/emails", {
18
18
  method: "POST",
19
19
  headers: {
20
20
  "Content-Type": "application/json",
@@ -27,9 +27,20 @@ export class AuthService {
27
27
  text: `Your login code is ${code}. It expires in 10 minutes.`
28
28
  })
29
29
  });
30
+ if (!response.ok) {
31
+ const body = await response.text();
32
+ throw new Error(`Unable to send OTP email (${response.status}): ${body || "empty response body"}`);
33
+ }
34
+ return {
35
+ delivery: "email"
36
+ };
30
37
  }
31
38
  else {
32
39
  console.log(`[vidfarm] OTP for ${email}: ${code}`);
40
+ return {
41
+ delivery: "console",
42
+ code: config.isProduction ? undefined : code
43
+ };
33
44
  }
34
45
  }
35
46
  verifyOtp(email, code, name) {
@@ -54,10 +65,9 @@ export class AuthService {
54
65
  email,
55
66
  name: name ?? existing?.name ?? null,
56
67
  defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
57
- isDeveloper: existing?.isDeveloper
58
- || config.adminEmails.includes(normalizedEmail)
59
- || config.developerEmails.includes(normalizedEmail),
60
- isPaidPlan: existing?.isPaidPlan ?? false
68
+ isDeveloper: existing?.isDeveloper || config.adminEmails.includes(normalizedEmail),
69
+ isPaidPlan: existing?.isPaidPlan ?? false,
70
+ planTier: existing?.planTier ?? "starter"
61
71
  });
62
72
  const rawApiKey = `vf_${createId("key")}`;
63
73
  database.insertApiKey({
@@ -88,9 +98,12 @@ export class AuthService {
88
98
  defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null,
89
99
  isDeveloper: Boolean(row.is_developer),
90
100
  isPaidPlan: Boolean(row.is_paid_plan),
101
+ planTier: normalizePlanTier(row.plan_tier),
91
102
  about: row.about ? String(row.about) : null,
92
103
  groupchatUrl: row.groupchat_url ? String(row.groupchat_url) : null,
93
- flockposterApiKey: row.flockposter_api_key ? String(row.flockposter_api_key) : null
104
+ flockposterApiKey: row.flockposter_api_key ? String(row.flockposter_api_key) : null,
105
+ createdAt: String(row.created_at),
106
+ updatedAt: String(row.updated_at)
94
107
  };
95
108
  }
96
109
  verifyOtpForBrowserLogin(email, code, name) {
@@ -134,11 +147,16 @@ export class AuthService {
134
147
  email: normalizedEmail,
135
148
  name: input.name ?? existing?.name ?? null,
136
149
  defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
137
- isDeveloper: existing?.isDeveloper
138
- || config.adminEmails.includes(normalizedEmail)
139
- || config.developerEmails.includes(normalizedEmail),
150
+ isDeveloper: existing?.isDeveloper || config.adminEmails.includes(normalizedEmail),
140
151
  isPaidPlan: true,
152
+ planTier: input.planTier ?? existing?.planTier ?? "starter",
141
153
  passwordHash: hashPassword(input.password)
142
154
  });
143
155
  }
144
156
  }
157
+ function normalizePlanTier(value) {
158
+ if (value === "starter" || value === "agency" || value === "standard") {
159
+ return value;
160
+ }
161
+ return "starter";
162
+ }