canopycms 0.0.68-int.93 → 0.0.68-int.94
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 +3 -2
- package/dist/api/admin.d.ts +13 -1
- package/dist/api/admin.js +18 -2
- package/dist/api/branch-status.js +13 -2
- package/dist/api/branch.js +11 -11
- package/dist/api/client.d.ts +1 -1
- package/dist/api/client.js +2 -3
- package/dist/api/content.js +7 -3
- package/dist/api/entries.js +10 -7
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/branch-workspace.js +6 -1
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/cli.js +800 -235
- package/dist/cli/generate-ai-content.js +353 -47
- package/dist/cli/init.js +6 -1
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +70 -26
- 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 +6 -0
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/schemas/config.d.ts +5 -0
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/url.d.ts +2 -0
- package/dist/config/schemas/url.js +7 -0
- package/dist/config/types.d.ts +17 -2
- package/dist/content-listing.d.ts +10 -10
- package/dist/content-listing.js +7 -3
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +19 -11
- package/dist/content-store.d.ts +35 -9
- package/dist/content-store.js +68 -42
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +1 -1
- package/dist/context.d.ts +12 -3
- package/dist/context.js +93 -58
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/Editor.d.ts +2 -0
- package/dist/editor/Editor.js +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +8 -1
- package/dist/editor/editor-utils.d.ts +17 -15
- package/dist/editor/editor-utils.js +43 -36
- package/dist/editor/hooks/useCommentSystem.js +5 -1
- package/dist/editor/preview-bridge.js +4 -1
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +8 -5
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +59 -5
- package/dist/git-manager.js +262 -18
- package/dist/http/handler.js +12 -5
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -1
- package/dist/paths/branch.js +15 -2
- package/dist/paths/index.d.ts +2 -2
- package/dist/paths/index.js +2 -2
- package/dist/reference-resolver.d.ts +3 -19
- package/dist/reference-resolver.js +6 -42
- package/dist/schema/schema-store.d.ts +48 -22
- package/dist/schema/schema-store.js +66 -22
- package/dist/services.d.ts +6 -1
- package/dist/services.js +41 -26
- package/dist/settings-workspace.js +9 -7
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +25 -6
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +8 -0
- package/dist/utils/content-write-lock.d.ts +21 -6
- package/dist/utils/content-write-lock.js +16 -9
- package/dist/utils/url-prefix.d.ts +15 -0
- package/dist/utils/url-prefix.js +35 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/cms-worker.js +23 -2
- package/dist/worker/git-sync.d.ts +4 -0
- package/dist/worker/git-sync.js +31 -4
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +8 -0
- package/package.json +1 -1
- package/dist/api/request-url.d.ts +0 -8
- package/dist/api/request-url.js +0 -15
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ export default defineCanopyConfig({
|
|
|
104
104
|
colors: { brand: '#4f46e5' },
|
|
105
105
|
},
|
|
106
106
|
// previewBase: { 'content/posts': '/blog' }, // optional overrides
|
|
107
|
+
// previewPrefix: '/preview', // optional, in front of every preview URL
|
|
107
108
|
},
|
|
108
109
|
// For prod mode, defaultRemoteUrl is required.
|
|
109
110
|
// For dev, it's optional - if omitted, uses auto-initialized local remote at .canopy-dev/remote.git
|
|
@@ -323,7 +324,7 @@ name) works, but leaves the entry's real `urlPath` as
|
|
|
323
324
|
URL-derived surface then has to be told about one at a time. The example app in this repo used to
|
|
324
325
|
do that and no longer does.
|
|
325
326
|
|
|
326
|
-
Both methods return `{ data, path }`. `read` throws if the content is missing; `readByUrlPath` returns `null` instead. Pass a `branch` option when you want branch-specific data (e.g., for preview); otherwise it defaults to your
|
|
327
|
+
Both methods return `{ data, path }`. `read` throws if the content is missing; `readByUrlPath` returns `null` instead. Pass a `branch` option when you want branch-specific data (e.g., for preview); otherwise it defaults to the active branch: `defaultActiveBranch`, or when that is unset, git HEAD in `dev` and your base branch in `prod`. Any other branch must already exist and be readable by the current user, or it reads as not found. Both enforce the same branch/path access rules as the API handlers.
|
|
327
328
|
|
|
328
329
|
**Index entries and URL resolution**
|
|
329
330
|
|
|
@@ -401,7 +402,7 @@ _TODO_ show real examples of what to do
|
|
|
401
402
|
|
|
402
403
|
### Preview branch awareness
|
|
403
404
|
|
|
404
|
-
- When building preview URLs, include the current branch as a query param (e.g., `/?branch=feature-foo` or `/posts/hello?branch=feature-foo`) so SSR preview pages read from the same branch workspace the editor is editing. The `Editor` component appends the branch param automatically to `previewBaseByCollection`; your page loaders should read `searchParams.branch` and pass it
|
|
405
|
+
- When building preview URLs, include the current branch as a query param (e.g., `/?branch=feature-foo` or `/posts/hello?branch=feature-foo`) so SSR preview pages read from the same branch workspace the editor is editing. The `Editor` component appends the branch param automatically to `previewBaseByCollection`; your page loaders should read `searchParams.branch` and pass it as the `branch` option to `getCanopy()`'s `read`/`readByUrlPath`.
|
|
405
406
|
- For public static builds, omit/ignore the branch param; this pattern is only for the editor/preview environment.
|
|
406
407
|
- Likewise, include `branch` in your editor route (e.g., `/edit?branch=feature-foo`) and have your editor page pass it to `<Editor>` so reloads/links preserve the selected branch. The `Editor` will also reflect branch switches back into the query string.
|
|
407
408
|
|
package/dist/api/admin.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import type { ApiResponse } from './types.js';
|
|
10
10
|
import type { Task, QueueStats, CorruptTaskFile } from '../task-queue/cms-task-queue.js';
|
|
11
|
-
import type { WorkerStatusReport } from '../types.js';
|
|
11
|
+
import type { BuildIdentity, WorkerStatusReport } from '../types.js';
|
|
12
12
|
import type { OperatingMode } from '../operating-mode/index.js';
|
|
13
13
|
export type { BranchHealthResponse, PurgeBranchDirResponse, RepairBranchDirResponse, RepairContentDuplicatesResponse, } from './admin-branch-health.js';
|
|
14
14
|
type WorkerLivenessState = 'alive' | 'stale' | 'absent';
|
|
@@ -31,6 +31,18 @@ export interface AdminStatusData {
|
|
|
31
31
|
worker: WorkerLiveness;
|
|
32
32
|
workerStatus: WorkerStatusReport | null;
|
|
33
33
|
statusReadError?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Why this process cannot provision the settings workspace (groups and path rules), when it
|
|
36
|
+
* cannot. Its requests that resolve a user answer 503 meanwhile; bootstrap admins still reach
|
|
37
|
+
* /admin.
|
|
38
|
+
*/
|
|
39
|
+
settingsWorkspaceError?: string;
|
|
40
|
+
/** Build of the API process answering this request. */
|
|
41
|
+
build: BuildIdentity;
|
|
42
|
+
/** Whether `media` is configured; without it every upload returns 501. */
|
|
43
|
+
assetStore: {
|
|
44
|
+
configured: boolean;
|
|
45
|
+
};
|
|
34
46
|
}
|
|
35
47
|
/** Response type for GET /admin/status */
|
|
36
48
|
export type AdminStatusResponse = ApiResponse<AdminStatusData>;
|
package/dist/api/admin.js
CHANGED
|
@@ -12,7 +12,8 @@ import { getQueueStats, listTasks, listCorruptTaskFiles, requeueFailedTask, } fr
|
|
|
12
12
|
import { getTaskQueueDir } from '../task-queue/task-queue-config.js';
|
|
13
13
|
import { WORKER_STATUS_FILE } from '../task-queue/worker-status.js';
|
|
14
14
|
import { defineEndpoint } from './route-builder.js';
|
|
15
|
-
import { getErrorMessage, isNotFoundError } from '../utils/error.js';
|
|
15
|
+
import { getErrorMessage, isNotFoundError, redactCredentials } from '../utils/error.js';
|
|
16
|
+
import { getBuildIdentity } from '../build-identity.js';
|
|
16
17
|
import { ADMIN_BRANCH_HEALTH_ROUTES } from './admin-branch-health.js';
|
|
17
18
|
/**
|
|
18
19
|
* 60_000 = DEFAULT_LOCK_STALE_MS in worker/cms-worker.ts, hardcoded rather
|
|
@@ -72,6 +73,15 @@ async function readWorkerStatus(taskDir) {
|
|
|
72
73
|
return { workerStatus: null, statusReadError: getErrorMessage(err) };
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
async function readSettingsWorkspaceError(ctx) {
|
|
77
|
+
try {
|
|
78
|
+
await ctx.services.getSettingsBranchRoot();
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return redactCredentials(getErrorMessage(err));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
75
85
|
/** Age (ms) of the oldest file in pending/, or undefined if empty/missing. */
|
|
76
86
|
async function getOldestPendingAgeMs(taskDir) {
|
|
77
87
|
const pendingDir = path.join(taskDir, 'pending');
|
|
@@ -126,11 +136,12 @@ const deleteTaskParamsSchema = z.object({
|
|
|
126
136
|
const getAdminStatusHandler = async (_gc, ctx, _req) => {
|
|
127
137
|
const taskDir = getTaskQueueDir(ctx.services.config);
|
|
128
138
|
try {
|
|
129
|
-
const [queueStats, oldestPendingAgeMs, worker, { workerStatus, statusReadError }] = await Promise.all([
|
|
139
|
+
const [queueStats, oldestPendingAgeMs, worker, { workerStatus, statusReadError }, settingsWorkspaceError,] = await Promise.all([
|
|
130
140
|
getQueueStats(taskDir),
|
|
131
141
|
getOldestPendingAgeMs(taskDir),
|
|
132
142
|
classifyWorkerLiveness(taskDir),
|
|
133
143
|
readWorkerStatus(taskDir),
|
|
144
|
+
readSettingsWorkspaceError(ctx),
|
|
134
145
|
]);
|
|
135
146
|
return {
|
|
136
147
|
ok: true,
|
|
@@ -145,6 +156,9 @@ const getAdminStatusHandler = async (_gc, ctx, _req) => {
|
|
|
145
156
|
worker,
|
|
146
157
|
workerStatus,
|
|
147
158
|
...(statusReadError ? { statusReadError } : {}),
|
|
159
|
+
...(settingsWorkspaceError ? { settingsWorkspaceError } : {}),
|
|
160
|
+
build: getBuildIdentity(),
|
|
161
|
+
assetStore: { configured: !!ctx.assetStore },
|
|
148
162
|
},
|
|
149
163
|
};
|
|
150
164
|
}
|
|
@@ -255,6 +269,8 @@ const getAdminStatus = defineEndpoint({
|
|
|
255
269
|
queue: { pending: 0, processing: 0, completed: 0, failed: 0, corrupt: 0 },
|
|
256
270
|
worker: { state: 'absent' },
|
|
257
271
|
workerStatus: null,
|
|
272
|
+
build: { canopycmsVersion: '0.0.0' },
|
|
273
|
+
assetStore: { configured: false },
|
|
258
274
|
},
|
|
259
275
|
guards: ['admin'],
|
|
260
276
|
handler: getAdminStatusHandler,
|
|
@@ -8,6 +8,7 @@ import { canPerformWorkflowAction, getBranchProtection } from '../authorization/
|
|
|
8
8
|
import { syncSubmitPr } from './github-sync.js';
|
|
9
9
|
import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from '../utils/error.js';
|
|
10
10
|
import { isNonFastForwardRejection } from '../utils/git.js';
|
|
11
|
+
import { ContentWriteLockBusyError } from '../utils/content-write-lock.js';
|
|
11
12
|
import { submissionEditorFromUser } from '../submission-attribution.js';
|
|
12
13
|
const getBranchStatusHandler = async (gc, _ctx, _req, _params) => {
|
|
13
14
|
const { branchContext } = gc;
|
|
@@ -57,6 +58,17 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
57
58
|
({ changedPaths } = await ctx.services.submitBranch({ context: branchContext, submitter }));
|
|
58
59
|
}
|
|
59
60
|
catch (err) {
|
|
61
|
+
// Retriable either way: a resubmit commits nothing new and pushes only
|
|
62
|
+
// what has not reached the remote.
|
|
63
|
+
if (err instanceof ContentWriteLockBusyError) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 409,
|
|
67
|
+
error: err.outcome === 'not-run'
|
|
68
|
+
? `Could not submit "${branchContext.branch.name}" right now: it is being synced with its base branch, or a save is in flight. Nothing was submitted; try again in a moment.`
|
|
69
|
+
: `"${branchContext.branch.name}" was being synced while it was submitted, so the submit may not have completed. Try submitting again.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
60
72
|
const message = getErrorMessage(err);
|
|
61
73
|
// Full path detail (including branchRoot, an absolute path) to server logs
|
|
62
74
|
// only; the client only ever sees the sanitized form (API-H2). Credentials
|
|
@@ -66,8 +78,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
66
78
|
// A non-fast-forward rejection means this branch and the deployment's
|
|
67
79
|
// local repository have diverged. Retrying the identical push can never
|
|
68
80
|
// succeed (see isNonFastForwardRejection), so surface 409 instead of the
|
|
69
|
-
// generic 500 below. Everything else (network, auth
|
|
70
|
-
// keeps the existing 500 path unchanged.
|
|
81
|
+
// generic 500 below. Everything else (network, auth) keeps the 500 path.
|
|
71
82
|
//
|
|
72
83
|
// This push targets the deployment's OWN local origin (remote.git), not
|
|
73
84
|
// GitHub, so the message deliberately states only the observable fact and
|
package/dist/api/branch.js
CHANGED
|
@@ -10,7 +10,7 @@ import { clientOperatingStrategy } from '../operating-mode/index.js';
|
|
|
10
10
|
import { isNotFoundError, getErrorMessage, sanitizeErrorMessage } from '../utils/error.js';
|
|
11
11
|
import { filePathExists } from '../utils/fs.js';
|
|
12
12
|
import { isNetworkRemoteUrl } from '../utils/git.js';
|
|
13
|
-
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, } from '../paths/index.js';
|
|
13
|
+
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, isSettingsBranchName, } from '../paths/index.js';
|
|
14
14
|
import { GitManager } from '../git-manager.js';
|
|
15
15
|
import { branchNameSchema, branchParamSchema } from './validators.js';
|
|
16
16
|
const log = createDebugLogger({ prefix: 'BranchAPI' });
|
|
@@ -95,15 +95,12 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
95
95
|
branchName,
|
|
96
96
|
userId: req.user.userId,
|
|
97
97
|
});
|
|
98
|
-
// Scope note: the
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// and never provision) provision system/known branch names, not
|
|
105
|
-
// user-chosen ones, and deliberately stay uncovered -- intentional, not
|
|
106
|
-
// an oversight.
|
|
98
|
+
// Scope note: the remote-mirror check further down applies ONLY to this
|
|
99
|
+
// user-facing creation path; http/handler.ts's auto-create (base/active
|
|
100
|
+
// branches) and loadOrCreateBranchContext provision known names. The two
|
|
101
|
+
// settings-branch checks below give this path a specific 400; every
|
|
102
|
+
// provisioning path, this one included, also refuses a settings branch in
|
|
103
|
+
// BranchWorkspaceManager.openOrCreateBranch.
|
|
107
104
|
// Prevent git branch name collision with the settings branch. Settings
|
|
108
105
|
// live in a separate directory but share the same git remote, and
|
|
109
106
|
// openOrCreateBranch (branch-workspace.ts) uses the SANITIZED name as the
|
|
@@ -337,7 +334,10 @@ export const listBranchesHandler = async (ctx, req) => {
|
|
|
337
334
|
error: 'Branch registry not initialized — ensure the workspace has been initialized',
|
|
338
335
|
};
|
|
339
336
|
}
|
|
340
|
-
|
|
337
|
+
// A settings-branch workspace is never resolvable as a content branch (see
|
|
338
|
+
// http/handler.ts), so one left on disk is hidden rather than listed unopenable.
|
|
339
|
+
const settingsBranch = operatingStrategy(ctx.services.config.mode).getSettingsBranchName(ctx.services.config);
|
|
340
|
+
const allBranches = (await ctx.services.registry.list()).filter((context) => !isSettingsBranchName(context.branch.name, settingsBranch));
|
|
341
341
|
// The branch the editor should open when none is pinned via URL/config.
|
|
342
342
|
// Read per-request so dev-mode refreshActiveBranch() updates are reflected.
|
|
343
343
|
// Sanitized: dev mode detects the RAW git HEAD name (e.g. 'claude/foo'),
|
package/dist/api/client.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface ApiClientOptions {
|
|
|
28
28
|
/**
|
|
29
29
|
* End request paths with `/` (by `withTrailingSlash`'s rule), matching a Next host built with
|
|
30
30
|
* `trailingSlash: true`. Defaults to the value `withCanopy` inlines at build time (see
|
|
31
|
-
* `
|
|
31
|
+
* `readTrailingSlashEnv`), else false.
|
|
32
32
|
*/
|
|
33
33
|
trailingSlash?: boolean;
|
|
34
34
|
/**
|
package/dist/api/client.js
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* (changes will be overwritten), edit the generator's templates instead.
|
|
7
7
|
*/
|
|
8
8
|
import { computeContentSha256Hex } from './request-body-hash.js';
|
|
9
|
-
import {
|
|
10
|
-
import { withTrailingSlash } from '../utils/url-prefix.js';
|
|
9
|
+
import { readTrailingSlashEnv, withTrailingSlash } from '../utils/url-prefix.js';
|
|
11
10
|
/**
|
|
12
11
|
* Typed client for the CanopyCMS API: one method per endpoint, grouped by namespace, each
|
|
13
12
|
* returning that endpoint's ApiResponse.
|
|
@@ -262,7 +261,7 @@ export class CanopyApiClient {
|
|
|
262
261
|
this.baseUrl = options.baseUrl ?? '/api/canopycms';
|
|
263
262
|
// An unbound fetch throws "Illegal invocation" in browsers; Node has no window.
|
|
264
263
|
this.fetchFn = options.fetch ?? (typeof window !== 'undefined' ? fetch.bind(window) : fetch);
|
|
265
|
-
this.trailingSlash = options.trailingSlash ??
|
|
264
|
+
this.trailingSlash = options.trailingSlash ?? readTrailingSlashEnv();
|
|
266
265
|
this.onUnauthorized = options.onUnauthorized;
|
|
267
266
|
}
|
|
268
267
|
buildPath(template, params) {
|
package/dist/api/content.js
CHANGED
|
@@ -91,12 +91,16 @@ const readContentHandler = async (gc, ctx, req, params) => {
|
|
|
91
91
|
const message = err instanceof ContentStoreError ? err.message : 'Invalid content request';
|
|
92
92
|
return { ok: false, status: 400, error: sanitizeErrorMessage(message) };
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
// One checker for the entry and every reference target it resolves, so a reference cannot
|
|
95
|
+
// carry a target's data past the rules that would refuse a direct read of it.
|
|
96
|
+
const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
|
|
97
|
+
if (!checkAccess(entryLogicalPath(schemaItem.logicalPath, slug), 'read').allowed) {
|
|
96
98
|
return { ok: false, status: 403, error: 'Forbidden' };
|
|
97
99
|
}
|
|
98
100
|
try {
|
|
99
|
-
const doc = await store.read(schemaItem.logicalPath, slug
|
|
101
|
+
const doc = await store.read(schemaItem.logicalPath, slug, {
|
|
102
|
+
referenceAccess: (targetPath) => checkAccess(targetPath, 'read').allowed,
|
|
103
|
+
});
|
|
100
104
|
return { ok: true, status: 200, data: doc };
|
|
101
105
|
}
|
|
102
106
|
catch (err) {
|
package/dist/api/entries.js
CHANGED
|
@@ -299,13 +299,16 @@ const deleteEntryHandler = async (gc, ctx, req, params) => {
|
|
|
299
299
|
// C6: order cleanup is best-effort hygiene after the entry is already deleted, so no
|
|
300
300
|
// failure here may turn an otherwise-successful delete into an error (a retry would
|
|
301
301
|
// just 404). Surface it as a warning instead (mirrors content.ts's `validationWarnings`).
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
// An `'unknown'` outcome means the update ran to completion: nothing to warn of.
|
|
303
|
+
if (!(err instanceof SchemaStoreBusyError && err.outcome === 'unknown')) {
|
|
304
|
+
const reason = err instanceof SchemaStoreBusyError ? 'schema is busy' : getErrorMessage(err);
|
|
305
|
+
log.warn('delete-entry', 'Skipped order cleanup', {
|
|
306
|
+
collectionPath,
|
|
307
|
+
contentId,
|
|
308
|
+
error: getErrorMessage(err),
|
|
309
|
+
});
|
|
310
|
+
orderCleanupWarning = `Entry deleted, but the collection order list could not be updated (${sanitizeErrorMessage(reason)}); it will still list this entry until the next schema change.`;
|
|
311
|
+
}
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ContentStore } from '../content-store.js';
|
|
3
3
|
import { defineEndpoint } from './route-builder.js';
|
|
4
|
-
import { ReferenceResolver } from '../reference-resolver.js';
|
|
5
|
-
import { buildResolvedReference } from '../entry-schema.js';
|
|
6
|
-
import { computeEntryUrl } from '../utils/entry-url.js';
|
|
7
|
-
import { entryLogicalPath } from '../paths/index.js';
|
|
8
4
|
import { branchNameSchema, contentIdSchema } from './validators.js';
|
|
9
5
|
/**
|
|
10
6
|
* Resolution does sequential per-ID file I/O, so the request body caps how much filesystem work
|
|
@@ -21,48 +17,25 @@ const resolveReferencesBodySchema = z.object({
|
|
|
21
17
|
const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
|
|
22
18
|
const { branchContext } = gc;
|
|
23
19
|
const { ids } = body;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const store = new ContentStore(branchContext.branchRoot, flatSchema, {
|
|
27
|
-
contentRootName,
|
|
20
|
+
const store = new ContentStore(branchContext.branchRoot, branchContext.flatSchema, {
|
|
21
|
+
contentRootName: ctx.services.config.contentRoot || 'content',
|
|
28
22
|
});
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// Resolve each ID to full document
|
|
32
|
-
const resolver = new ReferenceResolver(store, idIndex);
|
|
33
|
-
// Build the access checker once, reused for every id instead of re-loading per id in the loop.
|
|
34
|
-
// A failure here (e.g. settings workspace unavailable) surfaces as a handler error, not
|
|
35
|
-
// swallowed silently per id.
|
|
23
|
+
// Built once for every id. A failure here (e.g. settings workspace unavailable) surfaces as a
|
|
24
|
+
// handler error rather than being swallowed per id.
|
|
36
25
|
const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
|
|
26
|
+
const access = (logicalPath) => checkAccess(logicalPath, 'read').allowed;
|
|
27
|
+
// The resolution `read()` applies to a reference field, so live preview shows this user what
|
|
28
|
+
// `read()` would: the target's data, or a `RestrictedReference` for a target they may not
|
|
29
|
+
// read. The target's own references stay ids, as in `read()`.
|
|
37
30
|
const resolved = {};
|
|
38
31
|
for (const id of ids) {
|
|
39
32
|
try {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
const access = checkAccess(entryLogicalPath(result.collection, result.slug), 'read');
|
|
44
|
-
if (!access.allowed)
|
|
45
|
-
continue;
|
|
46
|
-
// `resolveReferences: false` matches the server-side resolver (content-store.ts's
|
|
47
|
-
// resolveSingleReferenceOnce), so a nested reference inside a target renders the same way
|
|
48
|
-
// in live preview as on the published site, instead of resolving one level deeper.
|
|
49
|
-
const doc = await store.read(result.collection, result.slug, { resolveReferences: false });
|
|
50
|
-
if (doc && doc.data) {
|
|
51
|
-
// Same shape as the server-side resolver: target data first, then the reserved keys.
|
|
52
|
-
// Order is the corruption guard (a target modelling `id` as content must not shadow the
|
|
53
|
-
// real content ID); the extra keys (incl. `urlPath`) keep live preview and production
|
|
54
|
-
// in sync for consumers like client-reference-resolver.ts.
|
|
55
|
-
resolved[id] = buildResolvedReference(doc.data, {
|
|
56
|
-
id,
|
|
57
|
-
slug: result.slug,
|
|
58
|
-
collection: result.collection,
|
|
59
|
-
urlPath: computeEntryUrl(result.collection, result.slug, contentRootName),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
33
|
+
const value = await store.resolveReferenceTarget(id, access);
|
|
34
|
+
if (value)
|
|
35
|
+
resolved[id] = value;
|
|
63
36
|
}
|
|
64
37
|
catch (error) {
|
|
65
|
-
//
|
|
38
|
+
// An id that fails to resolve is omitted rather than failing the whole request.
|
|
66
39
|
console.error(`Failed to resolve reference ID ${id}:`, error);
|
|
67
40
|
}
|
|
68
41
|
}
|
|
@@ -73,7 +46,8 @@ const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
|
|
|
73
46
|
};
|
|
74
47
|
};
|
|
75
48
|
/**
|
|
76
|
-
* Resolve reference IDs
|
|
49
|
+
* Resolve reference IDs as a reference field would: full target data, or title + URL tagged
|
|
50
|
+
* `unavailable` for a target the user may not read. An id naming no entry is omitted.
|
|
77
51
|
* POST /:branch/resolve-references
|
|
78
52
|
* Body: { ids: string[] }
|
|
79
53
|
*/
|
package/dist/api/user.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ApiResponse } from './types.js';
|
|
2
|
+
import type { BuildIdentity } from '../types.js';
|
|
2
3
|
export type UserInfoResponse = ApiResponse<{
|
|
3
4
|
userId: string;
|
|
4
5
|
groups: string[];
|
|
6
|
+
/** Admins only: the deployed version is reconnaissance for anyone else. */
|
|
7
|
+
build?: BuildIdentity;
|
|
5
8
|
}>;
|
|
6
9
|
export declare const USER_ROUTES: {
|
|
7
10
|
readonly whoami: import("./route-builder.js").RouteDefinition<undefined, undefined, UserInfoResponse>;
|
package/dist/api/user.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { defineEndpoint } from './route-builder.js';
|
|
2
|
+
import { isAdmin } from '../authorization/helpers.js';
|
|
3
|
+
import { getBuildIdentity } from '../build-identity.js';
|
|
2
4
|
/**
|
|
3
5
|
* This is a PUBLIC endpoint - no special permissions required
|
|
4
6
|
*/
|
|
@@ -10,6 +12,7 @@ const getUserInfoHandler = async (ctx, req) => {
|
|
|
10
12
|
data: {
|
|
11
13
|
userId: req.user.userId,
|
|
12
14
|
groups: req.user.groups,
|
|
15
|
+
...(isAdmin(req.user.groups) ? { build: getBuildIdentity() } : {}),
|
|
13
16
|
},
|
|
14
17
|
};
|
|
15
18
|
};
|
package/dist/assets/factory.d.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the one place that turns the (currently-unconsumed) `media` config
|
|
5
5
|
* into a real store — see .claude/future-tasks/resolved/assets-media-system.md.
|
|
6
|
+
*
|
|
7
|
+
* The store is branch-agnostic: a local one is rooted at the dev workspace's
|
|
8
|
+
* `assets/` or at `media.directory` resolved against the process's working
|
|
9
|
+
* directory, never inside a branch clone. So asset writes take no [SYNC-C1]
|
|
10
|
+
* content-write lock — the worker's rebase never touches them.
|
|
6
11
|
*/
|
|
7
12
|
import type { MediaConfig } from '../config/types.js';
|
|
8
13
|
import type { AssetStore } from './types.js';
|
package/dist/assets/factory.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the one place that turns the (currently-unconsumed) `media` config
|
|
5
5
|
* into a real store — see .claude/future-tasks/resolved/assets-media-system.md.
|
|
6
|
+
*
|
|
7
|
+
* The store is branch-agnostic: a local one is rooted at the dev workspace's
|
|
8
|
+
* `assets/` or at `media.directory` resolved against the process's working
|
|
9
|
+
* directory, never inside a branch clone. So asset writes take no [SYNC-C1]
|
|
10
|
+
* content-write lock — the worker's rebase never touches them.
|
|
6
11
|
*/
|
|
7
12
|
import { LocalAssetStore } from './store-local.js';
|
|
8
13
|
import { S3AssetStore } from './store-s3.js';
|
package/dist/branch-workspace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { ensureBranchRoot } from './paths/index.js';
|
|
2
|
+
import { BranchPathError, ensureBranchRoot, isSettingsBranchName } from './paths/index.js';
|
|
3
3
|
import { getBranchMetadataFileManager, loadBranchContext } from './branch-metadata.js';
|
|
4
4
|
import { readsFromCheckout } from './build-mode.js';
|
|
5
5
|
import { operatingStrategy } from './operating-mode/index.js';
|
|
@@ -77,6 +77,11 @@ export class BranchWorkspaceManager {
|
|
|
77
77
|
}
|
|
78
78
|
async openOrCreateBranch(options) {
|
|
79
79
|
const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
|
|
80
|
+
// resolveBranchPath refuses the reserved prefix; this also refuses an adopter's
|
|
81
|
+
// configured settings branch name, which only the config knows.
|
|
82
|
+
if (isSettingsBranchName(branchName, operatingStrategy(mode).getSettingsBranchName(this.config))) {
|
|
83
|
+
throw new BranchPathError('Settings branches are not content branches');
|
|
84
|
+
}
|
|
80
85
|
const { branchRoot, baseRoot, branchName: safeName, } = await ensureBranchRoot({
|
|
81
86
|
mode,
|
|
82
87
|
branchName,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CANOPYCMS_VERSION } from './version.js';
|
|
2
|
+
/**
|
|
3
|
+
* Set by the image build (Dockerfile.cms `ARG`), not by the infrastructure: a Lambda env var
|
|
4
|
+
* would name the infrastructure's commit and hide an image/infrastructure skew.
|
|
5
|
+
*/
|
|
6
|
+
const SOURCE_REVISION_ENV = 'CANOPY_SOURCE_SHA';
|
|
7
|
+
export function getBuildIdentity(env = process.env) {
|
|
8
|
+
// The Dockerfile sets the variable to the empty string when the build arg is not passed.
|
|
9
|
+
const sourceRevision = env[SOURCE_REVISION_ENV]?.trim();
|
|
10
|
+
return sourceRevision
|
|
11
|
+
? { canopycmsVersion: CANOPYCMS_VERSION, sourceRevision }
|
|
12
|
+
: { canopycmsVersion: CANOPYCMS_VERSION };
|
|
13
|
+
}
|