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.
- package/README.md +21 -14
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +111 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +122 -0
- package/dist/assets/materialize.js +305 -0
- package/dist/assets/store-local.d.ts +4 -0
- package/dist/assets/store-local.js +11 -0
- package/dist/assets/store-s3.d.ts +10 -1
- package/dist/assets/store-s3.js +55 -3
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +14 -1
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +30 -0
- package/dist/cli/asset-refs.js +168 -0
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.js +4843 -1870
- package/dist/cli/generate-ai-content.js +1584 -444
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +24 -20
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +11 -0
- package/dist/editor/hooks/useBranchManager.js +80 -2
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +8 -1
- package/dist/editor/preview-bridge.js +15 -2
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +24 -9
- package/dist/server.js +22 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +39 -4
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +2 -1
package/dist/github-service.d.ts
CHANGED
|
@@ -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;
|
package/dist/github-service.js
CHANGED
|
@@ -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) {
|
package/dist/http/handler.js
CHANGED
|
@@ -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
|
-
//
|
|
23
|
-
// would
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/http/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/http/index.js
CHANGED
|
@@ -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';
|
package/dist/http/router.d.ts
CHANGED
|
@@ -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'];
|
package/dist/http/router.js
CHANGED
|
@@ -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,
|
package/dist/http/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
*
|
package/dist/paths/branch.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/paths/branch.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
}
|
package/dist/paths/index.d.ts
CHANGED
|
@@ -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,
|
|
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';
|
package/dist/paths/index.js
CHANGED
|
@@ -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,
|
|
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.
|
package/dist/paths/normalize.js
CHANGED
|
@@ -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.
|
package/dist/paths/validation.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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?: (
|
|
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
|
}
|