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.
- package/dist/ai/index.d.ts +9 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +9 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/json-to-markdown.js +1 -2
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/ai/to-plain-text.d.ts +57 -0
- package/dist/ai/to-plain-text.d.ts.map +1 -0
- package/dist/ai/to-plain-text.js +255 -0
- package/dist/ai/to-plain-text.js.map +1 -0
- package/dist/ai/transform-components.d.ts.map +1 -1
- package/dist/ai/transform-components.js +70 -5
- package/dist/ai/transform-components.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +5 -1
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +9 -2
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +36 -1
- package/dist/api/admin-branch-health.d.ts.map +1 -1
- package/dist/api/admin-branch-health.js +176 -2
- package/dist/api/admin-branch-health.js.map +1 -1
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/admin.d.ts.map +1 -1
- package/dist/api/admin.js.map +1 -1
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +16 -2
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +6 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/content.d.ts +11 -5
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +79 -6
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts +10 -0
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +40 -15
- package/dist/api/entries.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +13 -13
- package/dist/api/schema.js.map +1 -1
- package/dist/assets/transform.d.ts.map +1 -1
- package/dist/assets/transform.js +19 -6
- package/dist/assets/transform.js.map +1 -1
- package/dist/authorization/branch.d.ts +55 -3
- package/dist/authorization/branch.d.ts.map +1 -1
- package/dist/authorization/branch.js +55 -9
- package/dist/authorization/branch.js.map +1 -1
- package/dist/authorization/types.d.ts +1 -1
- package/dist/authorization/types.d.ts.map +1 -1
- package/dist/branch-health.d.ts +13 -0
- package/dist/branch-health.d.ts.map +1 -1
- package/dist/branch-health.js +23 -0
- package/dist/branch-health.js.map +1 -1
- package/dist/build-canopy.d.ts +49 -0
- package/dist/build-canopy.d.ts.map +1 -0
- package/dist/build-canopy.js +51 -0
- package/dist/build-canopy.js.map +1 -0
- package/dist/cli/cli.js +935 -562
- package/dist/cli/generate-ai-content.js +837 -480
- package/dist/cli/init.js +25 -7
- package/dist/cli/template-files/Dockerfile.cms.template +19 -1
- package/dist/cli/template-files/canopycms.config.ts.template +25 -1
- package/dist/cli/template-files/cms-stack.ts.template +7 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config/schemas/field.d.ts +15 -15
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts +9 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +14 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/content-id-index.d.ts +97 -5
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +170 -18
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-listing.d.ts +110 -5
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +164 -23
- package/dist/content-listing.js.map +1 -1
- package/dist/content-reader.d.ts +41 -4
- package/dist/content-reader.d.ts.map +1 -1
- package/dist/content-reader.js +17 -4
- package/dist/content-reader.js.map +1 -1
- package/dist/content-store.d.ts +91 -8
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +443 -258
- package/dist/content-store.js.map +1 -1
- package/dist/content-tree.d.ts +31 -2
- package/dist/content-tree.d.ts.map +1 -1
- package/dist/content-tree.js +22 -5
- package/dist/content-tree.js.map +1 -1
- package/dist/context.d.ts +30 -10
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +55 -4
- package/dist/context.js.map +1 -1
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +18 -6
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/CanopyEditorPage.d.ts +9 -1
- package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
- package/dist/editor/CanopyEditorPage.js +9 -2
- package/dist/editor/CanopyEditorPage.js.map +1 -1
- package/dist/editor/Editor.d.ts +9 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +58 -24
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.js +6 -2
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
- package/dist/editor/components/EntryCreateModal.d.ts +9 -1
- package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
- package/dist/editor/components/EntryCreateModal.js +4 -1
- package/dist/editor/components/EntryCreateModal.js.map +1 -1
- package/dist/editor/fields/DateTimeField.d.ts +46 -0
- package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
- package/dist/editor/fields/DateTimeField.js +66 -0
- package/dist/editor/fields/DateTimeField.js.map +1 -0
- package/dist/editor/fields/NumberField.d.ts +34 -0
- package/dist/editor/fields/NumberField.d.ts.map +1 -0
- package/dist/editor/fields/NumberField.js +53 -0
- package/dist/editor/fields/NumberField.js.map +1 -0
- package/dist/editor/fields/NumberListField.d.ts +25 -0
- package/dist/editor/fields/NumberListField.d.ts.map +1 -0
- package/dist/editor/fields/NumberListField.js +53 -0
- package/dist/editor/fields/NumberListField.js.map +1 -0
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +115 -26
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +261 -21
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntryManager.d.ts +21 -0
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +25 -2
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/theme.d.ts +8 -8
- package/dist/entry-schema.d.ts +228 -8
- package/dist/entry-schema.d.ts.map +1 -1
- package/dist/entry-schema.js +124 -0
- package/dist/entry-schema.js.map +1 -1
- package/dist/git-manager.d.ts +51 -7
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +105 -14
- package/dist/git-manager.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +70 -27
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +40 -3
- package/dist/http/router.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
- package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name-fixtures.js +55 -0
- package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
- package/dist/operating-mode/deployment-name.d.ts +9 -0
- package/dist/operating-mode/deployment-name.d.ts.map +1 -1
- package/dist/operating-mode/deployment-name.js +9 -1
- package/dist/operating-mode/deployment-name.js.map +1 -1
- package/dist/operating-mode/index.d.ts +2 -1
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +4 -1
- package/dist/operating-mode/index.js.map +1 -1
- package/dist/operating-mode/mode-env.d.ts +58 -0
- package/dist/operating-mode/mode-env.d.ts.map +1 -0
- package/dist/operating-mode/mode-env.js +97 -0
- package/dist/operating-mode/mode-env.js.map +1 -0
- package/dist/resolve-canopy-user.d.ts +34 -0
- package/dist/resolve-canopy-user.d.ts.map +1 -0
- package/dist/resolve-canopy-user.js +55 -0
- package/dist/resolve-canopy-user.js.map +1 -0
- package/dist/schema/schema-store.d.ts +53 -2
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +92 -21
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/server.d.ts +60 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +58 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts +1 -2
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +42 -12
- package/dist/services.js.map +1 -1
- package/dist/settings-workspace.d.ts +40 -0
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +139 -151
- package/dist/settings-workspace.js.map +1 -1
- package/dist/static/index.d.ts +65 -5
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +65 -12
- package/dist/static/index.js.map +1 -1
- package/dist/static/seo.d.ts +150 -0
- package/dist/static/seo.d.ts.map +1 -0
- package/dist/static/seo.js +189 -0
- package/dist/static/seo.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/content-write-lock.d.ts +127 -0
- package/dist/utils/content-write-lock.d.ts.map +1 -0
- package/dist/utils/content-write-lock.js +170 -0
- package/dist/utils/content-write-lock.js.map +1 -0
- package/dist/utils/error.d.ts +1 -1
- package/dist/utils/error.js +1 -1
- package/dist/utils/git.d.ts +16 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +22 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/sanitize-href.d.ts +74 -3
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +125 -6
- package/dist/utils/sanitize-href.js.map +1 -1
- package/dist/validation/entry-link-validator.d.ts +1 -1
- package/dist/validation/entry-link-validator.js +3 -3
- package/dist/validation/entry-link-validator.js.map +1 -1
- package/dist/validation/entry-validator.js +1 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +13 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +326 -264
- package/dist/worker/cms-worker.js.map +1 -1
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAOvB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
873
|
-
//
|
|
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.
|