@mevdragon/vidfarm-devcli 0.20.6 → 0.20.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 (157) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,262 +0,0 @@
1
- import { DynamoDBClient, GetItemCommand, TransactWriteItemsCommand } from "@aws-sdk/client-dynamodb";
2
- import { config } from "../config.js";
3
- const dynamodb = new DynamoDBClient({});
4
- const OPERATIONS_TABLE_NAME = config.VIDFARM_RATE_LIMITS_TABLE_NAME?.trim() || "";
5
- const REST_LIMIT_SUMMARY = "General REST API limits: max 10 requests per second, 300 requests per minute, and 5000 requests per hour.";
6
- export class RateLimitExceededError extends Error {
7
- type;
8
- status;
9
- constructor(message, type, status) {
10
- super(message);
11
- this.type = type;
12
- this.status = status;
13
- }
14
- }
15
- const OPERATION_LIMITS = {
16
- client: {
17
- month: 750,
18
- week: 400,
19
- five_hour: 200
20
- },
21
- standalone: {
22
- month: 2500,
23
- week: 1250,
24
- five_hour: 500
25
- },
26
- agency: {
27
- month: 10_000,
28
- week: 5_000,
29
- five_hour: 2_000
30
- }
31
- };
32
- const REST_LIMITS = {
33
- second: 10,
34
- minute: 300,
35
- hour: 5_000
36
- };
37
- // Composition writes (working-copy PUT/PATCH + version snapshots) are far more
38
- // expensive than a generic read — each is an S3 write, and snapshots also grow
39
- // the fork's namespace. They get a dedicated, much tighter bucket on top of the
40
- // general REST limiter so a single (possibly compromised) key can't hammer S3
41
- // or spam versions.
42
- const PUBLISH_LIMITS = {
43
- minute: 20,
44
- hour: 300
45
- };
46
- const WINDOW_DEFINITIONS = {
47
- month: { durationMs: 30 * 24 * 60 * 60 * 1000, label: "30-day window" },
48
- week: { durationMs: 7 * 24 * 60 * 60 * 1000, label: "7-day window" },
49
- five_hour: { durationMs: 5 * 60 * 60 * 1000, label: "5-hour window" },
50
- second: { durationMs: 1000, label: "1-second window" },
51
- minute: { durationMs: 60 * 1000, label: "1-minute window" },
52
- hour: { durationMs: 60 * 60 * 1000, label: "1-hour window" }
53
- };
54
- export class RateLimitService {
55
- isConfigured() {
56
- return Boolean(OPERATIONS_TABLE_NAME);
57
- }
58
- async consumeTemplateOperation(customer) {
59
- if (!this.isConfigured()) {
60
- return;
61
- }
62
- await this.consumeWindows({
63
- scopePk: `customer#${customer.id}`,
64
- scopeLabel: `customer:${customer.id}`,
65
- anchorMs: new Date(customer.createdAt).getTime(),
66
- limits: OPERATION_LIMITS[customer.planTier],
67
- type: "operation_rate_limit_exceeded"
68
- });
69
- }
70
- async consumeGeneralRest(scopeId) {
71
- if (!this.isConfigured()) {
72
- return;
73
- }
74
- const statuses = await this.consumeWindows({
75
- scopePk: `rest#${scopeId}`,
76
- scopeLabel: scopeId,
77
- anchorMs: 0,
78
- limits: REST_LIMITS,
79
- type: "rest_rate_limit_exceeded"
80
- });
81
- const violations = statuses.filter((status) => status.used > status.limit);
82
- if (violations.length > 0) {
83
- throw new RateLimitExceededError(violations[0].message, "rest_rate_limit_exceeded", violations);
84
- }
85
- }
86
- async consumePublish(scopeId) {
87
- if (!this.isConfigured()) {
88
- return;
89
- }
90
- const statuses = await this.consumeWindows({
91
- scopePk: `publish#${scopeId}`,
92
- scopeLabel: scopeId,
93
- anchorMs: 0,
94
- limits: PUBLISH_LIMITS,
95
- type: "publish_rate_limit_exceeded"
96
- });
97
- const violations = statuses.filter((status) => status.used > status.limit);
98
- if (violations.length > 0) {
99
- throw new RateLimitExceededError(violations[0].message, "publish_rate_limit_exceeded", violations);
100
- }
101
- }
102
- async getTemplateOperationStatus(customer) {
103
- if (!this.isConfigured()) {
104
- return {
105
- monthly: this.buildDisabledStatus("month", "30-day window"),
106
- weekly: this.buildDisabledStatus("week", "7-day window"),
107
- five_hour: this.buildDisabledStatus("five_hour", "5-hour window")
108
- };
109
- }
110
- const statuses = await this.readWindows({
111
- scopePk: `customer#${customer.id}`,
112
- scopeLabel: `customer:${customer.id}`,
113
- anchorMs: new Date(customer.createdAt).getTime(),
114
- limits: OPERATION_LIMITS[customer.planTier]
115
- });
116
- const mapped = this.buildOperationStatuses(statuses);
117
- return {
118
- monthly: mapped.find((status) => status.key === "month"),
119
- weekly: mapped.find((status) => status.key === "week"),
120
- five_hour: mapped.find((status) => status.key === "five_hour")
121
- };
122
- }
123
- getRestSummary() {
124
- return {
125
- message: REST_LIMIT_SUMMARY,
126
- limits: {
127
- per_second: REST_LIMITS.second,
128
- per_minute: REST_LIMITS.minute,
129
- per_hour: REST_LIMITS.hour
130
- }
131
- };
132
- }
133
- async consumeWindows(input) {
134
- const windowSpecs = this.buildWindowSpecs(input.anchorMs, input.limits);
135
- let transactionCancelled = false;
136
- try {
137
- await dynamodb.send(new TransactWriteItemsCommand({
138
- TransactItems: windowSpecs.map((spec) => ({
139
- Update: {
140
- TableName: OPERATIONS_TABLE_NAME,
141
- Key: {
142
- pk: { S: input.scopePk },
143
- sk: { S: spec.sk }
144
- },
145
- UpdateExpression: "SET usage_count = if_not_exists(usage_count, :zero) + :one, #scope = if_not_exists(#scope, :scope), window_key = if_not_exists(window_key, :windowKey), window_started_at = if_not_exists(window_started_at, :windowStartedAt), window_ends_at = if_not_exists(window_ends_at, :windowEndsAt), limit_value = if_not_exists(limit_value, :limitValue), created_at = if_not_exists(created_at, :createdAt), updated_at = :updatedAt, expires_at_epoch_seconds = if_not_exists(expires_at_epoch_seconds, :ttl)",
146
- ConditionExpression: "attribute_not_exists(usage_count) OR usage_count < :limitValue",
147
- ExpressionAttributeNames: {
148
- "#scope": "scope",
149
- },
150
- ExpressionAttributeValues: {
151
- ":zero": { N: "0" },
152
- ":one": { N: "1" },
153
- ":scope": { S: input.scopeLabel },
154
- ":windowKey": { S: spec.key },
155
- ":windowStartedAt": { S: spec.windowStartedAt },
156
- ":windowEndsAt": { S: spec.nextAvailableAt },
157
- ":limitValue": { N: String(spec.limit) },
158
- ":createdAt": { S: new Date().toISOString() },
159
- ":updatedAt": { S: new Date().toISOString() },
160
- ":ttl": { N: String(Math.floor((new Date(spec.nextAvailableAt).getTime() + (2 * 24 * 60 * 60 * 1000)) / 1000)) }
161
- }
162
- }
163
- }))
164
- }));
165
- }
166
- catch (error) {
167
- if (!isTransactionCanceledError(error)) {
168
- throw error;
169
- }
170
- transactionCancelled = true;
171
- }
172
- const statuses = await this.readWindows({
173
- scopePk: input.scopePk,
174
- scopeLabel: input.scopeLabel,
175
- anchorMs: input.anchorMs,
176
- limits: input.limits
177
- });
178
- const violations = statuses.filter((status) => transactionCancelled ? status.used >= status.limit : status.used > status.limit);
179
- if (violations.length > 0) {
180
- throw new RateLimitExceededError(violations[0].message, input.type, violations);
181
- }
182
- return statuses;
183
- }
184
- async readWindows(input) {
185
- const windowSpecs = this.buildWindowSpecs(input.anchorMs, input.limits);
186
- const items = await Promise.all(windowSpecs.map(async (spec) => {
187
- const response = await dynamodb.send(new GetItemCommand({
188
- TableName: OPERATIONS_TABLE_NAME,
189
- Key: {
190
- pk: { S: input.scopePk },
191
- sk: { S: spec.sk }
192
- },
193
- ConsistentRead: true
194
- }));
195
- return this.toStatus(spec, response.Item);
196
- }));
197
- return items;
198
- }
199
- buildOperationStatuses(statuses) {
200
- return statuses.map((status) => ({
201
- ...status,
202
- key: status.key === "month" ? "month" : status.key === "week" ? "week" : "five_hour"
203
- }));
204
- }
205
- buildWindowSpecs(anchorMs, limits) {
206
- return Object.keys(limits).map((typedKey) => {
207
- const limit = limits[typedKey];
208
- const definition = WINDOW_DEFINITIONS[typedKey];
209
- const nowMs = Date.now();
210
- const effectiveAnchorMs = Number.isFinite(anchorMs) && anchorMs > 0 ? anchorMs : 0;
211
- const elapsedMs = Math.max(0, nowMs - effectiveAnchorMs);
212
- const windowIndex = Math.floor(elapsedMs / definition.durationMs);
213
- const windowStartedAtMs = effectiveAnchorMs + (windowIndex * definition.durationMs);
214
- const nextAvailableAtMs = windowStartedAtMs + definition.durationMs;
215
- const windowStartedAt = new Date(windowStartedAtMs).toISOString();
216
- const nextAvailableAt = new Date(nextAvailableAtMs).toISOString();
217
- return {
218
- key: typedKey,
219
- label: definition.label,
220
- limit,
221
- sk: `${typedKey}#${windowStartedAt}`,
222
- windowStartedAt,
223
- nextAvailableAt
224
- };
225
- });
226
- }
227
- toStatus(spec, item) {
228
- const used = Number(item?.usage_count?.N ?? "0");
229
- const remaining = Math.max(0, spec.limit - used);
230
- const consumedPercent = spec.limit > 0 ? Math.min(100, Number(((used / spec.limit) * 100).toFixed(2))) : 100;
231
- return {
232
- key: spec.key,
233
- label: spec.label,
234
- limit: spec.limit,
235
- used,
236
- remaining,
237
- consumedPercent,
238
- windowStartedAt: item?.window_started_at?.S ?? spec.windowStartedAt,
239
- nextAvailableAt: item?.window_ends_at?.S ?? spec.nextAvailableAt,
240
- message: used >= spec.limit
241
- ? `Rate limit reached for the ${spec.label}. Usage resets at ${item?.window_ends_at?.S ?? spec.nextAvailableAt}.`
242
- : `${used} of ${spec.limit} requests used in the ${spec.label}. Resets at ${item?.window_ends_at?.S ?? spec.nextAvailableAt}.`
243
- };
244
- }
245
- buildDisabledStatus(key, label) {
246
- return {
247
- key,
248
- label,
249
- limit: 0,
250
- used: 0,
251
- remaining: 0,
252
- consumedPercent: 0,
253
- windowStartedAt: new Date(0).toISOString(),
254
- nextAvailableAt: new Date(0).toISOString(),
255
- message: "Rate limiting is not configured for this environment."
256
- };
257
- }
258
- }
259
- function isTransactionCanceledError(error) {
260
- return error instanceof Error && error.name === "TransactionCanceledException";
261
- }
262
- //# sourceMappingURL=rate-limits.js.map
@@ -1,32 +0,0 @@
1
- import { DEFAULT_CONTENT_FORMAT } from "./hyperframes.js";
2
- // ==========================================================================
3
- // Scene-annotation pass state — persisted per fork under
4
- // compositions/forks/{forkId}/working/scene-annotations.json
5
- // and copied on fork/clone/version so the annotations travel with every
6
- // descendant composition. Drives the async /scene-annotations-poll flow,
7
- // mirroring the cast.json state machine: /auto-decompose seeds it "pending",
8
- // and /scene-annotations-poll runs the single annotation vision call (against
9
- // the finalized scenes persisted in smart-decompose.json) to completion.
10
- //
11
- // It's a SINGLE unit of work (one vision call), so the state machine is simple:
12
- // pending -> processing -> done | failed
13
- // The `processing` state exists only to keep two concurrent pollers from both
14
- // paying for the vision call (see the staleness guard in the poll handler).
15
- // ==========================================================================
16
- export const SCENE_ANNOTATIONS_STATE_VERSION = 1;
17
- export function buildPendingSceneAnnotationsState(input) {
18
- return {
19
- version: SCENE_ANNOTATIONS_STATE_VERSION,
20
- status: "pending",
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
- contentFormat: DEFAULT_CONTENT_FORMAT,
29
- annotations: []
30
- };
31
- }
32
- //# sourceMappingURL=scene-annotations.js.map
@@ -1,382 +0,0 @@
1
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
- import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
3
- import { config } from "../config.js";
4
- import { hashSecret, safeEqualHash } from "../lib/crypto.js";
5
- import { devLog } from "../lib/dev-log.js";
6
- import { displayNameFromEmail } from "../lib/display-name.js";
7
- import { createId } from "../lib/ids.js";
8
- import { nowIso } from "../lib/time.js";
9
- import { createLocalDynamoClient } from "./local-dynamo.js";
10
- // See local-dynamo.ts: `vidfarm serve` swaps the DynamoDB client for a
11
- // disk-backed shim; deployed environments keep the real client.
12
- const dynamodb = config.RECORDS_DRIVER === "local"
13
- ? createLocalDynamoClient()
14
- : DynamoDBDocumentClient.from(new DynamoDBClient({}), {
15
- marshallOptions: {
16
- removeUndefinedValues: true
17
- }
18
- });
19
- export class ServerlessAuthService {
20
- async insertOtpChallenge(record) {
21
- const timestamp = nowIso();
22
- const email = record.email.trim().toLowerCase();
23
- await dynamodb.send(new PutCommand({
24
- TableName: requireMainTableName(),
25
- Item: {
26
- pk: `EMAIL#${email}`,
27
- sk: `OTP#${timestamp}#${record.id}`,
28
- entity_type: "OtpChallenge",
29
- id: record.id,
30
- email,
31
- code_hash: record.codeHash,
32
- expires_at: record.expiresAt,
33
- consumed_at: null,
34
- created_at: timestamp
35
- }
36
- }));
37
- devLog("auth.otp.inserted", {
38
- otp_id: record.id,
39
- email,
40
- expires_at: record.expiresAt
41
- });
42
- }
43
- async findLatestOtpChallenge(email) {
44
- const response = await dynamodb.send(new QueryCommand({
45
- TableName: requireMainTableName(),
46
- KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
47
- ExpressionAttributeValues: {
48
- ":pk": `EMAIL#${email.trim().toLowerCase()}`,
49
- ":prefix": "OTP#"
50
- },
51
- ScanIndexForward: false,
52
- Limit: 1
53
- }));
54
- devLog("auth.otp.lookup", {
55
- email: email.trim().toLowerCase(),
56
- found: Boolean(response.Items?.[0])
57
- });
58
- return response.Items?.[0] ?? null;
59
- }
60
- async consumeOtpChallenge(email, id) {
61
- const challenge = await this.findLatestOtpChallenge(email);
62
- if (!challenge || String(challenge.id) !== id) {
63
- return;
64
- }
65
- await dynamodb.send(new UpdateCommand({
66
- TableName: requireMainTableName(),
67
- Key: {
68
- pk: String(challenge.pk),
69
- sk: String(challenge.sk)
70
- },
71
- UpdateExpression: "set consumed_at = :now",
72
- ExpressionAttributeValues: {
73
- ":now": nowIso()
74
- }
75
- }));
76
- devLog("auth.otp.consumed", {
77
- otp_id: id,
78
- email: email.trim().toLowerCase()
79
- });
80
- }
81
- async upsertCustomer(input) {
82
- const current = await this.getCustomer(requireMainTableName(), input.id);
83
- const timestamp = nowIso();
84
- const email = input.email.trim().toLowerCase();
85
- const customer = {
86
- id: input.id,
87
- email,
88
- name: input.name === undefined ? current?.name ?? displayNameFromEmail(email) : input.name,
89
- defaultWebhookUrl: input.defaultWebhookUrl === undefined ? current?.defaultWebhookUrl ?? null : input.defaultWebhookUrl,
90
- isDeveloper: input.isDeveloper === undefined ? current?.isDeveloper ?? false : input.isDeveloper,
91
- isAgency: input.isAgency === undefined ? current?.isAgency ?? false : input.isAgency,
92
- isPaidPlan: input.isPaidPlan === undefined ? current?.isPaidPlan ?? false : input.isPaidPlan,
93
- planTier: input.planTier === undefined ? current?.planTier ?? "client" : input.planTier,
94
- about: input.about === undefined ? current?.about ?? null : input.about,
95
- groupchatUrl: input.groupchatUrl === undefined ? current?.groupchatUrl ?? null : input.groupchatUrl,
96
- flockposterApiKey: input.flockposterApiKey === undefined ? current?.flockposterApiKey ?? null : input.flockposterApiKey,
97
- createdAt: current?.createdAt ?? timestamp,
98
- updatedAt: timestamp
99
- };
100
- await this.putCustomerItem(requireMainTableName(), customer, {
101
- passwordHash: input.passwordHash === undefined ? undefined : input.passwordHash,
102
- encryptedPassword: input.encryptedPassword === undefined ? undefined : input.encryptedPassword
103
- });
104
- devLog("auth.customer.upserted", {
105
- customer_id: customer.id,
106
- email: customer.email,
107
- is_paid_plan: customer.isPaidPlan,
108
- plan_tier: customer.planTier
109
- });
110
- return customer;
111
- }
112
- async getCustomerById(customerId) {
113
- return this.getCustomer(requireMainTableName(), customerId);
114
- }
115
- async getCustomerByEmailPublic(email) {
116
- return this.getCustomerByEmail(requireMainTableName(), email);
117
- }
118
- async getCustomerAuthByEmail(email) {
119
- const response = await dynamodb.send(new QueryCommand({
120
- TableName: requireMainTableName(),
121
- IndexName: "gsi1",
122
- KeyConditionExpression: "gsi1pk = :pk",
123
- ExpressionAttributeValues: {
124
- ":pk": `CUSTOMER_EMAIL#${email.trim().toLowerCase()}`
125
- },
126
- Limit: 1
127
- }));
128
- return response.Items?.[0] ?? null;
129
- }
130
- async updateCustomerPassword(customerId, passwordHash, encryptedPassword) {
131
- await dynamodb.send(new UpdateCommand({
132
- TableName: requireMainTableName(),
133
- Key: {
134
- pk: `CUSTOMER#${customerId}`,
135
- sk: "META"
136
- },
137
- UpdateExpression: "set password_hash = :hash, encrypted_password = :encrypted, updated_at = :updated",
138
- ExpressionAttributeValues: {
139
- ":hash": passwordHash,
140
- ":encrypted": encryptedPassword ?? null,
141
- ":updated": nowIso()
142
- }
143
- }));
144
- }
145
- async insertApiKey(record) {
146
- await this.putApiKey(requireMainTableName(), {
147
- id: record.id,
148
- keyHash: record.keyHash,
149
- customerId: record.customerId,
150
- rawValue: record.rawValue ?? null,
151
- label: record.label
152
- });
153
- devLog("auth.api_key.inserted", {
154
- api_key_id: record.id,
155
- customer_id: record.customerId,
156
- label: record.label
157
- });
158
- }
159
- async findApiKeyByHash(keyHash) {
160
- return this.getApiKeyItem(requireMainTableName(), keyHash);
161
- }
162
- async authenticate(apiKey) {
163
- if (!apiKey) {
164
- throw new Error("Missing vidfarm-api-key header.");
165
- }
166
- const tableName = requireMainTableName();
167
- const keyHash = hashSecret(apiKey + config.API_KEY_SALT);
168
- const apiKeyItem = await this.getApiKeyItem(tableName, keyHash);
169
- if (apiKeyItem) {
170
- const customer = await this.getCustomer(tableName, String(apiKeyItem.customer_id));
171
- if (!customer) {
172
- throw new Error("Invalid API credentials.");
173
- }
174
- if (!customer.isPaidPlan) {
175
- throw new Error("Paid plan required.");
176
- }
177
- await this.touchApiKey(tableName, keyHash);
178
- devLog("auth.api_key.authenticated", {
179
- customer_id: customer.id,
180
- email: customer.email
181
- });
182
- return customer;
183
- }
184
- const bootstrapped = await this.tryBootstrapFromEnv(tableName, apiKey, keyHash);
185
- if (bootstrapped) {
186
- devLog("auth.bootstrap.authenticated", {
187
- customer_id: bootstrapped.id,
188
- email: bootstrapped.email
189
- });
190
- return bootstrapped;
191
- }
192
- devLog("auth.api_key.rejected", {
193
- reason: "not_found"
194
- }, "warn");
195
- throw new Error("Invalid API credentials.");
196
- }
197
- async getApiKeyItem(tableName, keyHash) {
198
- const response = await dynamodb.send(new GetCommand({
199
- TableName: tableName,
200
- Key: {
201
- pk: `APIKEY#${keyHash}`,
202
- sk: "META"
203
- }
204
- }));
205
- if (!response.Item || response.Item.status !== "active") {
206
- return null;
207
- }
208
- return response.Item;
209
- }
210
- async getCustomer(tableName, customerId) {
211
- const response = await dynamodb.send(new GetCommand({
212
- TableName: tableName,
213
- Key: {
214
- pk: `CUSTOMER#${customerId}`,
215
- sk: "META"
216
- }
217
- }));
218
- return response.Item ? itemToCustomer(response.Item) : null;
219
- }
220
- async getCustomerByEmail(tableName, email) {
221
- const response = await dynamodb.send(new QueryCommand({
222
- TableName: tableName,
223
- IndexName: "gsi1",
224
- KeyConditionExpression: "gsi1pk = :pk",
225
- ExpressionAttributeValues: {
226
- ":pk": `CUSTOMER_EMAIL#${email.trim().toLowerCase()}`
227
- },
228
- Limit: 1
229
- }));
230
- const item = response.Items?.[0];
231
- return item ? itemToCustomer(item) : null;
232
- }
233
- async tryBootstrapFromEnv(tableName, apiKey, keyHash) {
234
- const bootstrapApiKey = process.env.VIDFARM_STAGING_BOOTSTRAP_API_KEY?.trim()
235
- || process.env.VIDFARM_API_KEY?.trim()
236
- || "";
237
- if (!bootstrapApiKey || !safeEqualHash(apiKey, hashSecret(bootstrapApiKey))) {
238
- return null;
239
- }
240
- const email = process.env.VIDFARM_STAGING_BOOTSTRAP_EMAIL?.trim()
241
- || config.adminEmails[0]
242
- || "staging@vidfarm.local";
243
- const existing = await this.getCustomerByEmail(tableName, email);
244
- const customer = existing ?? buildBootstrapCustomer(email);
245
- await this.putCustomerItem(tableName, customer, {});
246
- await this.putApiKey(tableName, {
247
- keyHash,
248
- customerId: customer.id,
249
- rawValue: apiKey,
250
- label: `Staging bootstrap ${nowIso()}`
251
- });
252
- return customer;
253
- }
254
- async putCustomerItem(tableName, customer, extra) {
255
- const existing = await dynamodb.send(new GetCommand({
256
- TableName: tableName,
257
- Key: {
258
- pk: `CUSTOMER#${customer.id}`,
259
- sk: "META"
260
- }
261
- }));
262
- await dynamodb.send(new PutCommand({
263
- TableName: tableName,
264
- Item: {
265
- ...customerToItem(customer),
266
- password_hash: extra.passwordHash === undefined ? existing.Item?.password_hash ?? null : extra.passwordHash,
267
- encrypted_password: extra.encryptedPassword === undefined ? existing.Item?.encrypted_password ?? null : extra.encryptedPassword
268
- }
269
- }));
270
- }
271
- async putApiKey(tableName, input) {
272
- const timestamp = nowIso();
273
- await dynamodb.send(new PutCommand({
274
- TableName: tableName,
275
- Item: {
276
- pk: `APIKEY#${input.keyHash}`,
277
- sk: "META",
278
- entity_type: "ApiKey",
279
- gsi1pk: `CUSTOMER#${input.customerId}#APIKEYS`,
280
- gsi1sk: `APIKEY#${timestamp}#${input.id ?? input.keyHash}`,
281
- api_key_id: input.id ?? input.keyHash,
282
- key_hash: input.keyHash,
283
- customer_id: input.customerId,
284
- raw_value: input.rawValue ?? null,
285
- label: input.label ?? null,
286
- status: "active",
287
- created_at: timestamp,
288
- updated_at: timestamp
289
- }
290
- }));
291
- }
292
- async touchApiKey(tableName, keyHash) {
293
- const timestamp = nowIso();
294
- await dynamodb.send(new UpdateCommand({
295
- TableName: tableName,
296
- Key: {
297
- pk: `APIKEY#${keyHash}`,
298
- sk: "META"
299
- },
300
- UpdateExpression: "set last_used_at = :now, updated_at = :now",
301
- ExpressionAttributeValues: {
302
- ":now": timestamp
303
- }
304
- }));
305
- }
306
- }
307
- function requireMainTableName() {
308
- if (config.RECORDS_DRIVER === "local") {
309
- return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME || "vidfarm-local-main";
310
- }
311
- if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
312
- throw new Error("Serverless auth requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
313
- }
314
- return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
315
- }
316
- function buildBootstrapCustomer(email) {
317
- const timestamp = nowIso();
318
- const id = process.env.VIDFARM_STAGING_BOOTSTRAP_USER_ID?.trim()
319
- || process.env.VIDFARM_USER_ID?.trim()
320
- || createId("cus");
321
- return {
322
- id,
323
- email: email.trim().toLowerCase(),
324
- name: displayNameFromEmail(email),
325
- defaultWebhookUrl: null,
326
- isDeveloper: true,
327
- isAgency: true,
328
- isPaidPlan: true,
329
- planTier: "agency",
330
- about: null,
331
- groupchatUrl: null,
332
- flockposterApiKey: null,
333
- createdAt: timestamp,
334
- updatedAt: timestamp
335
- };
336
- }
337
- function customerToItem(customer) {
338
- return {
339
- pk: `CUSTOMER#${customer.id}`,
340
- sk: "META",
341
- entity_type: "Customer",
342
- gsi1pk: `CUSTOMER_EMAIL#${customer.email.trim().toLowerCase()}`,
343
- gsi1sk: `CUSTOMER#${customer.id}`,
344
- customer_id: customer.id,
345
- email: customer.email,
346
- name: customer.name,
347
- default_webhook_url: customer.defaultWebhookUrl,
348
- is_developer: customer.isDeveloper,
349
- is_agency: customer.isAgency,
350
- is_paid_plan: customer.isPaidPlan,
351
- plan_tier: customer.planTier,
352
- about: customer.about,
353
- groupchat_url: customer.groupchatUrl,
354
- flockposter_api_key: customer.flockposterApiKey,
355
- created_at: customer.createdAt,
356
- updated_at: customer.updatedAt
357
- };
358
- }
359
- function itemToCustomer(item) {
360
- return {
361
- id: String(item.customer_id),
362
- email: String(item.email),
363
- name: typeof item.name === "string" ? item.name : null,
364
- defaultWebhookUrl: typeof item.default_webhook_url === "string" ? item.default_webhook_url : null,
365
- isDeveloper: Boolean(item.is_developer),
366
- isAgency: Boolean(item.is_agency),
367
- isPaidPlan: Boolean(item.is_paid_plan),
368
- planTier: normalizePlanTier(item.plan_tier),
369
- about: typeof item.about === "string" ? item.about : null,
370
- groupchatUrl: typeof item.groupchat_url === "string" ? item.groupchat_url : null,
371
- flockposterApiKey: typeof item.flockposter_api_key === "string" ? item.flockposter_api_key : null,
372
- createdAt: String(item.created_at),
373
- updatedAt: String(item.updated_at)
374
- };
375
- }
376
- function normalizePlanTier(value) {
377
- if (value === "client" || value === "standalone" || value === "agency") {
378
- return value;
379
- }
380
- return "client";
381
- }
382
- //# sourceMappingURL=serverless-auth.js.map