canopycms 0.0.67 → 0.0.68-int.100

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 (268) hide show
  1. package/README.md +21 -14
  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 +111 -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 +10 -1
  23. package/dist/api/github-sync.js +20 -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/index.d.ts +1 -1
  36. package/dist/assets/materialize.d.ts +122 -0
  37. package/dist/assets/materialize.js +305 -0
  38. package/dist/assets/store-local.d.ts +4 -0
  39. package/dist/assets/store-local.js +11 -0
  40. package/dist/assets/store-s3.d.ts +10 -1
  41. package/dist/assets/store-s3.js +55 -3
  42. package/dist/assets/transform-directives.d.ts +52 -13
  43. package/dist/assets/transform-directives.js +87 -27
  44. package/dist/assets/transform.d.ts +3 -3
  45. package/dist/assets/transform.js +13 -8
  46. package/dist/assets/types.d.ts +14 -1
  47. package/dist/auth/file-based-auth-cache.js +7 -8
  48. package/dist/authorization/content.d.ts +5 -4
  49. package/dist/authorization/content.js +5 -5
  50. package/dist/authorization/path.d.ts +11 -6
  51. package/dist/authorization/path.js +11 -6
  52. package/dist/authorization/types.d.ts +2 -2
  53. package/dist/branch-health.d.ts +7 -1
  54. package/dist/branch-health.js +8 -3
  55. package/dist/branch-metadata.d.ts +5 -5
  56. package/dist/branch-metadata.js +44 -36
  57. package/dist/branch-provisioning.d.ts +167 -0
  58. package/dist/branch-provisioning.js +501 -0
  59. package/dist/branch-schema-cache.d.ts +5 -3
  60. package/dist/branch-schema-cache.js +29 -11
  61. package/dist/branch-sparse.d.ts +23 -0
  62. package/dist/branch-sparse.js +118 -0
  63. package/dist/branch-workspace.d.ts +42 -3
  64. package/dist/branch-workspace.js +240 -94
  65. package/dist/build/asset-refs.d.ts +93 -0
  66. package/dist/build/asset-refs.js +251 -0
  67. package/dist/build-identity.d.ts +3 -0
  68. package/dist/build-identity.js +13 -0
  69. package/dist/cli/asset-refs.d.ts +30 -0
  70. package/dist/cli/asset-refs.js +168 -0
  71. package/dist/cli/cli.d.ts +2 -1
  72. package/dist/cli/cli.js +4843 -1870
  73. package/dist/cli/generate-ai-content.js +1584 -444
  74. package/dist/cli/github-app-manifest.d.ts +4 -3
  75. package/dist/cli/github-app-manifest.js +4 -3
  76. package/dist/cli/init.js +30 -27
  77. package/dist/cli/migrate.js +12 -8
  78. package/dist/cli/sync.js +83 -37
  79. package/dist/cli/template-files/Dockerfile.cms.template +8 -0
  80. package/dist/cli/template-files/cdk-app.ts.template +4 -0
  81. package/dist/cli/template-files/cms-stack.ts.template +23 -12
  82. package/dist/cli/template-files/deploy-cms.yml.template +2 -0
  83. package/dist/config/helpers.js +1 -2
  84. package/dist/config/schemas/config.d.ts +19 -32
  85. package/dist/config/schemas/config.js +9 -11
  86. package/dist/config/schemas/field.js +1 -0
  87. package/dist/config/schemas/media.d.ts +4 -13
  88. package/dist/config/schemas/media.js +5 -8
  89. package/dist/config/schemas/url.d.ts +4 -20
  90. package/dist/config/schemas/url.js +6 -22
  91. package/dist/config/types.d.ts +30 -20
  92. package/dist/config/validation.d.ts +6 -0
  93. package/dist/config/validation.js +37 -0
  94. package/dist/content-listing.d.ts +12 -12
  95. package/dist/content-listing.js +10 -6
  96. package/dist/content-reader.d.ts +11 -2
  97. package/dist/content-reader.js +22 -14
  98. package/dist/content-store.d.ts +37 -9
  99. package/dist/content-store.js +70 -43
  100. package/dist/content-tree.d.ts +1 -1
  101. package/dist/content-tree.js +2 -2
  102. package/dist/context.d.ts +27 -9
  103. package/dist/context.js +154 -70
  104. package/dist/editor/BranchManager.d.ts +7 -1
  105. package/dist/editor/BranchManager.js +29 -14
  106. package/dist/editor/CanopyEditor.d.ts +1 -1
  107. package/dist/editor/CanopyEditor.js +2 -6
  108. package/dist/editor/Editor.d.ts +4 -3
  109. package/dist/editor/Editor.js +24 -20
  110. package/dist/editor/FormRenderer.js +34 -18
  111. package/dist/editor/admin/SystemHealthPanel.js +47 -7
  112. package/dist/editor/components/BranchesDrawer.d.ts +9 -0
  113. package/dist/editor/components/BranchesDrawer.js +4 -0
  114. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  115. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  116. package/dist/editor/context/AssetContext.d.ts +18 -35
  117. package/dist/editor/context/AssetContext.js +20 -29
  118. package/dist/editor/context/index.d.ts +1 -1
  119. package/dist/editor/context/index.js +1 -1
  120. package/dist/editor/editor-config.d.ts +1 -2
  121. package/dist/editor/editor-config.js +0 -23
  122. package/dist/editor/editor-utils.d.ts +24 -21
  123. package/dist/editor/editor-utils.js +75 -62
  124. package/dist/editor/fields/BlockField.d.ts +1 -0
  125. package/dist/editor/fields/BlockField.js +6 -4
  126. package/dist/editor/fields/CodeField.d.ts +1 -0
  127. package/dist/editor/fields/CodeField.js +2 -2
  128. package/dist/editor/fields/DateTimeField.d.ts +1 -0
  129. package/dist/editor/fields/DateTimeField.js +2 -2
  130. package/dist/editor/fields/FieldDescription.d.ts +16 -0
  131. package/dist/editor/fields/FieldDescription.js +11 -0
  132. package/dist/editor/fields/ImageField.d.ts +1 -0
  133. package/dist/editor/fields/ImageField.js +3 -2
  134. package/dist/editor/fields/InlineGroupField.js +4 -1
  135. package/dist/editor/fields/MarkdownField.d.ts +1 -0
  136. package/dist/editor/fields/MarkdownField.js +115 -20
  137. package/dist/editor/fields/NumberField.d.ts +1 -0
  138. package/dist/editor/fields/NumberField.js +2 -2
  139. package/dist/editor/fields/NumberListField.d.ts +1 -0
  140. package/dist/editor/fields/NumberListField.js +2 -2
  141. package/dist/editor/fields/ObjectField.d.ts +1 -0
  142. package/dist/editor/fields/ObjectField.js +5 -2
  143. package/dist/editor/fields/ReferenceField.d.ts +1 -0
  144. package/dist/editor/fields/ReferenceField.js +5 -4
  145. package/dist/editor/fields/SelectField.d.ts +1 -0
  146. package/dist/editor/fields/SelectField.js +2 -2
  147. package/dist/editor/fields/StringListField.d.ts +1 -0
  148. package/dist/editor/fields/StringListField.js +2 -2
  149. package/dist/editor/fields/TextField.d.ts +1 -0
  150. package/dist/editor/fields/TextField.js +2 -2
  151. package/dist/editor/fields/ToggleField.d.ts +1 -0
  152. package/dist/editor/fields/ToggleField.js +7 -4
  153. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  154. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
  155. package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
  156. package/dist/editor/fields/mdx-jsx-support.js +137 -0
  157. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  158. package/dist/editor/hooks/create-branch-request.js +52 -0
  159. package/dist/editor/hooks/useBranchActions.d.ts +13 -2
  160. package/dist/editor/hooks/useBranchActions.js +48 -17
  161. package/dist/editor/hooks/useBranchManager.d.ts +11 -0
  162. package/dist/editor/hooks/useBranchManager.js +80 -2
  163. package/dist/editor/hooks/useBranchesData.d.ts +6 -0
  164. package/dist/editor/hooks/useBranchesData.js +9 -3
  165. package/dist/editor/hooks/useCommentSystem.js +25 -6
  166. package/dist/editor/hooks/useDraftManager.d.ts +16 -1
  167. package/dist/editor/hooks/useDraftManager.js +193 -36
  168. package/dist/editor/hooks/useEntriesData.js +3 -3
  169. package/dist/editor/hooks/useEntryManager.d.ts +2 -1
  170. package/dist/editor/hooks/useEntryManager.js +34 -19
  171. package/dist/editor/media/AssetCard.d.ts +1 -1
  172. package/dist/editor/media/crop-math.d.ts +3 -4
  173. package/dist/editor/media/crop-math.js +11 -22
  174. package/dist/editor/media/editor-image-src.d.ts +7 -0
  175. package/dist/editor/media/editor-image-src.js +12 -0
  176. package/dist/editor/preview-asset-base.d.ts +8 -0
  177. package/dist/editor/preview-asset-base.js +13 -0
  178. package/dist/editor/preview-bridge.d.ts +8 -1
  179. package/dist/editor/preview-bridge.js +15 -2
  180. package/dist/editor/preview-path.d.ts +10 -0
  181. package/dist/editor/preview-path.js +20 -0
  182. package/dist/editor/theme.js +2 -2
  183. package/dist/entry-schema-registry.d.ts +4 -4
  184. package/dist/entry-schema-registry.js +10 -6
  185. package/dist/entry-schema.d.ts +27 -4
  186. package/dist/entry-schema.js +20 -3
  187. package/dist/git-manager.d.ts +173 -6
  188. package/dist/git-manager.js +594 -76
  189. package/dist/github-service.d.ts +8 -0
  190. package/dist/github-service.js +5 -1
  191. package/dist/http/handler.js +37 -13
  192. package/dist/http/index.d.ts +2 -0
  193. package/dist/http/index.js +2 -0
  194. package/dist/http/router.d.ts +2 -0
  195. package/dist/http/router.js +2 -1
  196. package/dist/http/types.d.ts +2 -0
  197. package/dist/http/worker-not-ready.d.ts +10 -0
  198. package/dist/http/worker-not-ready.js +24 -0
  199. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  200. package/dist/operating-mode/types.d.ts +1 -4
  201. package/dist/paths/branch-name.d.ts +5 -0
  202. package/dist/paths/branch-name.js +9 -0
  203. package/dist/paths/branch.d.ts +6 -2
  204. package/dist/paths/branch.js +16 -10
  205. package/dist/paths/index.d.ts +3 -3
  206. package/dist/paths/index.js +3 -3
  207. package/dist/paths/normalize.d.ts +7 -0
  208. package/dist/paths/normalize.js +9 -0
  209. package/dist/paths/validation.js +0 -2
  210. package/dist/reference-resolver.d.ts +5 -21
  211. package/dist/reference-resolver.js +9 -44
  212. package/dist/resolve-canopy-user.js +2 -1
  213. package/dist/schema/schema-store.d.ts +54 -31
  214. package/dist/schema/schema-store.js +77 -31
  215. package/dist/server.d.ts +24 -9
  216. package/dist/server.js +22 -8
  217. package/dist/services.d.ts +18 -6
  218. package/dist/services.js +85 -70
  219. package/dist/settings-workspace.d.ts +23 -3
  220. package/dist/settings-workspace.js +197 -45
  221. package/dist/static/seo.d.ts +2 -14
  222. package/dist/static/seo.js +2 -25
  223. package/dist/submission-attribution.d.ts +73 -0
  224. package/dist/submission-attribution.js +221 -0
  225. package/dist/sync-core.d.ts +12 -1
  226. package/dist/sync-core.js +31 -13
  227. package/dist/task-queue/worker-status.d.ts +8 -0
  228. package/dist/task-queue/worker-status.js +17 -0
  229. package/dist/types.d.ts +31 -2
  230. package/dist/utils/content-write-lock.d.ts +18 -7
  231. package/dist/utils/content-write-lock.js +18 -9
  232. package/dist/utils/debug.d.ts +8 -0
  233. package/dist/utils/debug.js +10 -2
  234. package/dist/utils/git.d.ts +32 -0
  235. package/dist/utils/git.js +42 -0
  236. package/dist/utils/occ-json-write.js +10 -2
  237. package/dist/utils/provision-log.d.ts +25 -0
  238. package/dist/utils/provision-log.js +49 -0
  239. package/dist/utils/provisioning-lock.d.ts +35 -5
  240. package/dist/utils/provisioning-lock.js +73 -12
  241. package/dist/utils/request-timing.d.ts +25 -0
  242. package/dist/utils/request-timing.js +101 -0
  243. package/dist/utils/sanitize-href.d.ts +8 -22
  244. package/dist/utils/sanitize-href.js +11 -28
  245. package/dist/utils/url-prefix.d.ts +30 -0
  246. package/dist/utils/url-prefix.js +61 -0
  247. package/dist/validation/entry-validator.js +9 -3
  248. package/dist/version.d.ts +1 -0
  249. package/dist/version.js +3 -0
  250. package/dist/worker/canopy-state.d.ts +45 -0
  251. package/dist/worker/canopy-state.js +75 -0
  252. package/dist/worker/cms-worker.d.ts +8 -3
  253. package/dist/worker/cms-worker.js +38 -2
  254. package/dist/worker/git-sync.d.ts +30 -12
  255. package/dist/worker/git-sync.js +253 -58
  256. package/dist/worker/history-rewrite.d.ts +1 -1
  257. package/dist/worker/history-rewrite.js +1 -1
  258. package/dist/worker/provisioned-workspace.d.ts +35 -0
  259. package/dist/worker/provisioned-workspace.js +50 -0
  260. package/dist/worker/rebase.d.ts +1 -1
  261. package/dist/worker/rebase.js +95 -23
  262. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  263. package/dist/worker/remote-git-maintenance.js +11 -0
  264. package/dist/worker/sparse-cone.d.ts +20 -0
  265. package/dist/worker/sparse-cone.js +132 -0
  266. package/dist/worker/task-runner.js +39 -4
  267. package/dist/worker/worker-context.d.ts +6 -1
  268. package/package.json +2 -1
@@ -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';
@@ -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,122 @@
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 } 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
+ export type StoreTransformResult = {
23
+ ok: true;
24
+ data: Uint8Array;
25
+ contentType: string;
26
+ } | {
27
+ ok: false;
28
+ status: 400 | 404 | 413 | 422;
29
+ error: string;
30
+ };
31
+ /**
32
+ * Compute the transform `parsed` names from the asset's original and write it under
33
+ * `canonicalKey`. `parsed` must come from `canonicalizeTransformPath`, so the stored pixels
34
+ * always match their key. A rejection is a fact about the asset or the URL and is never worth
35
+ * retrying; a thrown error belongs to the store or the environment. A 404's `error` names what
36
+ * was missing, so a caller answering a browser replaces it.
37
+ */
38
+ export declare function storeTransform(store: AssetStore, parsed: ParsedTransformPath, canonicalKey: string, options?: {
39
+ tags?: Readonly<Record<string, string>>;
40
+ }): Promise<StoreTransformResult>;
41
+ /** @internal One key a build references, and where it was referenced, for the failure report. */
42
+ export interface MaterializeTarget {
43
+ /** `assets/t/{directives}/{hash32}/{slug}.{ext}`. */
44
+ key: string;
45
+ routes: readonly string[];
46
+ files: readonly string[];
47
+ }
48
+ /**
49
+ * `content`: the key cannot be produced from what the store holds (asset deleted, wrong slug,
50
+ * undecodable input, malformed key), so a retry would fail the same way and the page must change.
51
+ * `store`: the store kept failing, or refused the request outright; rerunning may succeed.
52
+ */
53
+ type MaterializeFailureKind = 'content' | 'store';
54
+ export type MaterializeResult = {
55
+ key: string;
56
+ routes: string[];
57
+ files: string[];
58
+ } & ({
59
+ status: 'existed' | 'created';
60
+ } | {
61
+ status: 'failed';
62
+ failure: MaterializeFailureKind;
63
+ error: string;
64
+ });
65
+ export interface MaterializeReport {
66
+ summary: {
67
+ total: number;
68
+ existed: number;
69
+ created: number;
70
+ failed: number;
71
+ contentFailures: number;
72
+ storeFailures: number;
73
+ };
74
+ /** Sorted by key. */
75
+ results: MaterializeResult[];
76
+ }
77
+ export interface MaterializeOptions {
78
+ store: AssetStore;
79
+ targets: readonly MaterializeTarget[];
80
+ /**
81
+ * `assets/{hash32}/{slug}.{ext}` keys (svg, pdf) a build references. These are only checked:
82
+ * finalize writes them at upload, so a missing one is a content failure nothing here can fix,
83
+ * unless the bucket itself is missing, which is a store failure.
84
+ */
85
+ statics?: readonly MaterializeTarget[];
86
+ /** Store requests in flight at once. Default 8. */
87
+ concurrency?: number;
88
+ /**
89
+ * Transforms in flight at once. Each holds a decoded image, up to about 1.2 GiB at
90
+ * `MAX_INPUT_PIXELS`, so this stays small whatever `concurrency` is. Default 2.
91
+ */
92
+ transformConcurrency?: number;
93
+ /**
94
+ * A directive string referenced by at least this many keys has its whole `assets/t/{directives}/`
95
+ * prefix listed once instead of a HEAD per key, when the store can list. Default 100.
96
+ */
97
+ listThreshold?: number;
98
+ /** Attempts per store operation, including the first. Default 3. */
99
+ attempts?: number;
100
+ /** First retry delay; each later one doubles. Default 500. */
101
+ baseDelayMs?: number;
102
+ /** @internal Test seam for the retry delay. */
103
+ sleep?: (ms: number) => Promise<void>;
104
+ }
105
+ /** sharp cannot load in this process, so nothing missing can be produced here. */
106
+ export declare class SharpUnavailableError extends Error {
107
+ constructor(cause: unknown);
108
+ }
109
+ /**
110
+ * @internal Exported for tests. Throttling, 5xx and dropped connections are transient. Any other
111
+ * 4xx (AccessDenied, NoSuchBucket) is a configuration fault that a retry only delays.
112
+ */
113
+ export declare function isTransientStoreError(err: unknown): boolean;
114
+ /**
115
+ * Make every referenced transform key exist in `store`, and check every static key does. A key
116
+ * already stored is left alone:
117
+ * keys are content-addressed, so an existing object is the right one. sharp is loaded only when
118
+ * something is missing, and a sharp that cannot load throws `SharpUnavailableError` rather than
119
+ * failing each key.
120
+ */
121
+ export declare function materializeAssets(options: MaterializeOptions): Promise<MaterializeReport>;
122
+ export {};