@mevdragon/vidfarm-devcli 0.20.5 → 0.20.8

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 (154) 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 +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -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 +265 -0
  13. package/SKILL.director.md +521 -261
  14. package/dist/src/cli.js +404 -96
  15. package/dist/src/devcli/clips.js +15 -10
  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 +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21602
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -756
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3517
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. 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