canopycms 0.0.68-int.108 → 0.0.68-int.110
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 -3
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/api/admin-branch-health.js +12 -3
- package/dist/api/admin.d.ts +8 -8
- package/dist/api/admin.js +2 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.js +1 -1
- package/dist/api/branch-status.js +12 -5
- package/dist/api/branch.js +4 -3
- package/dist/api/comments.d.ts +4 -3
- package/dist/api/comments.js +71 -7
- package/dist/api/content.d.ts +2 -2
- package/dist/api/content.js +15 -12
- package/dist/api/entries-constants.d.ts +8 -3
- package/dist/api/entries-constants.js +8 -3
- package/dist/api/entries.js +2 -4
- package/dist/api/github-sync.d.ts +2 -0
- package/dist/api/github-sync.js +3 -1
- package/dist/api/groups.js +1 -0
- package/dist/api/guards.js +23 -3
- package/dist/api/permissions.js +1 -0
- package/dist/api/route-builder.js +12 -1
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +5 -2
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.js +5 -4
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +34 -1
- package/dist/branch-metadata.js +81 -14
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-workspace.d.ts +1 -1
- package/dist/branch-workspace.js +7 -9
- package/dist/cli/cli.js +1813 -812
- package/dist/cli/generate-ai-content.js +914 -359
- package/dist/cli/init.js +153 -5
- package/dist/cli/template-files/cms-stack.ts.template +3 -2
- package/dist/comment-store.d.ts +13 -0
- package/dist/comment-store.js +42 -8
- package/dist/config/helpers.js +2 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +4 -2
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +47 -2
- package/dist/config/types.d.ts +41 -2
- package/dist/config/validation.d.ts +12 -1
- package/dist/config/validation.js +55 -0
- package/dist/content-reader.js +2 -4
- package/dist/content-store.d.ts +5 -1
- package/dist/content-store.js +18 -10
- package/dist/context.js +3 -1
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/Editor.d.ts +3 -1
- package/dist/editor/Editor.js +18 -17
- package/dist/editor/FormRenderer.d.ts +3 -1
- package/dist/editor/FormRenderer.js +11 -3
- package/dist/editor/PreviewFrame.d.ts +11 -3
- package/dist/editor/PreviewFrame.js +9 -6
- package/dist/editor/admin/SystemHealthPanel.js +4 -2
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/components/EditorSidebar.d.ts +4 -5
- package/dist/editor/components/EditorSidebar.js +19 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +1 -0
- package/dist/editor/context/index.js +1 -0
- package/dist/editor/field-props.d.ts +83 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/fields/BlockField.js +2 -1
- package/dist/editor/fields/MarkdownField.d.ts +3 -0
- package/dist/editor/fields/MarkdownField.js +43 -6
- package/dist/editor/fields/ObjectField.js +2 -1
- package/dist/editor/hooks/useCommentSystem.js +3 -2
- package/dist/editor/hooks/useDraftManager.js +23 -8
- package/dist/editor/hooks/useEntryManager.d.ts +5 -1
- package/dist/editor/hooks/useEntryManager.js +5 -3
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/preview-bridge.d.ts +10 -5
- package/dist/editor/preview-bridge.js +41 -14
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/entry-schema-registry.d.ts +2 -2
- package/dist/entry-schema-registry.js +5 -3
- package/dist/git-manager.d.ts +46 -7
- package/dist/git-manager.js +94 -30
- package/dist/github-service.js +2 -1
- package/dist/http/handler.js +31 -4
- package/dist/http/worker-not-ready.d.ts +8 -4
- package/dist/http/worker-not-ready.js +40 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/services.d.ts +16 -2
- package/dist/services.js +94 -23
- package/dist/static/index.d.ts +4 -3
- package/dist/static/index.js +11 -11
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +19 -0
- package/dist/task-queue/worker-status.d.ts +31 -5
- package/dist/task-queue/worker-status.js +61 -6
- package/dist/types.d.ts +16 -2
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-write-lock.js +4 -3
- package/dist/utils/git.d.ts +13 -1
- package/dist/utils/git.js +31 -2
- package/dist/utils/occ-json-write.js +8 -10
- package/dist/utils/provisioning-lock.d.ts +11 -0
- package/dist/utils/provisioning-lock.js +34 -29
- package/dist/validation/field-traversal.d.ts +2 -0
- package/dist/validation/field-traversal.js +2 -1
- package/dist/validation/markdown-safety.d.ts +18 -18
- package/dist/validation/markdown-safety.js +171 -179
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/cms-worker.d.ts +94 -27
- package/dist/worker/cms-worker.js +355 -74
- package/dist/worker/git-sync.d.ts +1 -1
- package/dist/worker/git-sync.js +46 -47
- package/dist/worker/github-mirror.d.ts +90 -0
- package/dist/worker/github-mirror.js +297 -0
- package/dist/worker/history-rewrite.d.ts +3 -4
- package/dist/worker/history-rewrite.js +11 -16
- package/dist/worker/rebase.js +14 -17
- package/dist/worker/remote-git-maintenance.js +2 -1
- package/dist/worker/shared-repo-git.d.ts +77 -0
- package/dist/worker/shared-repo-git.js +422 -0
- package/dist/worker/sparse-cone.js +7 -3
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +81 -103
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ you misunderstood, which is why it is worth resolving the ref rather than guessi
|
|
|
47
47
|
|
|
48
48
|
- Workspaces resolve per mode: `prod` uses `$CANOPYCMS_WORKSPACE_ROOT/content-branches` (default: `/mnt/efs/content-branches`), `dev` uses `.canopy-dev/content-branches/<branch>`.
|
|
49
49
|
- For `prod` mode, you must set `defaultRemoteUrl`. For `dev`, `defaultRemoteUrl` is **optional** - if omitted, a local remote is auto-created at `.canopy-dev/remote.git`.
|
|
50
|
-
- Optionally configure `defaultRemoteName` (default: `origin`) and `defaultBaseBranch` (
|
|
50
|
+
- Optionally configure `defaultRemoteName` (default: `origin`) and `defaultBaseBranch` (unset: the checked-out git branch in dev, the repository's default branch in prod).
|
|
51
51
|
- Git author identity is required for `prod` mode: set `gitBotAuthorName` and `gitBotAuthorEmail` so bot commits can be created reliably.
|
|
52
52
|
- Branch names are sanitized and traversal is blocked before creating directories.
|
|
53
53
|
- Metadata lives at `<workspace>/.canopy-meta/branch.json`; the registry lives at `<branchesRoot>/branches.json` and records the workspaceRoot for each branch.
|
|
@@ -442,7 +442,7 @@ export function PostView({ data }: { data: PostContent }) {
|
|
|
442
442
|
- `data`: The current content data (initial data on first render, then live updates from editor)
|
|
443
443
|
- `isLoading`: Object mirroring your data structure with boolean loading states for reference fields
|
|
444
444
|
- `highlightEnabled`: Boolean indicating if field highlighting is active in the editor
|
|
445
|
-
- `fieldProps`:
|
|
445
|
+
- `fieldProps`: marks the element rendering a field (`data-canopy-path`), with the path typed against your content; `fieldAttrs` and `scopeFieldProps` from `canopycms` thread it through section components (see the root README's Live Preview)
|
|
446
446
|
|
|
447
447
|
**Reference fields and loading states:**
|
|
448
448
|
|
|
@@ -515,7 +515,7 @@ export default defineCanopyConfig({
|
|
|
515
515
|
**How it works:**
|
|
516
516
|
|
|
517
517
|
1. When you create your first branch, CanopyCMS automatically creates a bare git repository at `.canopy-dev/remote.git`
|
|
518
|
-
2. Your current `baseBranch` (
|
|
518
|
+
2. Your current `baseBranch` (unset: the checked-out branch) is pushed to this local remote
|
|
519
519
|
3. Branch workspaces are cloned from this local remote into `.canopy-dev/content-branches/<branch-name>/`
|
|
520
520
|
4. All git operations (push, fetch, etc.) work against the local remote
|
|
521
521
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { loadOrCreateBranchContext } from '../branch-workspace.js';
|
|
6
6
|
import { readsFromCheckout } from '../build-mode.js';
|
|
7
|
+
import { GitManager } from '../git-manager.js';
|
|
8
|
+
import { baseBranchOf } from '../utils/base-branch.js';
|
|
7
9
|
import { detectHeadBranch } from '../utils/git.js';
|
|
8
10
|
/**
|
|
9
11
|
* Resolve the branch root directory for reading content.
|
|
@@ -15,6 +17,11 @@ export async function resolveBranchRoot(config) {
|
|
|
15
17
|
if (readsFromCheckout(config)) {
|
|
16
18
|
return process.cwd();
|
|
17
19
|
}
|
|
20
|
+
// Prod reads an unset base branch from the remote's HEAD, as services does; provisioning
|
|
21
|
+
// needs it even when the active branch is configured.
|
|
22
|
+
const resolved = config.mode === 'prod' && !config.defaultBaseBranch
|
|
23
|
+
? { ...config, defaultBaseBranch: await GitManager.detectBaseBranch(config, 'throw') }
|
|
24
|
+
: config;
|
|
18
25
|
let activeBranch;
|
|
19
26
|
if (config.defaultActiveBranch) {
|
|
20
27
|
activeBranch = config.defaultActiveBranch;
|
|
@@ -23,10 +30,10 @@ export async function resolveBranchRoot(config) {
|
|
|
23
30
|
activeBranch = await detectHeadBranch(process.cwd(), config.defaultBaseBranch ?? 'main');
|
|
24
31
|
}
|
|
25
32
|
else {
|
|
26
|
-
activeBranch =
|
|
33
|
+
activeBranch = baseBranchOf(resolved);
|
|
27
34
|
}
|
|
28
35
|
const context = await loadOrCreateBranchContext({
|
|
29
|
-
config,
|
|
36
|
+
config: resolved,
|
|
30
37
|
branchName: activeBranch,
|
|
31
38
|
mode: config.mode,
|
|
32
39
|
createdBy: 'canopycms-ai',
|
|
@@ -27,6 +27,7 @@ import { branchProvisioningLockName, tryAcquireProvisioningLock } from '../utils
|
|
|
27
27
|
import { withContentWriteLock, ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, } from '../utils/content-write-lock.js';
|
|
28
28
|
import { getErrorMessage, isNodeError, isNotFoundError } from '../utils/error.js';
|
|
29
29
|
import { defineEndpoint } from './route-builder.js';
|
|
30
|
+
import { baseBranchOf } from '../utils/base-branch.js';
|
|
30
31
|
/** [H1] A fresh (< 5 min old) init lock blocks purge -- provisioning may be running. */
|
|
31
32
|
const PROVISIONING_LOCK_FRESH_MS = 5 * 60_000;
|
|
32
33
|
// Mirrors deleteTaskHandler's fileName pattern in admin.ts: conservative
|
|
@@ -65,7 +66,7 @@ const getBranchHealthHandler = async (_gc, ctx, _req) => {
|
|
|
65
66
|
// admin handlers must agree with it or the scan silently looks at the
|
|
66
67
|
// wrong directory.
|
|
67
68
|
const baseRoot = getDefaultBranchBase(ctx.services.config.mode);
|
|
68
|
-
const baseBranchName = ctx.services.config
|
|
69
|
+
const baseBranchName = baseBranchOf(ctx.services.config);
|
|
69
70
|
const contentRootName = ctx.services.config.contentRoot || 'content';
|
|
70
71
|
try {
|
|
71
72
|
const entries = await scanBranchHealth(baseRoot, { baseBranchName, contentRootName });
|
|
@@ -102,7 +103,7 @@ const getBranchHealthHandler = async (_gc, ctx, _req) => {
|
|
|
102
103
|
*/
|
|
103
104
|
const purgeBranchDirHandler = async (_gc, ctx, _req, params) => {
|
|
104
105
|
const baseRoot = getDefaultBranchBase(ctx.services.config.mode);
|
|
105
|
-
const baseBranchName = ctx.services.config
|
|
106
|
+
const baseBranchName = baseBranchOf(ctx.services.config);
|
|
106
107
|
const sanitizedBaseBranchName = sanitizeBranchName(baseBranchName);
|
|
107
108
|
if (params.dirName === sanitizedBaseBranchName) {
|
|
108
109
|
return { ok: false, status: 400, error: 'The base branch directory can never be purged' };
|
|
@@ -412,7 +413,7 @@ async function checkStillCorrupt(dirPath) {
|
|
|
412
413
|
* rebuild) and invalidates any ContentStore already registered in THIS
|
|
413
414
|
* process for the same root.
|
|
414
415
|
*/
|
|
415
|
-
const repairContentDuplicatesHandler = async (_gc, ctx,
|
|
416
|
+
const repairContentDuplicatesHandler = async (_gc, ctx, req, params) => {
|
|
416
417
|
const baseRoot = getDefaultBranchBase(ctx.services.config.mode);
|
|
417
418
|
const dirPath = resolveDirWithinBase(baseRoot, params.dirName);
|
|
418
419
|
if (!dirPath) {
|
|
@@ -485,6 +486,14 @@ const repairContentDuplicatesHandler = async (_gc, ctx, _req, params) => {
|
|
|
485
486
|
: getErrorMessage(err),
|
|
486
487
|
};
|
|
487
488
|
}
|
|
489
|
+
finally {
|
|
490
|
+
// Archived files leave the branch at its next submit, a partial repair's included, so the
|
|
491
|
+
// admin is one of its editors. No `writableBranch` guard can say so: this route names a
|
|
492
|
+
// directory, not a branch.
|
|
493
|
+
if (resolved.length > 0) {
|
|
494
|
+
await ctx.services.recordBranchEditor({ branchRoot: dirPath, baseRoot }, req.user);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
488
497
|
};
|
|
489
498
|
/**
|
|
490
499
|
* Branch directory health scan (healthy/corrupt-metadata/orphan)
|
package/dist/api/admin.d.ts
CHANGED
|
@@ -78,10 +78,10 @@ declare const listAdminTasksParamsSchema: z.ZodObject<{
|
|
|
78
78
|
status: z.ZodEnum<["pending", "processing", "completed", "failed", "corrupt"]>;
|
|
79
79
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
80
80
|
}, "strip", z.ZodTypeAny, {
|
|
81
|
-
status: "failed" | "
|
|
81
|
+
status: "failed" | "pending" | "corrupt" | "processing" | "completed";
|
|
82
82
|
limit?: number | undefined;
|
|
83
83
|
}, {
|
|
84
|
-
status: "failed" | "
|
|
84
|
+
status: "failed" | "pending" | "corrupt" | "processing" | "completed";
|
|
85
85
|
limit?: number | undefined;
|
|
86
86
|
}>;
|
|
87
87
|
export type ListAdminTasksParams = z.infer<typeof listAdminTasksParamsSchema>;
|
|
@@ -89,10 +89,10 @@ declare const deleteTaskParamsSchema: z.ZodObject<{
|
|
|
89
89
|
status: z.ZodEnum<["pending", "failed", "corrupt"]>;
|
|
90
90
|
fileName: z.ZodEffects<z.ZodString, string, string>;
|
|
91
91
|
}, "strip", z.ZodTypeAny, {
|
|
92
|
-
status: "failed" | "
|
|
92
|
+
status: "failed" | "pending" | "corrupt";
|
|
93
93
|
fileName: string;
|
|
94
94
|
}, {
|
|
95
|
-
status: "failed" | "
|
|
95
|
+
status: "failed" | "pending" | "corrupt";
|
|
96
96
|
fileName: string;
|
|
97
97
|
}>;
|
|
98
98
|
export type DeleteTaskParams = z.infer<typeof deleteTaskParamsSchema>;
|
|
@@ -132,10 +132,10 @@ export declare const ADMIN_ROUTES: {
|
|
|
132
132
|
status: z.ZodEnum<["pending", "processing", "completed", "failed", "corrupt"]>;
|
|
133
133
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
134
134
|
}, "strip", z.ZodTypeAny, {
|
|
135
|
-
status: "failed" | "
|
|
135
|
+
status: "failed" | "pending" | "corrupt" | "processing" | "completed";
|
|
136
136
|
limit?: number | undefined;
|
|
137
137
|
}, {
|
|
138
|
-
status: "failed" | "
|
|
138
|
+
status: "failed" | "pending" | "corrupt" | "processing" | "completed";
|
|
139
139
|
limit?: number | undefined;
|
|
140
140
|
}>, undefined, AdminTasksResponse>;
|
|
141
141
|
readonly retryTask: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
@@ -149,10 +149,10 @@ export declare const ADMIN_ROUTES: {
|
|
|
149
149
|
status: z.ZodEnum<["pending", "failed", "corrupt"]>;
|
|
150
150
|
fileName: z.ZodEffects<z.ZodString, string, string>;
|
|
151
151
|
}, "strip", z.ZodTypeAny, {
|
|
152
|
-
status: "failed" | "
|
|
152
|
+
status: "failed" | "pending" | "corrupt";
|
|
153
153
|
fileName: string;
|
|
154
154
|
}, {
|
|
155
|
-
status: "failed" | "
|
|
155
|
+
status: "failed" | "pending" | "corrupt";
|
|
156
156
|
fileName: string;
|
|
157
157
|
}>, undefined, AdminDeleteTaskResponse>;
|
|
158
158
|
};
|
package/dist/api/admin.js
CHANGED
|
@@ -16,6 +16,7 @@ import { getErrorMessage, isNotFoundError, redactCredentials } from '../utils/er
|
|
|
16
16
|
import { getBuildIdentity } from '../build-identity.js';
|
|
17
17
|
import { loadSharp } from '../assets/sharp-loader.js';
|
|
18
18
|
import { ADMIN_BRANCH_HEALTH_ROUTES } from './admin-branch-health.js';
|
|
19
|
+
import { baseBranchOf } from '../utils/base-branch.js';
|
|
19
20
|
/**
|
|
20
21
|
* 60_000 = DEFAULT_LOCK_STALE_MS in worker/cms-worker.ts, hardcoded rather
|
|
21
22
|
* than threaded through config: an adopter who overrides `lockStaleMs` skews
|
|
@@ -91,7 +92,7 @@ async function readSettingsWorkspaceError(ctx) {
|
|
|
91
92
|
async function readBaseSchemaIssues(ctx) {
|
|
92
93
|
try {
|
|
93
94
|
const { config, branchSchemaCache, entrySchemaRegistry } = ctx.services;
|
|
94
|
-
const base = await ctx.getBranchContext(config
|
|
95
|
+
const base = await ctx.getBranchContext(baseBranchOf(config));
|
|
95
96
|
if (!base)
|
|
96
97
|
return [];
|
|
97
98
|
const { issues } = await branchSchemaCache.getSchema(base.branchRoot, entrySchemaRegistry, config.contentRoot || 'content');
|
package/dist/api/branch-merge.js
CHANGED
|
@@ -54,7 +54,7 @@ const markAsMergedHandler = async (gc, ctx, _req, params) => {
|
|
|
54
54
|
// console.error(`CanopyCMS: Failed to delete remote branch ${branchName}:`, err)
|
|
55
55
|
// }
|
|
56
56
|
// }
|
|
57
|
-
// Comments.json is already in the branch workspace at .
|
|
57
|
+
// Comments.json is already in the branch workspace at .canopy-meta/comments.json
|
|
58
58
|
// It will be preserved with the archived branch - no action needed
|
|
59
59
|
return {
|
|
60
60
|
ok: true,
|
|
@@ -17,7 +17,7 @@ const requestChangesHandler = async (gc, ctx, _req, _params) => {
|
|
|
17
17
|
const updated = await meta.save({
|
|
18
18
|
branch: { name: branchContext.branch.name, status: 'editing' },
|
|
19
19
|
});
|
|
20
|
-
// TODO: Optionally record comment in .
|
|
20
|
+
// TODO: Optionally record comment in .canopy-meta/comments.json when comment system is implemented
|
|
21
21
|
return {
|
|
22
22
|
ok: true,
|
|
23
23
|
status: 200,
|
|
@@ -12,6 +12,7 @@ import { isNonFastForwardRejection } from '../utils/git.js';
|
|
|
12
12
|
import { ContentWriteLockBusyError } from '../utils/content-write-lock.js';
|
|
13
13
|
import { submissionEditorFromUser } from '../submission-attribution.js';
|
|
14
14
|
import { NothingToSubmitError } from '../services.js';
|
|
15
|
+
import { baseBranchOf } from '../utils/base-branch.js';
|
|
15
16
|
const getBranchStatusHandler = async (gc, _ctx, _req, _params) => {
|
|
16
17
|
const { branchContext } = gc;
|
|
17
18
|
return { ok: true, status: 200, data: { branch: branchContext.branch } };
|
|
@@ -39,9 +40,8 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
39
40
|
// (merged) branch could be re-submitted by its creator, re-stamping it
|
|
40
41
|
// 'submitted' and overwriting the merged PR's title/body.
|
|
41
42
|
//
|
|
42
|
-
// Fails closed on a missing status
|
|
43
|
-
//
|
|
44
|
-
// `undefined`) -- same as runWritableBranchGuard's unreadable-status arm.
|
|
43
|
+
// Fails closed on a missing status, same as runWritableBranchGuard's
|
|
44
|
+
// unreadable-status arm.
|
|
45
45
|
const status = branchContext.branch.status;
|
|
46
46
|
if (status !== 'editing') {
|
|
47
47
|
return {
|
|
@@ -54,10 +54,16 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
54
54
|
}
|
|
55
55
|
// Commit and push changes
|
|
56
56
|
const submitter = submissionEditorFromUser(req.user);
|
|
57
|
+
const { authPlugin } = ctx;
|
|
57
58
|
let changedPaths;
|
|
59
|
+
let editors;
|
|
58
60
|
try {
|
|
59
61
|
;
|
|
60
|
-
({ changedPaths } = await ctx.services.submitBranch({
|
|
62
|
+
({ changedPaths, editors } = await ctx.services.submitBranch({
|
|
63
|
+
context: branchContext,
|
|
64
|
+
submitter,
|
|
65
|
+
lookupEditor: authPlugin ? (id) => authPlugin.getUserMetadata(id) : undefined,
|
|
66
|
+
}));
|
|
61
67
|
}
|
|
62
68
|
catch (err) {
|
|
63
69
|
if (err instanceof NothingToSubmitError) {
|
|
@@ -114,6 +120,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
114
120
|
const submittedAt = new Date().toISOString();
|
|
115
121
|
const prResult = await syncSubmitPr(ctx, branchContext, {
|
|
116
122
|
submitter,
|
|
123
|
+
editors,
|
|
117
124
|
changedPaths,
|
|
118
125
|
submittedAt,
|
|
119
126
|
});
|
|
@@ -124,7 +131,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
124
131
|
// stays editable; the stamp records that it is now on GitHub, for delete.
|
|
125
132
|
if (prResult.nothingToSubmit) {
|
|
126
133
|
await meta.save({ branch: { name: branchContext.branch.name, ...pushed } });
|
|
127
|
-
const base = branchContext.branch.baseBranch ?? ctx.services.config
|
|
134
|
+
const base = branchContext.branch.baseBranch ?? baseBranchOf(ctx.services.config);
|
|
128
135
|
return {
|
|
129
136
|
ok: false,
|
|
130
137
|
status: 400,
|
package/dist/api/branch.js
CHANGED
|
@@ -16,6 +16,7 @@ import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRA
|
|
|
16
16
|
import { GitManager } from '../git-manager.js';
|
|
17
17
|
import { syncDeleteRemoteBranch } from './github-sync.js';
|
|
18
18
|
import { branchNameSchema, branchParamSchema } from './validators.js';
|
|
19
|
+
import { baseBranchOf } from '../utils/base-branch.js';
|
|
19
20
|
const log = createDebugLogger({ prefix: 'BranchAPI' });
|
|
20
21
|
const createBranchBodySchema = z.object({
|
|
21
22
|
branch: branchNameSchema,
|
|
@@ -300,7 +301,7 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
300
301
|
// Load path permissions from the base branch's JSON file (the resolved
|
|
301
302
|
// fork point — baked into config at service creation; dev-mode git HEAD
|
|
302
303
|
// when not explicitly configured)
|
|
303
|
-
const baseBranch = ctx.services.config
|
|
304
|
+
const baseBranch = baseBranchOf(ctx.services.config);
|
|
304
305
|
const baseBranchContext = await ctx.getBranchContext(baseBranch);
|
|
305
306
|
let pathPermissions = [];
|
|
306
307
|
if (baseBranchContext) {
|
|
@@ -377,7 +378,7 @@ export const listBranchesHandler = async (ctx, req) => {
|
|
|
377
378
|
// but registry branch names are filesystem-sanitized ('claude-foo') — the
|
|
378
379
|
// editor matches defaultBranch against registry names, so return the form
|
|
379
380
|
// that can actually be found there.
|
|
380
|
-
const defaultBranch = sanitizeBranchName(ctx.services.config.defaultActiveBranch ?? ctx.services.config
|
|
381
|
+
const defaultBranch = sanitizeBranchName(ctx.services.config.defaultActiveBranch ?? baseBranchOf(ctx.services.config));
|
|
381
382
|
// Admins and Reviewers see all branches
|
|
382
383
|
if (isPrivileged(req.user.groups)) {
|
|
383
384
|
return {
|
|
@@ -551,7 +552,7 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
551
552
|
const sanitizedDeleted = sanitizeBranchName(branchContext.branch.name);
|
|
552
553
|
// Defense-in-depth only -- isProtected above already rejects the base
|
|
553
554
|
// branch, and settings branches never resolve through getBranchContext.
|
|
554
|
-
const sanitizedBase = sanitizeBranchName(branchContext.branch.baseBranch ?? ctx.services.config
|
|
555
|
+
const sanitizedBase = sanitizeBranchName(branchContext.branch.baseBranch ?? baseBranchOf(ctx.services.config));
|
|
555
556
|
const deletableHead = sanitizedDeleted !== sanitizedBase &&
|
|
556
557
|
!sanitizedDeleted.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX);
|
|
557
558
|
if (deletableHead &&
|
package/dist/api/comments.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ApiResponse } from './types.js';
|
|
3
3
|
import type { CommentThread, CommentType } from '../comment-store.js';
|
|
4
|
+
import type { LogicalPath } from '../paths/types.js';
|
|
4
5
|
export interface AddCommentBody {
|
|
5
6
|
text: string;
|
|
6
7
|
threadId?: string;
|
|
@@ -48,18 +49,18 @@ export declare const COMMENT_ROUTES: {
|
|
|
48
49
|
text: z.ZodString;
|
|
49
50
|
threadId: z.ZodOptional<z.ZodString>;
|
|
50
51
|
type: z.ZodEnum<["field", "entry", "branch"]>;
|
|
51
|
-
entryPath: z.ZodOptional<z.ZodType<
|
|
52
|
+
entryPath: z.ZodOptional<z.ZodType<LogicalPath, z.ZodTypeDef, LogicalPath>>;
|
|
52
53
|
canopyPath: z.ZodOptional<z.ZodString>;
|
|
53
54
|
}, "strip", z.ZodTypeAny, {
|
|
54
55
|
type: "branch" | "entry" | "field";
|
|
55
56
|
text: string;
|
|
56
|
-
entryPath?:
|
|
57
|
+
entryPath?: LogicalPath | undefined;
|
|
57
58
|
threadId?: string | undefined;
|
|
58
59
|
canopyPath?: string | undefined;
|
|
59
60
|
}, {
|
|
60
61
|
type: "branch" | "entry" | "field";
|
|
61
62
|
text: string;
|
|
62
|
-
entryPath?:
|
|
63
|
+
entryPath?: LogicalPath | undefined;
|
|
63
64
|
threadId?: string | undefined;
|
|
64
65
|
canopyPath?: string | undefined;
|
|
65
66
|
}>, AddCommentResponse>;
|
package/dist/api/comments.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CommentStore } from '../comment-store.js';
|
|
3
3
|
import { isReviewer } from '../authorization/index.js';
|
|
4
|
+
import { parseLogicalPath } from '../paths/validation.js';
|
|
5
|
+
import { normalizeFilesystemPath } from '../paths/normalize.js';
|
|
4
6
|
import { defineEndpoint } from './route-builder.js';
|
|
5
7
|
import { branchParamSchema, branchNameSchema, logicalPathSchema } from './validators.js';
|
|
6
8
|
// ============================================================================
|
|
@@ -17,13 +19,47 @@ const addCommentBodySchema = z.object({
|
|
|
17
19
|
entryPath: logicalPathSchema.optional(),
|
|
18
20
|
canopyPath: z.string().optional(), // Canopy field path, not a file path
|
|
19
21
|
});
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Whether `entryPath` is spelled the way path rules expect an entry's logical path: rooted at
|
|
24
|
+
* the content root, with no empty or `.` segment. Rules are globs over that spelling, so
|
|
25
|
+
* `secret/plan` (which the content API reads as `content/secret/plan`) and
|
|
26
|
+
* `content/./secret/plan` both miss a rule written for `content/secret/**`.
|
|
27
|
+
*/
|
|
28
|
+
const isCanonicalEntryPath = (entryPath, contentRoot) => entryPath.startsWith(`${normalizeFilesystemPath(contentRoot)}/`) &&
|
|
29
|
+
entryPath.split('/').every((segment) => segment !== '' && segment !== '.');
|
|
30
|
+
/**
|
|
31
|
+
* Whether the user may see a thread: its entry's path rules at `read`, the level a content
|
|
32
|
+
* read needs, so a thread discloses nothing about an entry its reader could not open. A
|
|
33
|
+
* field thread is governed by its entry. A thread with no entry path (a branch thread) is
|
|
34
|
+
* governed by branch access alone, which the `branchAccess` guard has already passed. An
|
|
35
|
+
* entry path that is not a canonical logical path is denied: comments.json is read without
|
|
36
|
+
* schema validation.
|
|
37
|
+
*/
|
|
38
|
+
const canReadThreadEntry = (checkAccess, contentRoot, entryPath) => {
|
|
39
|
+
if (entryPath === undefined)
|
|
40
|
+
return true;
|
|
41
|
+
const parsed = parseLogicalPath(entryPath);
|
|
42
|
+
return (parsed.ok &&
|
|
43
|
+
isCanonicalEntryPath(parsed.path, contentRoot) &&
|
|
44
|
+
checkAccess(parsed.path, 'read').allowed);
|
|
45
|
+
};
|
|
46
|
+
/** One checker per request, bound to the content root its paths are canonical against. */
|
|
47
|
+
const createThreadReadCheck = async (ctx, req, branchContext) => {
|
|
48
|
+
const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
|
|
49
|
+
const contentRoot = ctx.services.config.contentRoot || 'content';
|
|
50
|
+
return (entryPath) => canReadThreadEntry(checkAccess, contentRoot, entryPath);
|
|
51
|
+
};
|
|
52
|
+
const listCommentsHandler = async (gc, ctx, req, _params) => {
|
|
21
53
|
const { branchContext } = gc;
|
|
22
54
|
const commentStore = new CommentStore(branchContext.branchRoot);
|
|
23
|
-
const
|
|
55
|
+
const [allThreads, canRead] = await Promise.all([
|
|
56
|
+
commentStore.listThreads({ includeResolved: true }),
|
|
57
|
+
createThreadReadCheck(ctx, req, branchContext),
|
|
58
|
+
]);
|
|
59
|
+
const threads = allThreads.filter((thread) => canRead(thread.entryPath));
|
|
24
60
|
return { ok: true, status: 200, data: { threads } };
|
|
25
61
|
};
|
|
26
|
-
const addCommentHandler = async (gc,
|
|
62
|
+
const addCommentHandler = async (gc, ctx, req, _params, body) => {
|
|
27
63
|
const { branchContext } = gc;
|
|
28
64
|
// Validate required fields based on type
|
|
29
65
|
if (body.type === 'field' && !body.canopyPath) {
|
|
@@ -40,22 +76,50 @@ const addCommentHandler = async (gc, _ctx, req, _params, body) => {
|
|
|
40
76
|
error: 'entryPath required for field/entry comments',
|
|
41
77
|
};
|
|
42
78
|
}
|
|
79
|
+
// Commenting needs read access to the entry the comment lands on. A reply lands on the
|
|
80
|
+
// stored thread, so it is checked against that thread's entry path, which never changes
|
|
81
|
+
// once the thread exists; a reply to a thread the user cannot read answers as missing, as
|
|
82
|
+
// resolve does.
|
|
43
83
|
const commentStore = new CommentStore(branchContext.branchRoot);
|
|
84
|
+
const [existing, canRead] = await Promise.all([
|
|
85
|
+
body.threadId ? commentStore.getThread(body.threadId) : Promise.resolve(null),
|
|
86
|
+
createThreadReadCheck(ctx, req, branchContext),
|
|
87
|
+
]);
|
|
88
|
+
if (body.threadId) {
|
|
89
|
+
if (!existing || !canRead(existing.entryPath)) {
|
|
90
|
+
return { ok: false, status: 404, error: 'Thread not found' };
|
|
91
|
+
}
|
|
92
|
+
const reply = await commentStore.addReply({
|
|
93
|
+
userId: req.user.userId,
|
|
94
|
+
text: body.text,
|
|
95
|
+
threadId: body.threadId,
|
|
96
|
+
});
|
|
97
|
+
if (!reply) {
|
|
98
|
+
return { ok: false, status: 404, error: 'Thread not found' };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, status: 201, data: reply };
|
|
101
|
+
}
|
|
102
|
+
if (!canRead(body.entryPath)) {
|
|
103
|
+
return { ok: false, status: 403, error: 'Forbidden' };
|
|
104
|
+
}
|
|
44
105
|
const result = await commentStore.addComment({
|
|
45
106
|
userId: req.user.userId,
|
|
46
107
|
text: body.text,
|
|
47
|
-
threadId: body.threadId,
|
|
48
108
|
type: body.type,
|
|
49
109
|
entryPath: body.entryPath,
|
|
50
110
|
canopyPath: body.canopyPath,
|
|
51
111
|
});
|
|
52
112
|
return { ok: true, status: 201, data: result };
|
|
53
113
|
};
|
|
54
|
-
const resolveCommentHandler = async (gc,
|
|
114
|
+
const resolveCommentHandler = async (gc, ctx, req, params) => {
|
|
55
115
|
const { branchContext } = gc;
|
|
56
116
|
const commentStore = new CommentStore(branchContext.branchRoot);
|
|
57
|
-
const thread = await
|
|
58
|
-
|
|
117
|
+
const [thread, canRead] = await Promise.all([
|
|
118
|
+
commentStore.getThread(params.threadId),
|
|
119
|
+
createThreadReadCheck(ctx, req, branchContext),
|
|
120
|
+
]);
|
|
121
|
+
// A thread the user cannot read answers as missing, so its id does not confirm it exists.
|
|
122
|
+
if (!thread || !canRead(thread.entryPath)) {
|
|
59
123
|
return { ok: false, status: 404, error: 'Thread not found' };
|
|
60
124
|
}
|
|
61
125
|
// Check permissions: thread author, reviewer, or admin
|
package/dist/api/content.d.ts
CHANGED
|
@@ -104,14 +104,14 @@ export declare const CONTENT_ROUTES: {
|
|
|
104
104
|
expectedVersion: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
105
105
|
}, "strip", z.ZodTypeAny, {
|
|
106
106
|
format: "mdx" | "md" | "json" | "yaml";
|
|
107
|
+
data?: Record<string, unknown> | undefined;
|
|
107
108
|
expectedVersion?: number | null | undefined;
|
|
108
109
|
body?: string | undefined;
|
|
109
|
-
data?: Record<string, unknown> | undefined;
|
|
110
110
|
}, {
|
|
111
111
|
format: "mdx" | "md" | "json" | "yaml";
|
|
112
|
+
data?: Record<string, unknown> | undefined;
|
|
112
113
|
expectedVersion?: number | null | undefined;
|
|
113
114
|
body?: string | undefined;
|
|
114
|
-
data?: Record<string, unknown> | undefined;
|
|
115
115
|
}>, ContentWriteResponse>;
|
|
116
116
|
readonly validateReferences: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
117
117
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
package/dist/api/content.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { ENTRY_CHANGED_MESSAGE } from './entries-constants.js';
|
|
2
3
|
import { BranchSyncingError, ContentStore, ContentStoreError, ContentConflictError, DuplicateContentIdError, UrlPathConflictError, getDefaultEntryType, } from '../content-store.js';
|
|
3
4
|
import { defineEndpoint } from './route-builder.js';
|
|
4
5
|
import { SchemaUnavailableError } from '../schema/schema-unavailable-error.js';
|
|
@@ -132,7 +133,7 @@ function withoutKey(record, key) {
|
|
|
132
133
|
return rest;
|
|
133
134
|
}
|
|
134
135
|
/** The policy's issues in the stored entry, which a save may keep. */
|
|
135
|
-
const storedMarkdownSafetyIssues = async (store, collectionPath, slug, fields) => {
|
|
136
|
+
const storedMarkdownSafetyIssues = async (store, collectionPath, slug, fields, siteAllow) => {
|
|
136
137
|
// A stored file that does not read keeps nothing, so the save's code is refused, not a 500.
|
|
137
138
|
const doc = await store
|
|
138
139
|
.read(collectionPath, slug, { resolveReferences: false })
|
|
@@ -140,7 +141,7 @@ const storedMarkdownSafetyIssues = async (store, collectionPath, slug, fields) =
|
|
|
140
141
|
if (doc === undefined)
|
|
141
142
|
return [];
|
|
142
143
|
const data = 'body' in doc ? mergeBodyIntoData(fields, doc.data, doc.body) : doc.data;
|
|
143
|
-
return findMarkdownSafetyIssues(fields, doc.format, data);
|
|
144
|
+
return findMarkdownSafetyIssues(fields, doc.format, data, siteAllow);
|
|
144
145
|
};
|
|
145
146
|
const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
146
147
|
const { branchContext } = gc;
|
|
@@ -302,10 +303,11 @@ const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
|
302
303
|
const fieldErrors = validateEntryData(fields, dataForValidation);
|
|
303
304
|
// Code in markdown or MDX (validation/markdown-safety.ts). A field the stored entry held
|
|
304
305
|
// with code is kept, with a warning, when saved unchanged; any other code is refused.
|
|
305
|
-
const
|
|
306
|
+
const { mdxAllow } = ctx.services.config;
|
|
307
|
+
const unsafe = findMarkdownSafetyIssues(fields, body.format, dataForValidation, mdxAllow);
|
|
306
308
|
if (unsafe.length > 0) {
|
|
307
309
|
const stored = exists
|
|
308
|
-
? await storedMarkdownSafetyIssues(store, schemaItem.logicalPath, slug, fields)
|
|
310
|
+
? await storedMarkdownSafetyIssues(store, schemaItem.logicalPath, slug, fields, mdxAllow)
|
|
309
311
|
: [];
|
|
310
312
|
const { refused, kept } = splitByStored(unsafe, stored);
|
|
311
313
|
fieldErrors.push(...refused);
|
|
@@ -313,7 +315,7 @@ const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
|
313
315
|
keptWarnings.push({
|
|
314
316
|
level: 'warning',
|
|
315
317
|
fieldPath: e.fieldPath,
|
|
316
|
-
message: `holds
|
|
318
|
+
message: `holds content this field refuses, kept because the saved entry already had it: ${e.message}. Ask a developer to move it into a component, or to allow it.`,
|
|
317
319
|
});
|
|
318
320
|
}
|
|
319
321
|
}
|
|
@@ -479,7 +481,12 @@ const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
|
479
481
|
// refused outright, but a lock compromised mid-write can still land it — each case carries
|
|
480
482
|
// its own message, so pass `err.message` through rather than the generic conflict below.
|
|
481
483
|
if (err instanceof BranchSyncingError) {
|
|
482
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
status: 409,
|
|
487
|
+
error: err.message,
|
|
488
|
+
...(err.outcome === 'unknown' ? { code: 'WRITE_OUTCOME_UNKNOWN' } : {}),
|
|
489
|
+
};
|
|
483
490
|
}
|
|
484
491
|
// [F1] Also not an editor collision: this content ID is quarantined on two files
|
|
485
492
|
// (ContentIdIndex's duplicate-ID detection). The generic message below would send the
|
|
@@ -499,11 +506,7 @@ const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
|
499
506
|
if (expectedVersion === null) {
|
|
500
507
|
return { ok: false, status: 409, error: createConflictError };
|
|
501
508
|
}
|
|
502
|
-
return {
|
|
503
|
-
ok: false,
|
|
504
|
-
status: 409,
|
|
505
|
-
error: 'Content conflict: entry was modified by another editor',
|
|
506
|
-
};
|
|
509
|
+
return { ok: false, status: 409, error: ENTRY_CHANGED_MESSAGE };
|
|
507
510
|
}
|
|
508
511
|
// C2: a ContentStoreError is an expected client fault (bad slug, validation, etc.) and keeps
|
|
509
512
|
// its 400. Anything else — ENOSPC, EACCES, a bug — is a genuine server fault and must not be
|
|
@@ -623,7 +626,7 @@ const renameEntryHandler = async (gc, ctx, req, params, body) => {
|
|
|
623
626
|
return {
|
|
624
627
|
ok: false,
|
|
625
628
|
status: 409,
|
|
626
|
-
error: passThrough ? err.message :
|
|
629
|
+
error: passThrough ? err.message : ENTRY_CHANGED_MESSAGE,
|
|
627
630
|
};
|
|
628
631
|
}
|
|
629
632
|
// C2: same rule as writeContentHandler's catch above (ContentStoreError -> 400, everything
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* imports) so the editor's browser bundle can import
|
|
4
|
-
* `entries.ts` and its `node:fs`-backed deps
|
|
2
|
+
* Constants the entries and content endpoints share with the editor. Kept dependency-free (no
|
|
3
|
+
* server-only imports) so the editor's browser bundle can import them without pulling in
|
|
4
|
+
* `entries.ts` and its `node:fs`-backed deps.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Maximum entries the list endpoint returns per request; larger `limit` values are clamped to
|
|
@@ -10,3 +10,8 @@
|
|
|
10
10
|
export declare const MAX_ENTRIES_PER_PAGE = 200;
|
|
11
11
|
/** Default page size when a request omits `limit`. */
|
|
12
12
|
export declare const DEFAULT_ENTRIES_LIMIT = 50;
|
|
13
|
+
/**
|
|
14
|
+
* The generic 409 for a conflicting change to an entry, usually its version moving on since the
|
|
15
|
+
* caller read it. Shared with the editor, whose own pre-save staleness check reports the same.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ENTRY_CHANGED_MESSAGE = "This entry changed since you opened it. Reload to see the latest version (your unsaved edits will be lost).";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* imports) so the editor's browser bundle can import
|
|
4
|
-
* `entries.ts` and its `node:fs`-backed deps
|
|
2
|
+
* Constants the entries and content endpoints share with the editor. Kept dependency-free (no
|
|
3
|
+
* server-only imports) so the editor's browser bundle can import them without pulling in
|
|
4
|
+
* `entries.ts` and its `node:fs`-backed deps.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Maximum entries the list endpoint returns per request; larger `limit` values are clamped to
|
|
@@ -10,3 +10,8 @@
|
|
|
10
10
|
export const MAX_ENTRIES_PER_PAGE = 200;
|
|
11
11
|
/** Default page size when a request omits `limit`. */
|
|
12
12
|
export const DEFAULT_ENTRIES_LIMIT = 50;
|
|
13
|
+
/**
|
|
14
|
+
* The generic 409 for a conflicting change to an entry, usually its version moving on since the
|
|
15
|
+
* caller read it. Shared with the editor, whose own pre-save staleness check reports the same.
|
|
16
|
+
*/
|
|
17
|
+
export const ENTRY_CHANGED_MESSAGE = 'This entry changed since you opened it. Reload to see the latest version (your unsaved edits will be lost).';
|
package/dist/api/entries.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getErrorMessage, isNotFoundError, sanitizeErrorMessage } from '../utils
|
|
|
7
7
|
import { createDebugLogger } from '../utils/debug.js';
|
|
8
8
|
import { resolveEntryTitle } from '../utils/title-field.js';
|
|
9
9
|
import { branchNameSchema, logicalPathSchema, queryBooleanSchema } from './validators.js';
|
|
10
|
-
import { MAX_ENTRIES_PER_PAGE, DEFAULT_ENTRIES_LIMIT } from './entries-constants.js';
|
|
10
|
+
import { MAX_ENTRIES_PER_PAGE, DEFAULT_ENTRIES_LIMIT, ENTRY_CHANGED_MESSAGE, } from './entries-constants.js';
|
|
11
11
|
import { SchemaOps, SchemaStoreBusyError } from '../schema/schema-store.js';
|
|
12
12
|
import { listCollectionEntries as listCollectionEntriesShared, listEntries as listContentEntries, sortByOrder, } from '../content-listing.js';
|
|
13
13
|
import { findReferencingEntries } from '../validation/deletion-checker.js';
|
|
@@ -416,9 +416,7 @@ const deleteEntryHandler = async (gc, ctx, req, params) => {
|
|
|
416
416
|
return {
|
|
417
417
|
ok: false,
|
|
418
418
|
status: 409,
|
|
419
|
-
error: err instanceof BranchSyncingError
|
|
420
|
-
? err.message
|
|
421
|
-
: 'Content conflict: entry was modified by another editor',
|
|
419
|
+
error: err instanceof BranchSyncingError ? err.message : ENTRY_CHANGED_MESSAGE,
|
|
422
420
|
};
|
|
423
421
|
}
|
|
424
422
|
return {
|
|
@@ -18,6 +18,8 @@ export interface GitHubSyncResult {
|
|
|
18
18
|
/** What the PR body records about a submit. */
|
|
19
19
|
export interface SubmissionRecord {
|
|
20
20
|
submitter?: SubmissionEditor;
|
|
21
|
+
/** Every recorded editor of the branch; the PR body lists those other than the submitter. */
|
|
22
|
+
editors?: readonly SubmissionEditor[];
|
|
21
23
|
changedPaths: readonly string[];
|
|
22
24
|
/** The submit's `submittedAt`, carried to the worker so its failure handling targets this submit. */
|
|
23
25
|
submittedAt?: string;
|
package/dist/api/github-sync.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getErrorMessage, sanitizeErrorMessage } from '../utils/error.js';
|
|
|
5
5
|
import { sanitizeBranchName } from '../paths/branch-name.js';
|
|
6
6
|
import { buildPrSection, mergePrSection } from '../submission-attribution.js';
|
|
7
7
|
import { isNoCommitsBetweenError, isRefAlreadyGoneError } from '../github-service.js';
|
|
8
|
+
import { baseBranchOf } from '../utils/base-branch.js';
|
|
8
9
|
/**
|
|
9
10
|
* Uses githubService directly if available, otherwise queues a task for the worker.
|
|
10
11
|
*
|
|
@@ -18,10 +19,11 @@ export async function syncSubmitPr(ctx, context, submission) {
|
|
|
18
19
|
const prSection = buildPrSection({
|
|
19
20
|
description: context.branch.description,
|
|
20
21
|
submitter: submission.submitter,
|
|
22
|
+
editors: submission.editors,
|
|
21
23
|
changedPaths: submission.changedPaths,
|
|
22
24
|
});
|
|
23
25
|
// Target the fork point recorded at branch creation when available.
|
|
24
|
-
const baseBranch = context.branch.baseBranch ?? ctx.services.config
|
|
26
|
+
const baseBranch = context.branch.baseBranch ?? baseBranchOf(ctx.services.config);
|
|
25
27
|
if (!clientOperatingStrategy(mode).supportsPullRequests()) {
|
|
26
28
|
return {};
|
|
27
29
|
}
|
package/dist/api/groups.js
CHANGED