@growth-labs/cms 0.5.11 → 0.5.13
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 +22 -0
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +3 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publication.d.ts.map +1 -1
- package/dist/engine/publication.js +23 -5
- package/dist/engine/publication.js.map +1 -1
- package/dist/engine/published-content.d.ts.map +1 -1
- package/dist/engine/published-content.js +14 -5
- package/dist/engine/published-content.js.map +1 -1
- package/dist/engine/publisher.d.ts +3 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +39 -31
- package/dist/engine/publisher.js.map +1 -1
- package/dist/engine/revisions.d.ts +3 -0
- package/dist/engine/revisions.d.ts.map +1 -1
- package/dist/engine/revisions.js +7 -1
- package/dist/engine/revisions.js.map +1 -1
- package/dist/engine/tags.d.ts +9 -2
- package/dist/engine/tags.d.ts.map +1 -1
- package/dist/engine/tags.js +32 -21
- package/dist/engine/tags.js.map +1 -1
- package/dist/engine/taxonomy.d.ts +31 -0
- package/dist/engine/taxonomy.d.ts.map +1 -0
- package/dist/engine/taxonomy.js +125 -0
- package/dist/engine/taxonomy.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +98 -27
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/tags.d.ts.map +1 -1
- package/dist/routes/tags.js +36 -3
- package/dist/routes/tags.js.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +15 -1
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/types.d.ts +1 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/migrations/0023_content_tag_link_labels.sql +11 -0
- package/package.json +1 -1
- package/src/engine/index.ts +14 -0
- package/src/engine/publication.ts +27 -12
- package/src/engine/published-content.ts +14 -8
- package/src/engine/publisher.ts +76 -39
- package/src/engine/revisions.ts +10 -1
- package/src/engine/tags.ts +39 -19
- package/src/engine/taxonomy.ts +226 -0
- package/src/index.ts +2 -0
- package/src/routes/content.ts +91 -30
- package/src/routes/tags.ts +36 -3
- package/src/schema/migrations.ts +15 -1
- package/src/schema/types.ts +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { slugify } from './slug.js';
|
|
2
|
+
/** Maximum number of tags per content item. Covers the measured fleet max (16) with headroom. */
|
|
3
|
+
export const MAX_ITEM_TAG_COUNT = 32;
|
|
4
|
+
/** Maximum UTF-8 byte length for a single declared tag display label. */
|
|
5
|
+
export const MAX_TAG_LABEL_BYTES = 128;
|
|
6
|
+
/** Maximum total UTF-8 bytes across all declared tag labels for one content item. */
|
|
7
|
+
export const MAX_ITEM_TAGS_TOTAL_BYTES = 2_048;
|
|
8
|
+
export class TagInputError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
limit;
|
|
11
|
+
actual;
|
|
12
|
+
index;
|
|
13
|
+
constructor(code, message, limit, actual, index = null) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.limit = limit;
|
|
17
|
+
this.actual = actual;
|
|
18
|
+
this.index = index;
|
|
19
|
+
this.name = 'TagInputError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function utf8ByteLength(value) {
|
|
23
|
+
return new TextEncoder().encode(value).byteLength;
|
|
24
|
+
}
|
|
25
|
+
function fail(code, message, limit, actual, index = null) {
|
|
26
|
+
throw new TagInputError(code, message, limit, actual, index);
|
|
27
|
+
}
|
|
28
|
+
function assertTagCount(tags) {
|
|
29
|
+
if (tags.length > MAX_ITEM_TAG_COUNT) {
|
|
30
|
+
fail('tag_count_exceeded', `tag count exceeds the ${MAX_ITEM_TAG_COUNT}-item limit`, MAX_ITEM_TAG_COUNT, tags.length);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function validateDeclaredLabel(value, index) {
|
|
34
|
+
if (typeof value !== 'string') {
|
|
35
|
+
fail('tag_not_string', 'each tag must be a string', MAX_ITEM_TAG_COUNT, -1, index);
|
|
36
|
+
}
|
|
37
|
+
if (!value.trim()) {
|
|
38
|
+
fail('tag_blank', 'tag must not be blank or whitespace-only', MAX_ITEM_TAG_COUNT, -1, index);
|
|
39
|
+
}
|
|
40
|
+
if (value !== value.trim()) {
|
|
41
|
+
fail('tag_untrimmed', 'tag must not contain surrounding whitespace', MAX_ITEM_TAG_COUNT, -1, index);
|
|
42
|
+
}
|
|
43
|
+
if (value !== value.normalize('NFC')) {
|
|
44
|
+
fail('tag_not_nfc', 'tag must be NFC-normalized', MAX_ITEM_TAG_COUNT, -1, index);
|
|
45
|
+
}
|
|
46
|
+
const labelBytes = utf8ByteLength(value);
|
|
47
|
+
if (labelBytes > MAX_TAG_LABEL_BYTES) {
|
|
48
|
+
fail('tag_label_too_long', `tag label exceeds the ${MAX_TAG_LABEL_BYTES}-byte limit`, MAX_TAG_LABEL_BYTES, labelBytes, index);
|
|
49
|
+
}
|
|
50
|
+
const slug = slugify(value);
|
|
51
|
+
if (!slug) {
|
|
52
|
+
fail('tag_non_slugifiable', 'tag label must produce a non-empty slug after canonicalization', MAX_ITEM_TAG_COUNT, -1, index);
|
|
53
|
+
}
|
|
54
|
+
return { slug, label: value };
|
|
55
|
+
}
|
|
56
|
+
function validateCanonicalSlug(value, index) {
|
|
57
|
+
if (typeof value !== 'string') {
|
|
58
|
+
fail('tag_not_string', 'each canonical tag must be a string', MAX_ITEM_TAG_COUNT, -1, index);
|
|
59
|
+
}
|
|
60
|
+
if (!value ||
|
|
61
|
+
value !== value.trim() ||
|
|
62
|
+
value !== value.normalize('NFC') ||
|
|
63
|
+
slugify(value) !== value) {
|
|
64
|
+
fail('tag_canonical_invalid', 'canonical tag must be a normalized slug', MAX_ITEM_TAG_COUNT, -1, index);
|
|
65
|
+
}
|
|
66
|
+
const slugBytes = utf8ByteLength(value);
|
|
67
|
+
if (slugBytes > MAX_TAG_LABEL_BYTES) {
|
|
68
|
+
fail('tag_label_too_long', `canonical tag exceeds the ${MAX_TAG_LABEL_BYTES}-byte limit`, MAX_TAG_LABEL_BYTES, slugBytes, index);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function validatePairs(pairs) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
let totalBytes = 0;
|
|
75
|
+
for (let index = 0; index < pairs.length; index += 1) {
|
|
76
|
+
const pair = pairs[index];
|
|
77
|
+
if (seen.has(pair.slug)) {
|
|
78
|
+
fail('tag_duplicate', `duplicate tag after canonicalization: slug "${pair.slug}"`, MAX_ITEM_TAG_COUNT, -1, index);
|
|
79
|
+
}
|
|
80
|
+
seen.add(pair.slug);
|
|
81
|
+
totalBytes += utf8ByteLength(pair.label);
|
|
82
|
+
if (totalBytes > MAX_ITEM_TAGS_TOTAL_BYTES) {
|
|
83
|
+
fail('tags_total_bytes_exceeded', `total tag bytes exceed the ${MAX_ITEM_TAGS_TOTAL_BYTES}-byte limit`, MAX_ITEM_TAGS_TOTAL_BYTES, totalBytes, index);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return pairs;
|
|
87
|
+
}
|
|
88
|
+
export function validateDeclaredTagList(tags) {
|
|
89
|
+
if (!Array.isArray(tags)) {
|
|
90
|
+
fail('tags_not_array', 'tags must be an array', MAX_ITEM_TAG_COUNT, -1);
|
|
91
|
+
}
|
|
92
|
+
assertTagCount(tags);
|
|
93
|
+
return validatePairs(tags.map((tag, index) => validateDeclaredLabel(tag, index)));
|
|
94
|
+
}
|
|
95
|
+
export function validateSingleDeclaredTag(label) {
|
|
96
|
+
return validateDeclaredTagList([label])[0];
|
|
97
|
+
}
|
|
98
|
+
export function validateRevisionTaxonomy(input) {
|
|
99
|
+
const tags = input.tags;
|
|
100
|
+
if (!Array.isArray(tags)) {
|
|
101
|
+
fail('tags_not_array', 'revision tags must be an array', MAX_ITEM_TAG_COUNT, -1);
|
|
102
|
+
}
|
|
103
|
+
assertTagCount(tags);
|
|
104
|
+
const labels = input.tagLabels === undefined && input.allowLegacyLabels ? tags : input.tagLabels;
|
|
105
|
+
if (!Array.isArray(labels) || labels.length !== tags.length) {
|
|
106
|
+
fail('tag_labels_mismatch', 'revision tagLabels must be parallel to tags', tags.length, Array.isArray(labels) ? labels.length : -1);
|
|
107
|
+
}
|
|
108
|
+
const pairs = tags.map((tag, index) => {
|
|
109
|
+
const slug = validateCanonicalSlug(tag, index);
|
|
110
|
+
const label = validateDeclaredLabel(labels[index], index);
|
|
111
|
+
if (label.slug !== slug) {
|
|
112
|
+
fail('tag_label_mismatch', 'revision tag label does not canonicalize to its parallel tag', MAX_ITEM_TAG_COUNT, -1, index);
|
|
113
|
+
}
|
|
114
|
+
return { slug, label: label.label };
|
|
115
|
+
});
|
|
116
|
+
return validatePairs(pairs);
|
|
117
|
+
}
|
|
118
|
+
export function validateStoredTaxonomyRows(rows) {
|
|
119
|
+
return validateRevisionTaxonomy({
|
|
120
|
+
tags: rows.map((row) => row.slug),
|
|
121
|
+
tagLabels: rows.map((row) => row.label),
|
|
122
|
+
allowLegacyLabels: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=taxonomy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../../src/engine/taxonomy.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkCA,cAAc,mBAAmB,CAAA;AAGjC,cAAc,sBAAsB,CAAA;AAEpC,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,eAAe,EAEf,iBAAiB,EACjB,wBAAwB,EAExB,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,4BAA4B,EACjC,KAAK,8BAA8B,EACnC,KAAK,2BAA2B,EAChC,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAElB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAC/B,gCAAgC,EAChC,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GACzB,MAAM,mBAAmB,CAAA;AAC1B,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
// carries no Foundry/Hermes coupling.
|
|
30
30
|
// The publishing engine — the content data-access core (WS7-05). Also
|
|
31
31
|
// available at the `@growth-labs/cms/engine` subpath.
|
|
32
|
+
// Tag taxonomy bounds (MAX_ITEM_TAG_COUNT, MAX_TAG_LABEL_BYTES, MAX_ITEM_TAGS_TOTAL_BYTES)
|
|
33
|
+
// and TagInputError are re-exported via engine/index.js.
|
|
32
34
|
export * from './engine/index.js';
|
|
33
35
|
// Injected-provider interfaces + null implementations (spec §3.3).
|
|
34
36
|
export * from './providers/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,EAAE;AACF,mEAAmE;AACnE,qGAAqG;AACrG,uEAAuE;AACvE,6BAA6B;AAC7B,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,qEAAqE;AACrE,iDAAiD;AACjD,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,oEAAoE;AACpE,2EAA2E;AAC3E,+EAA+E;AAC/E,qBAAqB;AACrB,oEAAoE;AACpE,wEAAwE;AACxE,4EAA4E;AAC5E,yEAAyE;AACzE,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,6EAA6E;AAC7E,iDAAiD;AAEjD,sEAAsE;AACtE,sDAAsD;AACtD,cAAc,mBAAmB,CAAA;AAEjC,mEAAmE;AACnE,cAAc,sBAAsB,CAAA;AAGpC,OAAO,EAGN,eAAe;AACf,qBAAqB;AACrB,iBAAiB,EACjB,wBAAwB;AACxB,iBAAiB;AACjB,UAAU,EAyBV,gCAAgC,EAGhC,cAAc,EAGd,eAAe,GAMf,MAAM,mBAAmB,CAAA;AAC1B,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,EAAE;AACF,mEAAmE;AACnE,qGAAqG;AACrG,uEAAuE;AACvE,6BAA6B;AAC7B,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,qEAAqE;AACrE,iDAAiD;AACjD,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,oEAAoE;AACpE,2EAA2E;AAC3E,+EAA+E;AAC/E,qBAAqB;AACrB,oEAAoE;AACpE,wEAAwE;AACxE,4EAA4E;AAC5E,yEAAyE;AACzE,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,6EAA6E;AAC7E,iDAAiD;AAEjD,sEAAsE;AACtE,sDAAsD;AACtD,2FAA2F;AAC3F,yDAAyD;AACzD,cAAc,mBAAmB,CAAA;AAEjC,mEAAmE;AACnE,cAAc,sBAAsB,CAAA;AAGpC,OAAO,EAGN,eAAe;AACf,qBAAqB;AACrB,iBAAiB,EACjB,wBAAwB;AACxB,iBAAiB;AACjB,UAAU,EAyBV,gCAAgC,EAGhC,cAAc,EAGd,eAAe,GAMf,MAAM,mBAAmB,CAAA;AAC1B,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/routes/content.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/routes/content.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/content.js
CHANGED
|
@@ -20,10 +20,11 @@ import { createDekFailureNotifications } from '../engine/ai-dek-notifications.js
|
|
|
20
20
|
import { listContributors, setContributors } from '../engine/contributors.js';
|
|
21
21
|
import { applyFoundryCallback, dispatchToFoundry, FoundryDispatchPrerequisiteError, verifyHmac, } from '../engine/foundry-dispatch.js';
|
|
22
22
|
import { evaluateArticleBody } from '../engine/publish-guard.js';
|
|
23
|
-
import { createContent, createRevision, duplicateContentItem, evaluateContentBodyForPublish, getContentItem, getContentRelations, publishContent, scheduleContent, unpublishContent, updateContentItem, VerifiedMediaDurationConflictError, } from '../engine/publisher.js';
|
|
23
|
+
import { createContent, createRevision, duplicateContentItem, evaluateContentBodyForPublish, getContentItem, getContentRelations, getContentTagPairs, publishContent, scheduleContent, unpublishContent, updateContentItem, VerifiedMediaDurationConflictError, } from '../engine/publisher.js';
|
|
24
24
|
import { createRevisionWithDelta, getRevision, listRevisions, restoreRevision, } from '../engine/revisions.js';
|
|
25
25
|
import { applySlugRenameRedirects } from '../engine/slug-redirects.js';
|
|
26
26
|
import { softDeleteContent } from '../engine/soft-delete.js';
|
|
27
|
+
import { TagInputError } from '../engine/taxonomy.js';
|
|
27
28
|
import { canPerformContentAction } from './authz-matrix.js';
|
|
28
29
|
import { resolveConfig } from './config.js';
|
|
29
30
|
import { json, requireId } from './context.js';
|
|
@@ -432,11 +433,7 @@ async function getContentPayload(ctx, type, id) {
|
|
|
432
433
|
return null;
|
|
433
434
|
}
|
|
434
435
|
async function getContentTags(ctx, id) {
|
|
435
|
-
|
|
436
|
-
.prepare('SELECT t.slug FROM content_tag_links l JOIN content_tags t ON t.id = l.tag_id WHERE l.content_id = ? ORDER BY t.label ASC')
|
|
437
|
-
.bind(id)
|
|
438
|
-
.all();
|
|
439
|
-
return (result.results || []).map((row) => row.slug);
|
|
436
|
+
return (await getContentTagPairs(ctx.db, id)).map((row) => row.slug);
|
|
440
437
|
}
|
|
441
438
|
async function getMediaAssetUrls(db, ids) {
|
|
442
439
|
const uniqueIds = Array.from(new Set(ids.filter(Boolean)));
|
|
@@ -458,6 +455,15 @@ async function getMediaAssetUrls(db, ids) {
|
|
|
458
455
|
async function readJson(ctx) {
|
|
459
456
|
return ctx.request.json().catch(() => null);
|
|
460
457
|
}
|
|
458
|
+
function tagInputErrorResponse(error) {
|
|
459
|
+
return json({
|
|
460
|
+
error: 'Invalid tags',
|
|
461
|
+
code: error.code,
|
|
462
|
+
limit: error.limit,
|
|
463
|
+
actual: error.actual,
|
|
464
|
+
index: error.index,
|
|
465
|
+
}, 400);
|
|
466
|
+
}
|
|
461
467
|
export function createContentRoutes(config) {
|
|
462
468
|
const resolved = resolveConfig(config);
|
|
463
469
|
const { CreateSchema, UpdateSchema } = buildSchemas(resolved);
|
|
@@ -741,6 +747,8 @@ export function createContentRoutes(config) {
|
|
|
741
747
|
created = await createContent(ctx.db, parsed.data);
|
|
742
748
|
}
|
|
743
749
|
catch (error) {
|
|
750
|
+
if (error instanceof TagInputError)
|
|
751
|
+
return tagInputErrorResponse(error);
|
|
744
752
|
const conflict = verifiedMediaDurationConflictResponse(error);
|
|
745
753
|
if (conflict)
|
|
746
754
|
return conflict;
|
|
@@ -774,17 +782,32 @@ export function createContentRoutes(config) {
|
|
|
774
782
|
const item = await getContentItem(ctx.db, id);
|
|
775
783
|
if (!item)
|
|
776
784
|
return json({ error: 'Not found' }, 404);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
785
|
+
let content;
|
|
786
|
+
let tags;
|
|
787
|
+
let relations;
|
|
788
|
+
let author;
|
|
789
|
+
try {
|
|
790
|
+
const resolvedContent = await Promise.all([
|
|
791
|
+
getContentPayload(ctx, item.type, id),
|
|
792
|
+
getContentTags(ctx, id),
|
|
793
|
+
getContentRelations(ctx.db, id),
|
|
794
|
+
item.author_id
|
|
795
|
+
? ctx.db
|
|
796
|
+
.prepare('SELECT name, slug FROM authors WHERE id = ? LIMIT 1')
|
|
797
|
+
.bind(item.author_id)
|
|
798
|
+
.first()
|
|
799
|
+
: Promise.resolve(null),
|
|
800
|
+
]);
|
|
801
|
+
content = resolvedContent[0];
|
|
802
|
+
tags = resolvedContent[1];
|
|
803
|
+
relations = resolvedContent[2];
|
|
804
|
+
author = resolvedContent[3];
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
if (error instanceof TagInputError)
|
|
808
|
+
return tagInputErrorResponse(error);
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
788
811
|
const contentRecord = content;
|
|
789
812
|
const thumbnailImageId = typeof contentRecord?.thumbnail_image_id === 'string'
|
|
790
813
|
? contentRecord.thumbnail_image_id
|
|
@@ -871,6 +894,8 @@ export function createContentRoutes(config) {
|
|
|
871
894
|
updated = await updateContentItem(ctx.db, id, parsed.data);
|
|
872
895
|
}
|
|
873
896
|
catch (error) {
|
|
897
|
+
if (error instanceof TagInputError)
|
|
898
|
+
return tagInputErrorResponse(error);
|
|
874
899
|
const conflict = verifiedMediaDurationConflictResponse(error);
|
|
875
900
|
if (conflict)
|
|
876
901
|
return conflict;
|
|
@@ -1035,8 +1060,23 @@ export function createContentRoutes(config) {
|
|
|
1035
1060
|
}
|
|
1036
1061
|
}
|
|
1037
1062
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1063
|
+
try {
|
|
1064
|
+
await getContentTagPairs(ctx.db, id);
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
if (error instanceof TagInputError)
|
|
1068
|
+
return tagInputErrorResponse(error);
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
await publishContent(ctx.db, id, now);
|
|
1073
|
+
await createRevision(ctx.db, id, ctx.userId || null);
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
if (error instanceof TagInputError)
|
|
1077
|
+
return tagInputErrorResponse(error);
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
1040
1080
|
// Write first-party activity log row (cms_activity_log, P5 Task 3).
|
|
1041
1081
|
// Wrapped so a logging failure never breaks the publish response.
|
|
1042
1082
|
try {
|
|
@@ -1197,10 +1237,18 @@ export function createContentRoutes(config) {
|
|
|
1197
1237
|
return json({ status });
|
|
1198
1238
|
}
|
|
1199
1239
|
if (action === 'snapshot') {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1240
|
+
let revision;
|
|
1241
|
+
try {
|
|
1242
|
+
revision = await createRevisionWithDelta(ctx.db, id, {
|
|
1243
|
+
createdBy: ctx.userId ?? null,
|
|
1244
|
+
tag: 'autosave',
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
catch (error) {
|
|
1248
|
+
if (error instanceof TagInputError)
|
|
1249
|
+
return tagInputErrorResponse(error);
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1204
1252
|
if (!revision)
|
|
1205
1253
|
return json({ error: 'Not found' }, 404);
|
|
1206
1254
|
return json({ status: 'snapshot', revisionId: revision.id });
|
|
@@ -1265,9 +1313,24 @@ export function createContentRoutes(config) {
|
|
|
1265
1313
|
},
|
|
1266
1314
|
async foundryCallback(ctx) {
|
|
1267
1315
|
// HMAC-guarded machine callback — NOT behind requirePublisher.
|
|
1268
|
-
// The HMAC secret
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1316
|
+
// The HMAC secret arrives from ctx.env as either a plain per-Worker
|
|
1317
|
+
// secret string or a Secrets Store binding object (`{ get() }`) —
|
|
1318
|
+
// consumers bind it both ways. Never a value in code/D1. The old
|
|
1319
|
+
// string-only check silently nulled the binding shape and 500ed
|
|
1320
|
+
// every callback on Store-bound consumers.
|
|
1321
|
+
const rawHmac = ctx.env?.FOUNDRY_HMAC_SECRET;
|
|
1322
|
+
let hmacSecret = null;
|
|
1323
|
+
if (typeof rawHmac === 'string') {
|
|
1324
|
+
hmacSecret = rawHmac;
|
|
1325
|
+
}
|
|
1326
|
+
else if (rawHmac && typeof rawHmac.get === 'function') {
|
|
1327
|
+
try {
|
|
1328
|
+
hmacSecret = (await rawHmac.get()) || null;
|
|
1329
|
+
}
|
|
1330
|
+
catch {
|
|
1331
|
+
hmacSecret = null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1271
1334
|
if (!hmacSecret) {
|
|
1272
1335
|
// No secret configured — refuse with 500 (misconfiguration).
|
|
1273
1336
|
return json({ error: 'FOUNDRY_HMAC_SECRET is not configured' }, 500);
|
|
@@ -1480,7 +1543,15 @@ export function createContentRoutes(config) {
|
|
|
1480
1543
|
const rev = ctx.params.rev;
|
|
1481
1544
|
if (!rev)
|
|
1482
1545
|
return json({ error: 'Missing rev' }, 400);
|
|
1483
|
-
|
|
1546
|
+
let ok;
|
|
1547
|
+
try {
|
|
1548
|
+
ok = await restoreRevision(ctx.db, id, rev, { createdBy: ctx.userId ?? null });
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
if (error instanceof TagInputError)
|
|
1552
|
+
return tagInputErrorResponse(error);
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1484
1555
|
if (!ok)
|
|
1485
1556
|
return json({ error: 'Not found' }, 404);
|
|
1486
1557
|
return json({ restored: true });
|