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
@@ -76,6 +76,7 @@ export interface PullRequestDetails {
76
76
  state: 'open' | 'closed';
77
77
  merged: boolean;
78
78
  draft: boolean;
79
+ body: string;
79
80
  }
80
81
  export interface CreateOrUpdatePullRequestParams {
81
82
  octokit: Octokit;
@@ -87,6 +88,11 @@ export interface CreateOrUpdatePullRequestParams {
87
88
  body: string;
88
89
  /** Convert a pre-existing draft PR to ready-for-review after updating it. */
89
90
  markReadyIfDraft?: boolean;
91
+ /**
92
+ * `body` is the canopycms PR section: an existing PR keeps the text outside
93
+ * its section (see mergePrSection) instead of having its body replaced.
94
+ */
95
+ mergeSectionIntoBody?: boolean;
90
96
  /** Forwarded to all GitHub requests (worker task-timeout abort). */
91
97
  signal?: AbortSignal;
92
98
  }
@@ -128,6 +134,8 @@ export declare class GitHubService {
128
134
  body: string;
129
135
  /** Convert a pre-existing draft PR to ready-for-review after updating it. */
130
136
  markReadyIfDraft?: boolean;
137
+ /** See {@link CreateOrUpdatePullRequestParams.mergeSectionIntoBody}. */
138
+ mergeSectionIntoBody?: boolean;
131
139
  }): Promise<{
132
140
  number: number;
133
141
  url: string;
@@ -7,6 +7,7 @@ import { getErrorMessage } from './utils/error.js';
7
7
  // or CloudWatch folds it into the previous event. Under Lambda/dev the helper
8
8
  // is plain console. See utils/logger.ts.
9
9
  import { canopyLogWarn } from './utils/logger.js';
10
+ import { mergePrSection } from './submission-attribution.js';
10
11
  const ThrottledOctokit = Octokit.plugin(throttling);
11
12
  /**
12
13
  * Retry primary rate limits at most twice and only for short waits; beyond
@@ -67,6 +68,7 @@ export function createCanopyOctokit(options) {
67
68
  */
68
69
  export async function createOrUpdatePullRequest(params) {
69
70
  const { octokit, owner, repo, head, base, title, body, markReadyIfDraft, signal } = params;
71
+ const mergeSection = params.mergeSectionIntoBody === true;
70
72
  // CONDITIONAL spread: with no signal, the request objects below carry no
71
73
  // `request` key at all (github-service.test.ts asserts on their exact shape).
72
74
  const requestOption = signal ? { request: { signal } } : {};
@@ -92,7 +94,7 @@ export async function createOrUpdatePullRequest(params) {
92
94
  repo,
93
95
  pull_number: existing.number,
94
96
  title,
95
- body,
97
+ body: mergeSection ? mergePrSection(existing.body, body) : body,
96
98
  ...requestOption,
97
99
  });
98
100
  // Only a pre-existing PR can be a draft on this path — pulls.create
@@ -185,6 +187,7 @@ export class GitHubService {
185
187
  title: options.title,
186
188
  body: options.body,
187
189
  markReadyIfDraft: options.markReadyIfDraft,
190
+ mergeSectionIntoBody: options.mergeSectionIntoBody,
188
191
  });
189
192
  return { number: result.number, url: result.url };
190
193
  }
@@ -200,6 +203,7 @@ export class GitHubService {
200
203
  state: response.data.state,
201
204
  merged: response.data.merged ?? false,
202
205
  draft: response.data.draft ?? false,
206
+ body: response.data.body ?? '',
203
207
  };
204
208
  }
205
209
  async convertToDraft(prNumber) {
@@ -1,27 +1,31 @@
1
1
  import { jsonResponse, isCanopyBinaryResponse } from './types.js';
2
+ import { workerNotReadyResponse } from './worker-not-ready.js';
2
3
  import { createCanopyRouter } from './router.js';
3
4
  import { assertAuthPluginAllowedForMode } from '../auth/plugin.js';
4
5
  import { createCanopyServices } from '../services.js';
6
+ import { recordConfiguredSparseCone } from '../branch-sparse.js';
5
7
  import { loadBranchContext, BranchWorkspaceManager } from '../branch-workspace.js';
6
8
  import { BranchMetadataCorruptError } from '../branch-metadata.js';
7
9
  import { resolveCanopyUser } from '../resolve-canopy-user.js';
8
10
  import { authResultToCanopyUser } from '../user.js';
9
11
  import { isAdmin } from '../authorization/index.js';
12
+ import { isSettingsBranchName } from '../paths/branch-name.js';
10
13
  import { clientOperatingStrategy, operatingStrategy } from '../operating-mode/index.js';
11
14
  import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from '../utils/error.js';
12
15
  // canopyLogError, not console.error: this is shared code and not guaranteed to
13
16
  // stay out of the worker's runtime import closure, so new log lines here go
14
17
  // through the indirection (utils/logger.ts).
15
18
  import { canopyLogError } from '../utils/logger.js';
19
+ import { runWithRequestTiming, setRequestTimingRoute, timeRequestPhase, } from '../utils/request-timing.js';
16
20
  const buildContext = async (options) => {
17
21
  const services = options.services ?? (options.config ? await createCanopyServices(options.config) : undefined);
18
22
  if (!services) {
19
23
  throw new Error('CanopyCMS: config or services is required');
20
24
  }
25
+ await recordConfiguredSparseCone(services.config);
21
26
  const operatingMode = services.config.mode;
22
- // Derive from the strategy, which resolves deploymentName; a literal here
23
- // would not match a deployment-namespaced settings branch (say
24
- // canopycms-settings-acme), so getBranchContext could never auto-create it.
27
+ // Derived from the strategy, which resolves deploymentName; a literal here
28
+ // would miss a deployment-namespaced settings branch (say canopycms-settings-acme).
25
29
  const settingsBranch = operatingStrategy(operatingMode).getSettingsBranchName(services.config);
26
30
  const getBranchContext = options.getBranchContext ??
27
31
  (async (branch, opts) => {
@@ -42,7 +46,7 @@ const buildContext = async (options) => {
42
46
  const baseBranch = services.config.defaultBaseBranch ?? 'main';
43
47
  const activeBranch = services.config.defaultActiveBranch ?? baseBranch;
44
48
  const shouldAutoCreate = clientOperatingStrategy(operatingMode).supportsBranching() &&
45
- (branch === baseBranch || branch === activeBranch || branch === settingsBranch);
49
+ (branch === baseBranch || branch === activeBranch);
46
50
  if (shouldAutoCreate) {
47
51
  const manager = new BranchWorkspaceManager(services.config);
48
52
  const context = await manager.openOrCreateBranch({
@@ -62,7 +66,14 @@ const buildContext = async (options) => {
62
66
  return {
63
67
  services,
64
68
  assetStore: options.assetStore,
65
- getBranchContext,
69
+ // The settings branch lives only in the settings workspace. Resolving it as a
70
+ // content branch would let any request clone a content-history copy that a
71
+ // submit pushes under the settings name, which settings provisioning then refuses,
72
+ // leaving settings unavailable.
73
+ // Not found, like any branch the caller cannot resolve, even a workspace left on disk.
74
+ getBranchContext: (branch, opts) => isSettingsBranchName(branch, settingsBranch)
75
+ ? Promise.resolve(null)
76
+ : timeRequestPhase('branchContext', () => getBranchContext(branch, opts)),
66
77
  authPlugin: options.authPlugin,
67
78
  };
68
79
  };
@@ -109,10 +120,11 @@ export function createCanopyRequestHandler(options) {
109
120
  if (!match) {
110
121
  return jsonResponse({ ok: false, status: 404, error: 'Not found' }, 404);
111
122
  }
112
- const apiCtx = await getContext();
123
+ setRequestTimingRoute(match.pattern.join('/') || '(malformed path)');
124
+ const apiCtx = await timeRequestPhase('context', getContext);
113
125
  // In dev mode, re-check if the developer switched git branches
114
- await apiCtx.services.refreshActiveBranch();
115
- const authResult = await options.authPlugin.authenticate(req);
126
+ await timeRequestPhase('refreshBranch', () => apiCtx.services.refreshActiveBranch());
127
+ const authResult = await timeRequestPhase('auth', () => options.authPlugin.authenticate(req));
116
128
  // API routes require authentication. Anonymous callers are rejected BEFORE
117
129
  // any workspace provisioning below, so they can neither trigger expensive
118
130
  // git operations nor read provisioning error details.
@@ -141,6 +153,9 @@ export function createCanopyRequestHandler(options) {
141
153
  // (authenticated) client. Credentials, which git errors can embed, are
142
154
  // redacted even from the server logs.
143
155
  console.error(`CanopyCMS: Failed to provision workspace for base branch '${baseBranch}': ${redactCredentials(message)}`);
156
+ const notReady = workerNotReadyResponse(err);
157
+ if (notReady)
158
+ return notReady;
144
159
  return jsonResponse({
145
160
  ok: false,
146
161
  status: 503,
@@ -157,15 +172,21 @@ export function createCanopyRequestHandler(options) {
157
172
  // a silent authorization change.
158
173
  let user;
159
174
  try {
160
- user = await resolveCanopyUser(authResult, {
175
+ user = await timeRequestPhase('user', () => resolveCanopyUser(authResult, {
161
176
  getSettingsBranchRoot: apiCtx.services.getSettingsBranchRoot,
162
177
  mode: apiCtx.services.config.mode,
163
178
  bootstrapAdminIds: apiCtx.services.bootstrapAdminIds,
164
- });
179
+ }));
165
180
  }
166
181
  catch (err) {
167
182
  const message = getErrorMessage(err);
168
183
  canopyLogError(`CanopyCMS: Failed to resolve internal groups from the settings workspace: ${redactCredentials(message)}`);
184
+ // No remote means no settings workspace, so /admin cannot load either:
185
+ // the worker has not created the remote yet, so every caller gets the
186
+ // not-ready 503, bootstrap admins included.
187
+ const notReady = workerNotReadyResponse(err);
188
+ if (notReady)
189
+ return notReady;
169
190
  // Same trade as the base-branch degradation above: /admin is the recovery
170
191
  // surface for exactly this failure (a renamed settings branch trips
171
192
  // assertSettingsWorkspaceIdentity until a human intervenes), so 503ing it
@@ -231,7 +252,7 @@ export function createCanopyRequestHandler(options) {
231
252
  if (validationResult.body !== undefined) {
232
253
  handlerArgs.push(validationResult.body);
233
254
  }
234
- const result = await match.handler(...handlerArgs);
255
+ const result = await timeRequestPhase('route', () => match.handler(...handlerArgs));
235
256
  // Binary routes carry their own status and headers and MUST reach the
236
257
  // adapter untouched: jsonResponse would serialize raw bytes and drop
237
258
  // contentType/contentDisposition.
@@ -242,7 +263,7 @@ export function createCanopyRequestHandler(options) {
242
263
  else {
243
264
  // Every route should use defineEndpoint; this is the safety net for one
244
265
  // that carries no validate function.
245
- const result = await match.handler(apiCtx, apiReq, mergedParams);
266
+ const result = await timeRequestPhase('route', () => match.handler(apiCtx, apiReq, mergedParams));
246
267
  if (isCanopyBinaryResponse(result))
247
268
  return result;
248
269
  return jsonResponse(result, result.status);
@@ -250,12 +271,15 @@ export function createCanopyRequestHandler(options) {
250
271
  };
251
272
  return async (req, pathSegments) => {
252
273
  try {
253
- return await handleRequest(req, pathSegments);
274
+ return await runWithRequestTiming(req.method, () => handleRequest(req, pathSegments), (response) => response.status);
254
275
  }
255
276
  catch (err) {
256
277
  // Last-resort boundary (API-C1): see handleRequest's doc comment above.
257
278
  const message = getErrorMessage(err);
258
279
  console.error('CanopyCMS: Unhandled error in API request handler:', message);
280
+ const notReady = workerNotReadyResponse(err);
281
+ if (notReady)
282
+ return notReady;
259
283
  return jsonResponse({ ok: false, status: 500, error: sanitizeErrorMessage(message) }, 500);
260
284
  }
261
285
  };
@@ -4,3 +4,5 @@ export type { CanopyHandler, RouteDefinition, RouteMatch, CanopyRouter } from '.
4
4
  export { createCanopyRouter } from './router.js';
5
5
  export type { CanopyHandlerOptions, CanopyRequestHandler } from './handler.js';
6
6
  export { createCanopyRequestHandler, createCanopyRequestHandlerFromConfig } from './handler.js';
7
+ export { RemoteNotReadyError } from '../git-manager.js';
8
+ export { workerNotReadyResponse, WORKER_NOT_READY_MESSAGE } from './worker-not-ready.js';
@@ -1,3 +1,5 @@
1
1
  export { jsonResponse, isCanopyBinaryResponse } from './types.js';
2
2
  export { createCanopyRouter } from './router.js';
3
3
  export { createCanopyRequestHandler, createCanopyRequestHandlerFromConfig } from './handler.js';
4
+ export { RemoteNotReadyError } from '../git-manager.js';
5
+ export { workerNotReadyResponse, WORKER_NOT_READY_MESSAGE } from './worker-not-ready.js';
@@ -32,6 +32,8 @@ export interface RouteDefinition {
32
32
  }
33
33
  export interface RouteMatch {
34
34
  handler: CanopyHandler;
35
+ /** The matched route's pattern; empty for the synthetic malformed-path match. */
36
+ pattern: readonly string[];
35
37
  params: Record<string, string>;
36
38
  validate?: RouteDefinition['validate'];
37
39
  bodyFormat?: RouteDefinition['bodyFormat'];
@@ -122,10 +122,11 @@ export function matchRoute(routes, method, segments) {
122
122
  decodedParams = Object.fromEntries(Object.entries(best.params).map(([key, value]) => [key, decodeURIComponent(value)]));
123
123
  }
124
124
  catch {
125
- return { handler: malformedPathHandler, params: {} };
125
+ return { handler: malformedPathHandler, pattern: [], params: {} };
126
126
  }
127
127
  return {
128
128
  handler: best.route.handler,
129
+ pattern: best.route.pattern,
129
130
  params: decodedParams,
130
131
  validate: best.route.validate, // Include validation function if present
131
132
  bodyFormat: best.route.bodyFormat,
@@ -37,6 +37,8 @@ export interface CanopyBinaryResponse {
37
37
  contentDisposition?: string;
38
38
  cacheControl?: string;
39
39
  etag?: string;
40
+ /** Redirect target, for a 3xx `status`; `body` is then empty. */
41
+ location?: string;
40
42
  };
41
43
  }
42
44
  export declare function jsonResponse<T>(body: T, status?: number, headers?: Record<string, string>): CanopyResponse<T>;
@@ -0,0 +1,10 @@
1
+ import type { ApiResponse } from '../api/types.js';
2
+ import { type CanopyResponse } from './types.js';
3
+ export declare const WORKER_NOT_READY_MESSAGE = "CMS worker not ready \u2014 it may still be starting. Try again in a minute; if this persists, ask an admin to check the CMS worker.";
4
+ /**
5
+ * The retriable 503 for a request that hit {@link RemoteNotReadyError} or
6
+ * {@link BranchProvisioningBusyError}, or `undefined` for any other error. The single mapping
7
+ * point for the core handler, the AI route and every framework adapter's backstop, so the
8
+ * message and `Retry-After` cannot diverge.
9
+ */
10
+ export declare function workerNotReadyResponse(err: unknown): CanopyResponse<ApiResponse> | undefined;
@@ -0,0 +1,24 @@
1
+ import { BranchProvisioningBusyError } from '../branch-provisioning.js';
2
+ import { RemoteNotReadyError } from '../git-manager.js';
3
+ import { jsonResponse } from './types.js';
4
+ /** Seconds a client should wait before retrying while the worker boots. */
5
+ const RETRY_AFTER_SECONDS = '30';
6
+ export const WORKER_NOT_READY_MESSAGE = 'CMS worker not ready — it may still be starting. Try again in a minute; if this persists, ask an admin to check the CMS worker.';
7
+ /**
8
+ * The retriable 503 for a request that hit {@link RemoteNotReadyError} or
9
+ * {@link BranchProvisioningBusyError}, or `undefined` for any other error. The single mapping
10
+ * point for the core handler, the AI route and every framework adapter's backstop, so the
11
+ * message and `Retry-After` cannot diverge.
12
+ */
13
+ export function workerNotReadyResponse(err) {
14
+ const message = err instanceof RemoteNotReadyError
15
+ ? WORKER_NOT_READY_MESSAGE
16
+ : err instanceof BranchProvisioningBusyError
17
+ ? err.message
18
+ : undefined;
19
+ if (message === undefined)
20
+ return undefined;
21
+ return jsonResponse({ ok: false, status: 503, error: message }, 503, {
22
+ 'Retry-After': RETRY_AFTER_SECONDS,
23
+ });
24
+ }
@@ -57,9 +57,6 @@ class ProdStrategy extends ProdClientSafeStrategy {
57
57
  throw new Error('gitBotAuthorName and gitBotAuthorEmail are required in prod mode');
58
58
  }
59
59
  }
60
- shouldCreateSettingsPR(config) {
61
- return config.autoCreateSettingsPR ?? true;
62
- }
63
60
  }
64
61
  class DevStrategy extends DevClientSafeStrategy {
65
62
  getWorkspaceRoot(sourceRoot) {
@@ -107,9 +104,6 @@ class DevStrategy extends DevClientSafeStrategy {
107
104
  validateConfig(_config) {
108
105
  // No special validation for dev mode
109
106
  }
110
- shouldCreateSettingsPR(_config) {
111
- return false; // No real GitHub in local dev mode
112
- }
113
107
  }
114
108
  const strategyCache = new Map();
115
109
  /**
@@ -47,7 +47,7 @@ export interface ClientSafeStrategy {
47
47
  export interface ClientUnsafeStrategy extends ClientSafeStrategy {
48
48
  /**
49
49
  * The mode's workspace root; content-branches, settings and .cache all live
50
- * under it. prod: CANOPYCMS_WORKSPACE_ROOT ?? /mnt/efs/workspace.
50
+ * under it. prod: CANOPYCMS_WORKSPACE_ROOT ?? /mnt/efs.
51
51
  * dev: {sourceRoot ?? cwd}/.canopy-dev.
52
52
  */
53
53
  getWorkspaceRoot(sourceRoot?: string): string;
@@ -89,7 +89,4 @@ export interface ClientUnsafeStrategy extends ClientSafeStrategy {
89
89
  getSettingsRoot(sourceRoot?: string): string;
90
90
  usesSeparateSettingsBranch(): boolean;
91
91
  validateConfig(config: Partial<CanopyConfig>): void;
92
- shouldCreateSettingsPR(config: {
93
- autoCreateSettingsPR?: boolean;
94
- }): boolean;
95
92
  }
@@ -29,6 +29,11 @@ export declare function sanitizeBranchName(branchName: string): SanitizedBranchN
29
29
  * branch.
30
30
  */
31
31
  export declare const RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
32
+ /**
33
+ * Whether `branchName` is a settings branch, never a content branch: the reserved prefix, or the
34
+ * configured name (which need not carry it). Compared sanitized, the form git and disk use.
35
+ */
36
+ export declare function isSettingsBranchName(branchName: string, settingsBranchName?: string): boolean;
32
37
  /**
33
38
  * Branch names that collide with a static top-level API route namespace.
34
39
  *
@@ -33,6 +33,15 @@ export function sanitizeBranchName(branchName) {
33
33
  * branch.
34
34
  */
35
35
  export const RESERVED_SETTINGS_BRANCH_PREFIX = 'canopycms-settings-';
36
+ /**
37
+ * Whether `branchName` is a settings branch, never a content branch: the reserved prefix, or the
38
+ * configured name (which need not carry it). Compared sanitized, the form git and disk use.
39
+ */
40
+ export function isSettingsBranchName(branchName, settingsBranchName) {
41
+ const sanitized = sanitizeBranchName(branchName);
42
+ return (sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) ||
43
+ (settingsBranchName !== undefined && sanitized === sanitizeBranchName(settingsBranchName)));
44
+ }
36
45
  /**
37
46
  * Branch names that collide with a static top-level API route namespace.
38
47
  *
@@ -14,11 +14,15 @@ export interface BranchPathResult {
14
14
  /** @internal Exported for tests. */
15
15
  export declare class BranchPathError extends Error {
16
16
  }
17
+ /**
18
+ * True when a branch load failed because the name names no workspace: a traversal segment, a name
19
+ * too long for a filename, or a path through a file (ENOTDIR), as `?branch=branches.json` gives.
20
+ */
21
+ export declare function namesNoWorkspace(err: unknown): boolean;
17
22
  import { sanitizeBranchName } from './branch-name.js';
18
23
  /** @internal Exported for tests. */
19
24
  export { sanitizeBranchName };
20
- /** Resolve a branch name to workspace paths, rejecting path traversal. */
25
+ /** Resolve a branch name to workspace paths, rejecting traversal and settings-branch names. */
21
26
  export declare function resolveBranchPath(options: BranchPathOptions): BranchPathResult;
22
- export declare function ensureBranchRoot(options: BranchPathOptions): Promise<BranchPathResult>;
23
27
  export declare function getDefaultBranchBase(mode: OperatingMode, override?: string): string;
24
28
  export declare function resolveBranchPaths(branchContext: BranchContext, mode: OperatingMode, basePathOverride?: string): BranchPathResult;
@@ -1,23 +1,34 @@
1
1
  /** Branch path resolution utilities. */
2
- import fs from 'node:fs/promises';
3
2
  import path from 'node:path';
4
3
  import { operatingStrategy } from '../operating-mode/index.js';
4
+ import { isNodeError } from '../utils/error.js';
5
5
  /** @internal Exported for tests. */
6
6
  export class BranchPathError extends Error {
7
7
  }
8
- // Lives in ./branch-name (dependency-free); re-exported here for server-side
9
- // importers, who may safely reach this module's node:fs imports.
10
- import { sanitizeBranchName } from './branch-name.js';
8
+ /**
9
+ * True when a branch load failed because the name names no workspace: a traversal segment, a name
10
+ * too long for a filename, or a path through a file (ENOTDIR), as `?branch=branches.json` gives.
11
+ */
12
+ export function namesNoWorkspace(err) {
13
+ if (err instanceof BranchPathError)
14
+ return true;
15
+ return isNodeError(err) && (err.code === 'ENAMETOOLONG' || err.code === 'ENOTDIR');
16
+ }
17
+ // Lives in ./branch-name (dependency-free); re-exported for server-side importers.
18
+ import { sanitizeBranchName, isSettingsBranchName } from './branch-name.js';
11
19
  /** @internal Exported for tests. */
12
20
  export { sanitizeBranchName };
13
21
  const resolveContentBranchesRoot = (mode, override) => {
14
22
  return operatingStrategy(mode).getContentBranchesRoot(override);
15
23
  };
16
- /** Resolve a branch name to workspace paths, rejecting path traversal. */
24
+ /** Resolve a branch name to workspace paths, rejecting traversal and settings-branch names. */
17
25
  export function resolveBranchPath(options) {
18
26
  if (options.branchName.includes('..')) {
19
27
  throw new BranchPathError('Branch name cannot contain traversal segments');
20
28
  }
29
+ if (isSettingsBranchName(options.branchName)) {
30
+ throw new BranchPathError('Settings branches are not content branches');
31
+ }
21
32
  const safeBranch = sanitizeBranchName(options.branchName);
22
33
  const strategy = operatingStrategy(options.mode);
23
34
  const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
@@ -35,11 +46,6 @@ export function resolveBranchPath(options) {
35
46
  }
36
47
  return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
37
48
  }
38
- export async function ensureBranchRoot(options) {
39
- const result = resolveBranchPath(options);
40
- await fs.mkdir(result.branchRoot, { recursive: true });
41
- return result;
42
- }
43
49
  export function getDefaultBranchBase(mode, override) {
44
50
  return resolveContentBranchesRoot(mode, override);
45
51
  }
@@ -3,9 +3,9 @@
3
3
  * because this barrel pulls in node:fs; `pnpm lint:bundle` enforces that.
4
4
  */
5
5
  export type { LogicalPath, PhysicalPath, SanitizedBranchName, BranchName, ContentId, Slug, PathValidationResult, } from './types.js';
6
- export { normalizeFilesystemPath, normalizeCollectionPath, hasTraversalSequence, createLogicalPath, createPhysicalPath, joinPath, trimSlashes, } from './normalize.js';
6
+ export { normalizeFilesystemPath, normalizeCollectionPath, hasTraversalSequence, createLogicalPath, createPhysicalPath, entryLogicalPath, joinPath, trimSlashes, } from './normalize.js';
7
7
  export { validateAndNormalizePath } from './normalize-server.js';
8
8
  export { validateContentPath, isValidCollectionPath, sanitizeForPath, hasEmbeddedContentId, looksLikePhysicalPath, looksLikeLogicalPath, parseLogicalPath, parsePhysicalPath, isValidContentId, parseContentId, parseBranchName, parseSlug, } from './validation.js';
9
9
  export { resolveLogicalPath } from './resolve.js';
10
- export { resolveBranchPath, ensureBranchRoot, getDefaultBranchBase, resolveBranchPaths, BranchPathError, type BranchPathOptions, type BranchPathResult, } from './branch.js';
11
- export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, } from './branch-name.js';
10
+ export { resolveBranchPath, getDefaultBranchBase, resolveBranchPaths, BranchPathError, namesNoWorkspace, type BranchPathOptions, type BranchPathResult, } from './branch.js';
11
+ export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, isSettingsBranchName, } from './branch-name.js';
@@ -3,10 +3,10 @@
3
3
  * because this barrel pulls in node:fs; `pnpm lint:bundle` enforces that.
4
4
  */
5
5
  // Normalization utilities (client-safe)
6
- export { normalizeFilesystemPath, normalizeCollectionPath, hasTraversalSequence, createLogicalPath, createPhysicalPath, joinPath, trimSlashes, } from './normalize.js';
6
+ export { normalizeFilesystemPath, normalizeCollectionPath, hasTraversalSequence, createLogicalPath, createPhysicalPath, entryLogicalPath, joinPath, trimSlashes, } from './normalize.js';
7
7
  // Normalization utilities (server-only, requires Node.js path module)
8
8
  export { validateAndNormalizePath } from './normalize-server.js';
9
9
  export { validateContentPath, isValidCollectionPath, sanitizeForPath, hasEmbeddedContentId, looksLikePhysicalPath, looksLikeLogicalPath, parseLogicalPath, parsePhysicalPath, isValidContentId, parseContentId, parseBranchName, parseSlug, } from './validation.js';
10
10
  export { resolveLogicalPath } from './resolve.js';
11
- export { resolveBranchPath, ensureBranchRoot, getDefaultBranchBase, resolveBranchPaths, BranchPathError, } from './branch.js';
12
- export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, } from './branch-name.js';
11
+ export { resolveBranchPath, getDefaultBranchBase, resolveBranchPaths, BranchPathError, namesNoWorkspace, } from './branch.js';
12
+ export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, isSettingsBranchName, } from './branch-name.js';
@@ -25,6 +25,13 @@ export declare function normalizeCollectionPath(collectionPath: string, contentR
25
25
  export declare function hasTraversalSequence(path: string): boolean;
26
26
  /** Join segments into a LogicalPath, throwing on a traversal sequence. */
27
27
  export declare function createLogicalPath(...segments: string[]): LogicalPath;
28
+ /**
29
+ * An entry's logical path: `<collection logical path>/<slug>`, e.g. `content/blog/my-post`,
30
+ * or `content/about` for an entry of the root collection. Path-permission rules match this
31
+ * form. No traversal check, unlike `createLogicalPath`: a slug read off disk may contain
32
+ * `..`, and listing must not throw on one.
33
+ */
34
+ export declare function entryLogicalPath(collectionLogicalPath: LogicalPath | string, slug: string): LogicalPath;
28
35
  /**
29
36
  * Join segments into a PhysicalPath (segments may carry embedded content IDs,
30
37
  * e.g. `my-post.ABC123.mdx`), throwing on a traversal sequence.
@@ -48,6 +48,15 @@ export function createLogicalPath(...segments) {
48
48
  }
49
49
  return normalized;
50
50
  }
51
+ /**
52
+ * An entry's logical path: `<collection logical path>/<slug>`, e.g. `content/blog/my-post`,
53
+ * or `content/about` for an entry of the root collection. Path-permission rules match this
54
+ * form. No traversal check, unlike `createLogicalPath`: a slug read off disk may contain
55
+ * `..`, and listing must not throw on one.
56
+ */
57
+ export function entryLogicalPath(collectionLogicalPath, slug) {
58
+ return (collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug);
59
+ }
51
60
  /**
52
61
  * Join segments into a PhysicalPath (segments may carry embedded content IDs,
53
62
  * e.g. `my-post.ABC123.mdx`), throwing on a traversal sequence.
@@ -186,7 +186,6 @@ export function parseSlug(slug) {
186
186
  error: 'Slug is required',
187
187
  };
188
188
  }
189
- // Check length (filesystem path safety)
190
189
  if (slug.length > 64) {
191
190
  return { ok: false, error: 'Slug too long (max 64 characters)' };
192
191
  }
@@ -202,7 +201,6 @@ export function parseSlug(slug) {
202
201
  error: 'Slug cannot be a traversal sequence',
203
202
  };
204
203
  }
205
- // Normalize to lowercase for case-insensitive matching
206
204
  const normalized = slug.toLowerCase();
207
205
  if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
208
206
  return {
@@ -1,30 +1,19 @@
1
1
  import type { ContentStore } from './content-store.js';
2
2
  import type { ContentIdIndex } from './content-id-index.js';
3
- import type { LogicalPath, PhysicalPath, Slug } from './paths/index.js';
4
- export interface ResolvedReference {
5
- id: string;
6
- exists: boolean;
7
- displayValue: string;
8
- collection?: LogicalPath;
9
- slug?: Slug;
10
- }
3
+ import { type LogicalPath } from './paths/index.js';
11
4
  export interface ReferenceOption {
12
5
  id: string;
13
6
  label: string;
14
7
  collection: string;
15
8
  }
16
9
  /**
17
- * ReferenceResolver resolves content IDs to display values for reference fields.
10
+ * Loads the options a reference field's picker offers. Resolving a stored reference to its
11
+ * target is `ContentStore`'s job (`read()`, `resolveReferenceTarget`).
18
12
  */
19
13
  export declare class ReferenceResolver {
20
14
  private store;
21
15
  private idIndex;
22
16
  constructor(store: ContentStore, idIndex: ContentIdIndex);
23
- /**
24
- * Resolve a content ID to a display value.
25
- * Returns null if the ID doesn't exist or points to a collection.
26
- */
27
- resolve(id: string, displayField?: string): Promise<ResolvedReference | null>;
28
17
  /**
29
18
  * Load all available reference options for a reference field.
30
19
  *
@@ -32,14 +21,9 @@ export declare class ReferenceResolver {
32
21
  * At least one of `collections` or `entryTypes` should be provided.
33
22
  *
34
23
  * @param canAccess - Optional permission predicate, called with each candidate's
35
- * relative path before it is read. Returning false skips the entry entirely --
24
+ * logical path (`entryLogicalPath`) before it is read. Returning false skips the entry entirely --
36
25
  * no file I/O, no label, no option -- so a caller who can't read a path never
37
26
  * triggers a read for content they won't be allowed to see anyway.
38
27
  */
39
- loadReferenceOptions(collections?: LogicalPath[], displayField?: string, search?: string, entryTypes?: string[], canAccess?: (relativePath: PhysicalPath) => boolean): Promise<ReferenceOption[]>;
40
- /**
41
- * Resolve multiple IDs at once.
42
- * Useful for displaying lists of referenced items.
43
- */
44
- resolveMany(ids: string[], displayField?: string): Promise<(ResolvedReference | null)[]>;
28
+ loadReferenceOptions(collections?: LogicalPath[], displayField?: string, search?: string, entryTypes?: string[], canAccess?: (logicalPath: LogicalPath) => boolean): Promise<ReferenceOption[]>;
45
29
  }