@growth-labs/cms 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/engine/foundry-dispatch.d.ts +5 -1
- package/dist/engine/foundry-dispatch.d.ts.map +1 -1
- package/dist/engine/foundry-dispatch.js +19 -3
- package/dist/engine/foundry-dispatch.js.map +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publisher.d.ts +4 -0
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +190 -54
- package/dist/engine/publisher.js.map +1 -1
- package/dist/providers/types.d.ts +1 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +76 -16
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/context.d.ts +1 -0
- package/dist/routes/context.d.ts.map +1 -1
- package/dist/routes/context.js.map +1 -1
- package/dist/routes/media.d.ts.map +1 -1
- package/dist/routes/media.js +53 -31
- package/dist/routes/media.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +65 -10
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/content-payload.d.ts +2 -0
- package/dist/ui/editor/content-payload.d.ts.map +1 -1
- package/dist/ui/editor/content-payload.js +15 -4
- package/dist/ui/editor/content-payload.js.map +1 -1
- package/dist/ui/editor/editor-media-duration.d.ts +24 -0
- package/dist/ui/editor/editor-media-duration.d.ts.map +1 -0
- package/dist/ui/editor/editor-media-duration.js +51 -0
- package/dist/ui/editor/editor-media-duration.js.map +1 -0
- package/dist/ui/screens/MediaScreen.d.ts.map +1 -1
- package/dist/ui/screens/MediaScreen.js +8 -1
- package/dist/ui/screens/MediaScreen.js.map +1 -1
- package/dist/ui/screens/media-upload.d.ts +9 -2
- package/dist/ui/screens/media-upload.d.ts.map +1 -1
- package/dist/ui/screens/media-upload.js +24 -0
- package/dist/ui/screens/media-upload.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/foundry-dispatch.ts +32 -4
- package/src/engine/index.ts +1 -0
- package/src/engine/publisher.ts +297 -97
- package/src/providers/types.ts +1 -0
- package/src/routes/content.ts +91 -16
- package/src/routes/context.ts +1 -0
- package/src/routes/media.ts +63 -41
- package/src/ui/editor/ContentForm.tsx +121 -4
- package/src/ui/editor/content-payload.ts +20 -2
- package/src/ui/editor/editor-media-duration.ts +75 -0
- package/src/ui/screens/MediaScreen.tsx +13 -2
- package/src/ui/screens/media-upload.ts +41 -2
package/src/routes/content.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
applyFoundryCallback,
|
|
23
23
|
type DispatchResult,
|
|
24
24
|
dispatchToFoundry,
|
|
25
|
+
FoundryDispatchPrerequisiteError,
|
|
25
26
|
type ProcessingState,
|
|
26
27
|
verifyHmac,
|
|
27
28
|
} from '../engine/foundry-dispatch.js'
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
scheduleContent,
|
|
38
39
|
unpublishContent,
|
|
39
40
|
updateContentItem,
|
|
41
|
+
VerifiedMediaDurationConflictError,
|
|
40
42
|
} from '../engine/publisher.js'
|
|
41
43
|
import {
|
|
42
44
|
createRevisionWithDelta,
|
|
@@ -55,6 +57,24 @@ import type { DispatchWebhookHook } from './webhooks.js'
|
|
|
55
57
|
|
|
56
58
|
export type { DispatchWebhookHook }
|
|
57
59
|
|
|
60
|
+
type PodcastFoundryReason = 'missing_duration' | 'dispatch_failed' | null
|
|
61
|
+
|
|
62
|
+
interface PodcastFoundryAttempt {
|
|
63
|
+
result: DispatchResult | null
|
|
64
|
+
reason: PodcastFoundryReason
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function verifiedMediaDurationConflictResponse(error: unknown): Response | null {
|
|
68
|
+
if (!(error instanceof VerifiedMediaDurationConflictError)) return null
|
|
69
|
+
return json(
|
|
70
|
+
{
|
|
71
|
+
error: 'Media duration conflicts with verified upload metadata',
|
|
72
|
+
reason: error.code,
|
|
73
|
+
},
|
|
74
|
+
409,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
/**
|
|
59
79
|
* Optional injected hook to dispatch article-takeaways generation to Foundry
|
|
60
80
|
* (Hermes). The package never imports the Foundry glue — the site supplies this
|
|
@@ -260,7 +280,7 @@ function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
|
260
280
|
content: z.object({
|
|
261
281
|
script: z.string().default(''),
|
|
262
282
|
videoId: z.string().min(1),
|
|
263
|
-
durationSeconds: z.number().int().optional().nullable(),
|
|
283
|
+
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
264
284
|
thumbnailImageId: z.string().optional().nullable(),
|
|
265
285
|
processingSourceUrl: z.string().url().optional().nullable(),
|
|
266
286
|
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
@@ -275,7 +295,7 @@ function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
|
275
295
|
// the video `script` default-empty pattern above.
|
|
276
296
|
transcript: z.string().default(''),
|
|
277
297
|
audioR2Key: z.string().min(1),
|
|
278
|
-
durationSeconds: z.number().int().optional().nullable(),
|
|
298
|
+
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
279
299
|
}),
|
|
280
300
|
}),
|
|
281
301
|
BaseSchema.extend({
|
|
@@ -334,7 +354,7 @@ const ArticleContentUpdateSchema = z.object({
|
|
|
334
354
|
const VideoContentUpdateSchema = z.object({
|
|
335
355
|
script: z.string().optional(),
|
|
336
356
|
videoId: z.string().optional(),
|
|
337
|
-
durationSeconds: z.number().int().optional().nullable(),
|
|
357
|
+
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
338
358
|
thumbnailImageId: z.string().optional().nullable(),
|
|
339
359
|
processingSourceUrl: z.string().url().optional().nullable(),
|
|
340
360
|
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
@@ -343,7 +363,7 @@ const VideoContentUpdateSchema = z.object({
|
|
|
343
363
|
const PodcastContentUpdateSchema = z.object({
|
|
344
364
|
transcript: z.string().optional(),
|
|
345
365
|
audioR2Key: z.string().optional(),
|
|
346
|
-
durationSeconds: z.number().int().optional().nullable(),
|
|
366
|
+
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
347
367
|
})
|
|
348
368
|
|
|
349
369
|
function getContentUpdateSchema(type: string) {
|
|
@@ -692,6 +712,35 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
692
712
|
return dispatchToFoundry(ctx.db, contentId, resolved.hooks.foundryVideo)
|
|
693
713
|
}
|
|
694
714
|
|
|
715
|
+
async function dispatchPodcastAfterSave(
|
|
716
|
+
ctx: RouteContext,
|
|
717
|
+
contentId: string,
|
|
718
|
+
): Promise<PodcastFoundryAttempt> {
|
|
719
|
+
try {
|
|
720
|
+
return {
|
|
721
|
+
result: await maybeDispatchPodcastToFoundry(ctx, contentId),
|
|
722
|
+
reason: null,
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (error instanceof FoundryDispatchPrerequisiteError) {
|
|
726
|
+
return { result: null, reason: 'missing_duration' }
|
|
727
|
+
}
|
|
728
|
+
console.error('Podcast Foundry dispatch failed after content save', error)
|
|
729
|
+
return { result: null, reason: 'dispatch_failed' }
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function missingDurationResponse(): Response {
|
|
734
|
+
return json(
|
|
735
|
+
{
|
|
736
|
+
error: 'Content requires a positive finite duration before dispatch',
|
|
737
|
+
processingQueued: false,
|
|
738
|
+
reason: 'missing_duration',
|
|
739
|
+
},
|
|
740
|
+
409,
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
695
744
|
/**
|
|
696
745
|
* Fire the optional dispatchWebhook hook for a lifecycle transition.
|
|
697
746
|
*
|
|
@@ -973,9 +1022,18 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
973
1022
|
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400)
|
|
974
1023
|
}
|
|
975
1024
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1025
|
+
let created: Awaited<ReturnType<typeof createContent>>
|
|
1026
|
+
try {
|
|
1027
|
+
created = await createContent(ctx.db, parsed.data)
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
const conflict = verifiedMediaDurationConflictResponse(error)
|
|
1030
|
+
if (conflict) return conflict
|
|
1031
|
+
throw error
|
|
1032
|
+
}
|
|
1033
|
+
const podcastAttempt: PodcastFoundryAttempt =
|
|
1034
|
+
parsed.data.type === 'podcast'
|
|
1035
|
+
? await dispatchPodcastAfterSave(ctx, created.id)
|
|
1036
|
+
: { result: null, reason: null }
|
|
979
1037
|
|
|
980
1038
|
// Fire content.created webhook (lifecycle transition).
|
|
981
1039
|
await fireWebhook(ctx, 'content.created', {
|
|
@@ -988,8 +1046,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
988
1046
|
{
|
|
989
1047
|
id: created.id,
|
|
990
1048
|
slug: created.slug,
|
|
991
|
-
podcastFoundryQueued: Boolean(
|
|
992
|
-
podcastFoundryCorrelationId:
|
|
1049
|
+
podcastFoundryQueued: Boolean(podcastAttempt.result),
|
|
1050
|
+
podcastFoundryCorrelationId: podcastAttempt.result?.correlationId ?? null,
|
|
1051
|
+
podcastFoundryReason: podcastAttempt.reason,
|
|
993
1052
|
},
|
|
994
1053
|
201,
|
|
995
1054
|
)
|
|
@@ -1118,10 +1177,19 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1118
1177
|
}
|
|
1119
1178
|
|
|
1120
1179
|
const priorSlug = existing.slug
|
|
1121
|
-
|
|
1180
|
+
let updated: Awaited<ReturnType<typeof updateContentItem>>
|
|
1181
|
+
try {
|
|
1182
|
+
updated = await updateContentItem(ctx.db, id, parsed.data)
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
const conflict = verifiedMediaDurationConflictResponse(error)
|
|
1185
|
+
if (conflict) return conflict
|
|
1186
|
+
throw error
|
|
1187
|
+
}
|
|
1122
1188
|
if (!updated) return json({ error: 'Not found' }, 404)
|
|
1123
|
-
const
|
|
1124
|
-
existing.type === 'podcast'
|
|
1189
|
+
const podcastAttempt: PodcastFoundryAttempt =
|
|
1190
|
+
existing.type === 'podcast'
|
|
1191
|
+
? await dispatchPodcastAfterSave(ctx, id)
|
|
1192
|
+
: { result: null, reason: null }
|
|
1125
1193
|
|
|
1126
1194
|
// If the slug changed, record the old slug as a redirect (spec §8)
|
|
1127
1195
|
// and drop any stale redirect row for the now-live slug — atomically,
|
|
@@ -1133,8 +1201,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1133
1201
|
return json({
|
|
1134
1202
|
id,
|
|
1135
1203
|
slug: updated.slug,
|
|
1136
|
-
podcastFoundryQueued: Boolean(
|
|
1137
|
-
podcastFoundryCorrelationId:
|
|
1204
|
+
podcastFoundryQueued: Boolean(podcastAttempt.result),
|
|
1205
|
+
podcastFoundryCorrelationId: podcastAttempt.result?.correlationId ?? null,
|
|
1206
|
+
podcastFoundryReason: podcastAttempt.reason,
|
|
1138
1207
|
})
|
|
1139
1208
|
},
|
|
1140
1209
|
|
|
@@ -1206,6 +1275,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1206
1275
|
try {
|
|
1207
1276
|
podcastDispatch = await maybeDispatchPodcastToFoundry(ctx, id)
|
|
1208
1277
|
} catch (error) {
|
|
1278
|
+
if (error instanceof FoundryDispatchPrerequisiteError) {
|
|
1279
|
+
return missingDurationResponse()
|
|
1280
|
+
}
|
|
1209
1281
|
console.error('Podcast Foundry dispatch failed before publish', error)
|
|
1210
1282
|
return json({ error: 'Foundry dispatch failed' }, 502)
|
|
1211
1283
|
}
|
|
@@ -1488,6 +1560,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1488
1560
|
202,
|
|
1489
1561
|
)
|
|
1490
1562
|
} catch (error) {
|
|
1563
|
+
if (error instanceof FoundryDispatchPrerequisiteError) {
|
|
1564
|
+
return missingDurationResponse()
|
|
1565
|
+
}
|
|
1491
1566
|
console.error('Foundry dispatch failed', error)
|
|
1492
1567
|
return json({ error: 'Foundry dispatch failed' }, 502)
|
|
1493
1568
|
}
|
|
@@ -1587,9 +1662,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1587
1662
|
})
|
|
1588
1663
|
|
|
1589
1664
|
// Fire video.processed / video.failed webhook on terminal Foundry states.
|
|
1590
|
-
if (nextState === 'ready') {
|
|
1665
|
+
if (ok && nextState === 'ready') {
|
|
1591
1666
|
await fireWebhook(ctx, 'video.processed', { correlationId, eventId, eventKind })
|
|
1592
|
-
} else if (nextState === 'failed') {
|
|
1667
|
+
} else if (ok && nextState === 'failed') {
|
|
1593
1668
|
await fireWebhook(ctx, 'video.failed', {
|
|
1594
1669
|
correlationId,
|
|
1595
1670
|
eventId,
|
package/src/routes/context.ts
CHANGED
package/src/routes/media.ts
CHANGED
|
@@ -116,6 +116,7 @@ const initVideoUploadSchema = z.object({
|
|
|
116
116
|
fileName: z.string().min(1),
|
|
117
117
|
contentType: z.string().optional().nullable(),
|
|
118
118
|
sizeBytes: z.number().int().positive().max(MAX_EDITOR_VIDEO_SOURCE_BYTES),
|
|
119
|
+
durationSeconds: z.number().int().positive(),
|
|
119
120
|
})
|
|
120
121
|
|
|
121
122
|
const uploadedPartSchema = z.object({
|
|
@@ -130,6 +131,7 @@ const completeVideoUploadSchema = z.object({
|
|
|
130
131
|
fileName: z.string().min(1),
|
|
131
132
|
contentType: z.string().optional().nullable(),
|
|
132
133
|
sizeBytes: z.number().int().positive().max(MAX_EDITOR_VIDEO_SOURCE_BYTES),
|
|
134
|
+
durationSeconds: z.number().int().positive(),
|
|
133
135
|
parts: z.array(uploadedPartSchema).min(1).max(10_000),
|
|
134
136
|
})
|
|
135
137
|
|
|
@@ -143,6 +145,7 @@ const initPodcastUploadSchema = z.object({
|
|
|
143
145
|
fileName: z.string().min(1),
|
|
144
146
|
contentType: z.string().optional().nullable(),
|
|
145
147
|
sizeBytes: z.number().int().positive().max(MAX_EDITOR_AUDIO_SOURCE_BYTES),
|
|
148
|
+
durationSeconds: z.number().int().positive(),
|
|
146
149
|
})
|
|
147
150
|
|
|
148
151
|
const completePodcastUploadSchema = z.object({
|
|
@@ -152,6 +155,7 @@ const completePodcastUploadSchema = z.object({
|
|
|
152
155
|
fileName: z.string().min(1),
|
|
153
156
|
contentType: z.string().optional().nullable(),
|
|
154
157
|
sizeBytes: z.number().int().positive().max(MAX_EDITOR_AUDIO_SOURCE_BYTES),
|
|
158
|
+
durationSeconds: z.number().int().positive(),
|
|
155
159
|
parts: z.array(uploadedPartSchema).min(1).max(10_000),
|
|
156
160
|
})
|
|
157
161
|
|
|
@@ -490,6 +494,7 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
490
494
|
cacheControl: 'public, max-age=31536000, immutable',
|
|
491
495
|
},
|
|
492
496
|
customMetadata: {
|
|
497
|
+
durationSeconds: String(parsed.data.durationSeconds),
|
|
493
498
|
originalFilename: parsed.data.fileName,
|
|
494
499
|
sizeBytes: String(parsed.data.sizeBytes),
|
|
495
500
|
source: 'cms',
|
|
@@ -555,6 +560,9 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
555
560
|
if (typeof bucket.resumeMultipartUpload !== 'function') {
|
|
556
561
|
return json({ error: 'Missing configuration' }, 500)
|
|
557
562
|
}
|
|
563
|
+
if (typeof bucket.head !== 'function') {
|
|
564
|
+
return json({ error: 'R2 metadata verification is unavailable' }, 500)
|
|
565
|
+
}
|
|
558
566
|
|
|
559
567
|
const parsed = completePodcastUploadSchema.safeParse(await readJson(ctx))
|
|
560
568
|
if (!parsed.success) return json({ error: 'Invalid podcast upload completion request' }, 400)
|
|
@@ -599,25 +607,26 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
599
607
|
const upload = bucket.resumeMultipartUpload(parsed.data.key, parsed.data.uploadId)
|
|
600
608
|
await upload.complete(parts)
|
|
601
609
|
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
610
|
+
const object = await bucket.head(parsed.data.key)
|
|
611
|
+
if (
|
|
612
|
+
!object ||
|
|
613
|
+
object.size !== parsed.data.sizeBytes ||
|
|
614
|
+
object.customMetadata?.sizeBytes !== String(parsed.data.sizeBytes) ||
|
|
615
|
+
object.size > MAX_EDITOR_AUDIO_SOURCE_BYTES
|
|
616
|
+
) {
|
|
617
|
+
await deletePodcastAudioAsset(ctx, bucket, parsed.data.key)
|
|
618
|
+
return json(
|
|
619
|
+
{
|
|
620
|
+
error: 'Uploaded podcast audio size did not match the expected size',
|
|
621
|
+
expectedSizeBytes: parsed.data.sizeBytes,
|
|
622
|
+
actualSizeBytes: object?.size ?? null,
|
|
623
|
+
},
|
|
624
|
+
400,
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
if (object.customMetadata?.durationSeconds !== String(parsed.data.durationSeconds)) {
|
|
628
|
+
await deletePodcastAudioAsset(ctx, bucket, parsed.data.key)
|
|
629
|
+
return json({ error: 'Uploaded podcast duration did not match the multipart upload' }, 400)
|
|
621
630
|
}
|
|
622
631
|
|
|
623
632
|
try {
|
|
@@ -635,6 +644,7 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
635
644
|
height: null,
|
|
636
645
|
variantsJson: JSON.stringify({
|
|
637
646
|
contentType,
|
|
647
|
+
durationSeconds: parsed.data.durationSeconds,
|
|
638
648
|
sizeBytes: parsed.data.sizeBytes,
|
|
639
649
|
slug: slugify(parsed.data.slug) || 'podcast',
|
|
640
650
|
}),
|
|
@@ -655,8 +665,9 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
655
665
|
key: parsed.data.key,
|
|
656
666
|
publicUrl,
|
|
657
667
|
url: publicUrl,
|
|
658
|
-
sizeVerification,
|
|
668
|
+
sizeVerification: 'verified',
|
|
659
669
|
sizeBytes: parsed.data.sizeBytes,
|
|
670
|
+
durationSeconds: parsed.data.durationSeconds,
|
|
660
671
|
})
|
|
661
672
|
},
|
|
662
673
|
|
|
@@ -720,6 +731,7 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
720
731
|
const upload = await bucket.createMultipartUpload(key, {
|
|
721
732
|
httpMetadata: { contentType, cacheControl: 'public, max-age=3600' },
|
|
722
733
|
customMetadata: {
|
|
734
|
+
durationSeconds: String(parsed.data.durationSeconds),
|
|
723
735
|
originalFilename: parsed.data.fileName,
|
|
724
736
|
sizeBytes: String(parsed.data.sizeBytes),
|
|
725
737
|
source: 'cms',
|
|
@@ -786,6 +798,9 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
786
798
|
if (typeof bucket.resumeMultipartUpload !== 'function') {
|
|
787
799
|
return json({ error: 'Missing configuration' }, 500)
|
|
788
800
|
}
|
|
801
|
+
if (typeof bucket.head !== 'function') {
|
|
802
|
+
return json({ error: 'R2 metadata verification is unavailable' }, 500)
|
|
803
|
+
}
|
|
789
804
|
|
|
790
805
|
const parsed = completeVideoUploadSchema.safeParse(await readJson(ctx))
|
|
791
806
|
if (!parsed.success) return json({ error: 'Invalid video upload completion request' }, 400)
|
|
@@ -844,25 +859,26 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
844
859
|
publicDomain: domain,
|
|
845
860
|
})
|
|
846
861
|
|
|
847
|
-
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
862
|
+
const object = await bucket.head(parsed.data.key)
|
|
863
|
+
if (
|
|
864
|
+
!object ||
|
|
865
|
+
object.size !== parsed.data.sizeBytes ||
|
|
866
|
+
object.customMetadata?.sizeBytes !== String(parsed.data.sizeBytes) ||
|
|
867
|
+
object.size > MAX_EDITOR_VIDEO_SOURCE_BYTES
|
|
868
|
+
) {
|
|
869
|
+
await deleteStagedVideoSource(ctx.db, bucket, sourceUrl, domain)
|
|
870
|
+
return json(
|
|
871
|
+
{
|
|
872
|
+
error: 'Uploaded video source size did not match the expected size',
|
|
873
|
+
expectedSizeBytes: parsed.data.sizeBytes,
|
|
874
|
+
actualSizeBytes: object?.size ?? null,
|
|
875
|
+
},
|
|
876
|
+
400,
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
if (object.customMetadata?.durationSeconds !== String(parsed.data.durationSeconds)) {
|
|
880
|
+
await deleteStagedVideoSource(ctx.db, bucket, sourceUrl, domain)
|
|
881
|
+
return json({ error: 'Uploaded video duration did not match the multipart upload' }, 400)
|
|
866
882
|
}
|
|
867
883
|
|
|
868
884
|
try {
|
|
@@ -878,7 +894,12 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
878
894
|
url: sourceUrl,
|
|
879
895
|
width: null,
|
|
880
896
|
height: null,
|
|
881
|
-
variantsJson: JSON.stringify({
|
|
897
|
+
variantsJson: JSON.stringify({
|
|
898
|
+
contentType,
|
|
899
|
+
durationSeconds: parsed.data.durationSeconds,
|
|
900
|
+
sizeBytes: parsed.data.sizeBytes,
|
|
901
|
+
videoId,
|
|
902
|
+
}),
|
|
882
903
|
r2Binding: mediaR2Binding,
|
|
883
904
|
})
|
|
884
905
|
} catch (error) {
|
|
@@ -898,9 +919,10 @@ export function createMediaRoutes(config: CmsRouteConfig): MediaRouteHandlers {
|
|
|
898
919
|
processingStatus: 'processing',
|
|
899
920
|
sourceUrl,
|
|
900
921
|
sourceKind: 'http_url',
|
|
901
|
-
sizeVerification,
|
|
922
|
+
sizeVerification: 'verified',
|
|
902
923
|
videoId,
|
|
903
924
|
sizeBytes: parsed.data.sizeBytes,
|
|
925
|
+
durationSeconds: parsed.data.durationSeconds,
|
|
904
926
|
})
|
|
905
927
|
},
|
|
906
928
|
|