canopycms 0.0.55 → 0.0.56

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 (71) hide show
  1. package/dist/api/github-sync.d.ts.map +1 -1
  2. package/dist/api/github-sync.js +28 -7
  3. package/dist/api/github-sync.js.map +1 -1
  4. package/dist/auth/caching-auth-plugin.d.ts +8 -1
  5. package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
  6. package/dist/auth/caching-auth-plugin.js +6 -1
  7. package/dist/auth/caching-auth-plugin.js.map +1 -1
  8. package/dist/auth/plugin.d.ts +19 -10
  9. package/dist/auth/plugin.d.ts.map +1 -1
  10. package/dist/auth/plugin.js +12 -10
  11. package/dist/auth/plugin.js.map +1 -1
  12. package/dist/build/generate-ai-content.d.ts.map +1 -1
  13. package/dist/build/generate-ai-content.js +7 -0
  14. package/dist/build/generate-ai-content.js.map +1 -1
  15. package/dist/cli/generate-ai-content.js +438 -38
  16. package/dist/cli/init.d.ts.map +1 -1
  17. package/dist/cli/init.js +16 -8
  18. package/dist/cli/init.js.map +1 -1
  19. package/dist/cli/templates.d.ts.map +1 -1
  20. package/dist/cli/templates.js +6 -3
  21. package/dist/cli/templates.js.map +1 -1
  22. package/dist/config/helpers.d.ts.map +1 -1
  23. package/dist/config/helpers.js +3 -0
  24. package/dist/config/helpers.js.map +1 -1
  25. package/dist/config/schemas/config.d.ts +3 -4
  26. package/dist/config/schemas/config.d.ts.map +1 -1
  27. package/dist/config/schemas/config.js +6 -3
  28. package/dist/config/schemas/config.js.map +1 -1
  29. package/dist/config/types.d.ts +6 -1
  30. package/dist/config/types.d.ts.map +1 -1
  31. package/dist/config-test.d.ts +5 -3
  32. package/dist/config-test.d.ts.map +1 -1
  33. package/dist/config-test.js +9 -1
  34. package/dist/config-test.js.map +1 -1
  35. package/dist/content-listing.d.ts +3 -1
  36. package/dist/content-listing.d.ts.map +1 -1
  37. package/dist/content-listing.js +1 -0
  38. package/dist/content-listing.js.map +1 -1
  39. package/dist/content-store.d.ts.map +1 -1
  40. package/dist/content-store.js +11 -1
  41. package/dist/content-store.js.map +1 -1
  42. package/dist/editor/Editor.d.ts.map +1 -1
  43. package/dist/editor/Editor.js +19 -15
  44. package/dist/editor/Editor.js.map +1 -1
  45. package/dist/editor/hooks/useDraftManager.d.ts +7 -0
  46. package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
  47. package/dist/editor/hooks/useDraftManager.js +69 -31
  48. package/dist/editor/hooks/useDraftManager.js.map +1 -1
  49. package/dist/editor/hooks/useSchemaManager.d.ts +34 -10
  50. package/dist/editor/hooks/useSchemaManager.d.ts.map +1 -1
  51. package/dist/editor/hooks/useSchemaManager.js +20 -24
  52. package/dist/editor/hooks/useSchemaManager.js.map +1 -1
  53. package/dist/editor/schema-editor/CollectionEditor.d.ts +6 -5
  54. package/dist/editor/schema-editor/CollectionEditor.d.ts.map +1 -1
  55. package/dist/editor/schema-editor/CollectionEditor.js +9 -4
  56. package/dist/editor/schema-editor/CollectionEditor.js.map +1 -1
  57. package/dist/github-service.d.ts +63 -7
  58. package/dist/github-service.d.ts.map +1 -1
  59. package/dist/github-service.js +147 -39
  60. package/dist/github-service.js.map +1 -1
  61. package/dist/services.js +4 -0
  62. package/dist/services.js.map +1 -1
  63. package/dist/static/index.d.ts +32 -0
  64. package/dist/static/index.d.ts.map +1 -1
  65. package/dist/static/index.js +90 -0
  66. package/dist/static/index.js.map +1 -1
  67. package/dist/worker/cms-worker.d.ts +6 -3
  68. package/dist/worker/cms-worker.d.ts.map +1 -1
  69. package/dist/worker/cms-worker.js +21 -38
  70. package/dist/worker/cms-worker.js.map +1 -1
  71. package/package.json +2 -1
@@ -9,11 +9,11 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // dist/paths/normalize.js
12
- function normalizeFilesystemPath(path17) {
13
- return path17.split(/[\\/]+/).filter(Boolean).join("/");
12
+ function normalizeFilesystemPath(path18) {
13
+ return path18.split(/[\\/]+/).filter(Boolean).join("/");
14
14
  }
15
- function hasTraversalSequence(path17) {
16
- const normalized = normalizeFilesystemPath(path17);
15
+ function hasTraversalSequence(path18) {
16
+ const normalized = normalizeFilesystemPath(path18);
17
17
  return normalized.includes("..");
18
18
  }
19
19
  function createLogicalPath(...segments) {
@@ -23,14 +23,14 @@ function createLogicalPath(...segments) {
23
23
  }
24
24
  return normalized;
25
25
  }
26
- function trimSlashes(path17) {
26
+ function trimSlashes(path18) {
27
27
  let start = 0;
28
- let end = path17.length;
29
- while (start < end && path17[start] === "/")
28
+ let end = path18.length;
29
+ while (start < end && path18[start] === "/")
30
30
  start++;
31
- while (end > start && path17[end - 1] === "/")
31
+ while (end > start && path18[end - 1] === "/")
32
32
  end--;
33
- return path17.slice(start, end);
33
+ return path18.slice(start, end);
34
34
  }
35
35
  var init_normalize = __esm({
36
36
  "dist/paths/normalize.js"() {
@@ -319,7 +319,7 @@ var init_config = __esm({
319
319
  gitBotAuthorNameSchema = z4.string().min(1);
320
320
  gitBotAuthorEmailSchema = z4.string().email();
321
321
  githubTokenEnvVarSchema = z4.string().default("GITHUB_BOT_TOKEN");
322
- operatingModeSchema = z4.enum(["prod", "dev"]).default("dev");
322
+ operatingModeSchema = z4.enum(["prod", "dev"]);
323
323
  deployedAsSchema = z4.enum(["static", "server"]).default("server");
324
324
  contentRootSchema = relativePathSchema.default("content");
325
325
  sourceRootSchema = z4.string().min(1).optional();
@@ -357,8 +357,9 @@ var init_config = __esm({
357
357
  gitBotAuthorName: gitBotAuthorNameSchema,
358
358
  gitBotAuthorEmail: gitBotAuthorEmailSchema,
359
359
  githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
360
+ // Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
361
+ // validation loudly rather than silently running header-trusting dev auth semantics.
360
362
  mode: operatingModeSchema,
361
- // Has .default(), so not optional in output type
362
363
  deployedAs: deployedAsSchema,
363
364
  // Has .default('server'), so always present after validation
364
365
  settingsBranch: z4.string().optional(),
@@ -702,12 +703,12 @@ var init_operating_mode = __esm({
702
703
  });
703
704
 
704
705
  // dist/cli/generate-ai-content.js
705
- import path16 from "node:path";
706
+ import path17 from "node:path";
706
707
  import { createJiti } from "jiti";
707
708
 
708
709
  // dist/build/generate-ai-content.js
709
- import fs14 from "node:fs/promises";
710
- import path15 from "node:path";
710
+ import fs15 from "node:fs/promises";
711
+ import path16 from "node:path";
711
712
 
712
713
  // dist/content-store.js
713
714
  import fs6 from "node:fs/promises";
@@ -736,16 +737,16 @@ async function atomicWriteFile(filePath, content) {
736
737
  var locks = /* @__PURE__ */ new Map();
737
738
  async function withLock(key, fn) {
738
739
  const prev = locks.get(key) ?? Promise.resolve();
739
- let resolve;
740
+ let resolve2;
740
741
  const next = new Promise((r) => {
741
- resolve = r;
742
+ resolve2 = r;
742
743
  });
743
744
  locks.set(key, next);
744
745
  await prev;
745
746
  try {
746
747
  return await fn();
747
748
  } finally {
748
- resolve();
749
+ resolve2();
749
750
  if (locks.get(key) === next)
750
751
  locks.delete(key);
751
752
  }
@@ -1076,13 +1077,13 @@ function extractIdFromFilename(filename) {
1076
1077
  return null;
1077
1078
  }
1078
1079
  async function resolveCollectionPath(root, logicalPath) {
1079
- const fs15 = await import("node:fs/promises");
1080
- const path17 = await import("node:path");
1080
+ const fs16 = await import("node:fs/promises");
1081
+ const path18 = await import("node:path");
1081
1082
  const segments = logicalPath.split("/").filter(Boolean);
1082
1083
  let currentPath = root;
1083
1084
  for (const segment of segments) {
1084
1085
  try {
1085
- const entries = await fs15.readdir(currentPath, { withFileTypes: true });
1086
+ const entries = await fs16.readdir(currentPath, { withFileTypes: true });
1086
1087
  const matchingDir = entries.find((entry) => {
1087
1088
  if (!entry.isDirectory())
1088
1089
  return false;
@@ -1090,7 +1091,7 @@ async function resolveCollectionPath(root, logicalPath) {
1090
1091
  return logicalName === segment.toLowerCase();
1091
1092
  });
1092
1093
  if (matchingDir) {
1093
- currentPath = path17.join(currentPath, matchingDir.name);
1094
+ currentPath = path18.join(currentPath, matchingDir.name);
1094
1095
  } else {
1095
1096
  return null;
1096
1097
  }
@@ -1342,6 +1343,25 @@ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(v
1342
1343
  // dist/paths/index.js
1343
1344
  init_normalize();
1344
1345
 
1346
+ // dist/paths/normalize-server.js
1347
+ import { sep, resolve, relative } from "node:path";
1348
+ function validateAndNormalizePath(root, target) {
1349
+ const resolvedRoot = resolve(root);
1350
+ const withSep = resolvedRoot.endsWith(sep) ? resolvedRoot : `${resolvedRoot}${sep}`;
1351
+ const resolvedTarget = resolve(target);
1352
+ if (!resolvedTarget.startsWith(withSep) && resolvedTarget !== resolvedRoot) {
1353
+ return {
1354
+ valid: false,
1355
+ error: "Path traversal detected"
1356
+ };
1357
+ }
1358
+ const relativePath = relative(resolvedRoot, resolvedTarget).split(sep).join("/");
1359
+ return {
1360
+ valid: true,
1361
+ normalizedPath: relativePath
1362
+ };
1363
+ }
1364
+
1345
1365
  // dist/paths/branch.js
1346
1366
  init_operating_mode();
1347
1367
  import fs5 from "node:fs/promises";
@@ -1556,11 +1576,11 @@ var ContentStore = class {
1556
1576
  getSchemaItems() {
1557
1577
  return this.schemaIndex.values();
1558
1578
  }
1559
- assertSchemaItem(path17) {
1560
- const normalized = normalizeFilesystemPath(path17);
1579
+ assertSchemaItem(path18) {
1580
+ const normalized = normalizeFilesystemPath(path18);
1561
1581
  const item = this.schemaIndex.get(normalized);
1562
1582
  if (!item) {
1563
- throw new ContentStoreError(`Unknown schema item: ${path17}`, "NO_SCHEMA_ITEM");
1583
+ throw new ContentStoreError(`Unknown schema item: ${path18}`, "NO_SCHEMA_ITEM");
1564
1584
  }
1565
1585
  return item;
1566
1586
  }
@@ -1800,7 +1820,7 @@ var ContentStore = class {
1800
1820
  }
1801
1821
  if (existingId && !await filePathExists(absolutePath)) {
1802
1822
  const actualRelPath = await this.findEntryPathById(path7.dirname(absolutePath), existingId);
1803
- const indexedRelPath = (await this.idIndex()).findById(existingId)?.relativePath ?? null;
1823
+ const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
1804
1824
  if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
1805
1825
  throw new ContentConflictError();
1806
1826
  }
@@ -3033,8 +3053,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
3033
3053
  if (slug && slug !== "index") {
3034
3054
  segments.push(slug);
3035
3055
  }
3036
- const path17 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3037
- return path17.toLowerCase();
3056
+ const path18 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3057
+ return path18.toLowerCase();
3038
3058
  }
3039
3059
 
3040
3060
  // dist/entry-link-resolver.js
@@ -3638,7 +3658,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
3638
3658
  if (err instanceof BranchMetadataConflictError && attempt < maxAttempts) {
3639
3659
  const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
3640
3660
  const jitter = Math.random() * baseDelay;
3641
- await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
3661
+ await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
3642
3662
  continue;
3643
3663
  }
3644
3664
  throw err;
@@ -3861,8 +3881,8 @@ var GitManager = class _GitManager {
3861
3881
  }
3862
3882
  let hasCommits = false;
3863
3883
  try {
3864
- const log5 = await sourceGit.log(["-1"]);
3865
- hasCommits = log5.total > 0;
3884
+ const log6 = await sourceGit.log(["-1"]);
3885
+ hasCommits = log6.total > 0;
3866
3886
  } catch {
3867
3887
  hasCommits = false;
3868
3888
  }
@@ -4578,6 +4598,384 @@ async function resolveBranchRoot(config) {
4578
4598
  return context.branchRoot;
4579
4599
  }
4580
4600
 
4601
+ // dist/content-listing.js
4602
+ import fs14 from "node:fs/promises";
4603
+ import path15 from "node:path";
4604
+ import matter2 from "gray-matter";
4605
+ import { parse as yamlParse2 } from "yaml";
4606
+ var log5 = createDebugLogger({ prefix: "ContentListing" });
4607
+ var normalizePath = (root, target) => {
4608
+ const result = validateAndNormalizePath(root, target);
4609
+ if (!result.valid) {
4610
+ throw new ContentStoreError(result.error || "Path traversal detected", "VALIDATION");
4611
+ }
4612
+ return result.normalizedPath;
4613
+ };
4614
+ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
4615
+ try {
4616
+ const raw = await fs14.readFile(filePath, "utf8");
4617
+ if (format === "json") {
4618
+ return asRecord(JSON.parse(raw));
4619
+ }
4620
+ if (format === "yaml") {
4621
+ return asRecord(yamlParse2(raw));
4622
+ }
4623
+ const parsed = matter2(raw);
4624
+ const data = parsed.data ?? {};
4625
+ if (parsed.content) {
4626
+ data[bodyFieldName] = parsed.content;
4627
+ }
4628
+ return data;
4629
+ } catch (err) {
4630
+ if (isNotFoundError(err))
4631
+ return {};
4632
+ log5.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
4633
+ return {};
4634
+ }
4635
+ };
4636
+ var parseTypedFilename = (filename, entryTypes) => {
4637
+ const lastDot = filename.lastIndexOf(".");
4638
+ if (lastDot === -1)
4639
+ return null;
4640
+ const nameWithoutExt = filename.slice(0, lastDot);
4641
+ const parts = nameWithoutExt.split(".");
4642
+ if (parts.length >= 3) {
4643
+ const potentialType = parts[0];
4644
+ const matchingType = entryTypes.find((e) => e.name === potentialType);
4645
+ if (matchingType) {
4646
+ const id = parts[parts.length - 1];
4647
+ if (!isValidId(id))
4648
+ return null;
4649
+ const slug = parts.slice(1, -1).join(".").toLowerCase();
4650
+ return {
4651
+ type: potentialType,
4652
+ slug,
4653
+ id
4654
+ };
4655
+ }
4656
+ }
4657
+ return null;
4658
+ };
4659
+ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
4660
+ const rootPath = options?.rootPath ?? contentRootName;
4661
+ const extract = options?.extract;
4662
+ const filter = options?.filter;
4663
+ const customSort = options?.sort;
4664
+ const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
4665
+ const collectionResults = await Promise.all(collections.map(async (collection) => {
4666
+ const entries = await listCollectionEntries(branchRoot, collection);
4667
+ return entries.map((entry) => ({ entry, collection }));
4668
+ }));
4669
+ const contentPrefix = contentRootName ? `${contentRootName}/` : "";
4670
+ const items = [];
4671
+ for (const results of collectionResults) {
4672
+ for (const { entry, collection } of results) {
4673
+ const pathWithoutRoot = entry.logicalPath.startsWith(contentPrefix) ? entry.logicalPath.slice(contentPrefix.length) : entry.logicalPath;
4674
+ const pathSegments = pathWithoutRoot.split("/").filter(Boolean);
4675
+ const urlSegments = entry.slug === "index" ? pathSegments.slice(0, -1) : pathSegments;
4676
+ const urlPath = urlSegments.length > 0 ? `/${urlSegments.join("/")}`.toLowerCase() : "/";
4677
+ const raw = entry.data;
4678
+ const meta = {
4679
+ entryPath: entry.logicalPath,
4680
+ entryType: entry.entryType,
4681
+ format: entry.format
4682
+ };
4683
+ const data = extract ? extract(raw, meta) : raw;
4684
+ const item = {
4685
+ pathSegments,
4686
+ urlPath,
4687
+ slug: entry.slug,
4688
+ entryPath: entry.logicalPath,
4689
+ entryId: entry.contentId,
4690
+ collectionId: collection.contentId,
4691
+ collectionPath: entry.collectionPath,
4692
+ entryType: entry.entryType,
4693
+ format: entry.format,
4694
+ data,
4695
+ schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
4696
+ };
4697
+ if (filter && !filter(item))
4698
+ continue;
4699
+ items.push(item);
4700
+ }
4701
+ }
4702
+ if (customSort) {
4703
+ items.sort(customSort);
4704
+ } else {
4705
+ items.sort((a, b) => a.entryPath.localeCompare(b.entryPath));
4706
+ }
4707
+ return items;
4708
+ }
4709
+ var listCollectionEntries = async (root, collection) => {
4710
+ if (collection.type !== "collection" || !collection.entries) {
4711
+ return [];
4712
+ }
4713
+ const entryTypes = collection.entries;
4714
+ const extToTypes = /* @__PURE__ */ new Map();
4715
+ for (const entryType of entryTypes) {
4716
+ const ext = getFormatExtension(entryType.format);
4717
+ const existing = extToTypes.get(ext) || [];
4718
+ existing.push(entryType);
4719
+ extToTypes.set(ext, existing);
4720
+ }
4721
+ const validExts = Array.from(extToTypes.keys());
4722
+ const collectionRoot = await resolveCollectionPath(root, collection.logicalPath);
4723
+ if (!collectionRoot) {
4724
+ return [];
4725
+ }
4726
+ normalizePath(root, collectionRoot);
4727
+ let dirents;
4728
+ try {
4729
+ dirents = await fs14.readdir(collectionRoot, { withFileTypes: true });
4730
+ } catch (err) {
4731
+ if (isNotFoundError(err))
4732
+ return [];
4733
+ throw err;
4734
+ }
4735
+ const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
4736
+ const entries = await Promise.all(files.map(async (file) => {
4737
+ const absolutePath = path15.join(collectionRoot, file.name);
4738
+ const relativePath = normalizePath(root, absolutePath);
4739
+ const parsed = parseTypedFilename(file.name, entryTypes);
4740
+ if (!parsed) {
4741
+ log5.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
4742
+ return null;
4743
+ }
4744
+ const { type: entryTypeName, slug, id: contentId } = parsed;
4745
+ const entryType = entryTypes.find((e) => e.name === entryTypeName);
4746
+ const format = entryType?.format || "json";
4747
+ const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
4748
+ const [stats, data] = await Promise.all([
4749
+ fs14.stat(absolutePath),
4750
+ readEntryData(absolutePath, format, bodyField)
4751
+ ]);
4752
+ const item = {
4753
+ logicalPath: `${collection.logicalPath}/${slug}`,
4754
+ contentId,
4755
+ slug,
4756
+ collectionPath: collection.logicalPath,
4757
+ collectionName: collection.name,
4758
+ format,
4759
+ entryType: entryTypeName,
4760
+ physicalPath: relativePath,
4761
+ data,
4762
+ updatedAt: stats.mtime.toISOString()
4763
+ };
4764
+ return item;
4765
+ }));
4766
+ return entries.filter((e) => e !== null);
4767
+ };
4768
+
4769
+ // dist/validation/entry-validator.js
4770
+ init_config2();
4771
+
4772
+ // dist/validation/field-traversal.js
4773
+ function isPlainRecord(value) {
4774
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4775
+ }
4776
+ function resolveBlockItem(blockField, item) {
4777
+ const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
4778
+ if (!templateName)
4779
+ return void 0;
4780
+ const template = blockField.templates?.find((t) => t.name === templateName);
4781
+ if (!template?.fields)
4782
+ return void 0;
4783
+ const data = isPlainRecord(item.value) ? item.value : item;
4784
+ return { fields: template.fields, data };
4785
+ }
4786
+
4787
+ // dist/validation/entry-validator.js
4788
+ var KNOWN_FIELD_TYPES = new Set(fieldTypes);
4789
+ var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "rich-text", "markdown", "mdx", "image", "code"]);
4790
+ function isPlainRecord2(value) {
4791
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4792
+ }
4793
+ function referenceValueId(value) {
4794
+ if (typeof value === "string")
4795
+ return value;
4796
+ if (isPlainRecord2(value) && typeof value.id === "string")
4797
+ return value.id;
4798
+ return void 0;
4799
+ }
4800
+ var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
4801
+ function validateScalar(field, value, path18) {
4802
+ switch (field.type) {
4803
+ case "number":
4804
+ if (typeof value !== "number" || !Number.isFinite(value)) {
4805
+ return [{ fieldPath: path18, message: "Expected a number" }];
4806
+ }
4807
+ return [];
4808
+ case "boolean":
4809
+ if (typeof value !== "boolean") {
4810
+ return [{ fieldPath: path18, message: "Expected true or false" }];
4811
+ }
4812
+ return [];
4813
+ case "datetime":
4814
+ if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
4815
+ return [{ fieldPath: path18, message: "Expected a valid date/time string" }];
4816
+ }
4817
+ return [];
4818
+ case "select": {
4819
+ if (typeof value !== "string") {
4820
+ return [{ fieldPath: path18, message: "Expected a selection" }];
4821
+ }
4822
+ const options = selectOptionValues(field);
4823
+ if (value !== "" && !options.includes(value)) {
4824
+ return [{ fieldPath: path18, message: `Must be one of: ${options.join(", ")}` }];
4825
+ }
4826
+ return [];
4827
+ }
4828
+ case "reference":
4829
+ if (referenceValueId(value) === void 0) {
4830
+ return [{ fieldPath: path18, message: "Expected a reference id" }];
4831
+ }
4832
+ return [];
4833
+ default:
4834
+ if (STRING_FIELD_TYPES.has(field.type)) {
4835
+ if (typeof value !== "string") {
4836
+ return [{ fieldPath: path18, message: "Expected text" }];
4837
+ }
4838
+ }
4839
+ return [];
4840
+ }
4841
+ }
4842
+ function isEmptyForRequired(field, value) {
4843
+ if (value === void 0 || value === null)
4844
+ return true;
4845
+ if (Array.isArray(value))
4846
+ return value.length === 0;
4847
+ if (typeof value === "string") {
4848
+ return value.trim() === "";
4849
+ }
4850
+ return false;
4851
+ }
4852
+ function validateEntryData(fields, data, pathPrefix = "") {
4853
+ const errors = [];
4854
+ for (const field of fields) {
4855
+ if (field.type === "group") {
4856
+ errors.push(...validateEntryData(field.fields, data, pathPrefix));
4857
+ continue;
4858
+ }
4859
+ const path18 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
4860
+ const value = data[field.name];
4861
+ const isRequired = "required" in field && field.required === true;
4862
+ if (isRequired && isEmptyForRequired(field, value)) {
4863
+ const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
4864
+ errors.push({ fieldPath: path18, message });
4865
+ continue;
4866
+ }
4867
+ if (value === void 0 || value === null)
4868
+ continue;
4869
+ if (field.type === "block") {
4870
+ const blockField = field;
4871
+ if (!Array.isArray(value)) {
4872
+ errors.push({ fieldPath: path18, message: "Expected a list of blocks" });
4873
+ continue;
4874
+ }
4875
+ value.forEach((item, index) => {
4876
+ const itemPath = `${path18}[${index}]`;
4877
+ if (!isPlainRecord2(item)) {
4878
+ errors.push({ fieldPath: itemPath, message: "Expected a block object" });
4879
+ return;
4880
+ }
4881
+ const resolved = resolveBlockItem(blockField, item);
4882
+ if (!resolved) {
4883
+ const template = typeof item.template === "string" ? item.template : void 0;
4884
+ errors.push({
4885
+ fieldPath: itemPath,
4886
+ message: template ? `Unknown block template "${template}"` : "Missing block template"
4887
+ });
4888
+ return;
4889
+ }
4890
+ errors.push(...validateEntryData(resolved.fields, resolved.data, itemPath));
4891
+ });
4892
+ continue;
4893
+ }
4894
+ if (field.type === "object") {
4895
+ const objectField = field;
4896
+ if (objectField.list) {
4897
+ if (!Array.isArray(value)) {
4898
+ errors.push({ fieldPath: path18, message: "Expected a list of items" });
4899
+ continue;
4900
+ }
4901
+ value.forEach((item, index) => {
4902
+ const itemPath = `${path18}[${index}]`;
4903
+ if (!isPlainRecord2(item)) {
4904
+ errors.push({ fieldPath: itemPath, message: "Expected an object" });
4905
+ return;
4906
+ }
4907
+ if (objectField.fields) {
4908
+ errors.push(...validateEntryData(objectField.fields, item, itemPath));
4909
+ }
4910
+ });
4911
+ } else if (!isPlainRecord2(value)) {
4912
+ errors.push({ fieldPath: path18, message: "Expected an object" });
4913
+ } else if (objectField.fields) {
4914
+ errors.push(...validateEntryData(objectField.fields, value, path18));
4915
+ }
4916
+ continue;
4917
+ }
4918
+ if ("list" in field && field.list) {
4919
+ if (!Array.isArray(value)) {
4920
+ errors.push({ fieldPath: path18, message: "Expected a list of values" });
4921
+ continue;
4922
+ }
4923
+ value.forEach((item, index) => {
4924
+ errors.push(...validateScalar(field, item, `${path18}[${index}]`));
4925
+ });
4926
+ continue;
4927
+ }
4928
+ if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
4929
+ errors.push({ fieldPath: path18, message: "Expected a single value, not a list" });
4930
+ continue;
4931
+ }
4932
+ errors.push(...validateScalar(field, value, path18));
4933
+ }
4934
+ return errors;
4935
+ }
4936
+
4937
+ // dist/static/index.js
4938
+ function normalizeDatesDeep(value) {
4939
+ if (value instanceof Date)
4940
+ return value.toISOString();
4941
+ if (Array.isArray(value))
4942
+ return value.map(normalizeDatesDeep);
4943
+ if (value && typeof value === "object") {
4944
+ const result = {};
4945
+ for (const [key, v] of Object.entries(value)) {
4946
+ result[key] = normalizeDatesDeep(v);
4947
+ }
4948
+ return result;
4949
+ }
4950
+ return value;
4951
+ }
4952
+ function findInvalidEntries(items) {
4953
+ const invalid = [];
4954
+ for (const item of items) {
4955
+ if (!item.schema)
4956
+ continue;
4957
+ const data = normalizeDatesDeep(item.data);
4958
+ const errors = validateEntryData(item.schema, data);
4959
+ if (errors.length > 0) {
4960
+ invalid.push({ entryPath: item.entryPath, errors });
4961
+ }
4962
+ }
4963
+ return invalid;
4964
+ }
4965
+ function assertBuildEntriesValid(items, phaseLabel) {
4966
+ const invalid = findInvalidEntries(items);
4967
+ if (invalid.length === 0)
4968
+ return;
4969
+ const lines = invalid.map(({ entryPath, errors }) => {
4970
+ const [first, ...rest] = errors;
4971
+ const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
4972
+ return ` - ${entryPath} \u2014 ${summary}`;
4973
+ });
4974
+ throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
4975
+ ${lines.join("\n")}
4976
+ These are likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
4977
+ }
4978
+
4581
4979
  // dist/build/generate-ai-content.js
4582
4980
  async function generateAIContentFiles(options) {
4583
4981
  const { config, entrySchemaRegistry, outputDir, aiConfig, _testFlatSchema } = options;
@@ -4591,6 +4989,8 @@ async function generateAIContentFiles(options) {
4591
4989
  const cached = await schemaCache.getSchema(branchRoot, entrySchemaRegistry, contentRootName);
4592
4990
  flatSchema = cached.flatSchema;
4593
4991
  }
4992
+ const entriesForValidation = await listEntries(branchRoot, flatSchema, contentRootName);
4993
+ assertBuildEntriesValid(entriesForValidation, "AI content generation");
4594
4994
  const store = new ContentStore(branchRoot, flatSchema);
4595
4995
  const result = await generateAIContent({
4596
4996
  store,
@@ -4599,15 +4999,15 @@ async function generateAIContentFiles(options) {
4599
4999
  config: aiConfig,
4600
5000
  entryLinkUrl: config.entryLinkUrl
4601
5001
  });
4602
- const absoluteOutputDir = path15.resolve(outputDir) + path15.sep;
5002
+ const absoluteOutputDir = path16.resolve(outputDir) + path16.sep;
4603
5003
  let fileCount = 0;
4604
5004
  for (const [filePath, content] of result.files) {
4605
- const absolutePath = path15.resolve(path15.join(absoluteOutputDir, filePath));
5005
+ const absolutePath = path16.resolve(path16.join(absoluteOutputDir, filePath));
4606
5006
  if (!absolutePath.startsWith(absoluteOutputDir)) {
4607
5007
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
4608
5008
  }
4609
- await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
4610
- await fs14.writeFile(absolutePath, content, "utf-8");
5009
+ await fs15.mkdir(path16.dirname(absolutePath), { recursive: true });
5010
+ await fs15.writeFile(absolutePath, content, "utf-8");
4611
5011
  fileCount++;
4612
5012
  }
4613
5013
  return { fileCount, outputDir: absoluteOutputDir };
@@ -4618,7 +5018,7 @@ var jiti = createJiti(import.meta.url);
4618
5018
  async function generateAIContentCLI(options) {
4619
5019
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
4620
5020
  console.log("\nCanopyCMS generate-ai-content\n");
4621
- const canopyConfigPath = path16.join(projectDir, "canopycms.config.ts");
5021
+ const canopyConfigPath = path17.join(projectDir, "canopycms.config.ts");
4622
5022
  let canopyConfigModule;
4623
5023
  try {
4624
5024
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -4629,7 +5029,7 @@ async function generateAIContentCLI(options) {
4629
5029
  }
4630
5030
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
4631
5031
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
4632
- const schemasPath = path16.join(projectDir, appDir, "schemas.ts");
5032
+ const schemasPath = path17.join(projectDir, appDir, "schemas.ts");
4633
5033
  let entrySchemaRegistry = {};
4634
5034
  try {
4635
5035
  const schemasModule = await jiti.import(schemasPath);
@@ -4640,7 +5040,7 @@ async function generateAIContentCLI(options) {
4640
5040
  let aiConfig;
4641
5041
  if (configPath) {
4642
5042
  try {
4643
- const aiConfigModule = await jiti.import(path16.resolve(configPath));
5043
+ const aiConfigModule = await jiti.import(path17.resolve(configPath));
4644
5044
  aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
4645
5045
  } catch (err) {
4646
5046
  console.error(`Could not load AI config from ${configPath}`);
@@ -4657,7 +5057,7 @@ async function generateAIContentCLI(options) {
4657
5057
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
4658
5058
  process.exit(1);
4659
5059
  }
4660
- const resolvedOutput = path16.resolve(projectDir, outputDir);
5060
+ const resolvedOutput = path17.resolve(projectDir, outputDir);
4661
5061
  console.log(` Output: ${resolvedOutput}`);
4662
5062
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
4663
5063
  const result = await generateAIContentFiles({
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAIhF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,CAAA;AAE1C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;IACvB,EAAE,EAAE,OAAO,CAAA;IACX,4CAA4C;IAC5C,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;CACxB;AA+CD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA8I9D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C7E;AAmED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8EhB"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAIhF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,CAAA;AAE1C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;IACvB,EAAE,EAAE,OAAO,CAAA;IACX,4CAA4C;IAC5C,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;CACxB;AA+CD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA8I9D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C7E;AAwED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFhB"}