canopycms 0.0.61 → 0.0.63

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 (237) hide show
  1. package/dist/ai/index.d.ts +9 -0
  2. package/dist/ai/index.d.ts.map +1 -1
  3. package/dist/ai/index.js +9 -0
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/json-to-markdown.js +1 -2
  6. package/dist/ai/json-to-markdown.js.map +1 -1
  7. package/dist/ai/to-plain-text.d.ts +57 -0
  8. package/dist/ai/to-plain-text.d.ts.map +1 -0
  9. package/dist/ai/to-plain-text.js +255 -0
  10. package/dist/ai/to-plain-text.js.map +1 -0
  11. package/dist/ai/transform-components.d.ts.map +1 -1
  12. package/dist/ai/transform-components.js +70 -5
  13. package/dist/ai/transform-components.js.map +1 -1
  14. package/dist/api/__test__/mock-client.d.ts +5 -1
  15. package/dist/api/__test__/mock-client.d.ts.map +1 -1
  16. package/dist/api/__test__/mock-client.js +9 -2
  17. package/dist/api/__test__/mock-client.js.map +1 -1
  18. package/dist/api/admin-branch-health.d.ts +36 -1
  19. package/dist/api/admin-branch-health.d.ts.map +1 -1
  20. package/dist/api/admin-branch-health.js +176 -2
  21. package/dist/api/admin-branch-health.js.map +1 -1
  22. package/dist/api/admin.d.ts +8 -1
  23. package/dist/api/admin.d.ts.map +1 -1
  24. package/dist/api/admin.js.map +1 -1
  25. package/dist/api/branch.d.ts.map +1 -1
  26. package/dist/api/branch.js +16 -2
  27. package/dist/api/branch.js.map +1 -1
  28. package/dist/api/client.d.ts +5 -1
  29. package/dist/api/client.d.ts.map +1 -1
  30. package/dist/api/client.js +6 -0
  31. package/dist/api/client.js.map +1 -1
  32. package/dist/api/content.d.ts +11 -5
  33. package/dist/api/content.d.ts.map +1 -1
  34. package/dist/api/content.js +79 -6
  35. package/dist/api/content.js.map +1 -1
  36. package/dist/api/entries.d.ts +10 -0
  37. package/dist/api/entries.d.ts.map +1 -1
  38. package/dist/api/entries.js +40 -15
  39. package/dist/api/entries.js.map +1 -1
  40. package/dist/api/schema.d.ts +4 -4
  41. package/dist/api/schema.d.ts.map +1 -1
  42. package/dist/api/schema.js +13 -13
  43. package/dist/api/schema.js.map +1 -1
  44. package/dist/assets/transform.d.ts.map +1 -1
  45. package/dist/assets/transform.js +19 -6
  46. package/dist/assets/transform.js.map +1 -1
  47. package/dist/authorization/branch.d.ts +55 -3
  48. package/dist/authorization/branch.d.ts.map +1 -1
  49. package/dist/authorization/branch.js +55 -9
  50. package/dist/authorization/branch.js.map +1 -1
  51. package/dist/authorization/types.d.ts +1 -1
  52. package/dist/authorization/types.d.ts.map +1 -1
  53. package/dist/branch-health.d.ts +13 -0
  54. package/dist/branch-health.d.ts.map +1 -1
  55. package/dist/branch-health.js +23 -0
  56. package/dist/branch-health.js.map +1 -1
  57. package/dist/build-canopy.d.ts +49 -0
  58. package/dist/build-canopy.d.ts.map +1 -0
  59. package/dist/build-canopy.js +51 -0
  60. package/dist/build-canopy.js.map +1 -0
  61. package/dist/cli/cli.js +935 -562
  62. package/dist/cli/generate-ai-content.js +837 -480
  63. package/dist/cli/init.js +25 -7
  64. package/dist/cli/template-files/Dockerfile.cms.template +19 -1
  65. package/dist/cli/template-files/canopycms.config.ts.template +25 -1
  66. package/dist/cli/template-files/cms-stack.ts.template +7 -1
  67. package/dist/client.d.ts +1 -0
  68. package/dist/client.d.ts.map +1 -1
  69. package/dist/client.js.map +1 -1
  70. package/dist/config/schemas/field.d.ts +15 -15
  71. package/dist/config/types.d.ts +2 -2
  72. package/dist/config/types.d.ts.map +1 -1
  73. package/dist/config/types.js +0 -1
  74. package/dist/config/types.js.map +1 -1
  75. package/dist/config/validation.d.ts +9 -0
  76. package/dist/config/validation.d.ts.map +1 -1
  77. package/dist/config/validation.js +14 -0
  78. package/dist/config/validation.js.map +1 -1
  79. package/dist/content-id-index.d.ts +97 -5
  80. package/dist/content-id-index.d.ts.map +1 -1
  81. package/dist/content-id-index.js +170 -18
  82. package/dist/content-id-index.js.map +1 -1
  83. package/dist/content-listing.d.ts +110 -5
  84. package/dist/content-listing.d.ts.map +1 -1
  85. package/dist/content-listing.js +164 -23
  86. package/dist/content-listing.js.map +1 -1
  87. package/dist/content-reader.d.ts +41 -4
  88. package/dist/content-reader.d.ts.map +1 -1
  89. package/dist/content-reader.js +17 -4
  90. package/dist/content-reader.js.map +1 -1
  91. package/dist/content-store.d.ts +91 -8
  92. package/dist/content-store.d.ts.map +1 -1
  93. package/dist/content-store.js +443 -258
  94. package/dist/content-store.js.map +1 -1
  95. package/dist/content-tree.d.ts +31 -2
  96. package/dist/content-tree.d.ts.map +1 -1
  97. package/dist/content-tree.js +22 -5
  98. package/dist/content-tree.js.map +1 -1
  99. package/dist/context.d.ts +30 -10
  100. package/dist/context.d.ts.map +1 -1
  101. package/dist/context.js +55 -4
  102. package/dist/context.js.map +1 -1
  103. package/dist/editor/BranchManager.d.ts.map +1 -1
  104. package/dist/editor/BranchManager.js +18 -6
  105. package/dist/editor/BranchManager.js.map +1 -1
  106. package/dist/editor/CanopyEditor.d.ts.map +1 -1
  107. package/dist/editor/CanopyEditor.js +2 -2
  108. package/dist/editor/CanopyEditor.js.map +1 -1
  109. package/dist/editor/CanopyEditorPage.d.ts +9 -1
  110. package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
  111. package/dist/editor/CanopyEditorPage.js +9 -2
  112. package/dist/editor/CanopyEditorPage.js.map +1 -1
  113. package/dist/editor/Editor.d.ts +9 -1
  114. package/dist/editor/Editor.d.ts.map +1 -1
  115. package/dist/editor/Editor.js +58 -24
  116. package/dist/editor/Editor.js.map +1 -1
  117. package/dist/editor/FormRenderer.d.ts.map +1 -1
  118. package/dist/editor/FormRenderer.js +12 -0
  119. package/dist/editor/FormRenderer.js.map +1 -1
  120. package/dist/editor/admin/SystemHealthPanel.js +6 -2
  121. package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
  122. package/dist/editor/components/EntryCreateModal.d.ts +9 -1
  123. package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
  124. package/dist/editor/components/EntryCreateModal.js +4 -1
  125. package/dist/editor/components/EntryCreateModal.js.map +1 -1
  126. package/dist/editor/fields/DateTimeField.d.ts +46 -0
  127. package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
  128. package/dist/editor/fields/DateTimeField.js +66 -0
  129. package/dist/editor/fields/DateTimeField.js.map +1 -0
  130. package/dist/editor/fields/NumberField.d.ts +34 -0
  131. package/dist/editor/fields/NumberField.d.ts.map +1 -0
  132. package/dist/editor/fields/NumberField.js +53 -0
  133. package/dist/editor/fields/NumberField.js.map +1 -0
  134. package/dist/editor/fields/NumberListField.d.ts +25 -0
  135. package/dist/editor/fields/NumberListField.d.ts.map +1 -0
  136. package/dist/editor/fields/NumberListField.js +53 -0
  137. package/dist/editor/fields/NumberListField.js.map +1 -0
  138. package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
  139. package/dist/editor/hooks/useBranchManager.js +115 -26
  140. package/dist/editor/hooks/useBranchManager.js.map +1 -1
  141. package/dist/editor/hooks/useDraftManager.d.ts +8 -0
  142. package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
  143. package/dist/editor/hooks/useDraftManager.js +261 -21
  144. package/dist/editor/hooks/useDraftManager.js.map +1 -1
  145. package/dist/editor/hooks/useEntryManager.d.ts +21 -0
  146. package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
  147. package/dist/editor/hooks/useEntryManager.js +25 -2
  148. package/dist/editor/hooks/useEntryManager.js.map +1 -1
  149. package/dist/editor/theme.d.ts +8 -8
  150. package/dist/entry-schema.d.ts +228 -8
  151. package/dist/entry-schema.d.ts.map +1 -1
  152. package/dist/entry-schema.js +124 -0
  153. package/dist/entry-schema.js.map +1 -1
  154. package/dist/git-manager.d.ts +51 -7
  155. package/dist/git-manager.d.ts.map +1 -1
  156. package/dist/git-manager.js +105 -14
  157. package/dist/git-manager.js.map +1 -1
  158. package/dist/http/handler.d.ts.map +1 -1
  159. package/dist/http/handler.js +70 -27
  160. package/dist/http/handler.js.map +1 -1
  161. package/dist/http/router.d.ts.map +1 -1
  162. package/dist/http/router.js +40 -3
  163. package/dist/http/router.js.map +1 -1
  164. package/dist/index.d.ts +1 -0
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +7 -0
  167. package/dist/index.js.map +1 -1
  168. package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
  169. package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
  170. package/dist/operating-mode/deployment-name-fixtures.js +55 -0
  171. package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
  172. package/dist/operating-mode/deployment-name.d.ts +9 -0
  173. package/dist/operating-mode/deployment-name.d.ts.map +1 -1
  174. package/dist/operating-mode/deployment-name.js +9 -1
  175. package/dist/operating-mode/deployment-name.js.map +1 -1
  176. package/dist/operating-mode/index.d.ts +2 -1
  177. package/dist/operating-mode/index.d.ts.map +1 -1
  178. package/dist/operating-mode/index.js +4 -1
  179. package/dist/operating-mode/index.js.map +1 -1
  180. package/dist/operating-mode/mode-env.d.ts +58 -0
  181. package/dist/operating-mode/mode-env.d.ts.map +1 -0
  182. package/dist/operating-mode/mode-env.js +97 -0
  183. package/dist/operating-mode/mode-env.js.map +1 -0
  184. package/dist/resolve-canopy-user.d.ts +34 -0
  185. package/dist/resolve-canopy-user.d.ts.map +1 -0
  186. package/dist/resolve-canopy-user.js +55 -0
  187. package/dist/resolve-canopy-user.js.map +1 -0
  188. package/dist/schema/schema-store.d.ts +53 -2
  189. package/dist/schema/schema-store.d.ts.map +1 -1
  190. package/dist/schema/schema-store.js +92 -21
  191. package/dist/schema/schema-store.js.map +1 -1
  192. package/dist/server.d.ts +60 -0
  193. package/dist/server.d.ts.map +1 -1
  194. package/dist/server.js +58 -0
  195. package/dist/server.js.map +1 -1
  196. package/dist/services.d.ts +1 -2
  197. package/dist/services.d.ts.map +1 -1
  198. package/dist/services.js +42 -12
  199. package/dist/services.js.map +1 -1
  200. package/dist/settings-workspace.d.ts +40 -0
  201. package/dist/settings-workspace.d.ts.map +1 -1
  202. package/dist/settings-workspace.js +139 -151
  203. package/dist/settings-workspace.js.map +1 -1
  204. package/dist/static/index.d.ts +65 -5
  205. package/dist/static/index.d.ts.map +1 -1
  206. package/dist/static/index.js +65 -12
  207. package/dist/static/index.js.map +1 -1
  208. package/dist/static/seo.d.ts +150 -0
  209. package/dist/static/seo.d.ts.map +1 -0
  210. package/dist/static/seo.js +189 -0
  211. package/dist/static/seo.js.map +1 -0
  212. package/dist/types.d.ts +10 -0
  213. package/dist/types.d.ts.map +1 -1
  214. package/dist/utils/content-write-lock.d.ts +127 -0
  215. package/dist/utils/content-write-lock.d.ts.map +1 -0
  216. package/dist/utils/content-write-lock.js +170 -0
  217. package/dist/utils/content-write-lock.js.map +1 -0
  218. package/dist/utils/error.d.ts +1 -1
  219. package/dist/utils/error.js +1 -1
  220. package/dist/utils/git.d.ts +16 -0
  221. package/dist/utils/git.d.ts.map +1 -1
  222. package/dist/utils/git.js +22 -0
  223. package/dist/utils/git.js.map +1 -1
  224. package/dist/utils/sanitize-href.d.ts +74 -3
  225. package/dist/utils/sanitize-href.d.ts.map +1 -1
  226. package/dist/utils/sanitize-href.js +125 -6
  227. package/dist/utils/sanitize-href.js.map +1 -1
  228. package/dist/validation/entry-link-validator.d.ts +1 -1
  229. package/dist/validation/entry-link-validator.js +3 -3
  230. package/dist/validation/entry-link-validator.js.map +1 -1
  231. package/dist/validation/entry-validator.js +1 -1
  232. package/dist/validation/entry-validator.js.map +1 -1
  233. package/dist/worker/cms-worker.d.ts +13 -0
  234. package/dist/worker/cms-worker.d.ts.map +1 -1
  235. package/dist/worker/cms-worker.js +326 -264
  236. package/dist/worker/cms-worker.js.map +1 -1
  237. package/package.json +2 -2
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Single resolution point for the operating `mode`.
3
+ *
4
+ * `mode` is required in `canopycms.config.ts` (SEC-C1) and that file lives in
5
+ * the adopter's repo — the SAME file is loaded by `next dev` locally, by
6
+ * `next build` inside the image builder, and by the deployed Lambda. Those
7
+ * three want different answers, so a literal in the config file cannot be
8
+ * right for all of them:
9
+ *
10
+ * - `next dev` must be `dev` (workspace at `<cwd>/.canopy-dev`).
11
+ * - `next build` must be `dev` too: prod-mode build reads would try to open
12
+ * a branch workspace on EFS that cannot exist in an image builder (see
13
+ * the note in `cli/template-files/Dockerfile.cms.template`).
14
+ * - The deployed Lambda must be `prod`: dev mode resolves the workspace to
15
+ * `<cwd>/.canopy-dev`, and Lambda's filesystem is read-only outside /tmp,
16
+ * so the first write fails with EROFS.
17
+ *
18
+ * Only a value resolved at RUN time can satisfy all three, which is why this
19
+ * is an environment override rather than a flag baked into the generated
20
+ * config file. `CANOPY_MODE` is the name the deployment templates already
21
+ * referred to; before this module existed nothing read it.
22
+ *
23
+ * Precedence (highest wins):
24
+ * 1. the environment value for the current runtime (see `readModeEnv`)
25
+ * 2. `config.mode` (the literal in `canopycms.config.ts`)
26
+ *
27
+ * ## Two variable names, on purpose
28
+ *
29
+ * Server code reads `CANOPY_MODE`, which the CDK construct stamps onto the
30
+ * Lambda (`CanopyCmsService` sets `CANOPY_MODE=prod`). It is deliberately NOT
31
+ * set during `next build`, so build-time content reads stay in dev mode.
32
+ *
33
+ * Browser code cannot read a runtime environment at all — the editor page
34
+ * imports `canopycms.config.ts` directly (`config.client()`), so the browser's
35
+ * copy of `mode` is whatever was inlined when the bundle was built. Next.js
36
+ * only inlines `NEXT_PUBLIC_*`, so the browser reads
37
+ * `NEXT_PUBLIC_CANOPY_MODE`, which the generated CDK stack passes as a Docker
38
+ * build arg. Both names must be spelled as literal `process.env.X` member
39
+ * expressions here or the bundler cannot substitute them.
40
+ *
41
+ * An unrecognized value throws rather than falling back. Falling back would
42
+ * turn a typo (`CANOPY_MODE=production`) into a silent dev-mode deployment
43
+ * running header-trusting dev auth semantics — the exact failure SEC-C1 made
44
+ * `mode` required to prevent.
45
+ */
46
+ import { canopyLogWarn } from '../utils/logger.js';
47
+ export const SERVER_MODE_ENV_VAR = 'CANOPY_MODE';
48
+ export const BROWSER_MODE_ENV_VAR = 'NEXT_PUBLIC_CANOPY_MODE';
49
+ let warned = false;
50
+ /** Reset the once-per-process warning latch. Test-only. */
51
+ export function resetModeWarning() {
52
+ warned = false;
53
+ }
54
+ /**
55
+ * The environment value for the current runtime, or undefined when unset.
56
+ *
57
+ * Guarded with `typeof process` because a non-Next bundler may leave no
58
+ * `process` shim in the browser at all; the member expressions themselves stay
59
+ * literal so Next's DefinePlugin substitution still applies.
60
+ */
61
+ function readModeEnv() {
62
+ if (typeof process === 'undefined' || typeof process.env === 'undefined')
63
+ return undefined;
64
+ const inBrowser = typeof window !== 'undefined';
65
+ const name = inBrowser ? BROWSER_MODE_ENV_VAR : SERVER_MODE_ENV_VAR;
66
+ const raw = inBrowser ? process.env.NEXT_PUBLIC_CANOPY_MODE : process.env.CANOPY_MODE;
67
+ const value = raw?.trim();
68
+ return value ? { name, value } : undefined;
69
+ }
70
+ /**
71
+ * Resolve the effective operating mode from the environment and the config
72
+ * literal. Called from `validateCanopyConfig`, the one point every documented
73
+ * config-authoring path (`defineCanopyConfig`, `composeCanopyConfig`) funnels
74
+ * through.
75
+ */
76
+ export function resolveOperatingMode(configMode) {
77
+ const env = readModeEnv();
78
+ if (!env)
79
+ return configMode;
80
+ if (env.value !== 'prod' && env.value !== 'dev') {
81
+ throw new Error(`CanopyCMS: invalid ${env.name}=${JSON.stringify(env.value)}. ` +
82
+ `It must be exactly "prod" or "dev". Refusing to fall back to the configured mode ` +
83
+ `(${JSON.stringify(configMode)}): a typo here would silently deploy dev auth semantics.`);
84
+ }
85
+ if (env.value !== configMode && !warned) {
86
+ // canopyLogWarn, not console.warn: shared modules can run inside the
87
+ // worker daemon, where an unprefixed line is folded into the previous
88
+ // CloudWatch event (see utils/logger.ts).
89
+ canopyLogWarn(`CanopyCMS: ${env.name}="${env.value}" overrides config.mode="${configMode}". ` +
90
+ `The environment wins by design — it is the per-deployment value, while ` +
91
+ `canopycms.config.ts is shared by local dev, the image build and the deployment. ` +
92
+ `See operating-mode/mode-env.ts.`);
93
+ warned = true;
94
+ }
95
+ return env.value;
96
+ }
97
+ //# sourceMappingURL=mode-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode-env.js","sourceRoot":"","sources":["../../src/operating-mode/mode-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAG/C,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAA;AAChD,MAAM,CAAC,MAAM,oBAAoB,GAAG,yBAAyB,CAAA;AAE7D,IAAI,MAAM,GAAG,KAAK,CAAA;AAElB,2DAA2D;AAC3D,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,KAAK,CAAA;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW;IAClB,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,WAAW;QAAE,OAAO,SAAS,CAAA;IAC1F,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,WAAW,CAAA;IAC/C,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,mBAAmB,CAAA;IACnE,MAAM,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAA;IACrF,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,EAAE,CAAA;IACzB,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAyB;IAC5D,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;IACzB,IAAI,CAAC,GAAG;QAAE,OAAO,UAAU,CAAA;IAE3B,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CACb,sBAAsB,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI;YAC7D,mFAAmF;YACnF,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,0DAA0D,CAC3F,CAAA;IACH,CAAC;IAED,IAAI,GAAG,CAAC,KAAK,KAAK,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC;QACxC,qEAAqE;QACrE,sEAAsE;QACtE,0CAA0C;QAC1C,aAAa,CACX,cAAc,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,KAAK,4BAA4B,UAAU,KAAK;YAC7E,yEAAyE;YACzE,kFAAkF;YAClF,iCAAiC,CACpC,CAAA;QACD,MAAM,GAAG,IAAI,CAAA;IACf,CAAC;IAED,OAAO,GAAG,CAAC,KAAK,CAAA;AAClB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { AuthenticationResult } from './auth/types';
2
+ import type { CanopyUser } from './user';
3
+ import type { OperatingMode } from './operating-mode';
4
+ /** Test-only: reset the once-per-process "no admins configured" warning latch. */
5
+ export declare function resetResolveCanopyUserWarningForTests(): void;
6
+ export interface ResolveCanopyUserDeps {
7
+ /**
8
+ * Resolves (and ensures) the settings workspace root. Internal groups are
9
+ * the single source of truth for group-based privileges and MUST be
10
+ * loaded from here — the same root `createContentAccessChecker`
11
+ * (authorization/content.ts) uses for path permissions — never from a
12
+ * content branch clone.
13
+ *
14
+ * Must THROW if the settings workspace cannot be ensured, matching
15
+ * `createContentAccessChecker`'s fail-loud contract: a caught error here
16
+ * and a silent fallback to an empty group list would read as "no
17
+ * privileges", which is a silent authorization change, not a safe
18
+ * degradation.
19
+ */
20
+ getSettingsBranchRoot: () => Promise<string>;
21
+ mode: OperatingMode;
22
+ bootstrapAdminIds: Set<string>;
23
+ }
24
+ /**
25
+ * Resolve the CanopyUser for a request: load internal groups from the
26
+ * settings workspace and merge them (plus bootstrap admins) into an
27
+ * authentication result via `authResultToCanopyUser`.
28
+ *
29
+ * Framework-agnostic: callers supply the already-computed `authResult` (from
30
+ * whatever auth-plugin invocation their transport uses) and a small deps
31
+ * bag drawn from `CanopyServices`.
32
+ */
33
+ export declare function resolveCanopyUser(authResult: AuthenticationResult, deps: ResolveCanopyUserDeps): Promise<CanopyUser>;
34
+ //# sourceMappingURL=resolve-canopy-user.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-canopy-user.d.ts","sourceRoot":"","sources":["../src/resolve-canopy-user.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAGxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAKrD,kFAAkF;AAClF,wBAAgB,qCAAqC,IAAI,IAAI,CAE5D;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;;OAYG;IACH,qBAAqB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5C,IAAI,EAAE,aAAa,CAAA;IACnB,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC/B;AAcD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAOrB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Shared "authenticate -> resolve internal groups -> merge into CanopyUser"
3
+ * pipeline.
4
+ *
5
+ * This used to be duplicated between `http/handler.ts` (the core API
6
+ * handler) and `canopycms-next`'s `context-wrapper.ts` (Next.js SSR user
7
+ * extraction) - including a module-level `warnedNoAdmins` flag in each copy.
8
+ * The duplication let the two drift: `http/handler.ts` loaded internal
9
+ * groups from the BASE BRANCH content clone's `groups.json`, which nothing
10
+ * in the product ever writes (writes go to the settings workspace via
11
+ * `mutateGroupsFile` - `api/groups.ts`), so group-based privileges never
12
+ * actually took effect. Consolidating the pipeline here means both callers
13
+ * read from the same place and cannot silently diverge again.
14
+ *
15
+ * canopyLogWarn, not console.warn: this module has no worker-specific logic,
16
+ * but nothing prevents it from becoming reachable from the worker's runtime
17
+ * import closure later, and the existing "no admins configured" warning is
18
+ * exactly the kind of operationally interesting line that must not be
19
+ * silently folded into an unrelated worker.log event (see utils/logger.ts).
20
+ */
21
+ import { canopyLogWarn } from './utils/logger.js';
22
+ import { authResultToCanopyUser } from './user.js';
23
+ import { loadInternalGroups, RESERVED_GROUPS } from './authorization/index.js';
24
+ /** Module-level, matching the pre-extraction behavior: warn at most once per process. */
25
+ let warnedNoAdmins = false;
26
+ /** Test-only: reset the once-per-process "no admins configured" warning latch. */
27
+ export function resetResolveCanopyUserWarningForTests() {
28
+ warnedNoAdmins = false;
29
+ }
30
+ function warnIfNoAdmins(internalGroups, bootstrapAdminIds) {
31
+ if (warnedNoAdmins)
32
+ return;
33
+ const adminsGroup = internalGroups.find((g) => g.id === RESERVED_GROUPS.ADMINS);
34
+ const hasAdmins = (adminsGroup?.members.length ?? 0) > 0 || bootstrapAdminIds.size > 0;
35
+ if (!hasAdmins) {
36
+ canopyLogWarn('CanopyCMS: No admin users configured. Set CANOPY_BOOTSTRAP_ADMIN_IDS or add members to the Admins group.');
37
+ }
38
+ warnedNoAdmins = true;
39
+ }
40
+ /**
41
+ * Resolve the CanopyUser for a request: load internal groups from the
42
+ * settings workspace and merge them (plus bootstrap admins) into an
43
+ * authentication result via `authResultToCanopyUser`.
44
+ *
45
+ * Framework-agnostic: callers supply the already-computed `authResult` (from
46
+ * whatever auth-plugin invocation their transport uses) and a small deps
47
+ * bag drawn from `CanopyServices`.
48
+ */
49
+ export async function resolveCanopyUser(authResult, deps) {
50
+ const settingsRoot = await deps.getSettingsBranchRoot();
51
+ const internalGroups = await loadInternalGroups(settingsRoot, deps.mode, deps.bootstrapAdminIds);
52
+ warnIfNoAdmins(internalGroups, deps.bootstrapAdminIds);
53
+ return authResultToCanopyUser(authResult, deps.bootstrapAdminIds, internalGroups);
54
+ }
55
+ //# sourceMappingURL=resolve-canopy-user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-canopy-user.js","sourceRoot":"","sources":["../src/resolve-canopy-user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAG9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAsB,MAAM,iBAAiB,CAAA;AAGzF,yFAAyF;AACzF,IAAI,cAAc,GAAG,KAAK,CAAA;AAE1B,kFAAkF;AAClF,MAAM,UAAU,qCAAqC;IACnD,cAAc,GAAG,KAAK,CAAA;AACxB,CAAC;AAqBD,SAAS,cAAc,CAAC,cAA+B,EAAE,iBAA8B;IACrF,IAAI,cAAc;QAAE,OAAM;IAC1B,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,eAAe,CAAC,MAAM,CAAC,CAAA;IAC/E,MAAM,SAAS,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,iBAAiB,CAAC,IAAI,GAAG,CAAC,CAAA;IACtF,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,aAAa,CACX,0GAA0G,CAC3G,CAAA;IACH,CAAC;IACD,cAAc,GAAG,IAAI,CAAA;AACvB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,UAAgC,EAChC,IAA2B;IAE3B,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAA;IACvD,MAAM,cAAc,GAAG,MAAM,kBAAkB,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;IAEhG,cAAc,CAAC,cAAc,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;IAEtD,OAAO,sBAAsB,CAAC,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAA;AACnF,CAAC"}
@@ -130,6 +130,7 @@ declare const createCollectionInputSchema: z.ZodObject<{
130
130
  maxItems?: number | undefined;
131
131
  }>, "many">;
132
132
  }, "strip", z.ZodTypeAny, {
133
+ name: string;
133
134
  entries: {
134
135
  name: string;
135
136
  format: "mdx" | "md" | "json" | "yaml";
@@ -138,10 +139,10 @@ declare const createCollectionInputSchema: z.ZodObject<{
138
139
  label?: string | undefined;
139
140
  maxItems?: number | undefined;
140
141
  }[];
141
- name: string;
142
142
  label?: string | undefined;
143
143
  parentPath?: string | undefined;
144
144
  }, {
145
+ name: string;
145
146
  entries: {
146
147
  name: string;
147
148
  format: "mdx" | "md" | "json" | "yaml";
@@ -150,7 +151,6 @@ declare const createCollectionInputSchema: z.ZodObject<{
150
151
  label?: string | undefined;
151
152
  maxItems?: number | undefined;
152
153
  }[];
153
- name: string;
154
154
  label?: string | undefined;
155
155
  parentPath?: string | undefined;
156
156
  }>;
@@ -294,6 +294,51 @@ export declare class SchemaOps {
294
294
  * bulk-mutation call sites that use the combined helper.
295
295
  */
296
296
  private invalidateContentIdIndexes;
297
+ /**
298
+ * The single normalisation boundary for logical collection paths entering
299
+ * this class. Every public method that accepts a logical collection path
300
+ * (or, for `createCollection`, `input.parentPath`) calls this exactly once
301
+ * before doing anything else with the path — that is the whole fix for the
302
+ * "Collection not found" bug: `flattenSchema` (branch-schema-cache.ts)
303
+ * produces content-root-prefixed logical paths (e.g. "content/posts", or
304
+ * "cms/content/posts" for a multi-segment `contentRoot: 'cms/content'`),
305
+ * the editor round-trips those straight back into every mutator
306
+ * (`CollectionEditor.tsx` passes `editingCollection.logicalPath` as-is),
307
+ * but `resolveCollectionPath(this.contentRoot, ...)` treats its second
308
+ * argument as relative to `this.contentRoot` — which already embeds the
309
+ * content-root segment(s). A prefixed path therefore resolved one level
310
+ * too deep and was reported as not found. `updateCollectionInner` used to
311
+ * strip the prefix itself (the only mutator that did); that bespoke strip
312
+ * is gone now that every entry point normalises here instead.
313
+ *
314
+ * Prefix-only: strips ONE leading `"{contentRootName}/"` if present (exact
315
+ * string match against the full, possibly multi-segment, `contentRootName`
316
+ * — never `path.basename()`, which would break a multi-segment root like
317
+ * "cms/content"), otherwise returns the path unchanged. So both prefixed
318
+ * (production/editor) and unprefixed (existing store tests, adopters
319
+ * calling SchemaOps directly) input work. Deliberately does NOT touch the
320
+ * bare root-collection sentinel (`collectionPath === this.contentRootName`,
321
+ * no trailing segment): that has no trailing "/" to match, so it passes
322
+ * through unchanged and the `=== this.contentRootName` checks in
323
+ * `updateCollectionInner`/`updateOrderInner` keep working.
324
+ *
325
+ * CALL EXACTLY ONCE PER ENTRY POINT — it is NOT idempotent, and an earlier
326
+ * version of this comment claimed it was. A sub-collection literally named
327
+ * after the content root defeats the "already normalised" reasoning: with
328
+ * `contentRoot: 'content'`, "content/content/x" normalises to "content/x"
329
+ * and again to "x", and "content/content" normalises to "content", which
330
+ * the `=== this.contentRootName` checks above then treat as the ROOT
331
+ * collection — so a second call mutates a DIFFERENT collection rather than
332
+ * reporting not-found. Every entry point calls this exactly once today, so
333
+ * the ambiguity is unreachable; removing it needs the editor to send an
334
+ * unambiguously-scoped path, tracked in
335
+ * .claude/future-tasks/collection-path-content-root-ambiguity.md.
336
+ *
337
+ * A future public method that accepts a logical collection path must call
338
+ * this first, and only once, the same way every existing one does — that is
339
+ * the "safe by construction" contract this boundary is meant to provide.
340
+ */
341
+ private normalizeCollectionPath;
297
342
  /**
298
343
  * Validate that a schema reference exists in the registry
299
344
  */
@@ -343,6 +388,12 @@ export declare class SchemaOps {
343
388
  * read-modify-write. Called directly (not via the public `updateCollection`)
344
389
  * by `updateOrderInner` for non-root collections — `withSchemaLock` is NOT
345
390
  * re-entrant, so going through the public method there would deadlock.
391
+ *
392
+ * `collectionPath` here is always already normalized (content-root prefix
393
+ * stripped, if it had one) by whichever public method reached this — either
394
+ * `updateCollection` above or `updateOrder` via `updateOrderInner` — so this
395
+ * no longer re-strips the prefix itself; see normalizeCollectionPath's doc
396
+ * comment for the single boundary that owns that now.
346
397
  */
347
398
  private updateCollectionInner;
348
399
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"schema-store.d.ts","sourceRoot":"","sources":["../../src/schema/schema-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema-store.d.ts","sourceRoot":"","sources":["../../src/schema/schema-store.ts"],"names":[],"mappings":"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"}
@@ -54,7 +54,7 @@ import { withOccFileLock, OccWriteConflictError } from '../utils/occ-json-write.
54
54
  import { resolveCollectionPath } from '../content-id-index.js';
55
55
  import { invalidateBranchContentCaches } from '../content-index-generation.js';
56
56
  import { generateId, isValidId } from '../id.js';
57
- import { createLogicalPath, validateAndNormalizePath } from '../paths/index.js';
57
+ import { createLogicalPath, normalizeCollectionPath as stripContentRootPrefix, validateAndNormalizePath, } from '../paths/index.js';
58
58
  // ============================================================================
59
59
  // Zod Schemas for Validation
60
60
  // ============================================================================
@@ -270,6 +270,56 @@ export class SchemaOps {
270
270
  await invalidateBranchContentCaches(this.branchRoot);
271
271
  }
272
272
  // --------------------------------------------------------------------------
273
+ // Path Normalization
274
+ // --------------------------------------------------------------------------
275
+ /**
276
+ * The single normalisation boundary for logical collection paths entering
277
+ * this class. Every public method that accepts a logical collection path
278
+ * (or, for `createCollection`, `input.parentPath`) calls this exactly once
279
+ * before doing anything else with the path — that is the whole fix for the
280
+ * "Collection not found" bug: `flattenSchema` (branch-schema-cache.ts)
281
+ * produces content-root-prefixed logical paths (e.g. "content/posts", or
282
+ * "cms/content/posts" for a multi-segment `contentRoot: 'cms/content'`),
283
+ * the editor round-trips those straight back into every mutator
284
+ * (`CollectionEditor.tsx` passes `editingCollection.logicalPath` as-is),
285
+ * but `resolveCollectionPath(this.contentRoot, ...)` treats its second
286
+ * argument as relative to `this.contentRoot` — which already embeds the
287
+ * content-root segment(s). A prefixed path therefore resolved one level
288
+ * too deep and was reported as not found. `updateCollectionInner` used to
289
+ * strip the prefix itself (the only mutator that did); that bespoke strip
290
+ * is gone now that every entry point normalises here instead.
291
+ *
292
+ * Prefix-only: strips ONE leading `"{contentRootName}/"` if present (exact
293
+ * string match against the full, possibly multi-segment, `contentRootName`
294
+ * — never `path.basename()`, which would break a multi-segment root like
295
+ * "cms/content"), otherwise returns the path unchanged. So both prefixed
296
+ * (production/editor) and unprefixed (existing store tests, adopters
297
+ * calling SchemaOps directly) input work. Deliberately does NOT touch the
298
+ * bare root-collection sentinel (`collectionPath === this.contentRootName`,
299
+ * no trailing segment): that has no trailing "/" to match, so it passes
300
+ * through unchanged and the `=== this.contentRootName` checks in
301
+ * `updateCollectionInner`/`updateOrderInner` keep working.
302
+ *
303
+ * CALL EXACTLY ONCE PER ENTRY POINT — it is NOT idempotent, and an earlier
304
+ * version of this comment claimed it was. A sub-collection literally named
305
+ * after the content root defeats the "already normalised" reasoning: with
306
+ * `contentRoot: 'content'`, "content/content/x" normalises to "content/x"
307
+ * and again to "x", and "content/content" normalises to "content", which
308
+ * the `=== this.contentRootName` checks above then treat as the ROOT
309
+ * collection — so a second call mutates a DIFFERENT collection rather than
310
+ * reporting not-found. Every entry point calls this exactly once today, so
311
+ * the ambiguity is unreachable; removing it needs the editor to send an
312
+ * unambiguously-scoped path, tracked in
313
+ * .claude/future-tasks/collection-path-content-root-ambiguity.md.
314
+ *
315
+ * A future public method that accepts a logical collection path must call
316
+ * this first, and only once, the same way every existing one does — that is
317
+ * the "safe by construction" contract this boundary is meant to provide.
318
+ */
319
+ normalizeCollectionPath(collectionPath) {
320
+ return createLogicalPath(stripContentRootPrefix(collectionPath, this.contentRootName));
321
+ }
322
+ // --------------------------------------------------------------------------
273
323
  // Validation Helpers
274
324
  // --------------------------------------------------------------------------
275
325
  /**
@@ -310,8 +360,9 @@ export class SchemaOps {
310
360
  * Read a collection's .collection.json file
311
361
  */
312
362
  async readCollectionMeta(collectionPath) {
363
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
313
364
  // Resolve logical path to physical path with embedded IDs
314
- const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
365
+ const physicalPath = await resolveCollectionPath(this.contentRoot, normalizedPath);
315
366
  if (!physicalPath) {
316
367
  return null;
317
368
  }
@@ -347,7 +398,8 @@ export class SchemaOps {
347
398
  * Check if a collection is empty (has no content files or child collections)
348
399
  */
349
400
  async isCollectionEmpty(collectionPath) {
350
- const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
401
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
402
+ const physicalPath = await resolveCollectionPath(this.contentRoot, normalizedPath);
351
403
  if (!physicalPath) {
352
404
  // Collection doesn't exist, consider it empty
353
405
  return true;
@@ -422,7 +474,15 @@ export class SchemaOps {
422
474
  if (!schemaValidation.valid) {
423
475
  throw new Error(schemaValidation.error);
424
476
  }
425
- const result = await this.withSchemaLock(() => this.createCollectionInner(input));
477
+ // Normalize parentPath (may be content-root-prefixed, e.g. "content/docs",
478
+ // exactly like every other logical collection path the editor sends) —
479
+ // see normalizeCollectionPath's doc comment for why this must happen
480
+ // exactly once, here at the public entry point, before createCollectionInner
481
+ // ever resolves it.
482
+ const normalizedInput = input.parentPath
483
+ ? { ...input, parentPath: this.normalizeCollectionPath(input.parentPath) }
484
+ : input;
485
+ const result = await this.withSchemaLock(() => this.createCollectionInner(normalizedInput));
426
486
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
427
487
  await this.invalidateSchemaCache();
428
488
  return result;
@@ -497,7 +557,8 @@ export class SchemaOps {
497
557
  if (!parseResult.success) {
498
558
  throw new Error(`Invalid input: ${parseResult.error.message}`);
499
559
  }
500
- await this.withSchemaLock(() => this.updateCollectionInner(collectionPath, updates));
560
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
561
+ await this.withSchemaLock(() => this.updateCollectionInner(normalizedPath, updates));
501
562
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
502
563
  await this.invalidateSchemaCache();
503
564
  }
@@ -506,6 +567,12 @@ export class SchemaOps {
506
567
  * read-modify-write. Called directly (not via the public `updateCollection`)
507
568
  * by `updateOrderInner` for non-root collections — `withSchemaLock` is NOT
508
569
  * re-entrant, so going through the public method there would deadlock.
570
+ *
571
+ * `collectionPath` here is always already normalized (content-root prefix
572
+ * stripped, if it had one) by whichever public method reached this — either
573
+ * `updateCollection` above or `updateOrder` via `updateOrderInner` — so this
574
+ * no longer re-strips the prefix itself; see normalizeCollectionPath's doc
575
+ * comment for the single boundary that owns that now.
509
576
  */
510
577
  async updateCollectionInner(collectionPath, updates) {
511
578
  // Check if this is the root collection (path equals the configured content
@@ -526,18 +593,15 @@ export class SchemaOps {
526
593
  await this.writeRootCollectionMeta(meta);
527
594
  return;
528
595
  }
529
- // Strip contentRoot prefix to get relative path for regular collection
530
- // E.g., "content/posts" -> "posts"
531
- const relativePath = collectionPath.startsWith(`${this.contentRootName}/`)
532
- ? collectionPath.slice(this.contentRootName.length + 1)
533
- : collectionPath;
534
- // Resolve path for regular collection
535
- const physicalPath = await resolveCollectionPath(this.contentRoot, createLogicalPath(relativePath));
596
+ // Resolve path for regular collection. collectionPath is already
597
+ // normalized (no content-root prefix) by the caller — see this method's
598
+ // doc comment.
599
+ const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
536
600
  if (!physicalPath) {
537
601
  throw new Error(`Collection not found: ${collectionPath}`);
538
602
  }
539
603
  // Read existing meta
540
- const meta = await this.readCollectionMeta(relativePath);
604
+ const meta = await this.readCollectionMeta(collectionPath);
541
605
  if (!meta) {
542
606
  throw new Error(`Collection meta not found: ${collectionPath}`);
543
607
  }
@@ -608,7 +672,8 @@ export class SchemaOps {
608
672
  * Delete a collection (must be empty)
609
673
  */
610
674
  async deleteCollection(collectionPath) {
611
- await this.withSchemaLock(() => this.deleteCollectionInner(collectionPath));
675
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
676
+ await this.withSchemaLock(() => this.deleteCollectionInner(normalizedPath));
612
677
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
613
678
  await this.invalidateSchemaCache();
614
679
  }
@@ -645,7 +710,8 @@ export class SchemaOps {
645
710
  const available = Object.keys(this.entrySchemaRegistry).join(', ');
646
711
  throw new Error(`Schema reference "${entryType.schema}" not found. Available: ${available}`);
647
712
  }
648
- await this.withSchemaLock(() => this.addEntryTypeInner(collectionPath, entryType));
713
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
714
+ await this.withSchemaLock(() => this.addEntryTypeInner(normalizedPath, entryType));
649
715
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
650
716
  await this.invalidateSchemaCache();
651
717
  }
@@ -691,7 +757,8 @@ export class SchemaOps {
691
757
  const available = Object.keys(this.entrySchemaRegistry).join(', ');
692
758
  throw new Error(`Schema reference "${updates.schema}" not found. Available: ${available}`);
693
759
  }
694
- await this.withSchemaLock(() => this.updateEntryTypeInner(collectionPath, entryTypeName, updates));
760
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
761
+ await this.withSchemaLock(() => this.updateEntryTypeInner(normalizedPath, entryTypeName, updates));
695
762
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
696
763
  await this.invalidateSchemaCache();
697
764
  }
@@ -748,7 +815,8 @@ export class SchemaOps {
748
815
  * Remove an entry type from a collection
749
816
  */
750
817
  async removeEntryType(collectionPath, entryTypeName) {
751
- await this.withSchemaLock(() => this.removeEntryTypeInner(collectionPath, entryTypeName));
818
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
819
+ await this.withSchemaLock(() => this.removeEntryTypeInner(normalizedPath, entryTypeName));
752
820
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
753
821
  await this.invalidateSchemaCache();
754
822
  }
@@ -803,8 +871,9 @@ export class SchemaOps {
803
871
  * ```
804
872
  */
805
873
  async countEntriesUsingType(collectionPath, entryTypeName) {
874
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
806
875
  // Resolve collection physical path
807
- const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
876
+ const physicalPath = await resolveCollectionPath(this.contentRoot, normalizedPath);
808
877
  if (!physicalPath) {
809
878
  // Collection doesn't exist yet - return 0
810
879
  return 0;
@@ -852,7 +921,8 @@ export class SchemaOps {
852
921
  * Update the order of items in a collection
853
922
  */
854
923
  async updateOrder(collectionPath, order) {
855
- await this.withSchemaLock(() => this.updateOrderInner(collectionPath, order));
924
+ const normalizedPath = this.normalizeCollectionPath(collectionPath);
925
+ await this.withSchemaLock(() => this.updateOrderInner(normalizedPath, order));
856
926
  // Invalidate schema cache after mutation (outside the lock — see withSchemaLock's doc comment)
857
927
  await this.invalidateSchemaCache();
858
928
  }
@@ -869,8 +939,9 @@ export class SchemaOps {
869
939
  await this.writeRootCollectionMeta(meta);
870
940
  return;
871
941
  }
872
- // Update regular collection (handles contentRoot prefix stripping internally).
873
- // Calls updateCollectionInner DIRECTLY, never the public updateCollection:
942
+ // Update regular collection. collectionPath is already normalized (no
943
+ // content-root prefix) by the public updateOrder above. Calls
944
+ // updateCollectionInner DIRECTLY, never the public updateCollection:
874
945
  // we're already inside withSchemaLock's critical section here, and
875
946
  // withLock is not re-entrant — going through updateCollection would call
876
947
  // withSchemaLock again and deadlock waiting on the lock it itself holds.