@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,16 +1,255 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { database } from "../db.js";
2
3
  import { createId } from "../lib/ids.js";
4
+ import { config } from "../config.js";
5
+ const BILLING_COST_EVENTS_PATH = "/v1/cost-events";
6
+ const BILLING_WALLET_FUNDING_INTENTS_PATH = "/v1/wallet-funding-intents";
7
+ const BILLING_WALLET_FUNDING_CONFIRMED_PATH = "/v1/wallet-funding-confirmed";
8
+ const PAID_SIGNUP_BONUS_GRANT_TYPE = "paid_signup_bonus";
9
+ const PAID_SIGNUP_BONUS_USD = 5;
3
10
  export class BillingService {
11
+ async getWalletSummary(customerId) {
12
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
13
+ throw new Error("Billing service is not configured.");
14
+ }
15
+ const response = await fetch(new URL(`/v1/wallets/${encodeURIComponent(customerId)}`, config.VIDFARM_BILLING_URL), {
16
+ headers: {
17
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
18
+ }
19
+ });
20
+ if (!response.ok) {
21
+ const body = await response.text().catch(() => "");
22
+ throw new Error(`Billing wallet lookup failed (${response.status}): ${body || response.statusText}`);
23
+ }
24
+ const payload = await response.json();
25
+ return {
26
+ currency: typeof payload.currency === "string" ? payload.currency : "USD",
27
+ balanceUsd: Number(payload.balance_usd ?? 0),
28
+ balanceMicrosUsd: Number(payload.balance_micros_usd ?? 0)
29
+ };
30
+ }
31
+ async getJobBillingSummary(input) {
32
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
33
+ return {
34
+ currency: "USD",
35
+ eventCount: 0,
36
+ totalBaseCostUsd: 0,
37
+ totalChargeUsd: 0,
38
+ unavailableReason: "Billing service is not configured."
39
+ };
40
+ }
41
+ const url = new URL(`/v1/jobs/${encodeURIComponent(input.jobId)}/billing`, config.VIDFARM_BILLING_URL);
42
+ url.searchParams.set("customer_id", input.customerId);
43
+ if (input.templateId) {
44
+ url.searchParams.set("template_id", input.templateId);
45
+ }
46
+ const response = await fetch(url, {
47
+ headers: {
48
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
49
+ }
50
+ });
51
+ if (!response.ok) {
52
+ const body = await response.text().catch(() => "");
53
+ throw new Error(`Billing job summary lookup failed (${response.status}): ${body || response.statusText}`);
54
+ }
55
+ const payload = await response.json();
56
+ return {
57
+ currency: typeof payload.currency === "string" ? payload.currency : "USD",
58
+ eventCount: Number(payload.event_count ?? 0),
59
+ totalBaseCostUsd: Number(payload.total_base_cost_usd ?? 0),
60
+ totalChargeUsd: Number(payload.total_charge_usd ?? 0),
61
+ unavailableReason: null
62
+ };
63
+ }
4
64
  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
65
+ const normalizedCostCenterSlug = resolveCostCenterSlug(input);
66
+ const normalizedIdempotencyKey = resolveIdempotencyKey(input, normalizedCostCenterSlug);
67
+ const remoteEventCostCenterSlug = normalizedCostCenterSlug ?? null;
68
+ if (!remoteEventCostCenterSlug) {
69
+ return;
70
+ }
71
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
72
+ throw new Error(`Billing URL/secret missing for cost center ${remoteEventCostCenterSlug}.`);
73
+ }
74
+ const response = await fetch(new URL(BILLING_COST_EVENTS_PATH, config.VIDFARM_BILLING_URL), {
75
+ method: "POST",
76
+ headers: {
77
+ "content-type": "application/json",
78
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
79
+ },
80
+ body: JSON.stringify({
81
+ idempotency_key: normalizedIdempotencyKey ?? `${remoteEventCostCenterSlug}:${input.jobId}:${input.templateId}:${input.type}`,
82
+ customer_id: input.customerId,
83
+ job_id: input.jobId,
84
+ tracer: input.tracer,
85
+ cost_center_slug: remoteEventCostCenterSlug,
86
+ base_cost_usd: input.costUsd,
87
+ metadata: {
88
+ ...input.metadata,
89
+ template_id: input.templateId,
90
+ local_charge_usd: input.chargeUsd
91
+ },
92
+ occurred_at_ms: input.occurredAtMs ?? Date.now()
93
+ })
94
+ });
95
+ if (!response.ok) {
96
+ const body = await response.text().catch(() => "");
97
+ throw new Error(`Billing service rejected cost event (${response.status}): ${body || response.statusText}`);
98
+ }
99
+ await response.json().catch(() => ({}));
100
+ }
101
+ async grantPaidSignupBonus(customerId, metadata) {
102
+ const customer = database.getCustomerById(customerId);
103
+ if (!customer) {
104
+ throw new Error("Customer not found.");
105
+ }
106
+ if (!customer.isPaidPlan) {
107
+ return { granted: false, reason: "customer_not_paid" };
108
+ }
109
+ if (database.hasCustomerCreditGrant(customerId, PAID_SIGNUP_BONUS_GRANT_TYPE)) {
110
+ return { granted: false, reason: "already_granted" };
111
+ }
112
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
113
+ throw new Error("Billing service is not configured.");
114
+ }
115
+ const clientReferenceId = buildPaidSignupBonusClientReferenceId(customerId);
116
+ const stripeEventId = `${PAID_SIGNUP_BONUS_GRANT_TYPE}:${customerId}`;
117
+ const intentResponse = await fetch(new URL(BILLING_WALLET_FUNDING_INTENTS_PATH, config.VIDFARM_BILLING_URL), {
118
+ method: "POST",
119
+ headers: {
120
+ "content-type": "application/json",
121
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
122
+ },
123
+ body: JSON.stringify({
124
+ customer_id: customerId,
125
+ client_reference_id: clientReferenceId,
126
+ occurred_at_ms: Date.now(),
127
+ metadata: {
128
+ ...(metadata ?? {}),
129
+ grant_type: PAID_SIGNUP_BONUS_GRANT_TYPE
130
+ }
131
+ })
132
+ });
133
+ if (!intentResponse.ok) {
134
+ const body = await intentResponse.text().catch(() => "");
135
+ if (!/TransactionCanceledException|ConditionalCheckFailed|duplicate/i.test(body)) {
136
+ throw new Error(`Billing service rejected wallet funding intent (${intentResponse.status}): ${body || intentResponse.statusText}`);
137
+ }
138
+ }
139
+ const response = await fetch(new URL(BILLING_WALLET_FUNDING_CONFIRMED_PATH, config.VIDFARM_BILLING_URL), {
140
+ method: "POST",
141
+ headers: {
142
+ "content-type": "application/json",
143
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
144
+ },
145
+ body: JSON.stringify({
146
+ client_reference_id: clientReferenceId,
147
+ amount_usd: PAID_SIGNUP_BONUS_USD,
148
+ stripe_event_id: stripeEventId,
149
+ occurred_at_ms: Date.now(),
150
+ metadata: {
151
+ ...(metadata ?? {}),
152
+ grant_type: PAID_SIGNUP_BONUS_GRANT_TYPE
153
+ }
154
+ })
14
155
  });
156
+ if (!response.ok) {
157
+ const body = await response.text().catch(() => "");
158
+ throw new Error(`Billing service rejected signup wallet funding (${response.status}): ${body || response.statusText}`);
159
+ }
160
+ try {
161
+ database.insertCustomerCreditGrant({
162
+ id: createId("grant"),
163
+ customerId,
164
+ grantType: PAID_SIGNUP_BONUS_GRANT_TYPE,
165
+ amountUsd: PAID_SIGNUP_BONUS_USD,
166
+ metadata: metadata ?? {}
167
+ });
168
+ }
169
+ catch (error) {
170
+ if (!isUniqueConstraintError(error)) {
171
+ throw error;
172
+ }
173
+ }
174
+ return {
175
+ granted: true,
176
+ reason: "granted",
177
+ amountUsd: PAID_SIGNUP_BONUS_USD,
178
+ grantType: PAID_SIGNUP_BONUS_GRANT_TYPE
179
+ };
180
+ }
181
+ }
182
+ function resolveCostCenterSlug(input) {
183
+ if (input.costCenterSlug?.trim()) {
184
+ const explicitSlug = input.costCenterSlug.trim();
185
+ return explicitSlug === "remotion_lambda" ? explicitSlug : null;
186
+ }
187
+ if (input.type === "render" && input.metadata?.mode === "lambda") {
188
+ return "remotion_lambda";
15
189
  }
190
+ return null;
191
+ }
192
+ function resolveIdempotencyKey(input, costCenterSlug) {
193
+ if (input.idempotencyKey?.trim()) {
194
+ return input.idempotencyKey.trim();
195
+ }
196
+ const renderId = typeof input.metadata?.render_id === "string"
197
+ ? input.metadata.render_id
198
+ : typeof input.metadata?.renderId === "string"
199
+ ? input.metadata.renderId
200
+ : null;
201
+ if (costCenterSlug === "remotion_lambda" && renderId) {
202
+ return `remotion_lambda:${renderId}`;
203
+ }
204
+ return buildBillingEventFingerprint(input, costCenterSlug);
205
+ }
206
+ function buildBillingEventFingerprint(input, costCenterSlug) {
207
+ const fingerprintPayload = {
208
+ customerId: input.customerId,
209
+ jobId: input.jobId,
210
+ templateId: input.templateId,
211
+ tracer: input.tracer ?? null,
212
+ type: input.type,
213
+ costCenterSlug,
214
+ costUsd: roundUsd(input.costUsd),
215
+ chargeUsd: input.chargeUsd == null ? null : roundUsd(input.chargeUsd),
216
+ occurredAtMs: input.occurredAtMs ?? null,
217
+ metadata: sortJsonValue(input.metadata ?? {})
218
+ };
219
+ const digest = createHash("sha256")
220
+ .update(JSON.stringify(fingerprintPayload))
221
+ .digest("hex")
222
+ .slice(0, 24);
223
+ return `${costCenterSlug ?? input.type}:${input.jobId}:${digest}`;
224
+ }
225
+ function sortJsonValue(value) {
226
+ if (Array.isArray(value)) {
227
+ return value.map(sortJsonValue);
228
+ }
229
+ if (value && typeof value === "object") {
230
+ return Object.entries(value)
231
+ .sort(([left], [right]) => left.localeCompare(right))
232
+ .reduce((acc, [key, nestedValue]) => {
233
+ acc[key] = sortJsonValue(nestedValue);
234
+ return acc;
235
+ }, {});
236
+ }
237
+ return value;
238
+ }
239
+ function roundUsd(value) {
240
+ return Number(value.toFixed(6));
241
+ }
242
+ function isUniqueConstraintError(error) {
243
+ return Boolean(error instanceof Error && /unique constraint/i.test(error.message));
244
+ }
245
+ function buildPaidSignupBonusClientReferenceId(customerId) {
246
+ const digest = createHash("sha256").update(`${PAID_SIGNUP_BONUS_GRANT_TYPE}:${customerId}`).digest("hex");
247
+ const part1 = digest.slice(0, 8);
248
+ const part2 = digest.slice(8, 12);
249
+ const part3 = `4${digest.slice(13, 16)}`;
250
+ const variantNibble = (8 + (Number.parseInt(digest.slice(16, 17), 16) % 4)).toString(16);
251
+ const part4 = `${variantNibble}${digest.slice(17, 20)}`;
252
+ const part5 = digest.slice(20, 32);
253
+ return `vidfarm_wallet_topup_${part1}-${part2}-${part3}-${part4}-${part5}`;
16
254
  }
255
+ export { PAID_SIGNUP_BONUS_GRANT_TYPE, PAID_SIGNUP_BONUS_USD, resolveCostCenterSlug, resolveIdempotencyKey };
@@ -0,0 +1,88 @@
1
+ import { createId } from "../lib/ids.js";
2
+ import { nowIso } from "../lib/time.js";
3
+ import { database } from "../db.js";
4
+ import { StorageService } from "./storage.js";
5
+ const storage = new StorageService();
6
+ function normalizeTracerList(values) {
7
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
8
+ }
9
+ function buildThreadStoragePrefix(customerId, templateId, threadId) {
10
+ return storage.developerScopedKey(customerId, "chat-threads", templateId, threadId);
11
+ }
12
+ function buildThreadChunkKey(prefix, createdAt) {
13
+ const sortableTime = createdAt.replace(/[^0-9]/g, "");
14
+ return `${prefix}/chunks/${sortableTime}-${createId("chunk")}.json`;
15
+ }
16
+ async function readChunkMessages(key) {
17
+ const raw = await storage.readText(key);
18
+ if (!raw) {
19
+ return [];
20
+ }
21
+ const parsed = JSON.parse(raw);
22
+ return Array.isArray(parsed.messages) ? parsed.messages : [];
23
+ }
24
+ export const chatThreads = {
25
+ async listThreads(input) {
26
+ const threads = database.listChatThreadMetas(input);
27
+ const withMessages = await Promise.all(threads.map(async (thread) => ({
28
+ ...thread,
29
+ messages: await this.loadThreadMessages(thread)
30
+ })));
31
+ return withMessages;
32
+ },
33
+ async getThread(input) {
34
+ const thread = database.getChatThreadMeta(input.customerId, input.threadId);
35
+ if (!thread) {
36
+ return null;
37
+ }
38
+ return {
39
+ ...thread,
40
+ messages: await this.loadThreadMessages(thread)
41
+ };
42
+ },
43
+ async appendExchange(input) {
44
+ const createdAt = nowIso();
45
+ const storagePrefix = buildThreadStoragePrefix(input.customerId, input.templateId, input.threadId);
46
+ const chunk = {
47
+ version: 1,
48
+ threadId: input.threadId,
49
+ customerId: input.customerId,
50
+ templateId: input.templateId,
51
+ createdAt,
52
+ messages: input.messages
53
+ };
54
+ await storage.putJson(buildThreadChunkKey(storagePrefix, createdAt), chunk);
55
+ const updated = database.appendChatThreadMessages({
56
+ threadId: input.threadId,
57
+ customerId: input.customerId,
58
+ title: input.title || "New chat",
59
+ templateId: input.templateId,
60
+ storagePrefix,
61
+ tracers: normalizeTracerList(input.tracers),
62
+ messageCountDelta: input.messages.length,
63
+ lastMessageAt: createdAt
64
+ });
65
+ return updated ? this.getThread({ customerId: input.customerId, threadId: updated.id }) : null;
66
+ },
67
+ async updateThread(input) {
68
+ const updated = database.updateChatThreadState({
69
+ ...input,
70
+ tracers: input.tracers ? normalizeTracerList(input.tracers) : undefined
71
+ });
72
+ return updated ? this.getThread({ customerId: input.customerId, threadId: updated.id }) : null;
73
+ },
74
+ async deleteThread(input) {
75
+ const existing = database.deleteChatThread(input.customerId, input.threadId);
76
+ if (!existing) {
77
+ return null;
78
+ }
79
+ const keys = await storage.listKeys(`${existing.storagePrefix}/`);
80
+ await Promise.all(keys.map((key) => storage.deleteObject(key)));
81
+ return existing;
82
+ },
83
+ async loadThreadMessages(thread) {
84
+ const keys = (await storage.listKeys(`${thread.storagePrefix}/chunks/`)).sort();
85
+ const messageGroups = await Promise.all(keys.map((key) => readChunkMessages(key)));
86
+ return messageGroups.flat();
87
+ }
88
+ };
@@ -32,11 +32,18 @@ export class JobLogStore {
32
32
  }
33
33
  }
34
34
  async listEvents(input) {
35
- const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
36
35
  const persisted = await this.readPersistedEvents(input);
37
36
  const buffered = this.readBufferedEvents(input);
38
- const merged = dedupeAndSort([...persisted, ...buffered]);
39
- return merged.slice(-limit);
37
+ const merged = dedupeAndSort([...persisted, ...buffered]).reverse();
38
+ const filtered = input.cursor
39
+ ? merged.filter((event) => (Date.parse(event.createdAt) < Date.parse(input.cursor.createdAt)
40
+ || (event.createdAt === input.cursor.createdAt && event.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);
40
47
  }
41
48
  async shutdown() {
42
49
  clearInterval(this.flushTimer);
@@ -4,6 +4,13 @@ import { createId } from "../lib/ids.js";
4
4
  import { addSeconds, nowIso } from "../lib/time.js";
5
5
  import { jobLogStore } from "./job-logs.js";
6
6
  const PENDING_JOB_STATUSES = ["queued", "running", "waiting_for_child", "waiting_for_human"];
7
+ function normalizeWebhookUrl(value) {
8
+ if (typeof value !== "string") {
9
+ return null;
10
+ }
11
+ const trimmed = value.trim();
12
+ return trimmed ? trimmed : null;
13
+ }
7
14
  export class JobsService {
8
15
  assertQueueCapacity(customerId) {
9
16
  if (config.MAX_PENDING_JOBS_GLOBAL <= 0 && config.MAX_PENDING_JOBS_PER_CUSTOMER <= 0) {
@@ -36,7 +43,7 @@ export class JobsService {
36
43
  _providerHint: input.providerHint ?? null
37
44
  },
38
45
  progress: 0,
39
- webhookUrl: input.webhookUrl ?? input.customer.defaultWebhookUrl ?? null,
46
+ webhookUrl: normalizeWebhookUrl(input.webhookUrl) ?? normalizeWebhookUrl(input.customer.defaultWebhookUrl),
40
47
  parentJobId: null,
41
48
  priority: 100,
42
49
  attemptCount: 0,
@@ -73,6 +80,9 @@ export class JobsService {
73
80
  listJobs(input) {
74
81
  return database.listJobsForCustomer(input);
75
82
  }
83
+ renameTracer(input) {
84
+ return database.renameJobsTracer(input);
85
+ }
76
86
  async listLogs(input) {
77
87
  return jobLogStore.listEvents(input);
78
88
  }
@@ -93,6 +103,7 @@ export class JobsService {
93
103
  status: "succeeded",
94
104
  progress,
95
105
  result,
106
+ error: null,
96
107
  completedAt: nowIso()
97
108
  });
98
109
  }
@@ -109,7 +120,7 @@ export class JobsService {
109
120
  database.updateJobStatus({
110
121
  id: job.id,
111
122
  status: nextStatus,
112
- error,
123
+ error: nextStatus === "failed" ? error : null,
113
124
  runAfter: addSeconds(new Date(), delaySeconds),
114
125
  completedAt: nextStatus === "failed" ? nowIso() : null
115
126
  });