@mevdragon/vidfarm-devcli 0.2.12 → 0.2.14

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 (205) hide show
  1. package/.env.example +37 -1
  2. package/README.md +27 -183
  3. package/SKILL.director.md +293 -0
  4. package/SKILL.platform.md +312 -0
  5. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +275 -0
  6. package/auto-create-hyperframe-templates/extractor-system-prompt.md +128 -0
  7. package/auto-create-hyperframe-templates/input-manifest.schema.json +56 -0
  8. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +118 -0
  9. package/auto-create-hyperframe-templates/production-graph.schema.json +314 -0
  10. package/auto-create-hyperframe-templates/runbook.md +198 -0
  11. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +368 -0
  12. package/auto-create-hyperframe-templates/template-plan.schema.json +311 -0
  13. package/auto-create-hyperframe-templates/template-planner-prompt.md +144 -0
  14. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +116 -0
  15. package/auto-create-templates/extractor-system-prompt.md +6 -1
  16. package/auto-create-templates/template-plan.schema.json +73 -2
  17. package/auto-create-templates/template-planner-prompt.md +62 -1
  18. package/dist/src/account-pages-legacy.js +363 -89
  19. package/dist/src/account-pages-legacy.js.map +1 -0
  20. package/dist/src/account-pages.js +3 -2
  21. package/dist/src/account-pages.js.map +1 -0
  22. package/dist/src/app.js +7096 -1294
  23. package/dist/src/app.js.map +1 -0
  24. package/dist/src/cli.js +115 -29
  25. package/dist/src/cli.js.map +1 -0
  26. package/dist/src/composition-runtime.js +581 -0
  27. package/dist/src/composition-runtime.js.map +1 -0
  28. package/dist/src/config.js +69 -11
  29. package/dist/src/config.js.map +1 -0
  30. package/dist/src/context.js +280 -21
  31. package/dist/src/context.js.map +1 -0
  32. package/dist/src/dev-app-legacy.js +18 -17
  33. package/dist/src/dev-app-legacy.js.map +1 -0
  34. package/dist/src/dev-app.js +1 -0
  35. package/dist/src/dev-app.js.map +1 -0
  36. package/dist/src/dev-serve.js +169 -0
  37. package/dist/src/dev-serve.js.map +1 -0
  38. package/dist/src/domain.js +2 -1
  39. package/dist/src/domain.js.map +1 -0
  40. package/dist/src/editor-chat-history.js +10 -0
  41. package/dist/src/editor-chat-history.js.map +1 -0
  42. package/dist/src/editor-chat.js +135 -25
  43. package/dist/src/editor-chat.js.map +1 -0
  44. package/dist/src/editor-dark-theme.js +1128 -0
  45. package/dist/src/editor-dark-theme.js.map +1 -0
  46. package/dist/src/frontend/flockposter-cache-store.js +10 -2
  47. package/dist/src/frontend/flockposter-cache-store.js.map +1 -0
  48. package/dist/src/frontend/homepage-client.js +6 -3
  49. package/dist/src/frontend/homepage-client.js.map +1 -0
  50. package/dist/src/frontend/homepage-shared.js +1 -0
  51. package/dist/src/frontend/homepage-shared.js.map +1 -0
  52. package/dist/src/frontend/homepage-store.js +1 -0
  53. package/dist/src/frontend/homepage-store.js.map +1 -0
  54. package/dist/src/frontend/homepage-view.js +131 -18
  55. package/dist/src/frontend/homepage-view.js.map +1 -0
  56. package/dist/src/frontend/page-runtime-client.js +27 -3
  57. package/dist/src/frontend/page-runtime-client.js.map +1 -0
  58. package/dist/src/frontend/page-runtime-store.js +1 -0
  59. package/dist/src/frontend/page-runtime-store.js.map +1 -0
  60. package/dist/src/frontend/sentry.js +42 -0
  61. package/dist/src/frontend/sentry.js.map +1 -0
  62. package/dist/src/frontend/template-editor-chat.js +1711 -212
  63. package/dist/src/frontend/template-editor-chat.js.map +1 -0
  64. package/dist/src/help-page.js +333 -0
  65. package/dist/src/help-page.js.map +1 -0
  66. package/dist/src/homepage.js +169 -47
  67. package/dist/src/homepage.js.map +1 -0
  68. package/dist/src/hyperframes/composition.js +180 -0
  69. package/dist/src/hyperframes/composition.js.map +1 -0
  70. package/dist/src/index.js +1 -0
  71. package/dist/src/index.js.map +1 -0
  72. package/dist/src/instrument.js +30 -0
  73. package/dist/src/instrument.js.map +1 -0
  74. package/dist/src/lib/crypto.js +1 -0
  75. package/dist/src/lib/crypto.js.map +1 -0
  76. package/dist/src/lib/dev-log.js +54 -0
  77. package/dist/src/lib/dev-log.js.map +1 -0
  78. package/dist/src/lib/display-name.js +11 -0
  79. package/dist/src/lib/display-name.js.map +1 -0
  80. package/dist/src/lib/ids.js +21 -1
  81. package/dist/src/lib/ids.js.map +1 -0
  82. package/dist/src/lib/images.js +1 -0
  83. package/dist/src/lib/images.js.map +1 -0
  84. package/dist/src/lib/json.js +1 -0
  85. package/dist/src/lib/json.js.map +1 -0
  86. package/dist/src/lib/template-dna.js +1 -0
  87. package/dist/src/lib/template-dna.js.map +1 -0
  88. package/dist/src/lib/template-paths.js +1 -0
  89. package/dist/src/lib/template-paths.js.map +1 -0
  90. package/dist/src/lib/template-style-options.js +29 -3
  91. package/dist/src/lib/template-style-options.js.map +1 -0
  92. package/dist/src/lib/time.js +1 -0
  93. package/dist/src/lib/time.js.map +1 -0
  94. package/dist/src/lib/video-quality-harness.js +60 -0
  95. package/dist/src/lib/video-quality-harness.js.map +1 -0
  96. package/dist/src/page-runtime.js +1 -0
  97. package/dist/src/page-runtime.js.map +1 -0
  98. package/dist/src/page-shell.js +403 -29
  99. package/dist/src/page-shell.js.map +1 -0
  100. package/dist/src/primitive-context.js +144 -8
  101. package/dist/src/primitive-context.js.map +1 -0
  102. package/dist/src/primitive-registry.js +1919 -78
  103. package/dist/src/primitive-registry.js.map +1 -0
  104. package/dist/src/primitive-sdk.js +1 -0
  105. package/dist/src/primitive-sdk.js.map +1 -0
  106. package/dist/src/primitives/remotion/html-image.js +3 -1
  107. package/dist/src/primitives/remotion/html-image.js.map +1 -0
  108. package/dist/src/primitives/remotion/media-slideshow.js +60 -0
  109. package/dist/src/primitives/remotion/media-slideshow.js.map +1 -0
  110. package/dist/src/react-page-shell.js +1 -0
  111. package/dist/src/react-page-shell.js.map +1 -0
  112. package/dist/src/ready-post-schedule-component.js +1 -0
  113. package/dist/src/ready-post-schedule-component.js.map +1 -0
  114. package/dist/src/registry.js +237 -24
  115. package/dist/src/registry.js.map +1 -0
  116. package/dist/src/runtime.js +3 -0
  117. package/dist/src/runtime.js.map +1 -0
  118. package/dist/src/services/api-call-history.js +4 -0
  119. package/dist/src/services/api-call-history.js.map +1 -0
  120. package/dist/src/services/auth.js +40 -50
  121. package/dist/src/services/auth.js.map +1 -0
  122. package/dist/src/services/billing.js +20 -44
  123. package/dist/src/services/billing.js.map +1 -0
  124. package/dist/src/services/chat-threads.js +10 -6
  125. package/dist/src/services/chat-threads.js.map +1 -0
  126. package/dist/src/services/fork-access.js +93 -0
  127. package/dist/src/services/fork-access.js.map +1 -0
  128. package/dist/src/services/fork-manifest.js +43 -0
  129. package/dist/src/services/fork-manifest.js.map +1 -0
  130. package/dist/src/services/ghostcut.js +132 -0
  131. package/dist/src/services/ghostcut.js.map +1 -0
  132. package/dist/src/services/hyperframes.js +1014 -0
  133. package/dist/src/services/hyperframes.js.map +1 -0
  134. package/dist/src/services/job-capacity.js +14 -0
  135. package/dist/src/services/job-capacity.js.map +1 -0
  136. package/dist/src/services/job-logs.js +4 -0
  137. package/dist/src/services/job-logs.js.map +1 -0
  138. package/dist/src/services/jobs.js +99 -91
  139. package/dist/src/services/jobs.js.map +1 -0
  140. package/dist/src/services/media-processing.js +743 -0
  141. package/dist/src/services/media-processing.js.map +1 -0
  142. package/dist/src/services/primitive-media-lambda.js +280 -0
  143. package/dist/src/services/primitive-media-lambda.js.map +1 -0
  144. package/dist/src/services/providers.js +1560 -178
  145. package/dist/src/services/providers.js.map +1 -0
  146. package/dist/src/services/rate-limits.js +3 -2
  147. package/dist/src/services/rate-limits.js.map +1 -0
  148. package/dist/src/services/remotion.js +495 -92
  149. package/dist/src/services/remotion.js.map +1 -0
  150. package/dist/src/services/serverless-auth.js +374 -0
  151. package/dist/src/services/serverless-auth.js.map +1 -0
  152. package/dist/src/services/serverless-jobs.js +1074 -0
  153. package/dist/src/services/serverless-jobs.js.map +1 -0
  154. package/dist/src/services/serverless-provider-keys.js +401 -0
  155. package/dist/src/services/serverless-provider-keys.js.map +1 -0
  156. package/dist/src/services/serverless-records.js +1088 -0
  157. package/dist/src/services/serverless-records.js.map +1 -0
  158. package/dist/src/services/serverless-template-configs.js +67 -0
  159. package/dist/src/services/serverless-template-configs.js.map +1 -0
  160. package/dist/src/services/storage.js +171 -35
  161. package/dist/src/services/storage.js.map +1 -0
  162. package/dist/src/services/template-certification.js +295 -3
  163. package/dist/src/services/template-certification.js.map +1 -0
  164. package/dist/src/services/template-loader.js +45 -1
  165. package/dist/src/services/template-loader.js.map +1 -0
  166. package/dist/src/services/template-runtime-bundles.js +217 -0
  167. package/dist/src/services/template-runtime-bundles.js.map +1 -0
  168. package/dist/src/services/template-sources.js +452 -87
  169. package/dist/src/services/template-sources.js.map +1 -0
  170. package/dist/src/services/video-normalization.js +2 -0
  171. package/dist/src/services/video-normalization.js.map +1 -0
  172. package/dist/src/services/webhooks.js +7 -6
  173. package/dist/src/services/webhooks.js.map +1 -0
  174. package/dist/src/template-editor-pages.js +2051 -1869
  175. package/dist/src/template-editor-pages.js.map +1 -0
  176. package/dist/src/template-editor-shell.js +1376 -181
  177. package/dist/src/template-editor-shell.js.map +1 -0
  178. package/dist/src/template-sdk.js +1 -0
  179. package/dist/src/template-sdk.js.map +1 -0
  180. package/dist/src/worker.js +11 -226
  181. package/dist/src/worker.js.map +1 -0
  182. package/package.json +47 -12
  183. package/public/assets/homepage-client-app.js +34 -8
  184. package/public/assets/page-runtime-client-app.js +44 -18
  185. package/readme.secret.md +89 -0
  186. package/templates/vidfarm_template_0000/README.md +106 -0
  187. package/templates/vidfarm_template_0000/SKILL.md +266 -0
  188. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  189. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  190. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  191. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  192. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  193. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  194. package/templates/vidfarm_template_0000/composition.json +11 -0
  195. package/templates/vidfarm_template_0000/src/lib/images.js +221 -0
  196. package/templates/vidfarm_template_0000/src/remotion/Root.js +33 -0
  197. package/templates/vidfarm_template_0000/src/remotion/index.js +3 -0
  198. package/templates/vidfarm_template_0000/src/sdk.js +3 -0
  199. package/templates/vidfarm_template_0000/src/style-options.js +200 -0
  200. package/templates/vidfarm_template_0000/src/template-dna.js +9 -0
  201. package/templates/vidfarm_template_0000/src/template.js +1566 -0
  202. package/templates/vidfarm_template_0000/template.config.json +21 -0
  203. package/GETTING_STARTED.developers.md +0 -87
  204. package/SKILL.developer.md +0 -1186
  205. package/dist/src/db.js +0 -2007
@@ -0,0 +1,1088 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DeleteCommand, DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, ScanCommand } from "@aws-sdk/lib-dynamodb";
3
+ import { config } from "../config.js";
4
+ import { devLog } from "../lib/dev-log.js";
5
+ import { displayNameFromEmail } from "../lib/display-name.js";
6
+ import { createId, createIdV7 } from "../lib/ids.js";
7
+ import { nowIso } from "../lib/time.js";
8
+ import { ServerlessAuthService } from "./serverless-auth.js";
9
+ const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
10
+ marshallOptions: {
11
+ removeUndefinedValues: true
12
+ }
13
+ });
14
+ const METHOD_COLLECTIONS = {
15
+ archiveReadyPost: "ready_post",
16
+ createCompositionFork: "composition_fork",
17
+ deleteCompositionFork: "composition_fork",
18
+ getCompositionFork: "composition_fork",
19
+ listCompositionForksForCustomer: "composition_fork",
20
+ updateCompositionFork: "composition_fork",
21
+ createEmailChannel: "email_channel",
22
+ createPostSchedule: "post_schedule",
23
+ createReadyPost: "ready_post",
24
+ createTemplateRelease: "template_release",
25
+ createTemplateSource: "template_source",
26
+ createUserAttachment: "user_attachment",
27
+ createUserFileFolder: "user_file_folder",
28
+ createUserTemporaryFile: "user_temporary_file",
29
+ deleteEmailChannel: "email_channel",
30
+ deleteReadyPost: "ready_post",
31
+ deleteUserAttachment: "user_attachment",
32
+ deleteUserFileFolder: "user_file_folder",
33
+ deleteUserTemporaryFile: "user_temporary_file",
34
+ getEmailChannel: "email_channel",
35
+ getEmailChannelByEmail: "email_channel",
36
+ getPostSchedule: "post_schedule",
37
+ getReadyPost: "ready_post",
38
+ getTemplateRelease: "template_release",
39
+ getTemplateSource: "template_source",
40
+ getTemplateSourceByLocation: "template_source",
41
+ getTemplateSourceBySlugId: "template_source",
42
+ getTemplateSourceByTemplateId: "template_source",
43
+ getUserAttachment: "user_attachment",
44
+ getUserTemporaryFile: "user_temporary_file",
45
+ listEmailChannels: "email_channel",
46
+ listPostSchedulesForCalendar: "post_schedule",
47
+ listPostSchedulesForPost: "post_schedule",
48
+ listReadyPosts: "ready_post",
49
+ listTemplateReleases: "template_release",
50
+ listTemplateSources: "template_source",
51
+ listUserAttachments: "user_attachment",
52
+ listUserFileFolders: "user_file_folder",
53
+ listUserTemporaryFiles: "user_temporary_file",
54
+ markPostScheduleCancelled: "post_schedule",
55
+ markPostScheduleFailed: "post_schedule",
56
+ moveUserAttachmentFolder: "user_attachment",
57
+ moveUserFileFolder: "user_file_folder",
58
+ moveUserTemporaryFolder: "user_temporary_file",
59
+ queueWebhookDelivery: "webhook_delivery",
60
+ setReadyPostZip: "ready_post",
61
+ updateEmailChannel: "email_channel",
62
+ updateEmailChannelAvatar: "email_channel",
63
+ updateEmailChannelVerification: "email_channel",
64
+ updatePostScheduleProvider: "post_schedule",
65
+ updateTemplateSource: "template_source",
66
+ upsertExternalPostSchedule: "post_schedule"
67
+ };
68
+ class ServerlessRecordsServiceImpl {
69
+ auth = new ServerlessAuthService();
70
+ async getCustomerById(customerId) {
71
+ return this.auth.getCustomerById(customerId);
72
+ }
73
+ async getCustomerByEmail(email) {
74
+ return this.auth.getCustomerByEmailPublic(email);
75
+ }
76
+ async upsertCustomer(input) {
77
+ return this.auth.upsertCustomer(input);
78
+ }
79
+ async insertApiKey(input) {
80
+ return this.auth.insertApiKey(input);
81
+ }
82
+ async getFirstPaidCustomer() {
83
+ const response = await dynamodb.send(new ScanCommand({
84
+ TableName: requireMainTableName(),
85
+ FilterExpression: "entity_type = :type AND is_paid_plan = :paid",
86
+ ExpressionAttributeValues: {
87
+ ":type": "Customer",
88
+ ":paid": true
89
+ },
90
+ Limit: 1
91
+ }));
92
+ return response.Items?.[0] ? itemToCustomer(response.Items[0]) : null;
93
+ }
94
+ async setCustomerAccessByEmail(input) {
95
+ const normalizedEmail = input.email.trim().toLowerCase();
96
+ const existing = await this.auth.getCustomerByEmailPublic(normalizedEmail);
97
+ return this.auth.upsertCustomer({
98
+ id: existing?.id ?? createId("cus"),
99
+ email: normalizedEmail,
100
+ name: input.name?.trim() || existing?.name || displayNameFromEmail(normalizedEmail),
101
+ defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
102
+ isDeveloper: input.isDeveloper ?? existing?.isDeveloper ?? false,
103
+ isAgency: input.isAgency ?? existing?.isAgency ?? false,
104
+ isPaidPlan: input.isPaidPlan ?? existing?.isPaidPlan ?? false,
105
+ planTier: input.planTier ?? existing?.planTier ?? "client"
106
+ });
107
+ }
108
+ async updateCustomerPaidPlan(customerId, isPaidPlan, planTier = "client") {
109
+ const existing = await this.auth.getCustomerById(customerId);
110
+ if (!existing)
111
+ return null;
112
+ return this.auth.upsertCustomer({ ...existing, isPaidPlan, planTier });
113
+ }
114
+ async updateCustomerAgencyStatus(customerId, isAgency) {
115
+ const existing = await this.auth.getCustomerById(customerId);
116
+ if (!existing)
117
+ return null;
118
+ return this.auth.upsertCustomer({ ...existing, isAgency, planTier: isAgency ? "agency" : existing.planTier });
119
+ }
120
+ async updateCustomerProfile(input) {
121
+ const existing = await this.auth.getCustomerById(input.customerId);
122
+ if (!existing)
123
+ return null;
124
+ return this.auth.upsertCustomer({
125
+ ...existing,
126
+ name: input.name === undefined ? existing.name : input.name,
127
+ about: input.about === undefined ? existing.about : input.about,
128
+ defaultWebhookUrl: input.defaultWebhookUrl === undefined ? existing.defaultWebhookUrl : input.defaultWebhookUrl,
129
+ groupchatUrl: input.groupchatUrl === undefined ? existing.groupchatUrl : input.groupchatUrl,
130
+ flockposterApiKey: input.flockposterApiKey === undefined ? existing.flockposterApiKey : input.flockposterApiKey
131
+ });
132
+ }
133
+ async getLatestApiKeyForCustomer(customerId) {
134
+ const response = await dynamodb.send(new QueryCommand({
135
+ TableName: requireMainTableName(),
136
+ IndexName: "gsi1",
137
+ KeyConditionExpression: "gsi1pk = :pk",
138
+ FilterExpression: "#status = :status",
139
+ ExpressionAttributeNames: {
140
+ "#status": "status"
141
+ },
142
+ ExpressionAttributeValues: {
143
+ ":pk": `CUSTOMER#${customerId}#APIKEYS`,
144
+ ":status": "active"
145
+ },
146
+ ScanIndexForward: false,
147
+ Limit: 1
148
+ }));
149
+ return response.Items?.[0] ?? null;
150
+ }
151
+ async hasCustomerCreditGrant(customerId, grantType) {
152
+ return Boolean(await this.getById("credit_grant", `${customerId}:${grantType}`));
153
+ }
154
+ async insertCustomerCreditGrant(input) {
155
+ await this.putRecord("credit_grant", {
156
+ ...input,
157
+ id: `${input.customerId}:${input.grantType}`,
158
+ customerId: input.customerId
159
+ });
160
+ }
161
+ async grantAgencyClientAccess(input) {
162
+ return this.putRecord("agency_access", {
163
+ id: `${input.agencyCustomerId}:${input.clientCustomerId}`,
164
+ customerId: input.agencyCustomerId,
165
+ agencyCustomerId: input.agencyCustomerId,
166
+ clientCustomerId: input.clientCustomerId
167
+ });
168
+ }
169
+ async revokeAgencyClientAccess(input) {
170
+ const record = await this.getById("agency_access", `${input.agencyCustomerId}:${input.clientCustomerId}`);
171
+ await this.deleteById("agency_access", `${input.agencyCustomerId}:${input.clientCustomerId}`);
172
+ return record;
173
+ }
174
+ async listAgencyClients(input) {
175
+ const links = await this.listByCustomer("agency_access", input.agencyCustomerId);
176
+ const clients = [];
177
+ for (const link of links) {
178
+ const clientId = String(link.clientCustomerId ?? "");
179
+ const customer = clientId ? await this.auth.getCustomerById(clientId) : null;
180
+ if (customer)
181
+ clients.push({ ...link, customer });
182
+ }
183
+ return clients;
184
+ }
185
+ async getAgencyClient(agencyCustomerId, clientCustomerId) {
186
+ return this.getById("agency_access", `${agencyCustomerId}:${clientCustomerId}`);
187
+ }
188
+ async createCompositionFork(input) {
189
+ const timestamp = nowIso();
190
+ return this.putRecord("composition_fork", {
191
+ id: input.id ?? createIdV7("fork"),
192
+ customerId: input.customerId,
193
+ sourceSlugId: input.sourceSlugId,
194
+ sourceVideoId: input.sourceVideoId ?? null,
195
+ parentForkId: input.parentForkId ?? null,
196
+ parentVersion: input.parentVersion ?? null,
197
+ title: input.title ?? null,
198
+ visibility: input.visibility ?? "private",
199
+ latestVersion: 0,
200
+ currentRenderJobId: null,
201
+ deletedAt: null,
202
+ createdAt: timestamp,
203
+ updatedAt: timestamp
204
+ });
205
+ }
206
+ async getCompositionFork(forkId) {
207
+ const fork = await this.getById("composition_fork", forkId);
208
+ if (!fork)
209
+ return null;
210
+ return {
211
+ ...fork,
212
+ sourceVideoId: fork.sourceVideoId ?? null,
213
+ parentForkId: fork.parentForkId ?? null,
214
+ parentVersion: fork.parentVersion ?? null,
215
+ visibility: fork.visibility ?? "private",
216
+ latestVersion: fork.latestVersion ?? 0
217
+ };
218
+ }
219
+ async listCompositionForksForCustomer(customerId) {
220
+ return this.listByCustomer("composition_fork", customerId);
221
+ }
222
+ async listPublicCompositionForks() {
223
+ return (await this.scanCollection("composition_fork"))
224
+ .filter((fork) => !fork.deletedAt && fork.visibility === "public");
225
+ }
226
+ async updateCompositionFork(input) {
227
+ const existing = await this.getCompositionFork(input.forkId);
228
+ if (!existing)
229
+ return null;
230
+ return this.putRecord("composition_fork", {
231
+ ...existing,
232
+ ...input.patch,
233
+ id: existing.id,
234
+ customerId: existing.customerId,
235
+ updatedAt: nowIso()
236
+ });
237
+ }
238
+ async deleteCompositionFork(input) {
239
+ const existing = await this.getCompositionFork(input.forkId);
240
+ if (!existing || existing.customerId !== input.customerId)
241
+ return null;
242
+ return this.putRecord("composition_fork", {
243
+ ...existing,
244
+ deletedAt: nowIso(),
245
+ updatedAt: nowIso()
246
+ });
247
+ }
248
+ async grantForkPermission(input) {
249
+ const existing = (await this.listForkPermissions(input.forkId))
250
+ .find((perm) => perm.granteeType === input.granteeType && perm.granteeId === input.granteeId);
251
+ const timestamp = nowIso();
252
+ const id = existing?.id ?? createId("perm");
253
+ return this.putRecord("fork_permission", {
254
+ id,
255
+ forkId: input.forkId,
256
+ ownerCustomerId: input.ownerCustomerId,
257
+ granteeType: input.granteeType,
258
+ granteeId: input.granteeId,
259
+ role: input.role,
260
+ grantedBy: input.grantedBy,
261
+ expiresAt: input.expiresAt ?? null,
262
+ createdAt: existing?.createdAt ?? timestamp,
263
+ updatedAt: timestamp
264
+ }, { partitionKey: forkPartitionPk("fork_permission", input.forkId) });
265
+ }
266
+ async listForkPermissions(forkId) {
267
+ return this.listByPartition("fork_permission", forkPartitionPk("fork_permission", forkId));
268
+ }
269
+ async listForkPermissionsForGrantee(customerId) {
270
+ return (await this.scanCollection("fork_permission"))
271
+ .filter((perm) => perm.granteeType === "customer" && perm.granteeId === customerId);
272
+ }
273
+ async deleteForkPermission(input) {
274
+ const existing = (await this.listForkPermissions(input.forkId)).find((perm) => perm.id === input.permissionId);
275
+ if (!existing)
276
+ return null;
277
+ await this.deleteById("fork_permission", input.permissionId);
278
+ return existing;
279
+ }
280
+ async createForkShareLink(input) {
281
+ const timestamp = nowIso();
282
+ return this.putRecord("fork_share_link", {
283
+ id: input.token,
284
+ forkId: input.forkId,
285
+ ownerCustomerId: input.ownerCustomerId,
286
+ token: input.token,
287
+ role: input.role,
288
+ createdBy: input.createdBy,
289
+ expiresAt: input.expiresAt ?? null,
290
+ revokedAt: null,
291
+ createdAt: timestamp,
292
+ updatedAt: timestamp
293
+ }, { partitionKey: forkPartitionPk("fork_share_link", input.forkId) });
294
+ }
295
+ async getForkShareLinkByToken(token) {
296
+ return this.getById("fork_share_link", token);
297
+ }
298
+ async listForkShareLinks(forkId) {
299
+ return this.listByPartition("fork_share_link", forkPartitionPk("fork_share_link", forkId));
300
+ }
301
+ async revokeForkShareLink(input) {
302
+ const existing = await this.getForkShareLinkByToken(input.token);
303
+ if (!existing || existing.forkId !== input.forkId)
304
+ return null;
305
+ return this.putRecord("fork_share_link", {
306
+ ...existing,
307
+ revokedAt: nowIso(),
308
+ updatedAt: nowIso()
309
+ }, { partitionKey: forkPartitionPk("fork_share_link", input.forkId) });
310
+ }
311
+ async createForkVersion(input) {
312
+ const timestamp = nowIso();
313
+ return this.putRecord("fork_version", {
314
+ id: `${input.forkId}:${input.version}`,
315
+ forkId: input.forkId,
316
+ ownerCustomerId: input.ownerCustomerId,
317
+ version: input.version,
318
+ reason: input.reason,
319
+ message: input.message ?? null,
320
+ createdBy: input.createdBy,
321
+ storagePrefix: input.storagePrefix,
322
+ renderJobId: input.renderJobId ?? null,
323
+ createdAt: timestamp,
324
+ updatedAt: timestamp
325
+ }, { partitionKey: forkPartitionPk("fork_version", input.forkId) });
326
+ }
327
+ async getForkVersion(forkId, version) {
328
+ return this.getById("fork_version", `${forkId}:${version}`);
329
+ }
330
+ async listForkVersions(input) {
331
+ const limit = Math.max(1, Math.min(input.limit ?? 25, 200));
332
+ let ExclusiveStartKey = decodeDynamoCursor(input.cursor);
333
+ const items = [];
334
+ do {
335
+ const response = await dynamodb.send(new QueryCommand({
336
+ TableName: requireMainTableName(),
337
+ IndexName: "gsi1",
338
+ KeyConditionExpression: "gsi1pk = :pk",
339
+ ExpressionAttributeValues: {
340
+ ":pk": forkPartitionPk("fork_version", input.forkId)
341
+ },
342
+ ScanIndexForward: false,
343
+ Limit: Math.max(1, limit - items.length),
344
+ ExclusiveStartKey
345
+ }));
346
+ items.push(...(response.Items ?? []).map((item) => item.data));
347
+ ExclusiveStartKey = response.LastEvaluatedKey;
348
+ } while (ExclusiveStartKey && items.length < limit);
349
+ return { items, nextCursor: encodeDynamoCursor(ExclusiveStartKey) };
350
+ }
351
+ async createSubmittedVideo(input) {
352
+ const timestamp = nowIso();
353
+ return this.putRecord("submitted_video", {
354
+ id: input.id ?? createId("video"),
355
+ customerId: input.customerId,
356
+ sourceUrl: input.sourceUrl,
357
+ sourceHost: input.sourceHost,
358
+ status: "downloading",
359
+ downloadJobId: input.downloadJobId ?? null,
360
+ videoStorageKey: null,
361
+ videoUrl: null,
362
+ thumbnailUrl: null,
363
+ title: null,
364
+ durationSeconds: null,
365
+ defaultForkId: null,
366
+ errorMessage: null,
367
+ createdAt: timestamp,
368
+ updatedAt: timestamp
369
+ });
370
+ }
371
+ async getSubmittedVideo(videoId) {
372
+ return this.getById("submitted_video", videoId);
373
+ }
374
+ async findSubmittedVideoByUrl(sourceUrl) {
375
+ return this.findByScan("submitted_video", (item) => item.sourceUrl === sourceUrl);
376
+ }
377
+ async updateSubmittedVideo(input) {
378
+ const existing = await this.getSubmittedVideo(input.videoId);
379
+ if (!existing)
380
+ return null;
381
+ return this.putRecord("submitted_video", {
382
+ ...existing,
383
+ ...input.patch,
384
+ id: existing.id,
385
+ customerId: existing.customerId,
386
+ updatedAt: nowIso()
387
+ });
388
+ }
389
+ async listSubmittedVideosForCustomer(customerId) {
390
+ return this.listByCustomer("submitted_video", customerId);
391
+ }
392
+ async listSubmittedVideosFeed(input = {}) {
393
+ const limit = Math.max(1, Math.min(input.limit ?? 50, 200));
394
+ const all = (await this.scanCollection("submitted_video"))
395
+ .filter((video) => video.status === "ready");
396
+ return all
397
+ .sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")))
398
+ .slice(0, limit);
399
+ }
400
+ async createReadyPost(input) {
401
+ const timestamp = nowIso();
402
+ return this.putRecord("ready_post", {
403
+ id: input.id ?? createId("post"),
404
+ customerId: input.customerId,
405
+ status: input.status ?? "ready",
406
+ source: input.source ?? "hyperframes",
407
+ compositionId: input.compositionId ?? null,
408
+ tracer: input.tracer ?? null,
409
+ title: input.title ?? null,
410
+ caption: input.caption ?? "",
411
+ pinnedComment: input.pinnedComment ?? null,
412
+ mediaAssets: input.mediaAssets ?? [],
413
+ sharePasswordHash: input.sharePasswordHash ?? null,
414
+ zipStorageKey: input.zipStorageKey ?? null,
415
+ zipPublicUrl: input.zipPublicUrl ?? null,
416
+ archivedAt: input.archivedAt ?? null,
417
+ deletedAt: input.deletedAt ?? null,
418
+ createdAt: input.createdAt ?? timestamp,
419
+ updatedAt: timestamp
420
+ });
421
+ }
422
+ async getReadyPost(postId) {
423
+ return this.getById("ready_post", postId);
424
+ }
425
+ async listReadyPosts(customerId) {
426
+ return this.listByCustomer("ready_post", customerId);
427
+ }
428
+ async archiveReadyPost(customerId, postId) {
429
+ const post = await this.getReadyPost(postId);
430
+ if (!post || post.customerId !== customerId)
431
+ return null;
432
+ return this.putRecord("ready_post", { ...post, status: "archived", archivedAt: nowIso(), updatedAt: nowIso() });
433
+ }
434
+ async deleteReadyPost(customerId, postId) {
435
+ const post = await this.getReadyPost(postId);
436
+ if (!post || post.customerId !== customerId)
437
+ return null;
438
+ const updated = await this.putRecord("ready_post", { ...post, status: "deleted", deletedAt: nowIso(), updatedAt: nowIso() });
439
+ return updated;
440
+ }
441
+ async setReadyPostZip(input) {
442
+ const post = await this.getReadyPost(input.postId);
443
+ if (!post || post.customerId !== input.customerId)
444
+ return null;
445
+ return this.putRecord("ready_post", {
446
+ ...post,
447
+ zipStorageKey: input.zipStorageKey,
448
+ zipPublicUrl: input.zipPublicUrl,
449
+ updatedAt: nowIso()
450
+ });
451
+ }
452
+ async createPostSchedule(input) {
453
+ const timestamp = nowIso();
454
+ return this.putRecord("post_schedule", {
455
+ id: input.id ?? createId("sched"),
456
+ customerId: input.customerId,
457
+ readyPostId: input.readyPostId ?? null,
458
+ managedBy: input.managedBy ?? "vidfarm",
459
+ provider: input.provider ?? "flockposter",
460
+ destinationType: input.destinationType ?? "flockposter",
461
+ destinationId: input.destinationId ?? "",
462
+ scheduledAt: input.scheduledAt ?? timestamp,
463
+ timezone: input.timezone ?? null,
464
+ status: input.status ?? "scheduled",
465
+ providerStatus: input.providerStatus ?? null,
466
+ providerScheduleId: input.providerScheduleId ?? null,
467
+ title: input.title ?? null,
468
+ summary: input.summary ?? null,
469
+ url: input.url ?? null,
470
+ additionalNotes: input.additionalNotes ?? null,
471
+ providerPayload: input.providerPayload ?? {},
472
+ providerResponse: input.providerResponse ?? {},
473
+ lastSyncedAt: input.lastSyncedAt ?? null,
474
+ lastError: input.lastError ?? null,
475
+ cancelledAt: input.cancelledAt ?? null,
476
+ sentAt: input.sentAt ?? null,
477
+ createdAt: input.createdAt ?? timestamp,
478
+ updatedAt: timestamp
479
+ });
480
+ }
481
+ async getPostSchedule(customerId, scheduleId) {
482
+ const schedule = await this.getById("post_schedule", scheduleId);
483
+ return schedule?.customerId === customerId ? schedule : null;
484
+ }
485
+ async listPostSchedulesForPost(customerId, readyPostId) {
486
+ return (await this.listByCustomer("post_schedule", customerId)).filter((schedule) => schedule.readyPostId === readyPostId);
487
+ }
488
+ async listPostSchedulesForCalendar(input) {
489
+ return (await this.listByCustomer("post_schedule", input.customerId)).filter((schedule) => {
490
+ const scheduledAt = String(schedule.scheduledAt ?? "");
491
+ if (input.startTime && scheduledAt < input.startTime)
492
+ return false;
493
+ if (input.endTime && scheduledAt > input.endTime)
494
+ return false;
495
+ return true;
496
+ });
497
+ }
498
+ async updatePostScheduleProvider(input) {
499
+ return this.mergeByIdForCustomer("post_schedule", input.customerId, input.scheduleId, input);
500
+ }
501
+ async markPostScheduleCancelled(input) {
502
+ return this.mergeByIdForCustomer("post_schedule", input.customerId, input.scheduleId, {
503
+ ...input,
504
+ status: "cancelled",
505
+ cancelledAt: nowIso()
506
+ });
507
+ }
508
+ async markPostScheduleFailed(input) {
509
+ return this.mergeByIdForCustomer("post_schedule", input.customerId, input.scheduleId, {
510
+ ...input,
511
+ status: "failed",
512
+ lastError: input.lastError ?? "Schedule failed"
513
+ });
514
+ }
515
+ async upsertExternalPostSchedule(input) {
516
+ const existing = (await this.listByCustomer("post_schedule", input.customerId))
517
+ .find((schedule) => schedule.providerScheduleId === input.providerScheduleId);
518
+ return this.createPostSchedule({ ...existing, ...input, managedBy: "external" });
519
+ }
520
+ async createEmailChannel(input) {
521
+ const timestamp = nowIso();
522
+ return this.putRecord("email_channel", {
523
+ id: input.id ?? createId("email"),
524
+ customerId: input.customerId,
525
+ title: input.title ?? input.email,
526
+ email: input.email,
527
+ note: input.note ?? null,
528
+ avatarUrl: input.avatarUrl ?? null,
529
+ avatarStorageKey: input.avatarStorageKey ?? null,
530
+ status: input.status ?? "unverified",
531
+ verificationTokenHash: input.verificationTokenHash ?? null,
532
+ verificationSentAt: input.verificationSentAt ?? null,
533
+ verifiedAt: input.verifiedAt ?? null,
534
+ createdAt: input.createdAt ?? timestamp,
535
+ updatedAt: timestamp
536
+ });
537
+ }
538
+ async listEmailChannels(customerId) {
539
+ return this.listByCustomer("email_channel", customerId);
540
+ }
541
+ async getEmailChannel(customerId, channelId) {
542
+ const channel = await this.getById("email_channel", channelId);
543
+ return channel?.customerId === customerId ? channel : null;
544
+ }
545
+ async getEmailChannelByEmail(customerId, email) {
546
+ return (await this.listEmailChannels(customerId)).find((channel) => channel.email.toLowerCase() === email.toLowerCase()) ?? null;
547
+ }
548
+ async updateEmailChannel(input) {
549
+ return this.mergeByIdForCustomer("email_channel", input.customerId, input.channelId, input);
550
+ }
551
+ async updateEmailChannelAvatar(input) {
552
+ await this.updateEmailChannel(input);
553
+ }
554
+ async updateEmailChannelVerification(input) {
555
+ return this.updateEmailChannel(input);
556
+ }
557
+ async verifyEmailChannelByTokenHash(tokenHash) {
558
+ const response = await dynamodb.send(new ScanCommand({
559
+ TableName: requireMainTableName(),
560
+ FilterExpression: "entity_type = :type AND verificationTokenHash = :token",
561
+ ExpressionAttributeValues: {
562
+ ":type": "serverless_record#email_channel",
563
+ ":token": tokenHash
564
+ },
565
+ Limit: 1
566
+ }));
567
+ const channel = response.Items?.[0]?.data;
568
+ if (!channel)
569
+ return null;
570
+ return this.putRecord("email_channel", {
571
+ ...channel,
572
+ status: "verified",
573
+ verifiedAt: nowIso(),
574
+ verificationTokenHash: null,
575
+ updatedAt: nowIso()
576
+ });
577
+ }
578
+ async deleteEmailChannel(customerId, channelId) {
579
+ const existing = await this.getEmailChannel(customerId, channelId);
580
+ if (existing)
581
+ await this.deleteById("email_channel", channelId);
582
+ }
583
+ async createUserAttachment(input) {
584
+ const timestamp = nowIso();
585
+ return this.putRecord("user_attachment", {
586
+ id: input.id ?? createId("att"),
587
+ customerId: input.customerId,
588
+ fileName: input.fileName,
589
+ contentType: input.contentType ?? "application/octet-stream",
590
+ sizeBytes: input.sizeBytes ?? 0,
591
+ storageKey: input.storageKey,
592
+ folderPath: normalizeFolder(input.folderPath),
593
+ publicUrl: input.publicUrl ?? null,
594
+ createdAt: input.createdAt ?? timestamp
595
+ });
596
+ }
597
+ async listUserAttachments(customerId) {
598
+ return this.listByCustomer("user_attachment", customerId);
599
+ }
600
+ async getUserAttachment(customerId, attachmentId) {
601
+ const attachment = await this.getById("user_attachment", attachmentId);
602
+ return attachment?.customerId === customerId ? attachment : null;
603
+ }
604
+ async deleteUserAttachment(customerId, attachmentId) {
605
+ const existing = await this.getUserAttachment(customerId, attachmentId);
606
+ if (existing)
607
+ await this.deleteById("user_attachment", attachmentId);
608
+ }
609
+ async createUserTemporaryFile(input) {
610
+ const timestamp = nowIso();
611
+ return this.putRecord("user_temporary_file", {
612
+ id: input.id ?? createId("tmp"),
613
+ customerId: input.customerId,
614
+ fileName: input.fileName,
615
+ contentType: input.contentType ?? "application/octet-stream",
616
+ sizeBytes: input.sizeBytes ?? 0,
617
+ storageKey: input.storageKey,
618
+ folderPath: normalizeFolder(input.folderPath),
619
+ expiresAt: input.expiresAt ?? timestamp,
620
+ createdAt: input.createdAt ?? timestamp
621
+ });
622
+ }
623
+ async listUserTemporaryFiles(customerId) {
624
+ return this.listByCustomer("user_temporary_file", customerId);
625
+ }
626
+ async getUserTemporaryFile(customerId, fileId) {
627
+ const file = await this.getById("user_temporary_file", fileId);
628
+ return file?.customerId === customerId ? file : null;
629
+ }
630
+ async deleteUserTemporaryFile(customerId, fileId) {
631
+ const existing = await this.getUserTemporaryFile(customerId, fileId);
632
+ if (existing)
633
+ await this.deleteById("user_temporary_file", fileId);
634
+ }
635
+ async deleteExpiredUserTemporaryFiles() {
636
+ const now = nowIso();
637
+ const files = (await this.scanCollection("user_temporary_file"))
638
+ .filter(isExpiredUserTemporaryFileRecord);
639
+ const expired = files.filter((file) => file.expiresAt <= now);
640
+ for (const file of expired)
641
+ await this.deleteById("user_temporary_file", file.id);
642
+ return expired;
643
+ }
644
+ async createUserFileFolder(customerId, scope, folderPath) {
645
+ const normalized = normalizeFolder(folderPath);
646
+ await this.putRecord("user_file_folder", {
647
+ id: `${customerId}:${scope}:${normalized}`,
648
+ customerId,
649
+ scope,
650
+ folderPath: normalized
651
+ });
652
+ }
653
+ async listUserFileFolders(customerId, scope) {
654
+ return (await this.listByCustomer("user_file_folder", customerId))
655
+ .filter((folder) => folder.scope === scope)
656
+ .map((folder) => String(folder.folderPath));
657
+ }
658
+ async moveUserFileFolder(customerId, scope, fromFolder, toFolder) {
659
+ await this.deleteUserFileFolder(customerId, scope, fromFolder);
660
+ await this.createUserFileFolder(customerId, scope, toFolder);
661
+ }
662
+ async deleteUserFileFolder(customerId, scope, folderPath) {
663
+ await this.deleteById("user_file_folder", `${customerId}:${scope}:${normalizeFolder(folderPath)}`);
664
+ }
665
+ async moveUserAttachmentFolder(customerId, fromFolder, toFolder) {
666
+ await this.moveCollectionFolder("user_attachment", customerId, fromFolder, toFolder);
667
+ }
668
+ async deleteUserAttachmentFolder(customerId, folderPath) {
669
+ return this.deleteCollectionFolder("user_attachment", customerId, folderPath);
670
+ }
671
+ async moveUserTemporaryFolder(customerId, fromFolder, toFolder) {
672
+ await this.moveCollectionFolder("user_temporary_file", customerId, fromFolder, toFolder);
673
+ }
674
+ async deleteUserTemporaryFolder(customerId, folderPath) {
675
+ return this.deleteCollectionFolder("user_temporary_file", customerId, folderPath);
676
+ }
677
+ async queueWebhookDelivery(input) {
678
+ const timestamp = nowIso();
679
+ await this.putRecord("webhook_delivery", {
680
+ id: typeof input.id === "string" ? input.id : createId("webhook"),
681
+ customerId: typeof input.customerId === "string" ? input.customerId : undefined,
682
+ jobId: input.jobId,
683
+ destinationUrl: input.destinationUrl,
684
+ destination_url: input.destinationUrl,
685
+ eventType: input.eventType,
686
+ event_type: input.eventType,
687
+ payload: input.payload,
688
+ payload_json: JSON.stringify(input.payload ?? {}),
689
+ status: "pending",
690
+ attemptCount: 0,
691
+ attempt_count: 0,
692
+ nextAttemptAt: timestamp,
693
+ next_attempt_at: timestamp,
694
+ createdAt: timestamp,
695
+ updatedAt: timestamp
696
+ });
697
+ }
698
+ async listPendingWebhookDeliveries(limit = 100) {
699
+ const response = await dynamodb.send(new ScanCommand({
700
+ TableName: requireMainTableName(),
701
+ FilterExpression: "entity_type = :type AND #data.#status = :status",
702
+ ExpressionAttributeNames: { "#data": "data", "#status": "status" },
703
+ ExpressionAttributeValues: {
704
+ ":type": "serverless_record#webhook_delivery",
705
+ ":status": "pending"
706
+ },
707
+ Limit: limit
708
+ }));
709
+ return (response.Items ?? []).map((item) => item.data);
710
+ }
711
+ async markWebhookDelivery(input) {
712
+ const existing = await this.getById("webhook_delivery", input.id);
713
+ if (!existing)
714
+ return;
715
+ await this.putRecord("webhook_delivery", {
716
+ ...existing,
717
+ ...input,
718
+ attempt_count: input.attemptCount ?? input.attempt_count ?? existing.attempt_count,
719
+ next_attempt_at: input.nextAttemptAt ?? input.next_attempt_at ?? existing.next_attempt_at,
720
+ updatedAt: nowIso()
721
+ });
722
+ }
723
+ async listChatThreadMetas(input) {
724
+ const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
725
+ return (await this.listByCustomer("chat_thread", input.customerId))
726
+ .filter((thread) => !input.templateId || thread.templateId === input.templateId)
727
+ .sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))
728
+ .slice(0, limit);
729
+ }
730
+ async getChatThreadMeta(customerId, threadId) {
731
+ const thread = await this.getById("chat_thread", threadId);
732
+ return thread?.customerId === customerId ? thread : null;
733
+ }
734
+ async appendChatThreadMessages(input) {
735
+ const existing = await this.getChatThreadMeta(input.customerId, input.threadId);
736
+ const timestamp = nowIso();
737
+ const thread = {
738
+ id: input.threadId,
739
+ customerId: input.customerId,
740
+ templateId: input.templateId,
741
+ title: input.title,
742
+ storagePrefix: input.storagePrefix,
743
+ tracers: input.tracers,
744
+ messageCount: (existing?.messageCount ?? 0) + input.messageCountDelta,
745
+ lastMessageAt: input.lastMessageAt,
746
+ hiddenFromView: existing?.hiddenFromView ?? false,
747
+ archivedAt: existing?.archivedAt ?? null,
748
+ createdAt: existing?.createdAt ?? timestamp,
749
+ updatedAt: timestamp,
750
+ messages: []
751
+ };
752
+ return this.putRecord("chat_thread", thread);
753
+ }
754
+ async updateChatThreadState(input) {
755
+ const existing = await this.getChatThreadMeta(input.customerId, input.threadId);
756
+ if (!existing)
757
+ return null;
758
+ return this.putRecord("chat_thread", {
759
+ ...existing,
760
+ title: input.title ?? existing.title,
761
+ tracers: input.tracers ?? existing.tracers,
762
+ hiddenFromView: input.hiddenFromView ?? existing.hiddenFromView,
763
+ archivedAt: input.archivedAt === undefined ? existing.archivedAt : input.archivedAt,
764
+ updatedAt: nowIso()
765
+ });
766
+ }
767
+ async deleteChatThread(customerId, threadId) {
768
+ const existing = await this.getChatThreadMeta(customerId, threadId);
769
+ if (!existing)
770
+ return null;
771
+ await this.deleteById("chat_thread", threadId);
772
+ return existing;
773
+ }
774
+ async createTemplateSource(input) {
775
+ return this.putRecord("template_source", { id: input.id ?? createId("tsrc"), ...input });
776
+ }
777
+ async updateTemplateSource(input) {
778
+ const existing = await this.getById("template_source", String(input.id));
779
+ return this.putRecord("template_source", { ...existing, ...input, updatedAt: nowIso() });
780
+ }
781
+ async getTemplateSource(sourceId) {
782
+ return this.getById("template_source", sourceId);
783
+ }
784
+ async getTemplateSourceByTemplateId(templateId) {
785
+ return this.findByScan("template_source", (item) => item.templateId === templateId);
786
+ }
787
+ async getTemplateSourceBySlugId(slugId) {
788
+ return this.findByScan("template_source", (item) => item.slugId === slugId);
789
+ }
790
+ async getTemplateSourceByLocation(repoUrl, branch, templateModulePath) {
791
+ return this.findByScan("template_source", (item) => item.repoUrl === repoUrl && item.branch === branch && item.templateModulePath === templateModulePath);
792
+ }
793
+ async listTemplateSources() {
794
+ return this.scanCollection("template_source");
795
+ }
796
+ async createTemplateRelease(input) {
797
+ return this.putRecord("template_release", { id: input.id ?? createId("trel"), ...input });
798
+ }
799
+ async getTemplateRelease(releaseId) {
800
+ return this.getById("template_release", releaseId);
801
+ }
802
+ async getTemplateReleaseBySourceAndCommit(sourceId, commitSha) {
803
+ return this.findByScan("template_release", (item) => item.sourceId === sourceId && item.commitSha === commitSha);
804
+ }
805
+ async getLatestTemplateReleaseForSource(sourceId) {
806
+ return (await this.scanCollection("template_release"))
807
+ .filter((release) => release.sourceId === sourceId)
808
+ .sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")))[0] ?? null;
809
+ }
810
+ async listTemplateReleases() {
811
+ return this.scanCollection("template_release");
812
+ }
813
+ async listActiveTemplateReleases() {
814
+ const releases = [];
815
+ let cursor = null;
816
+ do {
817
+ const page = await this.listActiveTemplateReleasePage({ cursor, limit: 500 });
818
+ releases.push(...page.items);
819
+ cursor = page.nextCursor;
820
+ } while (cursor);
821
+ return releases;
822
+ }
823
+ async listActiveTemplateReleasePage(input = {}) {
824
+ const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
825
+ const releases = [];
826
+ let ExclusiveStartKey = decodeDynamoCursor(input.cursor);
827
+ do {
828
+ const response = await dynamodb.send(new QueryCommand({
829
+ TableName: requireMainTableName(),
830
+ IndexName: "gsi1",
831
+ KeyConditionExpression: "gsi1pk = :pk",
832
+ FilterExpression: "#data.#record_kind = :kind",
833
+ ExpressionAttributeNames: {
834
+ "#data": "data",
835
+ "#record_kind": "recordKind"
836
+ },
837
+ ExpressionAttributeValues: {
838
+ ":pk": collectionPk("active_template"),
839
+ ":kind": "active_template_release"
840
+ },
841
+ ScanIndexForward: false,
842
+ Limit: Math.max(1, limit - releases.length),
843
+ ExclusiveStartKey
844
+ }));
845
+ releases.push(...(response.Items ?? []).map((item) => item.data));
846
+ ExclusiveStartKey = response.LastEvaluatedKey;
847
+ } while (ExclusiveStartKey && releases.length < limit);
848
+ return {
849
+ items: releases,
850
+ nextCursor: encodeDynamoCursor(ExclusiveStartKey)
851
+ };
852
+ }
853
+ async getActiveTemplateReleaseByTemplateId(templateId) {
854
+ const direct = await this.getById("active_template", templateId);
855
+ if (direct?.recordKind === "active_template_release") {
856
+ return direct;
857
+ }
858
+ const alias = await this.getById("active_template", activeTemplateSlugAliasId(templateId));
859
+ if (alias?.recordKind === "active_template_slug_alias" && typeof alias.templateId === "string") {
860
+ const release = await this.getById("active_template", alias.templateId);
861
+ return release?.recordKind === "active_template_release"
862
+ ? release
863
+ : null;
864
+ }
865
+ return null;
866
+ }
867
+ async putActiveTemplateRelease(release) {
868
+ const slugId = release.templateSnapshot?.slugId;
869
+ const data = await this.putRecord("active_template", {
870
+ ...release,
871
+ id: release.templateId,
872
+ recordKind: "active_template_release",
873
+ updatedAt: release.activatedAt ?? release.updatedAt ?? nowIso()
874
+ });
875
+ if (slugId) {
876
+ await this.putRecord("active_template", {
877
+ id: activeTemplateSlugAliasId(slugId),
878
+ recordKind: "active_template_slug_alias",
879
+ templateId: release.templateId,
880
+ slugId,
881
+ releaseId: release.id,
882
+ updatedAt: release.activatedAt ?? release.updatedAt ?? nowIso()
883
+ });
884
+ }
885
+ return data;
886
+ }
887
+ async updateTemplateReleaseStatus(input) {
888
+ const existing = await this.getById("template_release", input.releaseId);
889
+ if (existing)
890
+ await this.putRecord("template_release", { ...existing, ...input, id: input.releaseId, updatedAt: nowIso() });
891
+ }
892
+ async clearActiveTemplateReleases(templateId) {
893
+ const active = await this.getActiveTemplateReleaseByTemplateId(templateId);
894
+ if (active) {
895
+ await this.putRecord("template_release", { ...active, id: String(active.id), status: "approved", activatedAt: null, updatedAt: nowIso() });
896
+ await this.deleteById("active_template", active.templateId);
897
+ if (active.templateSnapshot?.slugId) {
898
+ await this.deleteById("active_template", activeTemplateSlugAliasId(active.templateSnapshot.slugId));
899
+ }
900
+ }
901
+ }
902
+ async call(method, args) {
903
+ const collection = METHOD_COLLECTIONS[method];
904
+ if (!collection) {
905
+ throw new Error(`Serverless record method ${method} is not implemented.`);
906
+ }
907
+ devLog("serverless_records.dynamic_call", { method, collection });
908
+ throw new Error(`Serverless record method ${method} requires a typed implementation.`);
909
+ }
910
+ async mergeByIdForCustomer(collection, customerId, id, patch) {
911
+ const existing = await this.getById(collection, id);
912
+ if (!existing || existing.customerId !== customerId)
913
+ return null;
914
+ return this.putRecord(collection, { ...existing, ...patch, id, customerId, updatedAt: nowIso() });
915
+ }
916
+ async moveCollectionFolder(collection, customerId, fromFolder, toFolder) {
917
+ for (const record of await this.listByCustomer(collection, customerId)) {
918
+ if (normalizeFolder(String(record.folderPath ?? "")) === normalizeFolder(fromFolder)) {
919
+ await this.putRecord(collection, { ...record, folderPath: normalizeFolder(toFolder), updatedAt: nowIso() });
920
+ }
921
+ }
922
+ }
923
+ async deleteCollectionFolder(collection, customerId, folderPath) {
924
+ const deleted = [];
925
+ for (const record of await this.listByCustomer(collection, customerId)) {
926
+ if (normalizeFolder(String(record.folderPath ?? "")) === normalizeFolder(folderPath)) {
927
+ await this.deleteById(collection, String(record.id));
928
+ deleted.push(record);
929
+ }
930
+ }
931
+ return deleted;
932
+ }
933
+ async findByScan(collection, predicate) {
934
+ return (await this.scanCollection(collection)).find(predicate) ?? null;
935
+ }
936
+ async scanCollection(collection) {
937
+ const results = [];
938
+ let exclusiveStartKey;
939
+ // DDB Scan with FilterExpression only reads one page at a time; matching
940
+ // items later in the table are silently dropped unless you follow
941
+ // LastEvaluatedKey. Paginate until DDB stops returning a cursor.
942
+ do {
943
+ const response = await dynamodb.send(new ScanCommand({
944
+ TableName: requireMainTableName(),
945
+ FilterExpression: "entity_type = :type",
946
+ ExpressionAttributeValues: {
947
+ ":type": entityType(collection)
948
+ },
949
+ ExclusiveStartKey: exclusiveStartKey
950
+ }));
951
+ for (const item of response.Items ?? []) {
952
+ results.push(item.data);
953
+ }
954
+ exclusiveStartKey = response.LastEvaluatedKey;
955
+ } while (exclusiveStartKey);
956
+ return results;
957
+ }
958
+ async listByCustomer(collection, customerId) {
959
+ return this.listByPartition(collection, customerCollectionPk(collection, customerId));
960
+ }
961
+ async listByPartition(_collection, partitionKey) {
962
+ const response = await dynamodb.send(new QueryCommand({
963
+ TableName: requireMainTableName(),
964
+ IndexName: "gsi1",
965
+ KeyConditionExpression: "gsi1pk = :pk",
966
+ ExpressionAttributeValues: {
967
+ ":pk": partitionKey
968
+ },
969
+ ScanIndexForward: false
970
+ }));
971
+ return (response.Items ?? []).map((item) => item.data);
972
+ }
973
+ async getById(collection, id) {
974
+ const response = await dynamodb.send(new GetCommand({
975
+ TableName: requireMainTableName(),
976
+ Key: recordKey(collection, id)
977
+ }));
978
+ return response.Item?.data ?? null;
979
+ }
980
+ async deleteById(collection, id) {
981
+ await dynamodb.send(new DeleteCommand({
982
+ TableName: requireMainTableName(),
983
+ Key: recordKey(collection, id)
984
+ }));
985
+ devLog("serverless_records.deleted", { collection, id });
986
+ }
987
+ async putRecord(collection, record, options) {
988
+ const timestamp = nowIso();
989
+ const id = typeof record.id === "string" && record.id ? record.id : createId("rec");
990
+ const data = {
991
+ ...record,
992
+ id,
993
+ createdAt: record.createdAt ?? timestamp,
994
+ updatedAt: record.updatedAt ?? timestamp
995
+ };
996
+ const gsi1pk = options?.partitionKey
997
+ ?? (data.customerId ? customerCollectionPk(collection, String(data.customerId)) : collectionPk(collection));
998
+ await dynamodb.send(new PutCommand({
999
+ TableName: requireMainTableName(),
1000
+ Item: {
1001
+ ...recordKey(collection, id),
1002
+ entity_type: entityType(collection),
1003
+ gsi1pk,
1004
+ gsi1sk: `${String(data.updatedAt)}#${id}`,
1005
+ data
1006
+ }
1007
+ }));
1008
+ devLog("serverless_records.put", {
1009
+ collection,
1010
+ id,
1011
+ customer_id: data.customerId ?? null,
1012
+ partition_key: gsi1pk
1013
+ });
1014
+ return data;
1015
+ }
1016
+ }
1017
+ function forkPartitionPk(collection, forkId) {
1018
+ return `FORK#${forkId}#${collection}`;
1019
+ }
1020
+ function requireMainTableName() {
1021
+ if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
1022
+ throw new Error("Serverless record store requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
1023
+ }
1024
+ return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
1025
+ }
1026
+ function recordKey(collection, id) {
1027
+ return {
1028
+ pk: `${collectionPk(collection)}#${id}`,
1029
+ sk: "META"
1030
+ };
1031
+ }
1032
+ function collectionPk(collection) {
1033
+ return `SERVERLESS_RECORD#${collection}`;
1034
+ }
1035
+ function activeTemplateSlugAliasId(slugId) {
1036
+ return `slug#${slugId}`;
1037
+ }
1038
+ function encodeDynamoCursor(value) {
1039
+ return value ? Buffer.from(JSON.stringify(value), "utf8").toString("base64url") : null;
1040
+ }
1041
+ function decodeDynamoCursor(raw) {
1042
+ if (!raw) {
1043
+ return undefined;
1044
+ }
1045
+ try {
1046
+ const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
1047
+ return parsed && typeof parsed === "object" ? parsed : undefined;
1048
+ }
1049
+ catch {
1050
+ return undefined;
1051
+ }
1052
+ }
1053
+ function customerCollectionPk(collection, customerId) {
1054
+ return `CUSTOMER#${customerId}#${collection}`;
1055
+ }
1056
+ function entityType(collection) {
1057
+ return `serverless_record#${collection}`;
1058
+ }
1059
+ function normalizeFolder(value) {
1060
+ if (typeof value !== "string")
1061
+ return "";
1062
+ return value.trim().replace(/^\/+|\/+$/g, "");
1063
+ }
1064
+ function isExpiredUserTemporaryFileRecord(record) {
1065
+ return typeof record.id === "string"
1066
+ && record.id.length > 0
1067
+ && typeof record.expiresAt === "string"
1068
+ && record.expiresAt.length > 0;
1069
+ }
1070
+ function itemToCustomer(item) {
1071
+ return {
1072
+ id: String(item.customer_id),
1073
+ email: String(item.email),
1074
+ name: typeof item.name === "string" ? item.name : null,
1075
+ defaultWebhookUrl: typeof item.default_webhook_url === "string" ? item.default_webhook_url : null,
1076
+ isDeveloper: Boolean(item.is_developer),
1077
+ isAgency: Boolean(item.is_agency),
1078
+ isPaidPlan: Boolean(item.is_paid_plan),
1079
+ planTier: item.plan_tier === "standalone" || item.plan_tier === "agency" ? item.plan_tier : "client",
1080
+ about: typeof item.about === "string" ? item.about : null,
1081
+ groupchatUrl: typeof item.groupchat_url === "string" ? item.groupchat_url : null,
1082
+ flockposterApiKey: typeof item.flockposter_api_key === "string" ? item.flockposter_api_key : null,
1083
+ createdAt: typeof item.created_at === "string" ? item.created_at : nowIso(),
1084
+ updatedAt: typeof item.updated_at === "string" ? item.updated_at : nowIso()
1085
+ };
1086
+ }
1087
+ export const serverlessRecords = new ServerlessRecordsServiceImpl();
1088
+ //# sourceMappingURL=serverless-records.js.map