@growth-labs/cms 0.8.33 → 0.8.35
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/dist/engine/content-metadata.d.ts +1 -0
- package/dist/engine/content-metadata.d.ts.map +1 -1
- package/dist/engine/content-metadata.js +32 -0
- package/dist/engine/content-metadata.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 +25 -9
- package/dist/engine/publisher.js.map +1 -1
- package/dist/foundry-emit.d.ts +11 -0
- package/dist/foundry-emit.d.ts.map +1 -1
- package/dist/foundry-emit.js +3 -0
- package/dist/foundry-emit.js.map +1 -1
- package/dist/routes/content-insights.d.ts.map +1 -1
- package/dist/routes/content-insights.js +76 -1
- package/dist/routes/content-insights.js.map +1 -1
- package/dist/routes/content.d.ts +2 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +4 -0
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/foundry-publish.d.ts.map +1 -1
- package/dist/routes/foundry-publish.js +100 -8
- package/dist/routes/foundry-publish.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +7 -9
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/content-payload.d.ts +5 -0
- package/dist/ui/editor/content-payload.d.ts.map +1 -1
- package/dist/ui/editor/content-payload.js +13 -0
- package/dist/ui/editor/content-payload.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/content-metadata.ts +35 -0
- package/src/engine/publisher.ts +33 -7
- package/src/foundry-emit.ts +3 -0
- package/src/routes/content-insights.ts +79 -1
- package/src/routes/content.ts +6 -0
- package/src/routes/foundry-publish.ts +115 -7
- package/src/ui/editor/ContentForm.tsx +8 -13
- package/src/ui/editor/content-payload.ts +17 -0
|
@@ -49,6 +49,11 @@ export declare function canCreateContentDraft(contentType: ContentType, draft: C
|
|
|
49
49
|
requirePrimaryTopic?: boolean;
|
|
50
50
|
}): boolean;
|
|
51
51
|
export declare function foundryDispatchSourceForDraft(contentType: ContentType, draft: ContentDraftFields): string | null;
|
|
52
|
+
/** Explain the same prerequisites that gate the first save, using the host's field label. */
|
|
53
|
+
export declare function contentDraftSaveError(contentType: ContentType, draft: ContentDraftFields, requirements?: {
|
|
54
|
+
requirePrimaryTopic?: boolean;
|
|
55
|
+
primaryTopicLabel?: string;
|
|
56
|
+
}): string | null;
|
|
52
57
|
export declare function inferVideoSourceKind(sourceUrl: string): 'dropbox' | 'http_url';
|
|
53
58
|
export declare function buildContentUpdatePayload(contentType: ContentType, draft: ContentDraftFields): Record<string, unknown>;
|
|
54
59
|
export declare function buildContentCreatePayload(contentType: ContentType, draft: ContentDraftFields): Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-payload.d.ts","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"content-payload.d.ts","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AAEvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAA;CAC1B;AAED,MAAM,WAAW,2BAA2B;IAC3C,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,kBAAkB,EACrE,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,CAAC,EACR,SAAS,EAAE,2BAA2B,GACpC,CAAC,CAoBH;AAED,KAAK,2BAA2B,GAAG,IAAI,CACtC,0BAA0B,EAC1B,KAAK,GAAG,cAAc,GAAG,oBAAoB,CAC7C,GAAG;IACH,YAAY,EAAE,SAAS,WAAW,EAAE,CAAA;IACpC,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzC,CAAA;AAED,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,SAAS,2BAA2B,EAAE,EAC9C,WAAW,EAAE,WAAW,GACtB,eAAe,CAOjB;AAED,wBAAgB,8BAA8B,CAC7C,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,EACnC,KAAK,EAAE,2BAA2B,EAClC,KAAK,EAAE,MAAM,GACX,eAAe,CASjB;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG3E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,qBAAqB,CACpC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,kBAAkB,EACzB,YAAY,GAAE;IAAE,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,OAAO,CAqBT;AAED,wBAAgB,6BAA6B,CAC5C,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,kBAAkB,GACvB,MAAM,GAAG,IAAI,CAUf;AAED,6FAA6F;AAC7F,wBAAgB,qBAAqB,CACpC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,kBAAkB,EACzB,YAAY,GAAE;IAAE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9E,MAAM,GAAG,IAAI,CAUf;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAe9E;AAED,wBAAgB,yBAAyB,CACxC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,kBAAkB,GACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoEzB;AAED,wBAAgB,yBAAyB,CACxC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,kBAAkB,GACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB"}
|
|
@@ -99,6 +99,19 @@ export function foundryDispatchSourceForDraft(contentType, draft) {
|
|
|
99
99
|
}
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
|
+
/** Explain the same prerequisites that gate the first save, using the host's field label. */
|
|
103
|
+
export function contentDraftSaveError(contentType, draft, requirements = {}) {
|
|
104
|
+
if (canCreateContentDraft(contentType, draft, requirements))
|
|
105
|
+
return null;
|
|
106
|
+
if (requirements.requirePrimaryTopic && !draft.primaryTopic?.trim()) {
|
|
107
|
+
return `Select ${requirements.primaryTopicLabel || 'a primary topic'} before saving.`;
|
|
108
|
+
}
|
|
109
|
+
const mediaSource = foundryDispatchSourceForDraft(contentType, draft);
|
|
110
|
+
if (mediaSource && normalizeMediaDurationSeconds(draft.durationSeconds) === null) {
|
|
111
|
+
return 'Enter the authoritative media duration in whole seconds before saving.';
|
|
112
|
+
}
|
|
113
|
+
return 'Add the required title and content before saving.';
|
|
114
|
+
}
|
|
102
115
|
export function inferVideoSourceKind(sourceUrl) {
|
|
103
116
|
try {
|
|
104
117
|
const { hostname } = new URL(sourceUrl);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-payload.js","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"content-payload.js","sourceRoot":"","sources":["../../../src/ui/editor/content-payload.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -137,3 +137,38 @@ export const ContentMetadataSchema = z
|
|
|
137
137
|
}
|
|
138
138
|
})
|
|
139
139
|
.transform((metadata) => metadata as ContentMetadata)
|
|
140
|
+
|
|
141
|
+
// A prepared improvement supplies partial credits. Keep recorded names and
|
|
142
|
+
// their provenance when preparation has no new credit, rather than replacing
|
|
143
|
+
// the entire source record with UNKNOWN fields.
|
|
144
|
+
export function mergePinnedArticleMetadata(
|
|
145
|
+
recorded: ContentMetadata,
|
|
146
|
+
incoming: ContentMetadata,
|
|
147
|
+
): ContentMetadata {
|
|
148
|
+
const merged = { ...recorded, ...incoming }
|
|
149
|
+
const isObject = (value: ContentMetadataValue | undefined): value is ContentMetadata =>
|
|
150
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
151
|
+
if (!isObject(recorded.credits) || !isObject(incoming.credits)) return merged
|
|
152
|
+
const prior = recorded.credits
|
|
153
|
+
const updates = incoming.credits
|
|
154
|
+
const credits = { ...prior, ...updates }
|
|
155
|
+
for (const role of ['originalWriter', 'presenter', 'editor']) {
|
|
156
|
+
const statusKey = `${role}Status`
|
|
157
|
+
const name = updates[role]
|
|
158
|
+
const status = updates[statusKey]
|
|
159
|
+
const missingName = name === undefined || (typeof name === 'string' && name.trim() === '')
|
|
160
|
+
if (
|
|
161
|
+
prior[role] !== undefined &&
|
|
162
|
+
(status === 'UNKNOWN' || (missingName && status === undefined))
|
|
163
|
+
) {
|
|
164
|
+
credits[role] = prior[role]
|
|
165
|
+
if (prior[statusKey] !== undefined) credits[statusKey] = prior[statusKey]
|
|
166
|
+
else delete credits[statusKey]
|
|
167
|
+
} else if (!missingName && name !== prior[role] && status === undefined) {
|
|
168
|
+
// A different name cannot inherit the previous person's verification.
|
|
169
|
+
credits[statusKey] = 'UNKNOWN'
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
merged.credits = credits
|
|
173
|
+
return merged
|
|
174
|
+
}
|
package/src/engine/publisher.ts
CHANGED
|
@@ -343,6 +343,8 @@ export interface UpdateContentInput extends Partial<BaseContentInput> {
|
|
|
343
343
|
export interface UpdateContentOptions {
|
|
344
344
|
/** Atomically apply the update only while the content row has this status. */
|
|
345
345
|
requiredStatus?: ContentStatus
|
|
346
|
+
/** Pin a machine body update to the canonical version it inspected. Requires requiredStatus. */
|
|
347
|
+
expectedCanonicalVersion?: number
|
|
346
348
|
/** Apply a resolved media source only while this exact source identity is still stored. */
|
|
347
349
|
expectedMediaSource?: {
|
|
348
350
|
sourceUrl: string | null
|
|
@@ -354,6 +356,8 @@ export interface UpdateContentResult {
|
|
|
354
356
|
slug: string
|
|
355
357
|
/** Present when requiredStatus lost a race before the mutation transaction. */
|
|
356
358
|
statusConflict?: string
|
|
359
|
+
/** A newer canonical edit won before the atomic update. */
|
|
360
|
+
canonicalVersionConflict?: boolean
|
|
357
361
|
/** Present when a newer media-source update won while this request was resolving. */
|
|
358
362
|
mediaSourceConflict?: boolean
|
|
359
363
|
}
|
|
@@ -811,6 +815,14 @@ export async function updateContentItem(
|
|
|
811
815
|
input: UpdateContentInput,
|
|
812
816
|
options: UpdateContentOptions = {},
|
|
813
817
|
): Promise<UpdateContentResult | null> {
|
|
818
|
+
if (
|
|
819
|
+
options.expectedCanonicalVersion !== undefined &&
|
|
820
|
+
(!options.requiredStatus ||
|
|
821
|
+
!Number.isSafeInteger(options.expectedCanonicalVersion) ||
|
|
822
|
+
options.expectedCanonicalVersion < 1)
|
|
823
|
+
) {
|
|
824
|
+
throw new TypeError('Canonical-version updates require a valid version and status constraint')
|
|
825
|
+
}
|
|
814
826
|
const tagPairs = input.tags === undefined ? null : validateDeclaredTagList(input.tags)
|
|
815
827
|
const existing = await getContentItem(db, id)
|
|
816
828
|
if (!existing) return null
|
|
@@ -1047,6 +1059,10 @@ export async function updateContentItem(
|
|
|
1047
1059
|
// D1 batch is a transaction: the status predicate and every content-table
|
|
1048
1060
|
// write linearize together, so a native publish/schedule cannot interleave
|
|
1049
1061
|
// between the base row and its body row.
|
|
1062
|
+
const versionPredicate =
|
|
1063
|
+
options.expectedCanonicalVersion === undefined ? '' : ' AND canonical_version = ?'
|
|
1064
|
+
const versionBindings =
|
|
1065
|
+
options.expectedCanonicalVersion === undefined ? [] : [options.expectedCanonicalVersion]
|
|
1050
1066
|
const statements = [
|
|
1051
1067
|
db
|
|
1052
1068
|
.prepare(
|
|
@@ -1056,7 +1072,7 @@ export async function updateContentItem(
|
|
|
1056
1072
|
hero_image_id = ?, hero_image_alt = ?, hero_image_caption = ?, social_image_id = ?,
|
|
1057
1073
|
canonical_url = ?, ai_locked_fields = ?, seo_focus_keyword = ?, metadata_json = ?,
|
|
1058
1074
|
updated_at = unixepoch(), canonical_version = canonical_version + 1
|
|
1059
|
-
WHERE id = ? AND status = ? AND deleted_at IS NULL
|
|
1075
|
+
WHERE id = ? AND status = ? AND deleted_at IS NULL${versionPredicate}
|
|
1060
1076
|
RETURNING slug`,
|
|
1061
1077
|
)
|
|
1062
1078
|
.bind(
|
|
@@ -1081,6 +1097,7 @@ export async function updateContentItem(
|
|
|
1081
1097
|
metadataJson,
|
|
1082
1098
|
id,
|
|
1083
1099
|
options.requiredStatus,
|
|
1100
|
+
...versionBindings,
|
|
1084
1101
|
),
|
|
1085
1102
|
]
|
|
1086
1103
|
|
|
@@ -1137,7 +1154,7 @@ export async function updateContentItem(
|
|
|
1137
1154
|
WHERE content_id = ?
|
|
1138
1155
|
AND EXISTS (
|
|
1139
1156
|
SELECT 1 FROM content_items
|
|
1140
|
-
WHERE id = ? AND status = ? AND deleted_at IS NULL
|
|
1157
|
+
WHERE id = ? AND status = ? AND deleted_at IS NULL${versionPredicate}
|
|
1141
1158
|
)
|
|
1142
1159
|
RETURNING content_id`,
|
|
1143
1160
|
)
|
|
@@ -1167,6 +1184,7 @@ export async function updateContentItem(
|
|
|
1167
1184
|
id,
|
|
1168
1185
|
id,
|
|
1169
1186
|
options.requiredStatus,
|
|
1187
|
+
...versionBindings,
|
|
1170
1188
|
),
|
|
1171
1189
|
)
|
|
1172
1190
|
}
|
|
@@ -1178,11 +1196,11 @@ export async function updateContentItem(
|
|
|
1178
1196
|
// up front even when the guarded write below is refused.
|
|
1179
1197
|
const tagRows = tagPairs.length > 0 ? await ensureTags(db, tagPairs) : []
|
|
1180
1198
|
const statusGuard = `EXISTS (SELECT 1 FROM content_items
|
|
1181
|
-
WHERE id = ? AND status = ? AND deleted_at IS NULL)`
|
|
1199
|
+
WHERE id = ? AND status = ? AND deleted_at IS NULL${versionPredicate})`
|
|
1182
1200
|
statements.push(
|
|
1183
1201
|
db
|
|
1184
1202
|
.prepare(`DELETE FROM content_tag_links WHERE content_id = ? AND ${statusGuard}`)
|
|
1185
|
-
.bind(id, id, options.requiredStatus),
|
|
1203
|
+
.bind(id, id, options.requiredStatus, ...versionBindings),
|
|
1186
1204
|
)
|
|
1187
1205
|
for (const tag of tagRows) {
|
|
1188
1206
|
statements.push(
|
|
@@ -1191,11 +1209,16 @@ export async function updateContentItem(
|
|
|
1191
1209
|
`INSERT INTO content_tag_links (content_id, tag_id, label)
|
|
1192
1210
|
SELECT ?, ?, ? WHERE ${statusGuard}`,
|
|
1193
1211
|
)
|
|
1194
|
-
.bind(id, tag.id, tag.label, id, options.requiredStatus),
|
|
1212
|
+
.bind(id, tag.id, tag.label, id, options.requiredStatus, ...versionBindings),
|
|
1195
1213
|
)
|
|
1196
1214
|
}
|
|
1197
1215
|
}
|
|
1198
1216
|
|
|
1217
|
+
// D1 batch is atomic. Keep the base row's version unchanged while every
|
|
1218
|
+
// dependent body/tag write checks it; advance it only as the final write.
|
|
1219
|
+
const baseStatement = statements.shift()!
|
|
1220
|
+
const baseResultIndex = statements.length
|
|
1221
|
+
statements.push(baseStatement)
|
|
1199
1222
|
const statusResultIndex = statements.length
|
|
1200
1223
|
statements.push(
|
|
1201
1224
|
db
|
|
@@ -1204,10 +1227,13 @@ export async function updateContentItem(
|
|
|
1204
1227
|
)
|
|
1205
1228
|
const results = await db.batch(statements)
|
|
1206
1229
|
const baseChanged =
|
|
1207
|
-
(results[
|
|
1208
|
-
(results[
|
|
1230
|
+
(results[baseResultIndex]?.results?.length ?? 0) > 0 ||
|
|
1231
|
+
(results[baseResultIndex]?.meta as { changes?: number } | undefined)?.changes === 1
|
|
1209
1232
|
if (!baseChanged) {
|
|
1210
1233
|
const status = results[statusResultIndex]?.results?.[0]?.status
|
|
1234
|
+
if (status === options.requiredStatus && options.expectedCanonicalVersion !== undefined) {
|
|
1235
|
+
return { slug: existing.slug, canonicalVersionConflict: true }
|
|
1236
|
+
}
|
|
1211
1237
|
return typeof status === 'string' ? { slug: existing.slug, statusConflict: status } : null
|
|
1212
1238
|
}
|
|
1213
1239
|
return { slug }
|
package/src/foundry-emit.ts
CHANGED
|
@@ -35,6 +35,9 @@ const HERO_IMAGE_KEY = /^[A-Za-z0-9][A-Za-z0-9._\-/]*$/
|
|
|
35
35
|
export const FoundryArticleV2Schema = z
|
|
36
36
|
.object({
|
|
37
37
|
schemaVersion: z.literal(FOUNDRY_ARTICLE_SCHEMA_VERSION),
|
|
38
|
+
createOnly: z.literal(true).optional(),
|
|
39
|
+
/** Actual CMS source revision pinned by a strategy-led improvement. */
|
|
40
|
+
expectedRevisionId: z.string().uuid().optional(),
|
|
38
41
|
slug: z.string().refine(isCanonicalSlug, {
|
|
39
42
|
message: 'slug must already be canonical: lowercase a-z, 0-9 and single hyphens, 3-512 chars',
|
|
40
43
|
}),
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
undismissInsight,
|
|
12
12
|
} from '../engine/content-insights.js'
|
|
13
13
|
import { verifyHmac } from '../engine/foundry-dispatch.js'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getPublishedContentBySlug,
|
|
16
|
+
listPublishedContent,
|
|
17
|
+
PublishedContentReadError,
|
|
18
|
+
} from '../engine/published-content.js'
|
|
15
19
|
import { InsightsIngestPayloadSchema } from '../schema/insights-ingest.js'
|
|
16
20
|
import { InsightDismissalReasonCodeSchema } from '../schema/types.js'
|
|
17
21
|
import type { CmsRouteConfig } from './config.js'
|
|
@@ -56,6 +60,22 @@ const PublishedArticleStateSchema = z
|
|
|
56
60
|
})
|
|
57
61
|
.strict()
|
|
58
62
|
|
|
63
|
+
const PublishedArticleSourceSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
ok: z.literal(true),
|
|
66
|
+
schema: z.literal('cms.published-article-source.v1'),
|
|
67
|
+
site: z.string().min(1),
|
|
68
|
+
observed_at: z.string().datetime(),
|
|
69
|
+
article: PublishedArticleStateSchema.extend({
|
|
70
|
+
hero_image_url: z.string().url().nullable(),
|
|
71
|
+
body_markdown: z.string().min(1).max(1_000_000),
|
|
72
|
+
description: z.string().nullable(),
|
|
73
|
+
byline: z.string().nullable(),
|
|
74
|
+
tags: z.array(z.string()),
|
|
75
|
+
}).strict(),
|
|
76
|
+
})
|
|
77
|
+
.strict()
|
|
78
|
+
|
|
59
79
|
const PublishedInputsResponseSchema = z
|
|
60
80
|
.object({
|
|
61
81
|
ok: z.literal(true),
|
|
@@ -76,6 +96,64 @@ async function publishedInputs(ctx: RouteContext, url: URL, siteId: string): Pro
|
|
|
76
96
|
return json({ error: 'invalid_published_mode' }, 400)
|
|
77
97
|
if (siteId === 'default') return json({ error: 'publication_identity_not_configured' }, 503)
|
|
78
98
|
if (url.searchParams.get('site') !== siteId) return json({ error: 'publication_mismatch' }, 403)
|
|
99
|
+
const articleSlug = url.searchParams.get('article')
|
|
100
|
+
if (articleSlug !== null) {
|
|
101
|
+
const slug = z
|
|
102
|
+
.string()
|
|
103
|
+
.min(1)
|
|
104
|
+
.max(512)
|
|
105
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/)
|
|
106
|
+
.safeParse(articleSlug)
|
|
107
|
+
if (!slug.success || url.searchParams.has('cursor') || url.searchParams.has('limit'))
|
|
108
|
+
return json({ error: 'invalid_article_source_request' }, 400)
|
|
109
|
+
try {
|
|
110
|
+
const content = await getPublishedContentBySlug(ctx.db, slug.data)
|
|
111
|
+
if (!content || content.type !== 'article')
|
|
112
|
+
return json({ error: 'published_article_missing' }, 404)
|
|
113
|
+
const item = content.payload.item
|
|
114
|
+
let heroImageUrl: string | null = null
|
|
115
|
+
if (item.hero_image_id) {
|
|
116
|
+
const asset = await ctx.db
|
|
117
|
+
.prepare(
|
|
118
|
+
"SELECT public_url FROM media_assets WHERE id = ? AND asset_type = 'image' AND status = 'ready'",
|
|
119
|
+
)
|
|
120
|
+
.bind(item.hero_image_id)
|
|
121
|
+
.first<{ public_url: string | null }>()
|
|
122
|
+
const publicUrl = z.string().url().safeParse(asset?.public_url)
|
|
123
|
+
if (publicUrl.success) heroImageUrl = publicUrl.data
|
|
124
|
+
}
|
|
125
|
+
const result = PublishedArticleSourceSchema.safeParse({
|
|
126
|
+
ok: true,
|
|
127
|
+
schema: 'cms.published-article-source.v1',
|
|
128
|
+
site: siteId,
|
|
129
|
+
observed_at: new Date().toISOString(),
|
|
130
|
+
article: {
|
|
131
|
+
content_id: content.contentId,
|
|
132
|
+
revision_id: content.revisionId,
|
|
133
|
+
slug: content.slug,
|
|
134
|
+
title: item.title,
|
|
135
|
+
canonical_url: item.canonical_url ?? null,
|
|
136
|
+
published_at: content.publishedAt,
|
|
137
|
+
source_id: item.source_id ?? null,
|
|
138
|
+
youtube_video_id: content.metadata.videoId ?? null,
|
|
139
|
+
hero_image_id: item.hero_image_id ?? null,
|
|
140
|
+
hero_image_url: heroImageUrl,
|
|
141
|
+
hero_image_caption: item.hero_image_caption ?? null,
|
|
142
|
+
body_markdown: content.payload.content?.body_markdown,
|
|
143
|
+
description: item.description ?? null,
|
|
144
|
+
byline: item.byline ?? null,
|
|
145
|
+
tags: content.payload.tags,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
if (!result.success) return json({ error: 'published_article_source_invalid' }, 409)
|
|
149
|
+
if (new TextEncoder().encode(JSON.stringify(result.data)).byteLength > 4 << 20)
|
|
150
|
+
return json({ error: 'published_article_source_too_large' }, 409)
|
|
151
|
+
return json(result.data)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof PublishedContentReadError) return json({ error: error.code }, 409)
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
}
|
|
79
157
|
const limit = z.coerce
|
|
80
158
|
.number()
|
|
81
159
|
.int()
|
package/src/routes/content.ts
CHANGED
|
@@ -235,6 +235,8 @@ export interface ContentRouteConfig extends CmsRouteConfig {
|
|
|
235
235
|
videoSourceProductionEnabled?: boolean
|
|
236
236
|
/** Internal machine-route constraint: atomically update only in this status. */
|
|
237
237
|
updateStatusConstraint?: ContentStatus
|
|
238
|
+
/** Internal machine-route pin; joins the existing status-constrained atomic batch. */
|
|
239
|
+
updateCanonicalVersionConstraint?: number
|
|
238
240
|
/**
|
|
239
241
|
* Internal (Foundry machine route, M3-18): replaces ONLY the publish action's
|
|
240
242
|
* commit step (publishContent + createRevision) with the caller's — publishOne
|
|
@@ -1807,6 +1809,7 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1807
1809
|
try {
|
|
1808
1810
|
updated = await updateContentItem(ctx.db, id, parsed.data, {
|
|
1809
1811
|
requiredStatus: config.updateStatusConstraint,
|
|
1812
|
+
expectedCanonicalVersion: config.updateCanonicalVersionConstraint,
|
|
1810
1813
|
...(expectedMediaSource ? { expectedMediaSource } : {}),
|
|
1811
1814
|
})
|
|
1812
1815
|
} catch (error) {
|
|
@@ -1821,6 +1824,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1821
1824
|
if (updated.statusConflict) {
|
|
1822
1825
|
return json({ error: 'update_status_conflict', state: updated.statusConflict }, 409)
|
|
1823
1826
|
}
|
|
1827
|
+
if (updated.canonicalVersionConflict) {
|
|
1828
|
+
return json({ error: 'update_canonical_conflict' }, 409)
|
|
1829
|
+
}
|
|
1824
1830
|
if (updated.mediaSourceConflict) {
|
|
1825
1831
|
return json({ error: 'media_source_conflict' }, 409)
|
|
1826
1832
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isCanonicalHost, sha256Hex, verifyFoundryRequest } from '@growth-labs/foundry-signing'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
+
import { ContentMetadataSchema, mergePinnedArticleMetadata } from '../engine/content-metadata.js'
|
|
3
4
|
import { type PublicationReconciler, publishOne } from '../engine/publication.js'
|
|
4
5
|
import { PublicationCurrentSnapshotError } from '../engine/publication-current-guard.js'
|
|
5
6
|
import { PublishContentValidationError } from '../engine/publisher.js'
|
|
@@ -263,6 +264,9 @@ async function mapUpdateConflict(response: Response, id: string): Promise<Respon
|
|
|
263
264
|
if (response.status !== 409) return response
|
|
264
265
|
try {
|
|
265
266
|
const payload = (await response.clone().json()) as { error?: unknown; state?: unknown }
|
|
267
|
+
if (payload.error === 'update_canonical_conflict') {
|
|
268
|
+
return json({ error: 'human_edit_pending', external_id: id }, 409)
|
|
269
|
+
}
|
|
266
270
|
if (payload.error === 'update_status_conflict' && typeof payload.state === 'string') {
|
|
267
271
|
return approvalConflict(id, payload.state)
|
|
268
272
|
}
|
|
@@ -470,6 +474,7 @@ async function bindAdoption(
|
|
|
470
474
|
config: ContentRouteConfig,
|
|
471
475
|
id: string,
|
|
472
476
|
candidate: ContentCandidate,
|
|
477
|
+
expectedVersion?: number,
|
|
473
478
|
): Promise<boolean> {
|
|
474
479
|
const sourceId = `${SOURCE_PREFIX}${id}`
|
|
475
480
|
await ctx.db.batch([
|
|
@@ -477,9 +482,17 @@ async function bindAdoption(
|
|
|
477
482
|
.prepare(
|
|
478
483
|
`UPDATE content_items SET source_id = ?, updated_at = unixepoch()
|
|
479
484
|
WHERE id = ? AND deleted_at IS NULL AND type = 'article' AND source_id = ?
|
|
480
|
-
AND NOT EXISTS (SELECT 1 FROM cms_foundry_articles WHERE content_id = ?)
|
|
485
|
+
AND NOT EXISTS (SELECT 1 FROM cms_foundry_articles WHERE content_id = ?)
|
|
486
|
+
AND (? IS NULL OR canonical_version = ?)`,
|
|
481
487
|
)
|
|
482
|
-
.bind(
|
|
488
|
+
.bind(
|
|
489
|
+
sourceId,
|
|
490
|
+
candidate.id,
|
|
491
|
+
candidate.source_id,
|
|
492
|
+
candidate.id,
|
|
493
|
+
expectedVersion ?? null,
|
|
494
|
+
expectedVersion ?? null,
|
|
495
|
+
),
|
|
483
496
|
ctx.db
|
|
484
497
|
.prepare(
|
|
485
498
|
`INSERT INTO cms_activity_log (id, workspace_id, user_id, action, target_id, target_title, type)
|
|
@@ -518,6 +531,27 @@ async function canonicalVersion(ctx: RouteContext, contentId: string): Promise<n
|
|
|
518
531
|
return row?.canonical_version ?? null
|
|
519
532
|
}
|
|
520
533
|
|
|
534
|
+
/** A signed improvement names the actual immutable CMS revision it read. */
|
|
535
|
+
async function sourceRevisionVersion(
|
|
536
|
+
ctx: RouteContext,
|
|
537
|
+
contentId: string,
|
|
538
|
+
revisionId: string,
|
|
539
|
+
): Promise<number | null> {
|
|
540
|
+
const revision = await getRevision(ctx.db, contentId, revisionId)
|
|
541
|
+
if (!revision) return null
|
|
542
|
+
const item = revision.payload.item as Record<string, unknown> | undefined
|
|
543
|
+
const version = item?.canonical_version
|
|
544
|
+
if (
|
|
545
|
+
item?.id !== contentId ||
|
|
546
|
+
item.type !== 'article' ||
|
|
547
|
+
typeof version !== 'number' ||
|
|
548
|
+
!Number.isSafeInteger(version) ||
|
|
549
|
+
version < 1
|
|
550
|
+
)
|
|
551
|
+
return null
|
|
552
|
+
return (await canonicalVersion(ctx, contentId)) === version ? version : null
|
|
553
|
+
}
|
|
554
|
+
|
|
521
555
|
/**
|
|
522
556
|
* Record what this v2 PUT wrote as a History revision (the autosave/restore
|
|
523
557
|
* mechanism; it never moves the published pointer), created_by `foundry` and
|
|
@@ -595,6 +629,12 @@ async function putV2(
|
|
|
595
629
|
const body = parsed.data
|
|
596
630
|
|
|
597
631
|
const prior = await findArticle(ctx, id)
|
|
632
|
+
if (body.createOnly && prior && (prior.request_sha256 !== bodyHash || prior.status !== 'draft')) {
|
|
633
|
+
return json(
|
|
634
|
+
{ error: 'article_already_exists', external_id: id, content_id: prior.content_id },
|
|
635
|
+
409,
|
|
636
|
+
)
|
|
637
|
+
}
|
|
598
638
|
if (prior?.request_sha256 === bodyHash) {
|
|
599
639
|
console.info('[cms] foundry article request deduplicated', {
|
|
600
640
|
externalId: id,
|
|
@@ -610,8 +650,41 @@ async function putV2(
|
|
|
610
650
|
// Scheduled, review and archived items stay human-owned.
|
|
611
651
|
if (bound && !V2_WRITABLE.has(bound.status)) return approvalConflict(id, bound.status)
|
|
612
652
|
|
|
653
|
+
if (body.createOnly) {
|
|
654
|
+
if (body.expectedRevisionId) return json({ error: 'create_only_has_source_revision' }, 400)
|
|
655
|
+
if (body.adoptSourceId || bound)
|
|
656
|
+
return json({ error: 'article_already_exists', external_id: id }, 409)
|
|
657
|
+
const slugHolder = await findCandidate(ctx, 'slug', body.slug)
|
|
658
|
+
if (slugHolder) return slugConflict(id, body.slug, slugHolder.id)
|
|
659
|
+
const videoId = body.metadata?.videoId
|
|
660
|
+
if (typeof videoId !== 'string' || !/^[A-Za-z0-9_-]{11}$/.test(videoId))
|
|
661
|
+
return json({ error: 'create_only_requires_source_video' }, 400)
|
|
662
|
+
// Check the actual CMS working and immutable published projections, not
|
|
663
|
+
// just Foundry's own relationship ledger. A creation cannot adopt or edit.
|
|
664
|
+
const associated = await ctx.db
|
|
665
|
+
.prepare(`SELECT c.id FROM content_items c
|
|
666
|
+
LEFT JOIN content_revisions r ON r.id=c.published_revision_id AND r.content_id=c.id
|
|
667
|
+
WHERE c.type='article' AND c.deleted_at IS NULL AND (
|
|
668
|
+
CASE WHEN json_valid(c.metadata_json) THEN json_extract(c.metadata_json,'$.videoId') END = ?
|
|
669
|
+
OR CASE WHEN json_valid(json_extract(r.payload_json,'$.item.metadata_json')) THEN json_extract(json_extract(r.payload_json,'$.item.metadata_json'),'$.videoId') END = ?
|
|
670
|
+
) LIMIT 1`)
|
|
671
|
+
.bind(videoId, videoId)
|
|
672
|
+
.first<{ id: string }>()
|
|
673
|
+
if (associated)
|
|
674
|
+
return json(
|
|
675
|
+
{ error: 'article_video_already_exists', external_id: id, content_id: associated.id },
|
|
676
|
+
409,
|
|
677
|
+
)
|
|
678
|
+
}
|
|
613
679
|
let adoption: Adoption | null = null
|
|
680
|
+
let expectedSourceVersion: number | undefined
|
|
614
681
|
if (bound) {
|
|
682
|
+
if (body.expectedRevisionId) {
|
|
683
|
+
const version = await sourceRevisionVersion(ctx, bound.id, body.expectedRevisionId)
|
|
684
|
+
if (version === null) return json({ error: 'human_edit_pending', external_id: id }, 409)
|
|
685
|
+
expectedSourceVersion = version
|
|
686
|
+
}
|
|
687
|
+
|
|
615
688
|
const holder = await findCandidate(ctx, 'slug', body.slug)
|
|
616
689
|
if (holder && holder.id !== bound.id) return slugConflict(id, body.slug, holder.id)
|
|
617
690
|
} else {
|
|
@@ -619,7 +692,12 @@ async function putV2(
|
|
|
619
692
|
if ('error' in resolved) return resolved.error
|
|
620
693
|
if (resolved.candidate && resolved.matchedBy) {
|
|
621
694
|
const candidate = resolved.candidate
|
|
622
|
-
if (
|
|
695
|
+
if (body.expectedRevisionId) {
|
|
696
|
+
const version = await sourceRevisionVersion(ctx, candidate.id, body.expectedRevisionId)
|
|
697
|
+
if (version === null) return json({ error: 'human_edit_pending', external_id: id }, 409)
|
|
698
|
+
expectedSourceVersion = version
|
|
699
|
+
}
|
|
700
|
+
if (!(await bindAdoption(ctx, config, id, candidate, expectedSourceVersion))) {
|
|
623
701
|
return json(
|
|
624
702
|
{
|
|
625
703
|
error: 'adoption_conflict',
|
|
@@ -656,11 +734,34 @@ async function putV2(
|
|
|
656
734
|
}
|
|
657
735
|
}
|
|
658
736
|
|
|
659
|
-
//
|
|
660
|
-
|
|
737
|
+
// Preserve a newer human working copy before PUT, not merely at publication.
|
|
738
|
+
// The native update pins this same canonical version in its atomic batch.
|
|
739
|
+
if (body.expectedRevisionId && !bound)
|
|
740
|
+
return json({ error: 'source_revision_missing', external_id: id }, 409)
|
|
741
|
+
const priorVersion =
|
|
742
|
+
expectedSourceVersion ?? (bound ? await canonicalVersion(ctx, bound.id) : null)
|
|
661
743
|
const heroImageId = body.hero_image_url
|
|
662
744
|
? await heroAssetId(body.hero_image_url)
|
|
663
745
|
: (body.hero_image_key ?? undefined)
|
|
746
|
+
// A pinned improvement augments its exact source metadata. Existing dates,
|
|
747
|
+
// image provenance and other recorded fields survive preparation updates.
|
|
748
|
+
let metadata = body.metadata
|
|
749
|
+
if (body.expectedRevisionId && body.metadata !== undefined && bound) {
|
|
750
|
+
const source = await getRevision(ctx.db, bound.id, body.expectedRevisionId)
|
|
751
|
+
let original: unknown
|
|
752
|
+
try {
|
|
753
|
+
original = JSON.parse(String(source?.payload.item?.metadata_json ?? '{}'))
|
|
754
|
+
} catch {
|
|
755
|
+
return json({ error: 'source_metadata_invalid', external_id: id }, 409)
|
|
756
|
+
}
|
|
757
|
+
const recorded = ContentMetadataSchema.safeParse(original)
|
|
758
|
+
if (!recorded.success) return json({ error: 'source_metadata_invalid', external_id: id }, 409)
|
|
759
|
+
const merged = ContentMetadataSchema.safeParse(
|
|
760
|
+
mergePinnedArticleMetadata(recorded.data, body.metadata),
|
|
761
|
+
)
|
|
762
|
+
if (!merged.success) return json({ error: 'source_metadata_invalid', external_id: id }, 409)
|
|
763
|
+
metadata = merged.data
|
|
764
|
+
}
|
|
664
765
|
const nativeBody = {
|
|
665
766
|
title: body.title,
|
|
666
767
|
slug: body.slug,
|
|
@@ -672,7 +773,7 @@ async function putV2(
|
|
|
672
773
|
...(body.desk !== undefined ? { primaryCategory: body.desk } : {}),
|
|
673
774
|
...(body.tags !== undefined ? { tags: body.tags } : {}),
|
|
674
775
|
...(body.visibility !== undefined ? { visibility: body.visibility } : {}),
|
|
675
|
-
...(
|
|
776
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
676
777
|
content: { bodyMarkdown: body.body_html, bodyHtml: body.body_html },
|
|
677
778
|
}
|
|
678
779
|
|
|
@@ -681,7 +782,12 @@ async function putV2(
|
|
|
681
782
|
// Each write is a CAS on the status it was read in: a draft stays a
|
|
682
783
|
// draft write, and a published item is edited in place only while it is
|
|
683
784
|
// still published (its live revision changes only on POST /publish).
|
|
684
|
-
|
|
785
|
+
if (priorVersion === null) return json({ error: 'human_edit_pending', external_id: id }, 409)
|
|
786
|
+
const writer = createContentRoutes({
|
|
787
|
+
...machineConfig(config),
|
|
788
|
+
updateStatusConstraint: bound.status,
|
|
789
|
+
updateCanonicalVersionConstraint: priorVersion,
|
|
790
|
+
})
|
|
685
791
|
nativeResponse = await mapUpdateConflict(
|
|
686
792
|
await writer.update(nativeContext(ctx, 'PATCH', nativeBody, { id: bound.id })),
|
|
687
793
|
id,
|
|
@@ -698,6 +804,8 @@ async function putV2(
|
|
|
698
804
|
)
|
|
699
805
|
} catch (error) {
|
|
700
806
|
const raced = await findContentBySource(ctx, id)
|
|
807
|
+
if (body.createOnly && raced)
|
|
808
|
+
return json({ error: 'article_already_exists', external_id: id, content_id: raced.id }, 409)
|
|
701
809
|
if (raced?.type !== 'article') throw error
|
|
702
810
|
nativeResponse = await mapUpdateConflict(
|
|
703
811
|
await handlers.draft.update(nativeContext(ctx, 'PATCH', nativeBody, { id: raced.id })),
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
applyMediaSourceReadiness,
|
|
34
34
|
buildContentCreatePayload,
|
|
35
35
|
buildContentUpdatePayload,
|
|
36
|
-
|
|
36
|
+
contentDraftSaveError,
|
|
37
37
|
foundryDispatchSourceForDraft,
|
|
38
38
|
initialContentMetadata,
|
|
39
39
|
type MediaSourceReadinessPayload,
|
|
@@ -406,19 +406,14 @@ export function ContentForm({
|
|
|
406
406
|
const draft = currentForm
|
|
407
407
|
|
|
408
408
|
if (id === null) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
) {
|
|
414
|
-
const mediaSource = foundryDispatchSourceForDraft(contentType, draft)
|
|
415
|
-
const missingMediaDuration =
|
|
416
|
-
mediaSource && normalizeMediaDurationSeconds(draft.durationSeconds) === null
|
|
409
|
+
const saveError = contentDraftSaveError(contentType, draft, {
|
|
410
|
+
requirePrimaryTopic: primaryTopicEnabled,
|
|
411
|
+
primaryTopicLabel: primaryTopic?.label,
|
|
412
|
+
})
|
|
413
|
+
if (saveError) {
|
|
417
414
|
setForm((prev) => ({
|
|
418
415
|
...prev,
|
|
419
|
-
saveError
|
|
420
|
-
? 'Enter the authoritative media duration in whole seconds before saving.'
|
|
421
|
-
: 'Add the required title and content before saving.',
|
|
416
|
+
saveError,
|
|
422
417
|
}))
|
|
423
418
|
return
|
|
424
419
|
}
|
|
@@ -543,7 +538,7 @@ export function ContentForm({
|
|
|
543
538
|
}
|
|
544
539
|
}
|
|
545
540
|
},
|
|
546
|
-
[contentType, onDocCreated, primaryTopicEnabled, queueFoundryProcessing],
|
|
541
|
+
[contentType, onDocCreated, primaryTopicEnabled, primaryTopic?.label, queueFoundryProcessing],
|
|
547
542
|
)
|
|
548
543
|
|
|
549
544
|
useEffect(() => {
|