canopycms 0.0.67 → 0.0.68-int.101

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 (281) hide show
  1. package/README.md +24 -17
  2. package/dist/ai/handler.js +8 -0
  3. package/dist/api/admin-branch-health.js +17 -20
  4. package/dist/api/admin.d.ts +30 -9
  5. package/dist/api/admin.js +32 -2
  6. package/dist/api/assets.js +84 -76
  7. package/dist/api/branch-create-window.d.ts +12 -0
  8. package/dist/api/branch-create-window.js +13 -0
  9. package/dist/api/branch-review.d.ts +3 -3
  10. package/dist/api/branch-review.js +14 -5
  11. package/dist/api/branch-status.d.ts +5 -5
  12. package/dist/api/branch-status.js +26 -6
  13. package/dist/api/branch-withdraw.d.ts +2 -2
  14. package/dist/api/branch-withdraw.js +7 -2
  15. package/dist/api/branch.d.ts +15 -2
  16. package/dist/api/branch.js +118 -92
  17. package/dist/api/client.d.ts +18 -6
  18. package/dist/api/client.js +79 -12
  19. package/dist/api/content.d.ts +6 -5
  20. package/dist/api/content.js +44 -41
  21. package/dist/api/entries.js +16 -15
  22. package/dist/api/github-sync.d.ts +19 -1
  23. package/dist/api/github-sync.js +57 -10
  24. package/dist/api/index.d.ts +1 -1
  25. package/dist/api/reference-options.js +1 -1
  26. package/dist/api/resolve-references.js +14 -40
  27. package/dist/api/settings-helpers.d.ts +1 -1
  28. package/dist/api/settings-helpers.js +1 -4
  29. package/dist/api/user.d.ts +3 -0
  30. package/dist/api/user.js +3 -0
  31. package/dist/assets/asset-url.d.ts +22 -16
  32. package/dist/assets/asset-url.js +42 -25
  33. package/dist/assets/factory.d.ts +5 -0
  34. package/dist/assets/factory.js +5 -0
  35. package/dist/assets/finalize.js +3 -0
  36. package/dist/assets/index.d.ts +1 -1
  37. package/dist/assets/materialize.d.ts +153 -0
  38. package/dist/assets/materialize.js +402 -0
  39. package/dist/assets/store-local.d.ts +19 -5
  40. package/dist/assets/store-local.js +69 -10
  41. package/dist/assets/store-s3.d.ts +38 -7
  42. package/dist/assets/store-s3.js +128 -22
  43. package/dist/assets/transform-directives.d.ts +52 -13
  44. package/dist/assets/transform-directives.js +87 -27
  45. package/dist/assets/transform.d.ts +3 -3
  46. package/dist/assets/transform.js +13 -8
  47. package/dist/assets/types.d.ts +31 -4
  48. package/dist/auth/file-based-auth-cache.js +7 -8
  49. package/dist/authorization/content.d.ts +5 -4
  50. package/dist/authorization/content.js +5 -5
  51. package/dist/authorization/path.d.ts +11 -6
  52. package/dist/authorization/path.js +11 -6
  53. package/dist/authorization/types.d.ts +2 -2
  54. package/dist/branch-health.d.ts +7 -1
  55. package/dist/branch-health.js +8 -3
  56. package/dist/branch-metadata.d.ts +5 -5
  57. package/dist/branch-metadata.js +44 -36
  58. package/dist/branch-provisioning.d.ts +167 -0
  59. package/dist/branch-provisioning.js +501 -0
  60. package/dist/branch-schema-cache.d.ts +5 -3
  61. package/dist/branch-schema-cache.js +29 -11
  62. package/dist/branch-sparse.d.ts +23 -0
  63. package/dist/branch-sparse.js +118 -0
  64. package/dist/branch-workspace.d.ts +42 -3
  65. package/dist/branch-workspace.js +240 -94
  66. package/dist/build/asset-refs.d.ts +93 -0
  67. package/dist/build/asset-refs.js +251 -0
  68. package/dist/build-identity.d.ts +3 -0
  69. package/dist/build-identity.js +13 -0
  70. package/dist/cli/asset-refs.d.ts +52 -0
  71. package/dist/cli/asset-refs.js +194 -0
  72. package/dist/cli/cli.d.ts +15 -1
  73. package/dist/cli/cli.js +5928 -2195
  74. package/dist/cli/configured-asset-store.d.ts +12 -0
  75. package/dist/cli/configured-asset-store.js +46 -0
  76. package/dist/cli/generate-ai-content.js +2185 -587
  77. package/dist/cli/github-app-manifest.d.ts +4 -3
  78. package/dist/cli/github-app-manifest.js +4 -3
  79. package/dist/cli/init.js +30 -27
  80. package/dist/cli/migrate.js +12 -8
  81. package/dist/cli/sync.js +83 -37
  82. package/dist/cli/template-files/Dockerfile.cms.template +8 -0
  83. package/dist/cli/template-files/cdk-app.ts.template +4 -0
  84. package/dist/cli/template-files/cms-stack.ts.template +23 -12
  85. package/dist/cli/template-files/deploy-cms.yml.template +2 -0
  86. package/dist/client.d.ts +1 -1
  87. package/dist/client.js +1 -1
  88. package/dist/config/helpers.js +1 -2
  89. package/dist/config/schemas/config.d.ts +19 -32
  90. package/dist/config/schemas/config.js +9 -11
  91. package/dist/config/schemas/field.js +1 -0
  92. package/dist/config/schemas/media.d.ts +4 -13
  93. package/dist/config/schemas/media.js +5 -8
  94. package/dist/config/schemas/url.d.ts +4 -20
  95. package/dist/config/schemas/url.js +6 -22
  96. package/dist/config/types.d.ts +30 -20
  97. package/dist/config/validation.d.ts +6 -0
  98. package/dist/config/validation.js +37 -0
  99. package/dist/content-listing.d.ts +12 -12
  100. package/dist/content-listing.js +10 -6
  101. package/dist/content-reader.d.ts +11 -2
  102. package/dist/content-reader.js +22 -14
  103. package/dist/content-store.d.ts +37 -9
  104. package/dist/content-store.js +70 -43
  105. package/dist/content-tree.d.ts +1 -1
  106. package/dist/content-tree.js +2 -2
  107. package/dist/context.d.ts +27 -9
  108. package/dist/context.js +154 -70
  109. package/dist/editor/BranchManager.d.ts +7 -1
  110. package/dist/editor/BranchManager.js +29 -14
  111. package/dist/editor/CanopyEditor.d.ts +1 -1
  112. package/dist/editor/CanopyEditor.js +2 -6
  113. package/dist/editor/Editor.d.ts +4 -3
  114. package/dist/editor/Editor.js +32 -22
  115. package/dist/editor/FormRenderer.js +34 -18
  116. package/dist/editor/PreviewFrame.d.ts +24 -0
  117. package/dist/editor/PreviewFrame.js +140 -0
  118. package/dist/editor/admin/SystemHealthPanel.js +47 -7
  119. package/dist/editor/components/BranchesDrawer.d.ts +9 -0
  120. package/dist/editor/components/BranchesDrawer.js +4 -0
  121. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  122. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  123. package/dist/editor/context/AssetContext.d.ts +18 -35
  124. package/dist/editor/context/AssetContext.js +20 -29
  125. package/dist/editor/context/index.d.ts +1 -1
  126. package/dist/editor/context/index.js +1 -1
  127. package/dist/editor/editor-config.d.ts +1 -2
  128. package/dist/editor/editor-config.js +0 -23
  129. package/dist/editor/editor-utils.d.ts +24 -21
  130. package/dist/editor/editor-utils.js +75 -62
  131. package/dist/editor/fields/BlockField.d.ts +1 -0
  132. package/dist/editor/fields/BlockField.js +6 -4
  133. package/dist/editor/fields/CodeField.d.ts +1 -0
  134. package/dist/editor/fields/CodeField.js +2 -2
  135. package/dist/editor/fields/DateTimeField.d.ts +1 -0
  136. package/dist/editor/fields/DateTimeField.js +2 -2
  137. package/dist/editor/fields/FieldDescription.d.ts +16 -0
  138. package/dist/editor/fields/FieldDescription.js +11 -0
  139. package/dist/editor/fields/ImageField.d.ts +1 -0
  140. package/dist/editor/fields/ImageField.js +3 -2
  141. package/dist/editor/fields/InlineGroupField.js +4 -1
  142. package/dist/editor/fields/MarkdownField.d.ts +1 -0
  143. package/dist/editor/fields/MarkdownField.js +115 -20
  144. package/dist/editor/fields/NumberField.d.ts +1 -0
  145. package/dist/editor/fields/NumberField.js +2 -2
  146. package/dist/editor/fields/NumberListField.d.ts +1 -0
  147. package/dist/editor/fields/NumberListField.js +2 -2
  148. package/dist/editor/fields/ObjectField.d.ts +1 -0
  149. package/dist/editor/fields/ObjectField.js +5 -2
  150. package/dist/editor/fields/ReferenceField.d.ts +1 -0
  151. package/dist/editor/fields/ReferenceField.js +5 -4
  152. package/dist/editor/fields/SelectField.d.ts +1 -0
  153. package/dist/editor/fields/SelectField.js +2 -2
  154. package/dist/editor/fields/StringListField.d.ts +1 -0
  155. package/dist/editor/fields/StringListField.js +2 -2
  156. package/dist/editor/fields/TextField.d.ts +1 -0
  157. package/dist/editor/fields/TextField.js +2 -2
  158. package/dist/editor/fields/ToggleField.d.ts +1 -0
  159. package/dist/editor/fields/ToggleField.js +7 -4
  160. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  161. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
  162. package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
  163. package/dist/editor/fields/mdx-jsx-support.js +137 -0
  164. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  165. package/dist/editor/hooks/create-branch-request.js +52 -0
  166. package/dist/editor/hooks/useBranchActions.d.ts +13 -2
  167. package/dist/editor/hooks/useBranchActions.js +48 -17
  168. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  169. package/dist/editor/hooks/useBranchManager.js +204 -37
  170. package/dist/editor/hooks/useBranchesData.d.ts +6 -0
  171. package/dist/editor/hooks/useBranchesData.js +9 -3
  172. package/dist/editor/hooks/useCommentSystem.js +25 -6
  173. package/dist/editor/hooks/useDraftManager.d.ts +16 -1
  174. package/dist/editor/hooks/useDraftManager.js +193 -36
  175. package/dist/editor/hooks/useEntriesData.js +3 -3
  176. package/dist/editor/hooks/useEntryManager.d.ts +2 -1
  177. package/dist/editor/hooks/useEntryManager.js +34 -19
  178. package/dist/editor/media/AssetCard.d.ts +1 -1
  179. package/dist/editor/media/crop-math.d.ts +3 -4
  180. package/dist/editor/media/crop-math.js +11 -22
  181. package/dist/editor/media/editor-image-src.d.ts +7 -0
  182. package/dist/editor/media/editor-image-src.js +12 -0
  183. package/dist/editor/preview-asset-base.d.ts +8 -0
  184. package/dist/editor/preview-asset-base.js +13 -0
  185. package/dist/editor/preview-bridge.d.ts +9 -21
  186. package/dist/editor/preview-bridge.js +19 -139
  187. package/dist/editor/preview-path.d.ts +10 -0
  188. package/dist/editor/preview-path.js +20 -0
  189. package/dist/editor/theme.js +2 -2
  190. package/dist/entry-schema-registry.d.ts +4 -4
  191. package/dist/entry-schema-registry.js +10 -6
  192. package/dist/entry-schema.d.ts +27 -4
  193. package/dist/entry-schema.js +20 -3
  194. package/dist/git-manager.d.ts +173 -6
  195. package/dist/git-manager.js +594 -76
  196. package/dist/github-service.d.ts +15 -0
  197. package/dist/github-service.js +19 -1
  198. package/dist/http/handler.js +37 -13
  199. package/dist/http/index.d.ts +2 -0
  200. package/dist/http/index.js +2 -0
  201. package/dist/http/router.d.ts +2 -0
  202. package/dist/http/router.js +2 -1
  203. package/dist/http/types.d.ts +2 -0
  204. package/dist/http/worker-not-ready.d.ts +10 -0
  205. package/dist/http/worker-not-ready.js +24 -0
  206. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  207. package/dist/operating-mode/types.d.ts +1 -4
  208. package/dist/paths/branch-name.d.ts +5 -0
  209. package/dist/paths/branch-name.js +9 -0
  210. package/dist/paths/branch.d.ts +6 -2
  211. package/dist/paths/branch.js +16 -10
  212. package/dist/paths/index.d.ts +3 -3
  213. package/dist/paths/index.js +3 -3
  214. package/dist/paths/normalize.d.ts +7 -0
  215. package/dist/paths/normalize.js +9 -0
  216. package/dist/paths/validation.js +0 -2
  217. package/dist/preview.d.ts +8 -0
  218. package/dist/preview.js +7 -0
  219. package/dist/reference-resolver.d.ts +5 -21
  220. package/dist/reference-resolver.js +9 -44
  221. package/dist/resolve-canopy-user.js +2 -1
  222. package/dist/schema/schema-store.d.ts +54 -31
  223. package/dist/schema/schema-store.js +77 -31
  224. package/dist/server.d.ts +64 -9
  225. package/dist/server.js +48 -8
  226. package/dist/services.d.ts +18 -6
  227. package/dist/services.js +85 -70
  228. package/dist/settings-workspace.d.ts +23 -3
  229. package/dist/settings-workspace.js +197 -45
  230. package/dist/static/seo.d.ts +2 -14
  231. package/dist/static/seo.js +2 -25
  232. package/dist/submission-attribution.d.ts +73 -0
  233. package/dist/submission-attribution.js +221 -0
  234. package/dist/sync-core.d.ts +12 -1
  235. package/dist/sync-core.js +31 -13
  236. package/dist/task-queue/worker-status.d.ts +8 -0
  237. package/dist/task-queue/worker-status.js +17 -0
  238. package/dist/types.d.ts +31 -2
  239. package/dist/utils/content-serialize.d.ts +5 -2
  240. package/dist/utils/content-serialize.js +98 -24
  241. package/dist/utils/content-write-lock.d.ts +18 -7
  242. package/dist/utils/content-write-lock.js +18 -9
  243. package/dist/utils/debug.d.ts +8 -0
  244. package/dist/utils/debug.js +10 -2
  245. package/dist/utils/git.d.ts +32 -0
  246. package/dist/utils/git.js +42 -0
  247. package/dist/utils/occ-json-write.js +10 -2
  248. package/dist/utils/provision-log.d.ts +25 -0
  249. package/dist/utils/provision-log.js +49 -0
  250. package/dist/utils/provisioning-lock.d.ts +35 -5
  251. package/dist/utils/provisioning-lock.js +73 -12
  252. package/dist/utils/request-timing.d.ts +25 -0
  253. package/dist/utils/request-timing.js +101 -0
  254. package/dist/utils/sanitize-href.d.ts +8 -22
  255. package/dist/utils/sanitize-href.js +11 -28
  256. package/dist/utils/url-prefix.d.ts +30 -0
  257. package/dist/utils/url-prefix.js +61 -0
  258. package/dist/utils/yaml-source-splice.d.ts +58 -0
  259. package/dist/utils/yaml-source-splice.js +515 -0
  260. package/dist/validation/entry-validator.js +9 -3
  261. package/dist/version.d.ts +1 -0
  262. package/dist/version.js +3 -0
  263. package/dist/worker/canopy-state.d.ts +45 -0
  264. package/dist/worker/canopy-state.js +75 -0
  265. package/dist/worker/cms-worker.d.ts +8 -3
  266. package/dist/worker/cms-worker.js +38 -2
  267. package/dist/worker/git-sync.d.ts +30 -12
  268. package/dist/worker/git-sync.js +253 -58
  269. package/dist/worker/history-rewrite.d.ts +1 -1
  270. package/dist/worker/history-rewrite.js +1 -1
  271. package/dist/worker/provisioned-workspace.d.ts +35 -0
  272. package/dist/worker/provisioned-workspace.js +50 -0
  273. package/dist/worker/rebase.d.ts +1 -1
  274. package/dist/worker/rebase.js +95 -23
  275. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  276. package/dist/worker/remote-git-maintenance.js +11 -0
  277. package/dist/worker/sparse-cone.d.ts +20 -0
  278. package/dist/worker/sparse-cone.js +132 -0
  279. package/dist/worker/task-runner.js +99 -14
  280. package/dist/worker/worker-context.d.ts +6 -1
  281. package/package.json +8 -2
@@ -1,46 +1,15 @@
1
1
  import nodePath from 'node:path';
2
2
  import { extractSlugFromFilename, extractEntryTypeFromFilename } from './content-id-index.js';
3
+ import { entryLogicalPath } from './paths/index.js';
3
4
  /**
4
- * ReferenceResolver resolves content IDs to display values for reference fields.
5
+ * Loads the options a reference field's picker offers. Resolving a stored reference to its
6
+ * target is `ContentStore`'s job (`read()`, `resolveReferenceTarget`).
5
7
  */
6
8
  export class ReferenceResolver {
7
9
  constructor(store, idIndex) {
8
10
  this.store = store;
9
11
  this.idIndex = idIndex;
10
12
  }
11
- /**
12
- * Resolve a content ID to a display value.
13
- * Returns null if the ID doesn't exist or points to a collection.
14
- */
15
- async resolve(id, displayField = 'title') {
16
- const location = this.idIndex.findById(id);
17
- if (!location || location.type !== 'entry') {
18
- return {
19
- id,
20
- exists: false,
21
- displayValue: id, // Fallback to showing the ID itself
22
- };
23
- }
24
- try {
25
- const doc = await this.store.read(location.collection, location.slug);
26
- const displayValue = String(doc.data[displayField] || doc.data.title || location.slug);
27
- return {
28
- id,
29
- exists: true,
30
- displayValue,
31
- collection: location.collection,
32
- slug: location.slug,
33
- };
34
- }
35
- catch (error) {
36
- console.error('Failed to resolve reference:', { id, error });
37
- return {
38
- id,
39
- exists: false,
40
- displayValue: id,
41
- };
42
- }
43
- }
44
13
  /**
45
14
  * Load all available reference options for a reference field.
46
15
  *
@@ -48,7 +17,7 @@ export class ReferenceResolver {
48
17
  * At least one of `collections` or `entryTypes` should be provided.
49
18
  *
50
19
  * @param canAccess - Optional permission predicate, called with each candidate's
51
- * relative path before it is read. Returning false skips the entry entirely --
20
+ * logical path (`entryLogicalPath`) before it is read. Returning false skips the entry entirely --
52
21
  * no file I/O, no label, no option -- so a caller who can't read a path never
53
22
  * triggers a read for content they won't be allowed to see anyway.
54
23
  */
@@ -74,7 +43,7 @@ export class ReferenceResolver {
74
43
  if (!location.collection || !location.slug)
75
44
  continue;
76
45
  // Skip denied paths before any file I/O.
77
- if (canAccess && !canAccess(location.relativePath))
46
+ if (canAccess && !canAccess(entryLogicalPath(location.collection, location.slug)))
78
47
  continue;
79
48
  const id = this.idIndex.findByPath(location.relativePath);
80
49
  if (!id)
@@ -82,7 +51,10 @@ export class ReferenceResolver {
82
51
  try {
83
52
  const filename = nodePath.basename(location.relativePath);
84
53
  const normalizedSlug = extractSlugFromFilename(filename);
85
- const doc = await this.store.read(location.collection, normalizedSlug);
54
+ // Only the label is read, so the candidate's own references stay unresolved.
55
+ const doc = await this.store.read(location.collection, normalizedSlug, {
56
+ resolveReferences: false,
57
+ });
86
58
  const label = String(doc.data[displayField] || doc.data.title || normalizedSlug);
87
59
  if (search && !label.toLowerCase().includes(search.toLowerCase())) {
88
60
  continue;
@@ -104,11 +76,4 @@ export class ReferenceResolver {
104
76
  }
105
77
  return options.sort((a, b) => a.label.localeCompare(b.label));
106
78
  }
107
- /**
108
- * Resolve multiple IDs at once.
109
- * Useful for displaying lists of referenced items.
110
- */
111
- async resolveMany(ids, displayField = 'title') {
112
- return Promise.all(ids.map((id) => this.resolve(id, displayField)));
113
- }
114
79
  }
@@ -12,6 +12,7 @@
12
12
  import { canopyLogWarn } from './utils/logger.js';
13
13
  import { authResultToCanopyUser } from './user.js';
14
14
  import { loadInternalGroups, RESERVED_GROUPS } from './authorization/index.js';
15
+ import { timeRequestPhase } from './utils/request-timing.js';
15
16
  /** Module-level: warn at most once per process. */
16
17
  let warnedNoAdmins = false;
17
18
  /**
@@ -39,7 +40,7 @@ function warnIfNoAdmins(internalGroups, bootstrapAdminIds) {
39
40
  */
40
41
  export async function resolveCanopyUser(authResult, deps) {
41
42
  const settingsRoot = await deps.getSettingsBranchRoot();
42
- const internalGroups = await loadInternalGroups(settingsRoot, deps.mode, deps.bootstrapAdminIds);
43
+ const internalGroups = await timeRequestPhase('groups', () => loadInternalGroups(settingsRoot, deps.mode, deps.bootstrapAdminIds));
43
44
  warnIfNoAdmins(internalGroups, deps.bootstrapAdminIds);
44
45
  return authResultToCanopyUser(authResult, deps.bootstrapAdminIds, internalGroups);
45
46
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Schema Store - handles reading and writing .collection.json files.
3
- * All mutations are branch-specific (like content edits).
2
+ * Schema Store - reads and writes .collection.json files, per branch.
4
3
  *
5
4
  * ## Concurrency
6
5
  *
@@ -16,11 +15,11 @@
16
15
  *
17
16
  * Protection is instead layer 1 ({@link withLock}) + layer 3
18
17
  * ({@link withOccFileLock}) on one COARSE per-branch SURROGATE lock path
19
- * OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`, see
20
- * `withSchemaLock`), covering every schema mutation on the branch including
21
- * multi-file ones. Layer 2 (OCC read-back) is skipped, since there's no
22
- * version field to check; layer 4 (generation marker) guards the schema
23
- * CACHE separately, not this write path.
18
+ * OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`), covering
19
+ * every schema mutation including multi-file ones, inside the [SYNC-C1]
20
+ * content-write lock the worker's rebase also takes (`withBranchSchemaLock`).
21
+ * Layer 2 (OCC read-back) is skipped, since there's no version field to
22
+ * check; layer 4 (generation marker) guards the schema CACHE separately.
24
23
  *
25
24
  * Accepted residual: `deleteBranch` does not take this lock before its
26
25
  * recursive `rm`, so an in-flight write can race a concurrent branch
@@ -182,7 +181,30 @@ declare const updateEntryTypeInputSchema: z.ZodObject<{
182
181
  * into a 409 so the editor can retry rather than surfacing a raw 400.
183
182
  */
184
183
  export declare class SchemaStoreBusyError extends Error {
185
- constructor(message?: string);
184
+ /** As on `ContentWriteLockBusyError`: `'unknown'` means the mutation ran and may have landed. */
185
+ readonly outcome: 'not-run' | 'unknown';
186
+ constructor(message?: string,
187
+ /** As on `ContentWriteLockBusyError`: `'unknown'` means the mutation ran and may have landed. */
188
+ outcome?: 'not-run' | 'unknown');
189
+ }
190
+ /**
191
+ * Run `fn` holding every lock a schema mutation needs on `branchRoot`, in the
192
+ * global order (docs/concurrency.md, "Lock acquisition order"): the branch's
193
+ * content-write lock, then the `.canopy-meta/schema` surrogate's in-process
194
+ * mutex, then its lockfile. Shared with `cli/migrate.ts` so the two cannot
195
+ * drift.
196
+ *
197
+ * NOT re-entrant, and nothing inside `fn` may take the content-write lock
198
+ * (no `ContentStore` writes): proper-lockfile refuses a second acquisition in
199
+ * the same process, so a nested one burns its wait and fails busy.
200
+ *
201
+ * Errors propagate untranslated: `ContentWriteLockBusyError` (contention, or
202
+ * a compromise reported after `fn` completed) and `OccWriteConflictError`.
203
+ */
204
+ export declare function withBranchSchemaLock<T>(branchRoot: string, fn: () => Promise<T>, contentWriteLockWaitMs?: number): Promise<T>;
205
+ export interface SchemaOpsOptions {
206
+ /** Bounded wait for the branch's content-write lock; defaults to `DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS`. */
207
+ contentWriteLockWaitMs?: number;
186
208
  }
187
209
  export declare class SchemaOps {
188
210
  private readonly contentRoot;
@@ -199,34 +221,35 @@ export declare class SchemaOps {
199
221
  * and never match the root collection's logical path).
200
222
  */
201
223
  private readonly contentRootName;
202
- /** Coarse per-branch surrogate lock path — see the module doc comment. */
203
- private readonly schemaLockPath;
204
- constructor(contentRoot: string, entrySchemaRegistry: EntrySchemaRegistry, services?: CanopyServices | undefined, branchRoot?: string);
224
+ /** See SchemaOpsOptions.contentWriteLockWaitMs. */
225
+ private readonly contentWriteLockWaitMs;
226
+ constructor(contentRoot: string, entrySchemaRegistry: EntrySchemaRegistry, services?: CanopyServices | undefined, branchRoot?: string, options?: SchemaOpsOptions);
205
227
  /**
206
- * Serialize an entire read-modify-write schema mutation behind the coarse
207
- * per-branch surrogate lock described in the module doc comment: layer 1
208
- * ({@link withLock}, in-process FIFO mutex) wraps layer 3
209
- * ({@link withOccFileLock}, cross-process/cross-host mkdir-based mutual
210
- * exclusion, immune to NFS attribute caching), same structure as
211
- * `BranchMetadataFileManager.save()` in branch-metadata.ts.
228
+ * Serialize an entire read-modify-write schema mutation behind
229
+ * {@link withBranchSchemaLock}: the content-write lock, then the surrogate
230
+ * ({@link withLock} wrapping {@link withOccFileLock}, as in
231
+ * `BranchMetadataFileManager.save()`).
212
232
  *
213
233
  * NOT re-entrant — callers must never invoke this from inside a callback
214
- * already running under it, or `withLock` deadlocks waiting on itself;
215
- * that's why `updateOrderInner` calls `updateCollectionInner` directly
216
- * instead of the public `updateCollection`.
234
+ * already running under it, or the nested acquisition fails busy; that's
235
+ * why `updateOrderInner` calls `updateCollectionInner` directly instead of
236
+ * the public `updateCollection`.
217
237
  *
218
- * Phantom-resurrection guard: `branchRoot` can be removed by a concurrent
219
- * `deleteBranch` between the caller resolving its BranchContext and this
220
- * call's own lock acquisition, which would otherwise let
221
- * `withOccFileLock`'s `mkdir({recursive:true})` silently recreate
222
- * `.canopy-meta/` in an otherwise-deleted tree. Checking BEFORE the lock
223
- * fails fast instead. Residual (accepted, same shape as
224
- * branch-metadata.ts's): a call that passes this check can still race a
225
- * `deleteBranch` `rm` starting moments later — not closed here either,
226
- * since `deleteBranch` never takes this lock (see the module doc comment).
238
+ * `branchRoot` can be removed by a concurrent `deleteBranch` between the
239
+ * caller resolving its BranchContext and this call's lock acquisition. The
240
+ * stat below fails fast before any lock; past it, neither lock recreates a
241
+ * missing root (each creates only its own directory), so a later removal
242
+ * fails the acquisition, and any failure on a vanished root is reported as
243
+ * `SchemaStoreBusyError` below.
227
244
  *
228
245
  * Translation happens ONLY at this boundary: inner code always sees the
229
- * raw {@link OccWriteConflictError} bubble up here untranslated.
246
+ * raw {@link OccWriteConflictError} bubble up here untranslated, and a
247
+ * {@link ContentWriteLockBusyError} becomes a `SchemaStoreBusyError` with
248
+ * its message, so every caller's 409 mapping covers both.
249
+ *
250
+ * A content-lock compromise is reported after `fn` completed, so the
251
+ * mutation is on disk while the caller's post-lock `invalidateSchemaCache()`
252
+ * never runs; this runs it before throwing.
230
253
  */
231
254
  private withSchemaLock;
232
255
  /**
@@ -322,7 +345,7 @@ export declare class SchemaOps {
322
345
  * Body of updateCollection, holding the schema lock for its full
323
346
  * read-modify-write. Called directly (not via the public `updateCollection`)
324
347
  * by `updateOrderInner` for non-root collections — `withSchemaLock` is NOT
325
- * re-entrant, so going through the public method there would deadlock.
348
+ * re-entrant, so going through the public method there would fail busy.
326
349
  *
327
350
  * `collectionPath` here is always already normalized (content-root prefix
328
351
  * stripped, if it had one) by whichever public method reached this — either
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Schema Store - handles reading and writing .collection.json files.
3
- * All mutations are branch-specific (like content edits).
2
+ * Schema Store - reads and writes .collection.json files, per branch.
4
3
  *
5
4
  * ## Concurrency
6
5
  *
@@ -16,11 +15,11 @@
16
15
  *
17
16
  * Protection is instead layer 1 ({@link withLock}) + layer 3
18
17
  * ({@link withOccFileLock}) on one COARSE per-branch SURROGATE lock path
19
- * OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`, see
20
- * `withSchemaLock`), covering every schema mutation on the branch including
21
- * multi-file ones. Layer 2 (OCC read-back) is skipped, since there's no
22
- * version field to check; layer 4 (generation marker) guards the schema
23
- * CACHE separately, not this write path.
18
+ * OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`), covering
19
+ * every schema mutation including multi-file ones, inside the [SYNC-C1]
20
+ * content-write lock the worker's rebase also takes (`withBranchSchemaLock`).
21
+ * Layer 2 (OCC read-back) is skipped, since there's no version field to
22
+ * check; layer 4 (generation marker) guards the schema CACHE separately.
24
23
  *
25
24
  * Accepted residual: `deleteBranch` does not take this lock before its
26
25
  * recursive `rm`, so an in-flight write can race a concurrent branch
@@ -34,6 +33,7 @@ import { z } from 'zod';
34
33
  import { atomicWriteFile } from '../utils/atomic-write.js';
35
34
  import { withLock } from '../utils/async-mutex.js';
36
35
  import { createDebugLogger } from '../utils/debug.js';
36
+ import { ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from '../utils/content-write-lock.js';
37
37
  import { getErrorMessage, isNotFoundError } from '../utils/error.js';
38
38
  import { withOccFileLock, OccWriteConflictError } from '../utils/occ-json-write.js';
39
39
  import { resolveCollectionPath } from '../content-id-index.js';
@@ -107,13 +107,34 @@ const log = createDebugLogger({ prefix: 'SchemaOps' });
107
107
  * into a 409 so the editor can retry rather than surfacing a raw 400.
108
108
  */
109
109
  export class SchemaStoreBusyError extends Error {
110
- constructor(message = 'Schema is being modified by another operation, try again') {
110
+ constructor(message = 'Schema is being modified by another operation, try again',
111
+ /** As on `ContentWriteLockBusyError`: `'unknown'` means the mutation ran and may have landed. */
112
+ outcome = 'not-run') {
111
113
  super(message);
114
+ this.outcome = outcome;
112
115
  this.name = 'SchemaStoreBusyError';
113
116
  }
114
117
  }
118
+ /**
119
+ * Run `fn` holding every lock a schema mutation needs on `branchRoot`, in the
120
+ * global order (docs/concurrency.md, "Lock acquisition order"): the branch's
121
+ * content-write lock, then the `.canopy-meta/schema` surrogate's in-process
122
+ * mutex, then its lockfile. Shared with `cli/migrate.ts` so the two cannot
123
+ * drift.
124
+ *
125
+ * NOT re-entrant, and nothing inside `fn` may take the content-write lock
126
+ * (no `ContentStore` writes): proper-lockfile refuses a second acquisition in
127
+ * the same process, so a nested one burns its wait and fails busy.
128
+ *
129
+ * Errors propagate untranslated: `ContentWriteLockBusyError` (contention, or
130
+ * a compromise reported after `fn` completed) and `OccWriteConflictError`.
131
+ */
132
+ export async function withBranchSchemaLock(branchRoot, fn, contentWriteLockWaitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
133
+ const schemaLockPath = path.join(path.resolve(branchRoot), '.canopy-meta', 'schema');
134
+ return withContentWriteLock(branchRoot, () => withLock(schemaLockPath, () => withOccFileLock(schemaLockPath, fn)), contentWriteLockWaitMs);
135
+ }
115
136
  export class SchemaOps {
116
- constructor(contentRoot, entrySchemaRegistry, services, branchRoot) {
137
+ constructor(contentRoot, entrySchemaRegistry, services, branchRoot, options = {}) {
117
138
  this.contentRoot = contentRoot;
118
139
  this.entrySchemaRegistry = entrySchemaRegistry;
119
140
  this.services = services;
@@ -130,33 +151,35 @@ export class SchemaOps {
130
151
  .relative(this.branchRoot, resolvedContentRoot)
131
152
  .split(path.sep)
132
153
  .join('/');
133
- this.schemaLockPath = path.join(this.branchRoot, '.canopy-meta', 'schema');
154
+ this.contentWriteLockWaitMs =
155
+ options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
134
156
  }
135
157
  /**
136
- * Serialize an entire read-modify-write schema mutation behind the coarse
137
- * per-branch surrogate lock described in the module doc comment: layer 1
138
- * ({@link withLock}, in-process FIFO mutex) wraps layer 3
139
- * ({@link withOccFileLock}, cross-process/cross-host mkdir-based mutual
140
- * exclusion, immune to NFS attribute caching), same structure as
141
- * `BranchMetadataFileManager.save()` in branch-metadata.ts.
158
+ * Serialize an entire read-modify-write schema mutation behind
159
+ * {@link withBranchSchemaLock}: the content-write lock, then the surrogate
160
+ * ({@link withLock} wrapping {@link withOccFileLock}, as in
161
+ * `BranchMetadataFileManager.save()`).
142
162
  *
143
163
  * NOT re-entrant — callers must never invoke this from inside a callback
144
- * already running under it, or `withLock` deadlocks waiting on itself;
145
- * that's why `updateOrderInner` calls `updateCollectionInner` directly
146
- * instead of the public `updateCollection`.
164
+ * already running under it, or the nested acquisition fails busy; that's
165
+ * why `updateOrderInner` calls `updateCollectionInner` directly instead of
166
+ * the public `updateCollection`.
147
167
  *
148
- * Phantom-resurrection guard: `branchRoot` can be removed by a concurrent
149
- * `deleteBranch` between the caller resolving its BranchContext and this
150
- * call's own lock acquisition, which would otherwise let
151
- * `withOccFileLock`'s `mkdir({recursive:true})` silently recreate
152
- * `.canopy-meta/` in an otherwise-deleted tree. Checking BEFORE the lock
153
- * fails fast instead. Residual (accepted, same shape as
154
- * branch-metadata.ts's): a call that passes this check can still race a
155
- * `deleteBranch` `rm` starting moments later — not closed here either,
156
- * since `deleteBranch` never takes this lock (see the module doc comment).
168
+ * `branchRoot` can be removed by a concurrent `deleteBranch` between the
169
+ * caller resolving its BranchContext and this call's lock acquisition. The
170
+ * stat below fails fast before any lock; past it, neither lock recreates a
171
+ * missing root (each creates only its own directory), so a later removal
172
+ * fails the acquisition, and any failure on a vanished root is reported as
173
+ * `SchemaStoreBusyError` below.
157
174
  *
158
175
  * Translation happens ONLY at this boundary: inner code always sees the
159
- * raw {@link OccWriteConflictError} bubble up here untranslated.
176
+ * raw {@link OccWriteConflictError} bubble up here untranslated, and a
177
+ * {@link ContentWriteLockBusyError} becomes a `SchemaStoreBusyError` with
178
+ * its message, so every caller's 409 mapping covers both.
179
+ *
180
+ * A content-lock compromise is reported after `fn` completed, so the
181
+ * mutation is on disk while the caller's post-lock `invalidateSchemaCache()`
182
+ * never runs; this runs it before throwing.
160
183
  */
161
184
  async withSchemaLock(fn) {
162
185
  try {
@@ -168,13 +191,36 @@ export class SchemaOps {
168
191
  }
169
192
  throw err;
170
193
  }
194
+ let landed = false;
171
195
  try {
172
- return await withLock(this.schemaLockPath, () => withOccFileLock(this.schemaLockPath, fn));
196
+ return await withBranchSchemaLock(this.branchRoot, async () => {
197
+ const result = await fn();
198
+ landed = true;
199
+ return result;
200
+ }, this.contentWriteLockWaitMs);
173
201
  }
174
202
  catch (err) {
175
203
  if (err instanceof OccWriteConflictError) {
176
204
  throw new SchemaStoreBusyError();
177
205
  }
206
+ if (err instanceof ContentWriteLockBusyError) {
207
+ if (landed) {
208
+ await this.invalidateSchemaCache().catch((invalidateErr) => {
209
+ log.warn('schema-cache', 'Invalidation after a lost content-write lock failed', {
210
+ branchRoot: this.branchRoot,
211
+ error: getErrorMessage(invalidateErr),
212
+ });
213
+ });
214
+ }
215
+ throw new SchemaStoreBusyError(err.outcome === 'unknown'
216
+ ? 'This branch was being synced while the schema change was written, so it may or may not have been recorded. Reload before changing the schema again.'
217
+ : err.message, err.outcome);
218
+ }
219
+ // A delete that removes the branch root mid-operation fails it wherever it is (a lock's
220
+ // mkdir ENOENT, a vanished collection meta); the deletion is the cause, whatever the symptom.
221
+ const rootGone = await fs.stat(this.branchRoot).then(() => false, (statErr) => isNotFoundError(statErr));
222
+ if (rootGone)
223
+ throw new SchemaStoreBusyError('Branch no longer exists');
178
224
  throw err;
179
225
  }
180
226
  }
@@ -461,7 +507,7 @@ export class SchemaOps {
461
507
  * Body of updateCollection, holding the schema lock for its full
462
508
  * read-modify-write. Called directly (not via the public `updateCollection`)
463
509
  * by `updateOrderInner` for non-root collections — `withSchemaLock` is NOT
464
- * re-entrant, so going through the public method there would deadlock.
510
+ * re-entrant, so going through the public method there would fail busy.
465
511
  *
466
512
  * `collectionPath` here is always already normalized (content-root prefix
467
513
  * stripped, if it had one) by whichever public method reached this — either
package/dist/server.d.ts CHANGED
@@ -126,32 +126,87 @@ export { createAssetStore } from './assets/factory.js';
126
126
  /**
127
127
  * The five S3/local bucket-prefix strings (`asset-originals/`, `asset-staging/`, `asset-meta/`,
128
128
  * `assets/`, `assets/t/`). Exported so server-only consumers outside this package — notably the
129
- * prod transform Lambda (`packages/canopycms-cdk/lambda/asset-transform`) — build and parse
129
+ * lazy transform Lambda (`packages/canopycms-cdk/lambda/asset-transform`) — build and parse
130
130
  * asset keys through the same constants `S3AssetStore` uses rather than duplicating the strings.
131
131
  */
132
132
  export { ASSET_PREFIXES, type AssetPrefixes } from './assets/keys.js';
133
- /** Asset metadata sidecar shape (`asset-meta/{hash32}.json`), written by the finalize pipeline and read by the transform layer (dev-mode emulation and the prod transform Lambda). */
133
+ /** Asset metadata sidecar shape (`asset-meta/{hash32}.json`), written by the finalize pipeline and read by `storeTransform`. */
134
134
  export type { AssetMeta } from './assets/types.js';
135
135
  /**
136
136
  * Parse the three path segments after `assets/t/` (`{directives}/{hash32}/{slug}.{ext}`) into a
137
- * validated `TransformDirectives` set, or a structured parse error. Reused unchanged by the
138
- * dev-mode lazy `/assets/t/*` emulation (`api/assets.ts`) and the prod transform Lambda, so the
139
- * URL grammar is defined in exactly one place.
137
+ * validated `TransformDirectives` set, or a structured parse error, so the URL grammar is
138
+ * defined in exactly one place.
140
139
  */
141
140
  export { parseTransformPath } from './assets/transform-directives.js';
142
141
  export type { ParsedTransformPath, ParseTransformPathResult } from './assets/transform-directives.js';
143
142
  /**
144
143
  * Canonical string form of a `TransformDirectives` set — the cache key every transform output is
145
144
  * stored under (`assets/t/{formatDirectives(...)}/{hash32}/{slug}.{ext}`). Equivalent directive
146
- * sets (different key order or float formatting) always format to the same string. Reused by the
147
- * prod transform Lambda so its writes agree with the dev-mode emulation and `assetUrl()`.
145
+ * sets (different key order or float formatting) always format to the same string.
148
146
  */
149
147
  export { formatDirectives } from './assets/transform-directives.js';
148
+ /**
149
+ * Parse `assets/t/` path segments and resolve them to the canonical spelling their output is
150
+ * stored under, with directives re-parsed from that spelling. The lazy transform Lambda 301s a
151
+ * non-canonical request to it; the authenticated raw route serves the canonical bytes.
152
+ */
153
+ export { canonicalizeTransformPath } from './assets/transform-directives.js';
154
+ export type { CanonicalTransformPathResult, TransformWidthPolicy, } from './assets/transform-directives.js';
150
155
  /**
151
156
  * Apply a parsed `TransformDirectives` set to source image bytes with sharp:
152
157
  * resize/format/quality/crop, EXIF stripped on every re-encode, identity included. Server-only
153
- * (sharp); reused unchanged by the dev-mode lazy `/assets/t/*` emulation and the prod transform
154
- * Lambda.
158
+ * (sharp); `storeTransform` below wraps it for every caller that stores the output.
155
159
  */
156
160
  export { applyTransform } from './assets/transform.js';
157
161
  export type { ApplyTransformInput, TransformResult } from './assets/transform.js';
162
+ /**
163
+ * Compute the transform a canonical `assets/t/` path names and store it under that key: meta
164
+ * lookup, raster-only, slug and ext checks, then `applyTransform`. Shared by the authenticated raw
165
+ * route, `materialize-assets` and the lazy transform Lambda. A 404's `error` names hashes and
166
+ * slugs, so a caller answering a browser replaces it.
167
+ */
168
+ export { storeTransform, type StoreTransformResult } from './assets/materialize.js';
169
+ /** The `Cache-Control` every stored transform output carries: content-addressed, so immutable. */
170
+ export { TRANSFORM_CACHE_CONTROL } from './assets/materialize.js';
171
+ /** The object tag the lazy transform Lambda's outputs carry, which its `assets/t/` expiry filters on. */
172
+ export { LAZY_TRANSFORM_TAG } from './assets/materialize.js';
173
+ /**
174
+ * Make every `assets/t/` key a build references exist in `store` (transforming only the missing
175
+ * ones from the stored originals) and check every referenced svg/pdf key exists. Under an
176
+ * `outputPrefix`, keys are written beneath it, and those production already stores are copied. A
177
+ * release job that bundles its own tool calls this with a store from `createAssetStore` and gates
178
+ * on the returned report; no site config is involved. Needs sharp only when something is missing.
179
+ */
180
+ export { materializeAssets } from './assets/materialize.js';
181
+ /** The asset store `createAssetStore` returns and `materializeAssets` takes. */
182
+ export type { AssetStore } from './assets/types.js';
183
+ /** What a create-only store write did: `created`, or `already-exists` (the stored object was kept). */
184
+ export type { CreateOnlyResult } from './assets/types.js';
185
+ /** Thrown by `materializeAssets` when sharp cannot load and keys are missing, so none can be produced. */
186
+ export { SharpUnavailableError } from './assets/materialize.js';
187
+ /**
188
+ * Thrown by `materializeAssets` before any store request when `outputPrefix` is not a relative,
189
+ * `/`-terminated run of `[A-Za-z0-9._-]` segments outside every canopy prefix.
190
+ */
191
+ export { InvalidOutputPrefixError } from './assets/materialize.js';
192
+ /** The `schemaVersion` carried by every `MaterializeReport`. */
193
+ export { MATERIALIZE_REPORT_SCHEMA_VERSION } from './assets/materialize.js';
194
+ /** Options for `materializeAssets`: the store, the referenced keys, an output prefix, and concurrency/retry limits. */
195
+ export type { MaterializeOptions } from './assets/materialize.js';
196
+ /** What `materializeAssets` returns: a versioned summary and one result per key, sorted by key. */
197
+ export type { MaterializeReport } from './assets/materialize.js';
198
+ /** One key's outcome in a `MaterializeReport`: `existed`, `created`, `copied` or `failed` (`content` or `store`). */
199
+ export type { MaterializeResult } from './assets/materialize.js';
200
+ /** One key a build references, with the pages and output files that reference it. */
201
+ export type { MaterializeTarget } from './assets/materialize.js';
202
+ /**
203
+ * Scan a static build's output for `/assets/…` URLs and write `canopy-asset-refs.json` into it.
204
+ * Throws `AssetRefsError` for a URL no stored object can answer (non-canonical or malformed).
205
+ */
206
+ export { collectAssetRefs, type CollectAssetRefsResult } from './build/asset-refs.js';
207
+ /** Read and validate a `canopy-asset-refs.json`; throws when it is unreadable or malformed. */
208
+ export { readAssetRefsFile } from './build/asset-refs.js';
209
+ /** Thrown by `collectAssetRefs`; `problems` lists each offending file and URL. */
210
+ export { AssetRefsError } from './build/asset-refs.js';
211
+ /** The shape of `canopy-asset-refs.json`: the transform and static keys a build references. */
212
+ export type { AssetRefsFile } from './build/asset-refs.js';
package/dist/server.js CHANGED
@@ -115,28 +115,68 @@ export { createAssetStore } from './assets/factory.js';
115
115
  /**
116
116
  * The five S3/local bucket-prefix strings (`asset-originals/`, `asset-staging/`, `asset-meta/`,
117
117
  * `assets/`, `assets/t/`). Exported so server-only consumers outside this package — notably the
118
- * prod transform Lambda (`packages/canopycms-cdk/lambda/asset-transform`) — build and parse
118
+ * lazy transform Lambda (`packages/canopycms-cdk/lambda/asset-transform`) — build and parse
119
119
  * asset keys through the same constants `S3AssetStore` uses rather than duplicating the strings.
120
120
  */
121
121
  export { ASSET_PREFIXES } from './assets/keys.js';
122
122
  /**
123
123
  * Parse the three path segments after `assets/t/` (`{directives}/{hash32}/{slug}.{ext}`) into a
124
- * validated `TransformDirectives` set, or a structured parse error. Reused unchanged by the
125
- * dev-mode lazy `/assets/t/*` emulation (`api/assets.ts`) and the prod transform Lambda, so the
126
- * URL grammar is defined in exactly one place.
124
+ * validated `TransformDirectives` set, or a structured parse error, so the URL grammar is
125
+ * defined in exactly one place.
127
126
  */
128
127
  export { parseTransformPath } from './assets/transform-directives.js';
129
128
  /**
130
129
  * Canonical string form of a `TransformDirectives` set — the cache key every transform output is
131
130
  * stored under (`assets/t/{formatDirectives(...)}/{hash32}/{slug}.{ext}`). Equivalent directive
132
- * sets (different key order or float formatting) always format to the same string. Reused by the
133
- * prod transform Lambda so its writes agree with the dev-mode emulation and `assetUrl()`.
131
+ * sets (different key order or float formatting) always format to the same string.
134
132
  */
135
133
  export { formatDirectives } from './assets/transform-directives.js';
134
+ /**
135
+ * Parse `assets/t/` path segments and resolve them to the canonical spelling their output is
136
+ * stored under, with directives re-parsed from that spelling. The lazy transform Lambda 301s a
137
+ * non-canonical request to it; the authenticated raw route serves the canonical bytes.
138
+ */
139
+ export { canonicalizeTransformPath } from './assets/transform-directives.js';
136
140
  /**
137
141
  * Apply a parsed `TransformDirectives` set to source image bytes with sharp:
138
142
  * resize/format/quality/crop, EXIF stripped on every re-encode, identity included. Server-only
139
- * (sharp); reused unchanged by the dev-mode lazy `/assets/t/*` emulation and the prod transform
140
- * Lambda.
143
+ * (sharp); `storeTransform` below wraps it for every caller that stores the output.
141
144
  */
142
145
  export { applyTransform } from './assets/transform.js';
146
+ /**
147
+ * Compute the transform a canonical `assets/t/` path names and store it under that key: meta
148
+ * lookup, raster-only, slug and ext checks, then `applyTransform`. Shared by the authenticated raw
149
+ * route, `materialize-assets` and the lazy transform Lambda. A 404's `error` names hashes and
150
+ * slugs, so a caller answering a browser replaces it.
151
+ */
152
+ export { storeTransform } from './assets/materialize.js';
153
+ /** The `Cache-Control` every stored transform output carries: content-addressed, so immutable. */
154
+ export { TRANSFORM_CACHE_CONTROL } from './assets/materialize.js';
155
+ /** The object tag the lazy transform Lambda's outputs carry, which its `assets/t/` expiry filters on. */
156
+ export { LAZY_TRANSFORM_TAG } from './assets/materialize.js';
157
+ /**
158
+ * Make every `assets/t/` key a build references exist in `store` (transforming only the missing
159
+ * ones from the stored originals) and check every referenced svg/pdf key exists. Under an
160
+ * `outputPrefix`, keys are written beneath it, and those production already stores are copied. A
161
+ * release job that bundles its own tool calls this with a store from `createAssetStore` and gates
162
+ * on the returned report; no site config is involved. Needs sharp only when something is missing.
163
+ */
164
+ export { materializeAssets } from './assets/materialize.js';
165
+ /** Thrown by `materializeAssets` when sharp cannot load and keys are missing, so none can be produced. */
166
+ export { SharpUnavailableError } from './assets/materialize.js';
167
+ /**
168
+ * Thrown by `materializeAssets` before any store request when `outputPrefix` is not a relative,
169
+ * `/`-terminated run of `[A-Za-z0-9._-]` segments outside every canopy prefix.
170
+ */
171
+ export { InvalidOutputPrefixError } from './assets/materialize.js';
172
+ /** The `schemaVersion` carried by every `MaterializeReport`. */
173
+ export { MATERIALIZE_REPORT_SCHEMA_VERSION } from './assets/materialize.js';
174
+ /**
175
+ * Scan a static build's output for `/assets/…` URLs and write `canopy-asset-refs.json` into it.
176
+ * Throws `AssetRefsError` for a URL no stored object can answer (non-canonical or malformed).
177
+ */
178
+ export { collectAssetRefs } from './build/asset-refs.js';
179
+ /** Read and validate a `canopy-asset-refs.json`; throws when it is unreadable or malformed. */
180
+ export { readAssetRefsFile } from './build/asset-refs.js';
181
+ /** Thrown by `collectAssetRefs`; `problems` lists each offending file and URL. */
182
+ export { AssetRefsError } from './build/asset-refs.js';
@@ -24,11 +24,16 @@ import { GitManager } from './git-manager.js';
24
24
  import { BranchRegistry } from './branch-registry.js';
25
25
  import { type GitHubService } from './github-service.js';
26
26
  import { BranchSchemaCache } from './branch-schema-cache.js';
27
+ import { type SubmissionEditor } from './submission-attribution.js';
27
28
  /**
28
29
  * Bootstrap admin IDs from the environment. These users are treated as Admins
29
30
  * whatever their group membership.
30
31
  */
31
32
  export declare const getBootstrapAdminIds: () => Set<string>;
33
+ export interface SubmitBranchResult {
34
+ /** Repo-relative paths the branch changes, excluding canopycms runtime metadata. */
35
+ changedPaths: string[];
36
+ }
32
37
  export interface CanopyServices {
33
38
  config: CanopyConfig;
34
39
  /**
@@ -58,27 +63,34 @@ export interface CanopyServices {
58
63
  githubService?: GitHubService;
59
64
  /** Bootstrap admin user IDs that are always treated as Admins */
60
65
  bootstrapAdminIds: Set<string>;
61
- /** Commit files to git with automatic author handling */
66
+ /**
67
+ * Commit files to git with automatic author handling. Holds the branch's
68
+ * content-write lock; rejects with `ContentWriteLockBusyError` when busy.
69
+ */
62
70
  commitFiles: (options: {
63
71
  context: BranchContext;
64
72
  files: string | string[];
65
73
  message: string;
66
74
  }) => Promise<void>;
67
- /** Submit branch: commit all changes and push to remote */
75
+ /**
76
+ * Submit branch: commit all changes (with the submitter's trailers) and push
77
+ * to remote. Resolves with every path the branch changes against its base.
78
+ * Holds the branch's content-write lock; rejects with
79
+ * `ContentWriteLockBusyError` when busy.
80
+ */
68
81
  submitBranch: (options: {
69
82
  context: BranchContext;
83
+ submitter?: SubmissionEditor;
70
84
  message?: string;
71
- }) => Promise<void>;
72
- /** Commit to settings branch (for permissions/groups), with optional PR creation */
85
+ }) => Promise<SubmitBranchResult>;
86
+ /** Commit to the settings branch (for permissions/groups) and push it; never opens a PR */
73
87
  commitToSettingsBranch: (options: {
74
88
  branchRoot: string;
75
89
  files: string | string[];
76
90
  message: string;
77
- createPR?: boolean;
78
91
  }) => Promise<{
79
92
  committed: boolean;
80
93
  pushed: boolean;
81
- prUrl?: string;
82
94
  error?: string;
83
95
  syncStatus?: 'pending-sync' | 'synced' | 'sync-failed';
84
96
  }>;