@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.
Files changed (56) hide show
  1. package/README.md +22 -0
  2. package/dist/engine/index.d.ts +3 -2
  3. package/dist/engine/index.d.ts.map +1 -1
  4. package/dist/engine/index.js +3 -2
  5. package/dist/engine/index.js.map +1 -1
  6. package/dist/engine/publication.d.ts.map +1 -1
  7. package/dist/engine/publication.js +23 -5
  8. package/dist/engine/publication.js.map +1 -1
  9. package/dist/engine/published-content.d.ts.map +1 -1
  10. package/dist/engine/published-content.js +14 -5
  11. package/dist/engine/published-content.js.map +1 -1
  12. package/dist/engine/publisher.d.ts +3 -1
  13. package/dist/engine/publisher.d.ts.map +1 -1
  14. package/dist/engine/publisher.js +39 -31
  15. package/dist/engine/publisher.js.map +1 -1
  16. package/dist/engine/revisions.d.ts +3 -0
  17. package/dist/engine/revisions.d.ts.map +1 -1
  18. package/dist/engine/revisions.js +7 -1
  19. package/dist/engine/revisions.js.map +1 -1
  20. package/dist/engine/tags.d.ts +9 -2
  21. package/dist/engine/tags.d.ts.map +1 -1
  22. package/dist/engine/tags.js +32 -21
  23. package/dist/engine/tags.js.map +1 -1
  24. package/dist/engine/taxonomy.d.ts +31 -0
  25. package/dist/engine/taxonomy.d.ts.map +1 -0
  26. package/dist/engine/taxonomy.js +125 -0
  27. package/dist/engine/taxonomy.js.map +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/routes/content.d.ts.map +1 -1
  32. package/dist/routes/content.js +98 -27
  33. package/dist/routes/content.js.map +1 -1
  34. package/dist/routes/tags.d.ts.map +1 -1
  35. package/dist/routes/tags.js +36 -3
  36. package/dist/routes/tags.js.map +1 -1
  37. package/dist/schema/migrations.d.ts.map +1 -1
  38. package/dist/schema/migrations.js +15 -1
  39. package/dist/schema/migrations.js.map +1 -1
  40. package/dist/schema/types.d.ts +1 -0
  41. package/dist/schema/types.d.ts.map +1 -1
  42. package/dist/schema/types.js.map +1 -1
  43. package/migrations/0023_content_tag_link_labels.sql +11 -0
  44. package/package.json +1 -1
  45. package/src/engine/index.ts +14 -0
  46. package/src/engine/publication.ts +27 -12
  47. package/src/engine/published-content.ts +14 -8
  48. package/src/engine/publisher.ts +76 -39
  49. package/src/engine/revisions.ts +10 -1
  50. package/src/engine/tags.ts +39 -19
  51. package/src/engine/taxonomy.ts +226 -0
  52. package/src/index.ts +2 -0
  53. package/src/routes/content.ts +91 -30
  54. package/src/routes/tags.ts +36 -3
  55. package/src/schema/migrations.ts +15 -1
  56. 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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,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"}
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":"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"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/routes/content.ts"],"names":[],"mappings":"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"}
@@ -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
- const result = await ctx.db
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
- const [content, tags, relations, author] = await Promise.all([
778
- getContentPayload(ctx, item.type, id),
779
- getContentTags(ctx, id),
780
- getContentRelations(ctx.db, id),
781
- item.author_id
782
- ? ctx.db
783
- .prepare('SELECT name, slug FROM authors WHERE id = ? LIMIT 1')
784
- .bind(item.author_id)
785
- .first()
786
- : Promise.resolve(null),
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
- await publishContent(ctx.db, id, now);
1039
- await createRevision(ctx.db, id, ctx.userId || null);
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
- const revision = await createRevisionWithDelta(ctx.db, id, {
1201
- createdBy: ctx.userId ?? null,
1202
- tag: 'autosave',
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 is read from ctx.env (Secrets Store binding on the
1269
- // Workers side; `env.FOUNDRY_HMAC_SECRET`). Never a value in code/D1.
1270
- const hmacSecret = typeof ctx.env?.FOUNDRY_HMAC_SECRET === 'string' ? ctx.env.FOUNDRY_HMAC_SECRET : null;
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
- const ok = await restoreRevision(ctx.db, id, rev, { createdBy: ctx.userId ?? null });
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 });