@mevdragon/vidfarm-devcli 0.5.3 → 0.6.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.
Files changed (113) hide show
  1. package/README.md +3 -3
  2. package/demo/README.md +28 -0
  3. package/demo/dist/app.css +1 -0
  4. package/demo/dist/app.js +1184 -0
  5. package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
  6. package/demo/dist/chunks/chunk-S7OWAJDS.js +36 -0
  7. package/demo/dist/chunks/chunk-VTIBZ6AN.js +1 -0
  8. package/demo/dist/chunks/dist-ADSJKBVE.js +332 -0
  9. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +1 -0
  10. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +425 -0
  11. package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
  12. package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
  13. package/demo/dist/favicon.ico +0 -0
  14. package/demo/dist/icons/timeline/audio.svg +7 -0
  15. package/demo/dist/icons/timeline/captions.svg +5 -0
  16. package/demo/dist/icons/timeline/composition.svg +12 -0
  17. package/demo/dist/icons/timeline/image.svg +18 -0
  18. package/demo/dist/icons/timeline/music.svg +10 -0
  19. package/demo/dist/icons/timeline/text.svg +3 -0
  20. package/demo/dist/index.html +15 -0
  21. package/dist/src/account-pages-legacy.js +9396 -0
  22. package/dist/src/account-pages.js +61 -0
  23. package/dist/src/app.js +14378 -0
  24. package/dist/src/cli.js +1 -1
  25. package/dist/src/composition-runtime.js +613 -0
  26. package/dist/src/config.js +166 -0
  27. package/dist/src/context.js +447 -0
  28. package/dist/src/dev-app-legacy.js +739 -0
  29. package/dist/src/dev-app.js +6 -0
  30. package/dist/src/domain.js +2 -0
  31. package/dist/src/editor-chat-history.js +82 -0
  32. package/dist/src/editor-chat.js +449 -0
  33. package/dist/src/editor-dark-theme.js +1128 -0
  34. package/dist/src/frontend/debug.js +71 -0
  35. package/dist/src/frontend/flockposter-cache-store.js +124 -0
  36. package/dist/src/frontend/homepage-client.js +182 -0
  37. package/dist/src/frontend/homepage-shared.js +4 -0
  38. package/dist/src/frontend/homepage-store.js +28 -0
  39. package/dist/src/frontend/homepage-view.js +547 -0
  40. package/dist/src/frontend/page-runtime-client.js +132 -0
  41. package/dist/src/frontend/page-runtime-store.js +9 -0
  42. package/dist/src/frontend/sentry.js +42 -0
  43. package/dist/src/frontend/template-editor-chat.js +3960 -0
  44. package/dist/src/help-page.js +346 -0
  45. package/dist/src/homepage.js +1235 -0
  46. package/dist/src/hyperframes/composition.js +180 -0
  47. package/dist/src/index.js +16 -0
  48. package/dist/src/instrument.js +30 -0
  49. package/dist/src/lib/crypto.js +45 -0
  50. package/dist/src/lib/dev-log.js +54 -0
  51. package/dist/src/lib/display-name.js +11 -0
  52. package/dist/src/lib/ids.js +24 -0
  53. package/dist/src/lib/images.js +19 -0
  54. package/dist/src/lib/json.js +15 -0
  55. package/dist/src/lib/package-root.js +47 -0
  56. package/dist/src/lib/template-paths.js +28 -0
  57. package/dist/src/lib/time.js +7 -0
  58. package/dist/src/lib/url-clean.js +85 -0
  59. package/dist/src/page-runtime.js +2 -0
  60. package/dist/src/page-shell.js +1381 -0
  61. package/dist/src/primitive-context.js +357 -0
  62. package/dist/src/primitive-registry.js +2436 -0
  63. package/dist/src/primitive-sdk.js +4 -0
  64. package/dist/src/primitives/hyperframes-media.js +108 -0
  65. package/dist/src/react-page-shell.js +35 -0
  66. package/dist/src/ready-post-schedule-component.js +1540 -0
  67. package/dist/src/registry.js +296 -0
  68. package/dist/src/runtime.js +35 -0
  69. package/dist/src/services/api-call-history.js +249 -0
  70. package/dist/src/services/auth.js +152 -0
  71. package/dist/src/services/billing-pricing.js +39 -0
  72. package/dist/src/services/billing.js +228 -0
  73. package/dist/src/services/cast.js +127 -0
  74. package/dist/src/services/chat-threads.js +92 -0
  75. package/dist/src/services/composition-sanitize.js +124 -0
  76. package/dist/src/services/composition-watch.js +79 -0
  77. package/dist/src/services/fork-access.js +93 -0
  78. package/dist/src/services/fork-manifest.js +42 -0
  79. package/dist/src/services/ghostcut.js +179 -0
  80. package/dist/src/services/hyperframes.js +2307 -0
  81. package/dist/src/services/job-capacity.js +14 -0
  82. package/dist/src/services/job-logs.js +197 -0
  83. package/dist/src/services/jobs.js +136 -0
  84. package/dist/src/services/local-dynamo.js +0 -0
  85. package/dist/src/services/media-processing.js +766 -0
  86. package/dist/src/services/primitive-media-lambda.js +280 -0
  87. package/dist/src/services/providers.js +2926 -0
  88. package/dist/src/services/rate-limits.js +262 -0
  89. package/dist/src/services/serverless-auth.js +382 -0
  90. package/dist/src/services/serverless-jobs.js +1082 -0
  91. package/dist/src/services/serverless-provider-keys.js +409 -0
  92. package/dist/src/services/serverless-records.js +1385 -0
  93. package/dist/src/services/serverless-template-configs.js +75 -0
  94. package/dist/src/services/storage.js +383 -0
  95. package/dist/src/services/template-certification.js +413 -0
  96. package/dist/src/services/template-loader.js +99 -0
  97. package/dist/src/services/template-runtime-bundles.js +217 -0
  98. package/dist/src/services/template-sources.js +1017 -0
  99. package/dist/src/services/video-normalization.js +2 -0
  100. package/dist/src/services/webhooks.js +62 -0
  101. package/dist/src/template-editor-pages.js +2576 -0
  102. package/dist/src/template-editor-shell.js +2840 -0
  103. package/dist/src/template-sdk.js +4 -0
  104. package/dist/src/worker.js +17 -0
  105. package/package.json +6 -4
  106. package/public/assets/homepage-app.js +54 -0
  107. package/public/assets/homepage-client-app.js +80 -0
  108. package/public/assets/page-runtime-client-app.js +94 -0
  109. package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
  110. package/src/assets/SELLING_WITH_HOOKS.md +377 -0
  111. package/src/assets/SELLING_WITH_VSLS.md +606 -0
  112. package/src/assets/favicon.ico +0 -0
  113. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,152 @@
1
+ import { randomInt } from "node:crypto";
2
+ import { config } from "../config.js";
3
+ import { encryptString, hashPassword, hashSecret, safeEqualHash, verifyPassword } from "../lib/crypto.js";
4
+ import { displayNameFromEmail } from "../lib/display-name.js";
5
+ import { createId } from "../lib/ids.js";
6
+ import { addSeconds, nowIso } from "../lib/time.js";
7
+ import { ServerlessAuthService } from "./serverless-auth.js";
8
+ export class AuthService {
9
+ serverless = new ServerlessAuthService();
10
+ async requestOtp(email) {
11
+ const code = `${randomInt(100000, 999999)}`;
12
+ await this.serverless.insertOtpChallenge({
13
+ id: createId("otp"),
14
+ email,
15
+ codeHash: hashSecret(code),
16
+ expiresAt: addSeconds(new Date(), 600)
17
+ });
18
+ if (config.RESEND_API_KEY) {
19
+ const response = await fetch("https://api.resend.com/emails", {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ Authorization: `Bearer ${config.RESEND_API_KEY}`
24
+ },
25
+ body: JSON.stringify({
26
+ from: config.RESEND_FROM_EMAIL,
27
+ to: email,
28
+ subject: "Your Vidfarm login code",
29
+ text: `Your login code is ${code}. It expires in 10 minutes.`
30
+ })
31
+ });
32
+ if (!response.ok) {
33
+ const body = await response.text();
34
+ throw new Error(`Unable to send OTP email (${response.status}): ${body || "empty response body"}`);
35
+ }
36
+ return {
37
+ delivery: "email"
38
+ };
39
+ }
40
+ else {
41
+ console.log(`[vidfarm] OTP for ${email}: ${code}`);
42
+ return {
43
+ delivery: "console",
44
+ code: config.isProduction ? undefined : code
45
+ };
46
+ }
47
+ }
48
+ async verifyOtp(email, code, name) {
49
+ const challenge = await this.serverless.findLatestOtpChallenge(email);
50
+ if (!challenge) {
51
+ throw new Error("No OTP challenge found for that email.");
52
+ }
53
+ if (challenge.consumed_at) {
54
+ throw new Error("OTP challenge already used.");
55
+ }
56
+ if (new Date(String(challenge.expires_at)).getTime() < Date.now()) {
57
+ throw new Error("OTP challenge expired.");
58
+ }
59
+ if (!safeEqualHash(code, String(challenge.code_hash))) {
60
+ throw new Error("Invalid OTP code.");
61
+ }
62
+ await this.serverless.consumeOtpChallenge(email, String(challenge.id));
63
+ const existing = await this.serverless.getCustomerByEmailPublic(email);
64
+ const normalizedEmail = email.trim().toLowerCase();
65
+ const customer = await this.serverless.upsertCustomer({
66
+ id: existing?.id ?? createId("cus"),
67
+ email: normalizedEmail,
68
+ name: name?.trim() || existing?.name || displayNameFromEmail(normalizedEmail),
69
+ defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
70
+ isDeveloper: existing?.isDeveloper || config.adminEmails.includes(normalizedEmail),
71
+ isPaidPlan: existing?.isPaidPlan ?? false,
72
+ planTier: existing?.planTier ?? "client"
73
+ });
74
+ const rawApiKey = `vf_${createId("key")}`;
75
+ await this.serverless.insertApiKey({
76
+ id: createId("api"),
77
+ customerId: customer.id,
78
+ keyHash: hashSecret(rawApiKey + config.API_KEY_SALT),
79
+ rawValue: rawApiKey,
80
+ label: `Issued ${nowIso()}`
81
+ });
82
+ return { customer, apiKey: rawApiKey };
83
+ }
84
+ authenticate(apiKey) {
85
+ throw new Error("Use authenticateAsync; DynamoDB authentication is asynchronous.");
86
+ }
87
+ async authenticateAsync(apiKey) {
88
+ return this.serverless.authenticate(apiKey);
89
+ }
90
+ async verifyOtpForBrowserLogin(email, code, name) {
91
+ const result = await this.verifyOtp(email, code, name);
92
+ if (!result.customer.isPaidPlan) {
93
+ return {
94
+ status: "pricing",
95
+ customer: result.customer
96
+ };
97
+ }
98
+ return {
99
+ status: "authenticated",
100
+ customer: result.customer
101
+ };
102
+ }
103
+ async authenticateWithPassword(email, password) {
104
+ const row = await this.serverless.getCustomerAuthByEmail(email);
105
+ if (!row || !row.password_hash || !verifyPassword(password, String(row.password_hash))) {
106
+ throw new Error("Invalid email or password.");
107
+ }
108
+ const customer = await this.serverless.getCustomerById(String(row.customer_id ?? row.id));
109
+ if (!customer) {
110
+ throw new Error("Customer not found.");
111
+ }
112
+ await this.serverless.updateCustomerPassword(customer.id, String(row.password_hash), encryptString(password, config.ENCRYPTION_SECRET));
113
+ if (!customer.isPaidPlan) {
114
+ return {
115
+ status: "pricing",
116
+ customer
117
+ };
118
+ }
119
+ return {
120
+ status: "authenticated",
121
+ customer
122
+ };
123
+ }
124
+ async createPasswordUser(input) {
125
+ const normalizedEmail = input.email.trim().toLowerCase();
126
+ const existing = await this.serverless.getCustomerByEmailPublic(normalizedEmail);
127
+ return this.serverless.upsertCustomer({
128
+ id: existing?.id ?? createId("cus"),
129
+ email: normalizedEmail,
130
+ name: input.name?.trim() || existing?.name || displayNameFromEmail(normalizedEmail),
131
+ defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
132
+ isDeveloper: existing?.isDeveloper || config.adminEmails.includes(normalizedEmail),
133
+ isPaidPlan: true,
134
+ planTier: input.planTier ?? existing?.planTier ?? "client",
135
+ passwordHash: hashPassword(input.password),
136
+ encryptedPassword: encryptString(input.password, config.ENCRYPTION_SECRET)
137
+ });
138
+ }
139
+ }
140
+ function normalizePlanTier(value) {
141
+ if (value === "client" || value === "standalone" || value === "agency") {
142
+ return value;
143
+ }
144
+ if (value === "invisible" || value === "starter") {
145
+ return "client";
146
+ }
147
+ if (value === "standard") {
148
+ return "standalone";
149
+ }
150
+ return "client";
151
+ }
152
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,39 @@
1
+ // Single source of truth for the render/compute billing markup.
2
+ //
3
+ // The billing lambda debits the wallet by `base_provider_cost * markup` (see
4
+ // infra/lambda/billing.ts). Every surface that *displays* a render's cost — and
5
+ // every place that pre-computes the marked-up `chargeUsd` stashed in render
6
+ // metadata — must apply the SAME markup, otherwise the number shown to the user
7
+ // diverges from what their wallet is actually charged.
8
+ //
9
+ // The markup is expressed in basis points so it matches the billing lambda's
10
+ // integer micros math exactly. It is overridable per-environment via
11
+ // `VIDFARM_BILLING_MARKUP_BPS`; keep that env var in sync across the billing,
12
+ // job-runner, and api lambdas so display and charge never drift.
13
+ export const DEFAULT_MARKUP_BPS = 12_000; // 1.2x
14
+ const BPS_DENOMINATOR = 10_000;
15
+ // Resolve the active markup (basis points). Falls back to the default when the
16
+ // env var is unset, empty, or not a positive integer.
17
+ export function resolveMarkupBps() {
18
+ const raw = process.env.VIDFARM_BILLING_MARKUP_BPS;
19
+ if (raw === undefined || raw === null || raw.trim() === "") {
20
+ return DEFAULT_MARKUP_BPS;
21
+ }
22
+ const parsed = Number.parseInt(raw, 10);
23
+ if (!Number.isFinite(parsed) || parsed <= 0) {
24
+ return DEFAULT_MARKUP_BPS;
25
+ }
26
+ return parsed;
27
+ }
28
+ export function roundUsd(value) {
29
+ return Number(value.toFixed(4));
30
+ }
31
+ // Convert a raw provider cost (USD) into the amount the wallet is actually
32
+ // charged. Non-positive or non-finite inputs yield 0 (nothing to bill).
33
+ export function applyMarkupUsd(baseCostUsd, markupBps = resolveMarkupBps()) {
34
+ if (!Number.isFinite(baseCostUsd) || baseCostUsd <= 0) {
35
+ return 0;
36
+ }
37
+ return roundUsd((baseCostUsd * markupBps) / BPS_DENOMINATOR);
38
+ }
39
+ //# sourceMappingURL=billing-pricing.js.map
@@ -0,0 +1,228 @@
1
+ import { createHash } from "node:crypto";
2
+ import { serverlessRecords } from "./serverless-records.js";
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_GRANTS_PATH = "/v1/wallet-grants";
7
+ const PAID_SIGNUP_BONUS_GRANT_TYPE = "paid_signup_bonus";
8
+ const PAID_SIGNUP_BONUS_USD = 5;
9
+ export class BillingService {
10
+ async getWalletSummary(customerId) {
11
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
12
+ throw new Error("Billing service is not configured.");
13
+ }
14
+ const response = await fetch(new URL(`/v1/wallets/${encodeURIComponent(customerId)}`, config.VIDFARM_BILLING_URL), {
15
+ headers: {
16
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
17
+ }
18
+ });
19
+ if (!response.ok) {
20
+ const body = await response.text().catch(() => "");
21
+ throw new Error(`Billing wallet lookup failed (${response.status}): ${body || response.statusText}`);
22
+ }
23
+ const payload = await response.json();
24
+ return {
25
+ currency: typeof payload.currency === "string" ? payload.currency : "USD",
26
+ balanceUsd: Number(payload.balance_usd ?? 0),
27
+ balanceMicrosUsd: Number(payload.balance_micros_usd ?? 0)
28
+ };
29
+ }
30
+ async getJobBillingSummary(input) {
31
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
32
+ return {
33
+ currency: "USD",
34
+ eventCount: 0,
35
+ totalBaseCostUsd: 0,
36
+ totalChargeUsd: 0,
37
+ unavailableReason: "Billing service is not configured."
38
+ };
39
+ }
40
+ const url = new URL(`/v1/jobs/${encodeURIComponent(input.jobId)}/billing`, config.VIDFARM_BILLING_URL);
41
+ url.searchParams.set("customer_id", input.customerId);
42
+ if (input.templateId) {
43
+ url.searchParams.set("template_id", input.templateId);
44
+ }
45
+ const response = await fetch(url, {
46
+ headers: {
47
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
48
+ }
49
+ });
50
+ if (!response.ok) {
51
+ const body = await response.text().catch(() => "");
52
+ throw new Error(`Billing job summary lookup failed (${response.status}): ${body || response.statusText}`);
53
+ }
54
+ const payload = await response.json();
55
+ return {
56
+ currency: typeof payload.currency === "string" ? payload.currency : "USD",
57
+ eventCount: Number(payload.event_count ?? 0),
58
+ totalBaseCostUsd: Number(payload.total_base_cost_usd ?? 0),
59
+ totalChargeUsd: Number(payload.total_charge_usd ?? 0),
60
+ unavailableReason: null
61
+ };
62
+ }
63
+ async record(input) {
64
+ const normalizedCostCenterSlug = resolveCostCenterSlug(input);
65
+ const normalizedIdempotencyKey = resolveIdempotencyKey(input, normalizedCostCenterSlug);
66
+ const remoteEventCostCenterSlug = normalizedCostCenterSlug ?? null;
67
+ if (!remoteEventCostCenterSlug) {
68
+ return;
69
+ }
70
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
71
+ throw new Error(`Billing URL/secret missing for cost center ${remoteEventCostCenterSlug}.`);
72
+ }
73
+ const response = await fetch(new URL(BILLING_COST_EVENTS_PATH, config.VIDFARM_BILLING_URL), {
74
+ method: "POST",
75
+ headers: {
76
+ "content-type": "application/json",
77
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
78
+ },
79
+ body: JSON.stringify({
80
+ idempotency_key: normalizedIdempotencyKey ?? `${remoteEventCostCenterSlug}:${input.jobId}:${input.templateId}:${input.type}`,
81
+ customer_id: input.customerId,
82
+ job_id: input.jobId,
83
+ tracer: input.tracer,
84
+ cost_center_slug: remoteEventCostCenterSlug,
85
+ base_cost_usd: input.costUsd,
86
+ metadata: {
87
+ ...input.metadata,
88
+ template_id: input.templateId,
89
+ local_charge_usd: input.chargeUsd
90
+ },
91
+ occurred_at_ms: input.occurredAtMs ?? Date.now()
92
+ })
93
+ });
94
+ if (!response.ok) {
95
+ const body = await response.text().catch(() => "");
96
+ throw new Error(`Billing service rejected cost event (${response.status}): ${body || response.statusText}`);
97
+ }
98
+ await response.json().catch(() => ({}));
99
+ }
100
+ async grantPaidSignupBonus(customerId, metadata) {
101
+ const customer = await serverlessRecords.getCustomerById(customerId);
102
+ if (!customer) {
103
+ throw new Error("Customer not found.");
104
+ }
105
+ if (!customer.isPaidPlan) {
106
+ return { granted: false, reason: "customer_not_paid" };
107
+ }
108
+ if (await serverlessRecords.hasCustomerCreditGrant(customerId, PAID_SIGNUP_BONUS_GRANT_TYPE)) {
109
+ return { granted: false, reason: "already_granted" };
110
+ }
111
+ if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
112
+ throw new Error("Billing service is not configured.");
113
+ }
114
+ const response = await fetch(new URL(BILLING_WALLET_GRANTS_PATH, config.VIDFARM_BILLING_URL), {
115
+ method: "POST",
116
+ headers: {
117
+ "content-type": "application/json",
118
+ "x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
119
+ },
120
+ body: JSON.stringify({
121
+ idempotency_key: `${PAID_SIGNUP_BONUS_GRANT_TYPE}:${customerId}`,
122
+ customer_id: customerId,
123
+ amount_usd: PAID_SIGNUP_BONUS_USD,
124
+ grant_type: PAID_SIGNUP_BONUS_GRANT_TYPE,
125
+ occurred_at_ms: Date.now(),
126
+ metadata: {
127
+ ...(metadata ?? {}),
128
+ grant_type: PAID_SIGNUP_BONUS_GRANT_TYPE
129
+ }
130
+ })
131
+ });
132
+ if (!response.ok) {
133
+ const body = await response.text().catch(() => "");
134
+ throw new Error(`Billing service rejected signup wallet grant (${response.status}): ${body || response.statusText}`);
135
+ }
136
+ try {
137
+ await serverlessRecords.insertCustomerCreditGrant({
138
+ id: createId("grant"),
139
+ customerId,
140
+ grantType: PAID_SIGNUP_BONUS_GRANT_TYPE,
141
+ amountUsd: PAID_SIGNUP_BONUS_USD,
142
+ metadata: metadata ?? {}
143
+ });
144
+ }
145
+ catch (error) {
146
+ if (!isUniqueConstraintError(error)) {
147
+ throw error;
148
+ }
149
+ }
150
+ return {
151
+ granted: true,
152
+ reason: "granted",
153
+ amountUsd: PAID_SIGNUP_BONUS_USD,
154
+ grantType: PAID_SIGNUP_BONUS_GRANT_TYPE
155
+ };
156
+ }
157
+ }
158
+ function resolveCostCenterSlug(input) {
159
+ if (input.costCenterSlug?.trim()) {
160
+ const explicitSlug = input.costCenterSlug.trim();
161
+ return explicitSlug === "hyperframes_lambda"
162
+ || explicitSlug === "video_normalization"
163
+ || explicitSlug === "rapidapi_video_download"
164
+ || explicitSlug === "video_download_lambda"
165
+ || explicitSlug === "rapidapi_remove_background"
166
+ || explicitSlug === "primitive_media_lambda"
167
+ || explicitSlug === "job_runner_lambda"
168
+ || explicitSlug === "step_functions_standard"
169
+ ? explicitSlug
170
+ : null;
171
+ }
172
+ return null;
173
+ }
174
+ function resolveIdempotencyKey(input, costCenterSlug) {
175
+ if (input.idempotencyKey?.trim()) {
176
+ return input.idempotencyKey.trim();
177
+ }
178
+ const renderId = typeof input.metadata?.render_id === "string"
179
+ ? input.metadata.render_id
180
+ : typeof input.metadata?.renderId === "string"
181
+ ? input.metadata.renderId
182
+ : null;
183
+ if (costCenterSlug === "hyperframes_lambda" && renderId) {
184
+ return `hyperframes_lambda:${renderId}`;
185
+ }
186
+ return buildBillingEventFingerprint(input, costCenterSlug);
187
+ }
188
+ function buildBillingEventFingerprint(input, costCenterSlug) {
189
+ const fingerprintPayload = {
190
+ customerId: input.customerId,
191
+ jobId: input.jobId,
192
+ templateId: input.templateId,
193
+ tracer: input.tracer ?? null,
194
+ type: input.type,
195
+ costCenterSlug,
196
+ costUsd: roundUsd(input.costUsd),
197
+ chargeUsd: input.chargeUsd == null ? null : roundUsd(input.chargeUsd),
198
+ occurredAtMs: input.occurredAtMs ?? null,
199
+ metadata: sortJsonValue(input.metadata ?? {})
200
+ };
201
+ const digest = createHash("sha256")
202
+ .update(JSON.stringify(fingerprintPayload))
203
+ .digest("hex")
204
+ .slice(0, 24);
205
+ return `${costCenterSlug ?? input.type}:${input.jobId}:${digest}`;
206
+ }
207
+ function sortJsonValue(value) {
208
+ if (Array.isArray(value)) {
209
+ return value.map(sortJsonValue);
210
+ }
211
+ if (value && typeof value === "object") {
212
+ return Object.entries(value)
213
+ .sort(([left], [right]) => left.localeCompare(right))
214
+ .reduce((acc, [key, nestedValue]) => {
215
+ acc[key] = sortJsonValue(nestedValue);
216
+ return acc;
217
+ }, {});
218
+ }
219
+ return value;
220
+ }
221
+ function roundUsd(value) {
222
+ return Number(value.toFixed(6));
223
+ }
224
+ function isUniqueConstraintError(error) {
225
+ return Boolean(error instanceof Error && /unique constraint/i.test(error.message));
226
+ }
227
+ export { PAID_SIGNUP_BONUS_GRANT_TYPE, PAID_SIGNUP_BONUS_USD, resolveCostCenterSlug, resolveIdempotencyKey };
228
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1,127 @@
1
+ import { extractVideoFrameAsset } from "./media-processing.js";
2
+ // ==========================================================================
3
+ // Cast pass state — persisted per fork under
4
+ // compositions/forks/{forkId}/working/cast.json
5
+ // and copied on fork/clone/version so the identified cast (and its reference
6
+ // stills) travel with every descendant composition. Drives the async
7
+ // /cast-poll flow, mirroring the ghostcut.json state machine: /auto-decompose
8
+ // seeds it "pending", and /cast-poll advances it one unit of work per call
9
+ // (analyze -> per-member still -> per-member isolation -> done).
10
+ //
11
+ // The cast/ reference images live under the same public fork prefix, so their
12
+ // URLs are directly fetchable by every user and by AI generation primitives
13
+ // (as input_references / prompt_attachments) without a proxy hop.
14
+ // ==========================================================================
15
+ export const CAST_STATE_VERSION = 1;
16
+ export function buildPendingCastState(input) {
17
+ return {
18
+ version: CAST_STATE_VERSION,
19
+ status: "pending",
20
+ stage: "analyze",
21
+ savedAtMs: Date.now(),
22
+ compositionId: input.compositionId,
23
+ sourceUrl: input.sourceUrl,
24
+ title: input.title,
25
+ provider: null,
26
+ model: null,
27
+ durationSeconds: 0,
28
+ width: 0,
29
+ height: 0,
30
+ members: []
31
+ };
32
+ }
33
+ export function castMemberFromRaw(raw) {
34
+ return {
35
+ slug: raw.slug,
36
+ name: raw.name,
37
+ role: raw.role,
38
+ description: raw.description,
39
+ wardrobe: raw.wardrobe,
40
+ appears_in: raw.appears_in,
41
+ best_frame_ms: Math.max(0, Math.round(raw.best_frame_seconds * 1000)),
42
+ bbox: raw.bbox,
43
+ is_primary: raw.is_primary,
44
+ still_url: null,
45
+ reference_url: null,
46
+ status: "pending"
47
+ };
48
+ }
49
+ // The registry's default image model per provider is module-private; mirror it
50
+ // here so the cast isolation edit picks a supported model without importing
51
+ // the whole primitive registry.
52
+ function defaultImageModelForProvider(provider) {
53
+ switch (provider) {
54
+ case "gemini":
55
+ return "gemini-2.5-flash-image";
56
+ case "openrouter":
57
+ return "google/gemini-2.5-flash-image-preview";
58
+ default:
59
+ return "gpt-image-2";
60
+ }
61
+ }
62
+ // Pick the customer's best available image-capable provider. Order mirrors the
63
+ // vision fallback (gemini -> openai -> openrouter). perplexity has no image
64
+ // capability so it's never a candidate.
65
+ export function pickImageProvider(keys) {
66
+ if (keys.gemini)
67
+ return "gemini";
68
+ if (keys.openai)
69
+ return "openai";
70
+ if (keys.openrouter)
71
+ return "openrouter";
72
+ return null;
73
+ }
74
+ function describeBbox(bbox) {
75
+ if (!bbox)
76
+ return "";
77
+ const pct = (v) => `${Math.round(v * 100)}%`;
78
+ const cx = bbox.x + bbox.width / 2;
79
+ const horizontal = cx < 0.34 ? "on the left" : cx > 0.66 ? "on the right" : "in the center";
80
+ return ` They are located ${horizontal} of the frame (roughly x=${pct(bbox.x)}, y=${pct(bbox.y)}, width=${pct(bbox.width)}, height=${pct(bbox.height)}).`;
81
+ }
82
+ export function buildCastIsolationInstruction(member) {
83
+ const wardrobe = member.wardrobe ? ` They are wearing: ${member.wardrobe}.` : "";
84
+ return [
85
+ `Isolate ONLY the single person described as: ${member.description}.${wardrobe}${describeBbox(member.bbox)}`,
86
+ "Cut that one person out cleanly and place them on a fully transparent background.",
87
+ "Preserve their exact face, hairstyle, skin tone, body proportions, and clothing without alteration.",
88
+ "Remove every other person and the entire original background. If multiple people are present, keep only the described one.",
89
+ "Output a single-subject cutout suitable for reuse as a character reference."
90
+ ].join(" ");
91
+ }
92
+ // Extracts the best-frame still for a member and stores it under the fork's
93
+ // public working/cast/ prefix. Returns the public URL (or null if the key is
94
+ // not in a public prefix, which shouldn't happen for fork keys).
95
+ export async function extractCastStill(input) {
96
+ const frame = await extractVideoFrameAsset({
97
+ sourceUrl: input.sourceUrl,
98
+ atMs: input.member.best_frame_ms,
99
+ format: "png"
100
+ });
101
+ const key = input.storage.compositionForkWorkingKey(input.forkId, `cast/${input.member.slug}-still.png`);
102
+ const stored = await input.storage.putBuffer(key, frame.bytes, frame.contentType);
103
+ return { url: stored.url ?? input.storage.getPublicUrl(key) };
104
+ }
105
+ // Runs the image-edit "isolate this person on transparent background" pass over
106
+ // the extracted still and stores the cutout under the fork's public prefix.
107
+ // There is no true person-segmentation primitive in the platform, so this
108
+ // approximates instance masking via a prompt-driven edit (bbox-guided).
109
+ export async function isolateCastMember(input) {
110
+ const provider = pickImageProvider(input.keys);
111
+ if (!provider) {
112
+ throw new Error("No image-capable provider key available for cast isolation");
113
+ }
114
+ const edited = await input.providers.editImage({
115
+ customerId: input.customerId,
116
+ jobId: input.jobId,
117
+ workerId: `cast-isolate:${input.member.slug}`,
118
+ provider,
119
+ model: defaultImageModelForProvider(provider),
120
+ sourceImageUrl: input.stillUrl,
121
+ instruction: buildCastIsolationInstruction(input.member)
122
+ });
123
+ const key = input.storage.compositionForkWorkingKey(input.forkId, `cast/${input.member.slug}.png`);
124
+ const stored = await input.storage.putBuffer(key, edited.bytes, edited.contentType ?? "image/png");
125
+ return { url: stored.url ?? input.storage.getPublicUrl(key) };
126
+ }
127
+ //# sourceMappingURL=cast.js.map
@@ -0,0 +1,92 @@
1
+ import { createId } from "../lib/ids.js";
2
+ import { nowIso } from "../lib/time.js";
3
+ import { serverlessRecords } from "./serverless-records.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 = await serverlessRecords.listChatThreadMetas(input);
27
+ if (input.includeMessages === false) {
28
+ return threads;
29
+ }
30
+ const withMessages = await Promise.all(threads.map(async (thread) => ({
31
+ ...thread,
32
+ messages: await this.loadThreadMessages(thread)
33
+ })));
34
+ return withMessages;
35
+ },
36
+ async getThread(input) {
37
+ const thread = await serverlessRecords.getChatThreadMeta(input.customerId, input.threadId);
38
+ if (!thread) {
39
+ return null;
40
+ }
41
+ return {
42
+ ...thread,
43
+ messages: await this.loadThreadMessages(thread)
44
+ };
45
+ },
46
+ async appendExchange(input) {
47
+ const createdAt = nowIso();
48
+ const storagePrefix = buildThreadStoragePrefix(input.customerId, input.templateId, input.threadId);
49
+ const chunk = {
50
+ version: 1,
51
+ threadId: input.threadId,
52
+ customerId: input.customerId,
53
+ templateId: input.templateId,
54
+ createdAt,
55
+ messages: input.messages
56
+ };
57
+ await storage.putJson(buildThreadChunkKey(storagePrefix, createdAt), chunk);
58
+ const updated = await serverlessRecords.appendChatThreadMessages({
59
+ threadId: input.threadId,
60
+ customerId: input.customerId,
61
+ title: input.title || "New chat",
62
+ templateId: input.templateId,
63
+ storagePrefix,
64
+ tracers: normalizeTracerList(input.tracers),
65
+ messageCountDelta: input.messages.length,
66
+ lastMessageAt: createdAt
67
+ });
68
+ return updated ? this.getThread({ customerId: input.customerId, threadId: updated.id }) : null;
69
+ },
70
+ async updateThread(input) {
71
+ const updated = await serverlessRecords.updateChatThreadState({
72
+ ...input,
73
+ tracers: input.tracers ? normalizeTracerList(input.tracers) : undefined
74
+ });
75
+ return updated ? this.getThread({ customerId: input.customerId, threadId: updated.id }) : null;
76
+ },
77
+ async deleteThread(input) {
78
+ const existing = await serverlessRecords.deleteChatThread(input.customerId, input.threadId);
79
+ if (!existing) {
80
+ return null;
81
+ }
82
+ const keys = await storage.listKeys(`${existing.storagePrefix}/`);
83
+ await Promise.all(keys.map((key) => storage.deleteObject(key)));
84
+ return existing;
85
+ },
86
+ async loadThreadMessages(thread) {
87
+ const keys = (await storage.listKeys(`${thread.storagePrefix}/chunks/`)).sort();
88
+ const messageGroups = await Promise.all(keys.map((key) => readChunkMessages(key)));
89
+ return messageGroups.flat();
90
+ }
91
+ };
92
+ //# sourceMappingURL=chat-threads.js.map