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
@@ -70,7 +70,7 @@ const getReferenceOptionsHandler = async (gc, ctx, req, _params) => {
70
70
  // for an option the caller isn't allowed to see.
71
71
  const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
72
72
  const resolver = new ReferenceResolver(store, idIndex);
73
- const options = await resolver.loadReferenceOptions(collections, displayField, search, entryTypes, (relativePath) => checkAccess(relativePath, 'read').allowed);
73
+ const options = await resolver.loadReferenceOptions(collections, displayField, search, entryTypes, (logicalPath) => checkAccess(logicalPath, 'read').allowed);
74
74
  return {
75
75
  ok: true,
76
76
  status: 200,
@@ -1,9 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ContentStore } from '../content-store.js';
3
3
  import { defineEndpoint } from './route-builder.js';
4
- import { ReferenceResolver } from '../reference-resolver.js';
5
- import { buildResolvedReference } from '../entry-schema.js';
6
- import { computeEntryUrl } from '../utils/entry-url.js';
7
4
  import { branchNameSchema, contentIdSchema } from './validators.js';
8
5
  /**
9
6
  * Resolution does sequential per-ID file I/O, so the request body caps how much filesystem work
@@ -20,49 +17,25 @@ const resolveReferencesBodySchema = z.object({
20
17
  const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
21
18
  const { branchContext } = gc;
22
19
  const { ids } = body;
23
- const flatSchema = branchContext.flatSchema;
24
- const contentRootName = ctx.services.config.contentRoot || 'content';
25
- const store = new ContentStore(branchContext.branchRoot, flatSchema, {
26
- contentRootName,
20
+ const store = new ContentStore(branchContext.branchRoot, branchContext.flatSchema, {
21
+ contentRootName: ctx.services.config.contentRoot || 'content',
27
22
  });
28
- // Get ID index (automatically loads if needed)
29
- const idIndex = await store.idIndex();
30
- // Resolve each ID to full document
31
- const resolver = new ReferenceResolver(store, idIndex);
32
- // Build the access checker once, reused for every id instead of re-loading per id in the loop.
33
- // A failure here (e.g. settings workspace unavailable) surfaces as a handler error, not
34
- // swallowed silently per id.
23
+ // Built once for every id. A failure here (e.g. settings workspace unavailable) surfaces as a
24
+ // handler error rather than being swallowed per id.
35
25
  const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
26
+ const access = (logicalPath) => checkAccess(logicalPath, 'read').allowed;
27
+ // The resolution `read()` applies to a reference field, so live preview shows this user what
28
+ // `read()` would: the target's data, or a `RestrictedReference` for a target they may not
29
+ // read. The target's own references stay ids, as in `read()`.
36
30
  const resolved = {};
37
31
  for (const id of ids) {
38
32
  try {
39
- const result = await resolver.resolve(id);
40
- if (result && result.exists && result.collection && result.slug) {
41
- // Check path-level read permission before returning content
42
- const resolvedPath = await store.resolveDocumentPath(result.collection, result.slug);
43
- const access = checkAccess(resolvedPath.relativePath, 'read');
44
- if (!access.allowed)
45
- continue;
46
- // `resolveReferences: false` matches the server-side resolver (content-store.ts's
47
- // resolveSingleReferenceOnce), so a nested reference inside a target renders the same way
48
- // in live preview as on the published site, instead of resolving one level deeper.
49
- const doc = await store.read(result.collection, result.slug, { resolveReferences: false });
50
- if (doc && doc.data) {
51
- // Same shape as the server-side resolver: target data first, then the reserved keys.
52
- // Order is the corruption guard (a target modelling `id` as content must not shadow the
53
- // real content ID); the extra keys (incl. `urlPath`) keep live preview and production
54
- // in sync for consumers like client-reference-resolver.ts.
55
- resolved[id] = buildResolvedReference(doc.data, {
56
- id,
57
- slug: result.slug,
58
- collection: result.collection,
59
- urlPath: computeEntryUrl(result.collection, result.slug, contentRootName),
60
- });
61
- }
62
- }
33
+ const value = await store.resolveReferenceTarget(id, access);
34
+ if (value)
35
+ resolved[id] = value;
63
36
  }
64
37
  catch (error) {
65
- // Skip failed resolutions, don't block entire request
38
+ // An id that fails to resolve is omitted rather than failing the whole request.
66
39
  console.error(`Failed to resolve reference ID ${id}:`, error);
67
40
  }
68
41
  }
@@ -73,7 +46,8 @@ const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
73
46
  };
74
47
  };
75
48
  /**
76
- * Resolve reference IDs to full document objects
49
+ * Resolve reference IDs as a reference field would: full target data, or title + URL tagged
50
+ * `unavailable` for a target the user may not read. An id naming no entry is omitted.
77
51
  * POST /:branch/resolve-references
78
52
  * Body: { ids: string[] }
79
53
  */
@@ -30,7 +30,7 @@ export interface CommitSettingsResult {
30
30
  /**
31
31
  * Commit and push settings changes based on the mode.
32
32
  * Both prod and dev use commitToSettingsBranch.
33
- * In dev mode, commits to the settings branch but does not create a PR.
33
+ * Settings changes are never reviewed through a PR (see commitToSettingsBranch).
34
34
  */
35
35
  export declare function commitSettings(ctx: ApiContext, options: {
36
36
  context: {
@@ -23,7 +23,7 @@ export async function getSettingsBranchContext(ctx) {
23
23
  /**
24
24
  * Commit and push settings changes based on the mode.
25
25
  * Both prod and dev use commitToSettingsBranch.
26
- * In dev mode, commits to the settings branch but does not create a PR.
26
+ * Settings changes are never reviewed through a PR (see commitToSettingsBranch).
27
27
  */
28
28
  export async function commitSettings(ctx, options) {
29
29
  const strategy = operatingStrategy(options.mode);
@@ -38,9 +38,6 @@ export async function commitSettings(ctx, options) {
38
38
  branchRoot: options.branchRoot,
39
39
  files: options.fileName,
40
40
  message: options.message,
41
- createPR: strategy.shouldCreateSettingsPR({
42
- autoCreateSettingsPR: ctx.services.config.autoCreateSettingsPR,
43
- }),
44
41
  });
45
42
  if (!result.pushed) {
46
43
  console.warn(`${options.message} committed but not pushed:`, result.error);
@@ -1,7 +1,10 @@
1
1
  import type { ApiResponse } from './types.js';
2
+ import type { BuildIdentity } from '../types.js';
2
3
  export type UserInfoResponse = ApiResponse<{
3
4
  userId: string;
4
5
  groups: string[];
6
+ /** Admins only: the deployed version is reconnaissance for anyone else. */
7
+ build?: BuildIdentity;
5
8
  }>;
6
9
  export declare const USER_ROUTES: {
7
10
  readonly whoami: import("./route-builder.js").RouteDefinition<undefined, undefined, UserInfoResponse>;
package/dist/api/user.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { defineEndpoint } from './route-builder.js';
2
+ import { isAdmin } from '../authorization/helpers.js';
3
+ import { getBuildIdentity } from '../build-identity.js';
2
4
  /**
3
5
  * This is a PUBLIC endpoint - no special permissions required
4
6
  */
@@ -10,6 +12,7 @@ const getUserInfoHandler = async (ctx, req) => {
10
12
  data: {
11
13
  userId: req.user.userId,
12
14
  groups: req.user.groups,
15
+ ...(isAdmin(req.user.groups) ? { build: getBuildIdentity() } : {}),
13
16
  },
14
17
  };
15
18
  };
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Build/adjust transform URLs for `<img>`/srcset without pulling in the
3
- * server-only transform engine. Isomorphic - depends only on
4
- * transform-directives.ts, the plain `ASSET_PREFIXES` constant, and
5
- * utils/url-prefix.ts, none of which import node builtins, so this is safe to
6
- * import from client (editor) code as well as during static builds.
3
+ * server-only transform engine. Isomorphic - none of its imports reach a node
4
+ * builtin, so this is safe to import from client (editor) code as well as during
5
+ * static builds.
7
6
  */
8
7
  import { type CropRect, type OutputFormat } from './transform-directives.js';
9
- /** The minimal shape `assetUrl`/`assetSrcSet` need from a stored asset reference. */
8
+ /**
9
+ * The minimal shape `assetUrl`/`assetSrcSet` need from a stored asset reference. An
10
+ * `ImageFieldValue` is one, so passing a field value renders the crop the editor stored.
11
+ */
10
12
  export interface AssetRef {
11
13
  src: string;
14
+ crop?: CropRect;
12
15
  }
13
16
  export interface AssetUrlOptions {
14
17
  width?: number;
@@ -23,16 +26,14 @@ export interface AssetUrlOptions {
23
26
  * option beside it. Two shapes are legitimate, and they are alternatives, never composed:
24
27
  *
25
28
  * - An absolute origin (`https://assets.example.com`) — assets served from another origin.
26
- * `media.publicBaseUrl` is one source of this, and is the source the editor uses; it is
27
- * validated as an absolute URL, so it structurally cannot carry the second shape.
28
29
  * - A same-origin path prefix (`/preview-123`) — the site is deployed under a Next `basePath`
29
30
  * AND its assets are served by Next (`withCanopy`'s `/assets/:path*` rewrite, which Next
30
31
  * auto-prefixes). NOT the right value on a CloudFront/CDK deployment, where the asset
31
32
  * behaviors are anchored at the distribution root and a `basePath` does not move them —
32
33
  * there the correct value is none at all. See the README's asset-mount table.
33
34
  *
34
- * It is a per-render option rather than a config field precisely because the editor and the
35
- * public site can legitimately have different answers.
35
+ * It is a per-render option rather than a config field because different renderers see the
36
+ * `/assets` space at different places.
36
37
  *
37
38
  * **Render-time only — never stored.** A stored `src` is always root-relative (see
38
39
  * `assets/asset-src.ts`), because content moves between branches and environments. Nothing
@@ -41,16 +42,21 @@ export interface AssetUrlOptions {
41
42
  baseUrl?: string;
42
43
  }
43
44
  /**
44
- * Build a transform URL, merging `opts` over the directives already present
45
- * in `ref.src` (opts win). For static srcs (svg/pdf under `/assets/{hash}/...`,
46
- * or any src that isn't one of our own transform URLs) the src is returned
47
- * unchanged and `opts` are ignored - there is nothing to transform.
45
+ * Build a transform URL, merging `opts` and `ref.crop` over the directives already present
46
+ * in `ref.src` (see `mergeDirectives` for precedence). For static srcs (svg/pdf under
47
+ * `/assets/{hash}/...`, or any src that isn't one of our own transform URLs) the src is
48
+ * returned unchanged and every directive is ignored - there is nothing to transform.
49
+ *
50
+ * In a live preview, a transform URL goes behind the editor's authenticated route
51
+ * (`editor/preview-asset-base.ts`) instead of `opts.baseUrl`: a draft's crop or width may exist
52
+ * nowhere a build put it. Static srcs keep `opts.baseUrl`, since finalize wrote them.
48
53
  */
49
54
  export declare function assetUrl(ref: AssetRef, opts?: AssetUrlOptions): string;
50
55
  /**
51
56
  * Build a comma-joined `url w` srcset descriptor list. `widths` must all be
52
- * on the transform width allowlist (multiples of 160 in [160, 4096]) - this
53
- * is developer-facing (a host app's own responsive-image markup), so an
54
- * invalid width throws rather than silently dropping it.
57
+ * integers in [1, 8192] (the `any` width policy) - this is developer-facing (a
58
+ * host app's own responsive-image markup), so an invalid width throws rather
59
+ * than silently dropping it. A site serving the opt-in lazy public path, which
60
+ * transforms only allowlisted widths, must pick widths from that allowlist.
55
61
  */
56
62
  export declare function assetSrcSet(ref: AssetRef, widths: readonly number[], opts?: Omit<AssetUrlOptions, 'width'>): string;
@@ -1,36 +1,51 @@
1
1
  /**
2
2
  * Build/adjust transform URLs for `<img>`/srcset without pulling in the
3
- * server-only transform engine. Isomorphic - depends only on
4
- * transform-directives.ts, the plain `ASSET_PREFIXES` constant, and
5
- * utils/url-prefix.ts, none of which import node builtins, so this is safe to
6
- * import from client (editor) code as well as during static builds.
3
+ * server-only transform engine. Isomorphic - none of its imports reach a node
4
+ * builtin, so this is safe to import from client (editor) code as well as during
5
+ * static builds.
7
6
  */
8
7
  import { isUnprefixablePath, joinUrlPrefix, sanitizeUnprefixedPath, stripTrailingSlashes, } from '../utils/url-prefix.js';
8
+ import { getPreviewAssetBase } from '../editor/preview-asset-base.js';
9
9
  import { ASSET_PREFIXES } from './asset-prefixes.js';
10
10
  import { formatDirectives, isAllowedTransformWidth, parseTransformPath, } from './transform-directives.js';
11
11
  const TRANSFORM_URL_PREFIX = `/${ASSET_PREFIXES.transform}/`;
12
12
  /**
13
- * Merge `opts` over an already-parsed directive set - opts win when given,
14
- * otherwise the existing value (if any) carries over. Returns `undefined`
15
- * fields as "unset" (there is no way to explicitly clear a directive via
16
- * opts - only to override it).
13
+ * Test-only. A function stored on `globalThis` under this symbol is called with every URL
14
+ * `assetUrl` returns; the dual-build fixture installs one during a static export to prove
15
+ * `collect-asset-refs` finds every URL its pages emitted.
17
16
  */
18
- function mergeDirectives(current, opts) {
17
+ const EMITTED_URL_LISTENER = Symbol.for('canopycms.assetUrl.emitted');
18
+ function emitted(url) {
19
+ const listener = globalThis[EMITTED_URL_LISTENER];
20
+ if (typeof listener === 'function')
21
+ listener(url);
22
+ return url;
23
+ }
24
+ /**
25
+ * Merge `opts` and the ref's crop over the directives already in its src. Precedence per
26
+ * directive: `opts`, then `ref.crop` (crop only), then the src. There is no way to clear a
27
+ * directive, only to override it.
28
+ */
29
+ function mergeDirectives(current, refCrop, opts) {
19
30
  const existing = current.identity ? undefined : current;
20
31
  const width = opts.width ?? existing?.width;
21
32
  const format = opts.format ?? existing?.format;
22
33
  const quality = opts.quality ?? existing?.quality;
23
- const crop = opts.crop ?? existing?.crop;
34
+ const crop = opts.crop ?? refCrop ?? existing?.crop;
24
35
  if (width === undefined && format === undefined && quality === undefined && crop === undefined) {
25
36
  return { identity: true };
26
37
  }
27
38
  return { identity: false, width, format, quality, crop };
28
39
  }
29
40
  /**
30
- * Build a transform URL, merging `opts` over the directives already present
31
- * in `ref.src` (opts win). For static srcs (svg/pdf under `/assets/{hash}/...`,
32
- * or any src that isn't one of our own transform URLs) the src is returned
33
- * unchanged and `opts` are ignored - there is nothing to transform.
41
+ * Build a transform URL, merging `opts` and `ref.crop` over the directives already present
42
+ * in `ref.src` (see `mergeDirectives` for precedence). For static srcs (svg/pdf under
43
+ * `/assets/{hash}/...`, or any src that isn't one of our own transform URLs) the src is
44
+ * returned unchanged and every directive is ignored - there is nothing to transform.
45
+ *
46
+ * In a live preview, a transform URL goes behind the editor's authenticated route
47
+ * (`editor/preview-asset-base.ts`) instead of `opts.baseUrl`: a draft's crop or width may exist
48
+ * nowhere a build put it. Static srcs keep `opts.baseUrl`, since finalize wrote them.
34
49
  */
35
50
  export function assetUrl(ref, opts = {}) {
36
51
  const { src } = ref;
@@ -46,35 +61,37 @@ export function assetUrl(ref, opts = {}) {
46
61
  // `joinUrlPrefix`'s own contract. Plain `!opts.baseUrl` made '' and '/' disagree.
47
62
  const mount = opts.baseUrl ? stripTrailingSlashes(opts.baseUrl) : '';
48
63
  if (!mount || isUnprefixablePath(src))
49
- return sanitizeUnprefixedPath(src);
50
- return joinUrlPrefix(opts.baseUrl, src);
64
+ return emitted(sanitizeUnprefixedPath(src));
65
+ return emitted(joinUrlPrefix(opts.baseUrl, src));
51
66
  }
67
+ const base = getPreviewAssetBase() ?? opts.baseUrl;
52
68
  const rest = src.slice(TRANSFORM_URL_PREFIX.length);
53
- const parsed = parseTransformPath(rest.split('/'));
69
+ const parsed = parseTransformPath(rest.split('/'), 'any');
54
70
  if (!parsed.ok) {
55
71
  // Malformed src (shouldn't happen for a src canopycms itself wrote) -
56
72
  // nothing sensible to merge onto, so return it unchanged rather than throw.
57
- return joinUrlPrefix(opts.baseUrl, src);
73
+ return emitted(joinUrlPrefix(base, src));
58
74
  }
59
- const merged = mergeDirectives(parsed.directives, opts);
75
+ const merged = mergeDirectives(parsed.directives, ref.crop, opts);
60
76
  // Ext follows the format: an explicit format (new or carried over) always
61
77
  // wins; with no format at all, the ext must keep preserving the source's
62
78
  // real extension, which is exactly what `parsed.ext` already is here.
63
79
  const ext = !merged.identity && merged.format !== undefined ? merged.format : parsed.ext;
64
80
  const newSrc = `${TRANSFORM_URL_PREFIX}${formatDirectives(merged)}/${parsed.hash32}/${parsed.slug}.${ext}`;
65
- return joinUrlPrefix(opts.baseUrl, newSrc);
81
+ return emitted(joinUrlPrefix(base, newSrc));
66
82
  }
67
83
  /**
68
84
  * Build a comma-joined `url w` srcset descriptor list. `widths` must all be
69
- * on the transform width allowlist (multiples of 160 in [160, 4096]) - this
70
- * is developer-facing (a host app's own responsive-image markup), so an
71
- * invalid width throws rather than silently dropping it.
85
+ * integers in [1, 8192] (the `any` width policy) - this is developer-facing (a
86
+ * host app's own responsive-image markup), so an invalid width throws rather
87
+ * than silently dropping it. A site serving the opt-in lazy public path, which
88
+ * transforms only allowlisted widths, must pick widths from that allowlist.
72
89
  */
73
90
  export function assetSrcSet(ref, widths, opts = {}) {
74
91
  return widths
75
92
  .map((width) => {
76
- if (!isAllowedTransformWidth(width)) {
77
- throw new Error(`assetSrcSet: width ${width} is not allowed (must be a multiple of 160 between 160 and 4096)`);
93
+ if (!isAllowedTransformWidth(width, 'any')) {
94
+ throw new Error(`assetSrcSet: width ${width} is not allowed (must be an integer between 1 and 8192)`);
78
95
  }
79
96
  return `${assetUrl(ref, { ...opts, width })} ${width}w`;
80
97
  })
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * This is the one place that turns the (currently-unconsumed) `media` config
5
5
  * into a real store — see .claude/future-tasks/resolved/assets-media-system.md.
6
+ *
7
+ * The store is branch-agnostic: a local one is rooted at the dev workspace's
8
+ * `assets/` or at `media.directory` resolved against the process's working
9
+ * directory, never inside a branch clone. So asset writes take no [SYNC-C1]
10
+ * content-write lock — the worker's rebase never touches them.
6
11
  */
7
12
  import type { MediaConfig } from '../config/types.js';
8
13
  import type { AssetStore } from './types.js';
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * This is the one place that turns the (currently-unconsumed) `media` config
5
5
  * into a real store — see .claude/future-tasks/resolved/assets-media-system.md.
6
+ *
7
+ * The store is branch-agnostic: a local one is rooted at the dev workspace's
8
+ * `assets/` or at `media.directory` resolved against the process's working
9
+ * directory, never inside a branch clone. So asset writes take no [SYNC-C1]
10
+ * content-write lock — the worker's rebase never touches them.
6
11
  */
7
12
  import { LocalAssetStore } from './store-local.js';
8
13
  import { S3AssetStore } from './store-s3.js';
@@ -39,6 +39,9 @@ export async function finalizeAsset(store, input) {
39
39
  const existing = await store.getMeta(meta.hash32);
40
40
  if (existing)
41
41
  return { ok: true, meta: existing };
42
+ // Either put may find its key taken (`already-exists`): by an earlier attempt that stopped
43
+ // before the meta commit, or by a concurrent identical upload. The object kept there is this
44
+ // content's, so finalize goes on to commit the meta either way.
42
45
  await store.putOriginal({ hash32: meta.hash32, ext: meta.ext, data, contentType: meta.mime });
43
46
  if (publicObject) {
44
47
  await store.putPublicObject(publicObject);
@@ -14,6 +14,6 @@ export { ALLOWED_UPLOAD_CONTENT_TYPES, runFinalizePipeline, type FinalizeInput,
14
14
  export { finalizeAsset, finalizeStagedUpload, type FinalizeAssetResult } from './finalize.js';
15
15
  export { sanitizeSvg } from './svg-sanitizer.js';
16
16
  export { assetSrc } from './asset-src.js';
17
- export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, type CropRect, type OutputFormat, type ParsedTransformPath, type ParseTransformPathResult, type TransformDirectives, } from './transform-directives.js';
17
+ export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, type CropRect, type OutputFormat, type ParsedTransformPath, type ParseTransformPathResult, type TransformDirectives, type TransformWidthPolicy, } from './transform-directives.js';
18
18
  export { applyTransform, type ApplyTransformInput, type TransformResult } from './transform.js';
19
19
  export { assetUrl, assetSrcSet, type AssetRef, type AssetUrlOptions } from './asset-url.js';
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Writing transform outputs into the store. Server-only.
3
+ *
4
+ * `storeTransform` computes one key and is shared by the authenticated raw route
5
+ * (api/assets.ts), `materializeAssets` (the batch run that writes every key a build references
6
+ * before that build is released) and canopycms-cdk's lazy transform Lambda.
7
+ */
8
+ import { type ParsedTransformPath } from './transform-directives.js';
9
+ import type { AssetStore, CreateOnlyResult } from './types.js';
10
+ /** Every transform output is stored under a content-addressed key, so it never changes. */
11
+ export declare const TRANSFORM_CACHE_CONTROL = "public, max-age=31536000, immutable";
12
+ /**
13
+ * The object tag on every derivative the lazy transform Lambda writes. Its bucket's `assets/t/`
14
+ * expiry filters on it, so what `materializeAssets` writes, which the Lambda's allowlist may refuse
15
+ * to recompute, is never expired. canopycms-cdk's `AssetSupport` copies these two strings as
16
+ * literals.
17
+ */
18
+ export declare const LAZY_TRANSFORM_TAG: {
19
+ readonly key: "canopy-transform";
20
+ readonly value: "lazy";
21
+ };
22
+ /**
23
+ * `data` is the computed output whether or not it was stored: `stored: 'already-exists'` means
24
+ * another writer stored the key first, and the store kept that object.
25
+ */
26
+ export type StoreTransformResult = {
27
+ ok: true;
28
+ data: Uint8Array;
29
+ contentType: string;
30
+ stored: CreateOnlyResult;
31
+ } | {
32
+ ok: false;
33
+ status: 400 | 404 | 413 | 422;
34
+ error: string;
35
+ };
36
+ /**
37
+ * Compute the transform `parsed` names from the asset's original and write it under `key`: the
38
+ * canonical key, or that key beneath an output prefix. `parsed` must come from
39
+ * `canonicalizeTransformPath`, so the stored pixels always match their key. A rejection is a fact
40
+ * about the asset or the URL and is never worth retrying; a thrown error belongs to the store or
41
+ * the environment. A 404's `error` names what was missing, so a caller answering a browser
42
+ * replaces it.
43
+ */
44
+ export declare function storeTransform(store: AssetStore, parsed: ParsedTransformPath, key: string, options?: {
45
+ tags?: Readonly<Record<string, string>>;
46
+ }): Promise<StoreTransformResult>;
47
+ /** One key a build references, and where it was referenced, for the failure report. */
48
+ export interface MaterializeTarget {
49
+ /** `assets/t/{directives}/{hash32}/{slug}.{ext}`. */
50
+ key: string;
51
+ routes: readonly string[];
52
+ files: readonly string[];
53
+ }
54
+ /**
55
+ * `content`: the key cannot be produced from what the store holds (asset deleted, wrong slug,
56
+ * undecodable input, malformed key), so a retry would fail the same way and the page must change.
57
+ * `store`: the store kept failing, or refused the request outright; rerunning may succeed.
58
+ */
59
+ type MaterializeFailureKind = 'content' | 'store';
60
+ export type MaterializeResult = {
61
+ key: string;
62
+ routes: string[];
63
+ files: string[];
64
+ } & ({
65
+ status: 'existed' | 'created' | 'copied';
66
+ } | {
67
+ status: 'failed';
68
+ failure: MaterializeFailureKind;
69
+ error: string;
70
+ });
71
+ /** The `schemaVersion` of every `MaterializeReport`; a consumer gating on the JSON checks it. */
72
+ export declare const MATERIALIZE_REPORT_SCHEMA_VERSION = 1;
73
+ export interface MaterializeReport {
74
+ schemaVersion: typeof MATERIALIZE_REPORT_SCHEMA_VERSION;
75
+ /** `MaterializeOptions.outputPrefix`, present only when one was given. */
76
+ outputPrefix?: string;
77
+ summary: {
78
+ total: number;
79
+ existed: number;
80
+ created: number;
81
+ /** Keys copied from the canonical prefix into an output prefix; 0 until one is configured. */
82
+ copied: number;
83
+ failed: number;
84
+ contentFailures: number;
85
+ storeFailures: number;
86
+ };
87
+ /** Sorted by key. */
88
+ results: MaterializeResult[];
89
+ }
90
+ export interface MaterializeOptions {
91
+ store: AssetStore;
92
+ targets: readonly MaterializeTarget[];
93
+ /**
94
+ * `assets/{hash32}/{slug}.{ext}` keys (svg, pdf) a build references. These are never produced
95
+ * here (finalize writes them at upload), only checked, or copied under an `outputPrefix`, so a
96
+ * missing one is a content failure, unless the bucket itself is missing: a store failure.
97
+ */
98
+ statics?: readonly MaterializeTarget[];
99
+ /**
100
+ * Write every key `k` at `outputPrefix + k` instead, for a build whose writes must never land
101
+ * where production reads (a PR preview). A key production already stores is copied from there;
102
+ * the rest are transformed from the canonical originals. Production's own run, with no prefix,
103
+ * never reads beneath it. See `assertValidOutputPrefix` for what is accepted.
104
+ */
105
+ outputPrefix?: string;
106
+ /** Store requests in flight at once. Default 8. */
107
+ concurrency?: number;
108
+ /**
109
+ * Transforms in flight at once. Each holds a decoded image, up to about 1.2 GiB at
110
+ * `MAX_INPUT_PIXELS`, so this stays small whatever `concurrency` is. Default 2.
111
+ */
112
+ transformConcurrency?: number;
113
+ /**
114
+ * A directive string referenced by at least this many keys has its whole `assets/t/{directives}/`
115
+ * prefix listed once instead of a HEAD per key, when the store can list. Default 100.
116
+ */
117
+ listThreshold?: number;
118
+ /** Attempts per store operation, including the first. Default 3. */
119
+ attempts?: number;
120
+ /** First retry delay; each later one doubles. Default 500. */
121
+ baseDelayMs?: number;
122
+ /** @internal Test seam for the retry delay. */
123
+ sleep?: (ms: number) => Promise<void>;
124
+ }
125
+ /** sharp cannot load in this process, so nothing missing can be produced here. */
126
+ export declare class SharpUnavailableError extends Error {
127
+ constructor(cause: unknown);
128
+ }
129
+ /** `MaterializeOptions.outputPrefix` broke a rule of `assertValidOutputPrefix`; nothing was read or written. */
130
+ export declare class InvalidOutputPrefixError extends Error {
131
+ constructor(prefix: string, reason: string);
132
+ }
133
+ /**
134
+ * Throws `InvalidOutputPrefixError` unless `prefix` is relative, ends in `/`, and is a run of
135
+ * `[A-Za-z0-9._-]` segments, none empty, `.` or `..`, whose first segments are not a canopy
136
+ * prefix. Production trusts whatever is under its own prefixes, so a write beneath one is a write
137
+ * production serves. Compared by segment: `assets-x/` is accepted, `assets/x/` is not.
138
+ */
139
+ export declare function assertValidOutputPrefix(prefix: string): void;
140
+ /**
141
+ * @internal Exported for tests. Throttling, 5xx and dropped connections are transient. Any other
142
+ * 4xx (AccessDenied, NoSuchBucket) is a configuration fault that a retry only delays.
143
+ */
144
+ export declare function isTransientStoreError(err: unknown): boolean;
145
+ /**
146
+ * Make every referenced transform key exist in `store`, and check every static key does. A key
147
+ * already stored is left alone:
148
+ * keys are content-addressed, so an existing object is the right one. sharp is loaded only when
149
+ * something must be transformed, and a sharp that cannot load throws `SharpUnavailableError` rather
150
+ * than failing each key. An invalid `outputPrefix` throws `InvalidOutputPrefixError` first.
151
+ */
152
+ export declare function materializeAssets(options: MaterializeOptions): Promise<MaterializeReport>;
153
+ export {};