canopycms 0.0.68-int.113 → 0.0.68-int.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/generate.d.ts +4 -4
- package/dist/ai/generate.js +75 -14
- package/dist/ai/types.d.ts +12 -0
- package/dist/api/branch.js +13 -13
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +11 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +2 -1
- package/dist/cli/cli.js +160 -28
- package/dist/cli/generate-ai-content.js +156 -24
- package/dist/content-reader.js +2 -1
- package/dist/context.js +2 -1
- package/dist/editor/BranchManager.js +13 -17
- package/dist/editor/CommentsPanel.js +7 -6
- package/dist/editor/Editor.js +152 -30
- package/dist/editor/EditorAuthGate.js +2 -2
- package/dist/editor/EditorPanes.d.ts +10 -0
- package/dist/editor/EditorPanes.js +32 -8
- package/dist/editor/FormRenderer.d.ts +10 -0
- package/dist/editor/FormRenderer.js +92 -73
- package/dist/editor/PreviewFrame.d.ts +3 -1
- package/dist/editor/PreviewFrame.js +72 -9
- package/dist/editor/admin/SystemHealthPanel.js +27 -24
- package/dist/editor/branch-status.d.ts +16 -0
- package/dist/editor/branch-status.js +28 -0
- package/dist/editor/comments/BranchComments.js +1 -1
- package/dist/editor/comments/EntryComments.d.ts +3 -0
- package/dist/editor/comments/EntryComments.js +3 -3
- package/dist/editor/comments/FieldWrapper.d.ts +3 -0
- package/dist/editor/comments/FieldWrapper.js +2 -2
- package/dist/editor/comments/InlineCommentThread.js +2 -1
- package/dist/editor/comments/ThreadCarousel.js +2 -2
- package/dist/editor/components/ConfirmDeleteModal.js +1 -1
- package/dist/editor/components/EditorCrashScreen.js +1 -1
- package/dist/editor/components/EditorHeader.d.ts +2 -0
- package/dist/editor/components/EditorHeader.js +15 -36
- package/dist/editor/components/EditorSidebar.d.ts +5 -6
- package/dist/editor/components/EditorSidebar.js +2 -2
- package/dist/editor/components/EntryCreateModal.js +1 -1
- package/dist/editor/components/NoEditPermissionNotice.d.ts +3 -2
- package/dist/editor/components/NoEditPermissionNotice.js +3 -2
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/RenameEntryModal.js +1 -1
- package/dist/editor/components/StagedChangesDrawer.js +3 -3
- package/dist/editor/components/UndoToastMessage.d.ts +14 -0
- package/dist/editor/components/UndoToastMessage.js +9 -0
- package/dist/editor/components/UserBadge.js +9 -5
- package/dist/editor/copy.d.ts +14 -0
- package/dist/editor/copy.js +14 -0
- package/dist/editor/fields/BlockField.d.ts +5 -0
- package/dist/editor/fields/BlockField.js +38 -32
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +5 -2
- package/dist/editor/fields/DateTimeField.d.ts +2 -0
- package/dist/editor/fields/DateTimeField.js +5 -2
- package/dist/editor/fields/FieldCrashFallback.d.ts +3 -3
- package/dist/editor/fields/FieldCrashFallback.js +4 -4
- package/dist/editor/fields/FieldLabel.d.ts +24 -0
- package/dist/editor/fields/FieldLabel.js +9 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +17 -8
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/MarkdownField.d.ts +6 -0
- package/dist/editor/fields/MarkdownField.js +41 -11
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +4 -2
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +4 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -4
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +9 -3
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +8 -2
- package/dist/editor/fields/StringListField.d.ts +2 -0
- package/dist/editor/fields/StringListField.js +5 -2
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +5 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +6 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/group-manager/GroupCard.js +1 -1
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +1 -1
- package/dist/editor/group-manager/MemberList.js +1 -1
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +1 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +1 -1
- package/dist/editor/group-manager/index.js +26 -2
- package/dist/editor/hooks/useBranchActions.js +2 -2
- package/dist/editor/hooks/useBranchManager.js +36 -22
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.js +27 -5
- package/dist/editor/hooks/useEditorLayout.d.ts +45 -3
- package/dist/editor/hooks/useEditorLayout.js +97 -7
- package/dist/editor/hooks/useUserMetadata.js +12 -1
- package/dist/editor/media/AssetCard.js +1 -1
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibraryBody.js +3 -3
- package/dist/editor/permission-manager/PermissionEditor.js +3 -3
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.js +2 -2
- package/dist/editor/schema-editor/CollectionEditor.js +1 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +1 -1
- package/dist/editor/theme.d.ts +1 -1
- package/dist/editor/theme.js +11 -11
- package/dist/editor/undo-removal.d.ts +44 -0
- package/dist/editor/undo-removal.js +129 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/git-manager.d.ts +6 -6
- package/dist/git-manager.js +16 -15
- package/dist/github-service.d.ts +25 -1
- package/dist/github-service.js +65 -6
- package/dist/http/handler.js +2 -1
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/types.d.ts +9 -1
- package/dist/utils/error.js +4 -3
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +68 -17
- package/dist/worker/cms-worker.js +116 -33
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +8 -7
- package/dist/worker/git-sync.js +184 -74
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +589 -0
- package/dist/worker/github-gateway-wire.js +484 -0
- package/dist/worker/github-gateway.d.ts +152 -58
- package/dist/worker/github-gateway.js +485 -129
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +271 -71
- package/dist/worker/github-mirror.js +755 -238
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +20 -7
- package/dist/worker/shared-repo-git.js +51 -22
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +2 -0
- package/dist/worker/task-runner.js +49 -20
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
package/dist/ai/generate.d.ts
CHANGED
|
@@ -43,9 +43,9 @@ export interface GenerateResult {
|
|
|
43
43
|
* and produces per-entry files, per-collection all.md files,
|
|
44
44
|
* bundle files, and a manifest.
|
|
45
45
|
*
|
|
46
|
-
* A reference may only show a target that is itself exported, so every
|
|
47
|
-
* filtered first, which settles the exported set. Only then are
|
|
48
|
-
*
|
|
49
|
-
* fails (see the loop below).
|
|
46
|
+
* A reference or body `entry:` link may only show a target that is itself exported, so every
|
|
47
|
+
* entry is read and filtered first, which settles the exported set. Only then are body links
|
|
48
|
+
* resolved for output, references to anything outside the set masked, entry transforms run, and markdown
|
|
49
|
+
* rendered, in rounds that repeat while a render fails (see the loop below).
|
|
50
50
|
*/
|
|
51
51
|
export declare function generateAIContent(options: GenerateOptions): Promise<GenerateResult>;
|
package/dist/ai/generate.js
CHANGED
|
@@ -15,6 +15,7 @@ import { getErrorMessage, isNodeError } from '../utils/error.js';
|
|
|
15
15
|
import { entryToMarkdown, frontmatterTitle } from './json-to-markdown.js';
|
|
16
16
|
import { createReferenceRendering, createReferenceTargetResolver, maskUnexportedTargets, resolveReferenceFields, } from './references.js';
|
|
17
17
|
import { resolveEntryLinksInText } from '../entry-link-resolver.js';
|
|
18
|
+
import { DEFAULT_SEO_FIELD_NAMES, isNoindexEntry } from '../static/seo.js';
|
|
18
19
|
/**
|
|
19
20
|
* Generate all AI content from the content store.
|
|
20
21
|
*
|
|
@@ -22,13 +23,14 @@ import { resolveEntryLinksInText } from '../entry-link-resolver.js';
|
|
|
22
23
|
* and produces per-entry files, per-collection all.md files,
|
|
23
24
|
* bundle files, and a manifest.
|
|
24
25
|
*
|
|
25
|
-
* A reference may only show a target that is itself exported, so every
|
|
26
|
-
* filtered first, which settles the exported set. Only then are
|
|
27
|
-
*
|
|
28
|
-
* fails (see the loop below).
|
|
26
|
+
* A reference or body `entry:` link may only show a target that is itself exported, so every
|
|
27
|
+
* entry is read and filtered first, which settles the exported set. Only then are body links
|
|
28
|
+
* resolved for output, references to anything outside the set masked, entry transforms run, and markdown
|
|
29
|
+
* rendered, in rounds that repeat while a render fails (see the loop below).
|
|
29
30
|
*/
|
|
30
31
|
export async function generateAIContent(options) {
|
|
31
32
|
const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
|
|
33
|
+
assertValidNoindexConfig(config);
|
|
32
34
|
const files = new Map();
|
|
33
35
|
const idIndex = await store.idIndex();
|
|
34
36
|
const read = {
|
|
@@ -69,13 +71,23 @@ export async function generateAIContent(options) {
|
|
|
69
71
|
mountPath: config?.mountPath ?? '/ai',
|
|
70
72
|
files: exported,
|
|
71
73
|
});
|
|
72
|
-
// An entry whose render throws is not in the export after all, so references
|
|
73
|
-
// like any other left-out target, and the entries that changed run their
|
|
74
|
-
// again. That can fail another entry, so this repeats until a round fails
|
|
75
|
-
// removes at least one entry, so it ends.
|
|
74
|
+
// An entry whose render throws is not in the export after all, so references and body links to
|
|
75
|
+
// it are withheld like any other left-out target, and the entries that changed run their
|
|
76
|
+
// transforms and render again. That can fail another entry, so this repeats until a round fails
|
|
77
|
+
// none; each round removes at least one entry, so it ends.
|
|
76
78
|
let round = allPending;
|
|
77
79
|
for (;;) {
|
|
78
80
|
for (const pending of round) {
|
|
81
|
+
if (pending.body !== undefined) {
|
|
82
|
+
const linked = new Set();
|
|
83
|
+
pending.entry.body = resolveEntryLinksInText(pending.body, idIndex, contentRoot, entryLinkUrl, {
|
|
84
|
+
isLinkable: (id) => {
|
|
85
|
+
linked.add(id);
|
|
86
|
+
return exported.has(id);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
pending.linkedIds = [...linked];
|
|
90
|
+
}
|
|
79
91
|
maskUnexportedTargets(pending.entry, exported);
|
|
80
92
|
// Fold in adopter-supplied markdown (e.g. a colocated sibling artifact), on data as masked
|
|
81
93
|
pending.entry.appendedSections = undefined;
|
|
@@ -84,11 +96,19 @@ export async function generateAIContent(options) {
|
|
|
84
96
|
const failed = renderAll(round, config, references);
|
|
85
97
|
if (failed.length === 0)
|
|
86
98
|
break;
|
|
99
|
+
const failedIds = new Set();
|
|
87
100
|
for (const pending of failed) {
|
|
88
|
-
if (pending.contentId
|
|
89
|
-
|
|
101
|
+
if (pending.contentId === null)
|
|
102
|
+
continue;
|
|
103
|
+
exported.delete(pending.contentId);
|
|
104
|
+
failedIds.add(pending.contentId);
|
|
90
105
|
}
|
|
91
|
-
round = allPending.filter((pending) =>
|
|
106
|
+
round = allPending.filter((pending) => {
|
|
107
|
+
if (!isRendered(pending))
|
|
108
|
+
return false;
|
|
109
|
+
const masked = maskUnexportedTargets(pending.entry, exported);
|
|
110
|
+
return masked || pending.linkedIds.some((id) => failedIds.has(id));
|
|
111
|
+
});
|
|
92
112
|
}
|
|
93
113
|
const manifestCollections = collectionNodes.map((node) => emitCollection(node, files));
|
|
94
114
|
const manifestRootEntries = emitEntries(rootEntries, files);
|
|
@@ -166,6 +186,7 @@ async function collectCollection(read, collection, flatSchema) {
|
|
|
166
186
|
/** Read, resolve and filter the entries directly in `collection` (not its subcollections). */
|
|
167
187
|
async function collectEntries(read, collection, cleanPath) {
|
|
168
188
|
const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget } = read;
|
|
189
|
+
const skipNoindex = config?.exclude?.noindex !== false;
|
|
169
190
|
const pending = [];
|
|
170
191
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
171
192
|
// Filter to only entries in this exact collection (not subcollections)
|
|
@@ -183,15 +204,21 @@ async function collectEntries(read, collection, cleanPath) {
|
|
|
183
204
|
const doc = await store.read(listEntry.collection, listEntry.slug, {
|
|
184
205
|
resolveReferences: false,
|
|
185
206
|
});
|
|
207
|
+
// AI content advertises an entry, so `noindex` leaves it out here as it does the sitemap.
|
|
208
|
+
if (skipNoindex && isNoindexEntry(doc.data, config?.seo))
|
|
209
|
+
continue;
|
|
186
210
|
doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget);
|
|
187
211
|
const aiEntry = docToAIEntry(doc, listEntry.slug, entryTypeName, entryTypeConfig, cleanPath);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
212
|
+
const body = aiEntry.body;
|
|
213
|
+
// `where` sees every link resolved; the export's own rendering re-resolves from `body`
|
|
214
|
+
if (body)
|
|
215
|
+
aiEntry.body = resolveEntryLinksInText(body, idIndex, contentRoot, entryLinkUrl);
|
|
191
216
|
if (config?.exclude?.where?.(aiEntry))
|
|
192
217
|
continue;
|
|
193
218
|
pending.push({
|
|
194
219
|
entry: aiEntry,
|
|
220
|
+
body,
|
|
221
|
+
linkedIds: [],
|
|
195
222
|
contentId: extractIdFromFilename(path.basename(listEntry.relativePath)),
|
|
196
223
|
absolutePath: doc.absolutePath,
|
|
197
224
|
collectionPath: collection.logicalPath,
|
|
@@ -236,6 +263,40 @@ function emitCollection(node, files) {
|
|
|
236
263
|
subcollections: manifestSubcollections.length > 0 ? manifestSubcollections : undefined,
|
|
237
264
|
};
|
|
238
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Refuse an `exclude.noindex` or `seo` the noindex gate would misread. A non-boolean
|
|
268
|
+
* `noindex: 'false'` would leave the gate on while the adopter believes it is off, and a
|
|
269
|
+
* misshapen `seo` reads the flag from a field no entry sets, so every noindex entry ships.
|
|
270
|
+
*/
|
|
271
|
+
function assertValidNoindexConfig(config) {
|
|
272
|
+
const noindex = config?.exclude?.noindex;
|
|
273
|
+
if (noindex !== undefined && typeof noindex !== 'boolean') {
|
|
274
|
+
throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
|
|
275
|
+
}
|
|
276
|
+
const seo = config?.seo;
|
|
277
|
+
if (seo === undefined)
|
|
278
|
+
return;
|
|
279
|
+
if (typeof seo !== 'object' || seo === null || Array.isArray(seo)) {
|
|
280
|
+
throw new Error('AI content config: seo must be an object of { group?, fields? }');
|
|
281
|
+
}
|
|
282
|
+
const { group, fields } = seo;
|
|
283
|
+
if (group !== undefined && typeof group !== 'string') {
|
|
284
|
+
throw new Error('AI content config: seo.group must be a string');
|
|
285
|
+
}
|
|
286
|
+
if (fields === undefined)
|
|
287
|
+
return;
|
|
288
|
+
if (typeof fields !== 'object' ||
|
|
289
|
+
fields === null ||
|
|
290
|
+
Array.isArray(fields) ||
|
|
291
|
+
Object.values(fields).some((name) => typeof name !== 'string')) {
|
|
292
|
+
throw new Error('AI content config: seo.fields must map SEO fields to field-name strings');
|
|
293
|
+
}
|
|
294
|
+
// An unknown key (`noIndex`) would leave the gate reading the default field name
|
|
295
|
+
const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
|
|
296
|
+
if (unknown.length > 0) {
|
|
297
|
+
throw new Error(`AI content config: unknown seo.fields key(s): ${unknown.join(', ')}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
239
300
|
function stripContentRoot(logicalPath, contentRoot) {
|
|
240
301
|
if (logicalPath.startsWith(contentRoot + '/')) {
|
|
241
302
|
return logicalPath.slice(contentRoot.length + 1);
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { FieldConfig } from '../config/index.js';
|
|
8
8
|
import type { MissingReference, ResolvedReferenceMeta } from '../entry-schema.js';
|
|
9
|
+
import type { SeoFieldLocation } from '../static/seo.js';
|
|
9
10
|
/** A reference whose target this export shows: the target's own data plus where it lives. */
|
|
10
11
|
export type AIResolvedReference = Record<string, unknown> & ResolvedReferenceMeta & {
|
|
11
12
|
unavailable?: undefined;
|
|
@@ -60,6 +61,12 @@ export interface ExcludeConfig {
|
|
|
60
61
|
entryTypes?: string[];
|
|
61
62
|
/** Custom predicate — return true to exclude the entry */
|
|
62
63
|
where?: (entry: AIEntryMeta) => boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Leave out entries marked `noindex`, read through `isNoindexEntry` at
|
|
66
|
+
* {@link AIContentConfig.seo}, the predicate the sitemap and robots metadata use. Default
|
|
67
|
+
* `true`; `false` exports them.
|
|
68
|
+
*/
|
|
69
|
+
noindex?: boolean;
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
72
|
* Filter criteria for bundles. Filters are AND'd when combined.
|
|
@@ -211,6 +218,11 @@ export interface AIContentConfig {
|
|
|
211
218
|
mountPath?: string;
|
|
212
219
|
/** Opt-out exclusions — content to skip */
|
|
213
220
|
exclude?: ExcludeConfig;
|
|
221
|
+
/**
|
|
222
|
+
* Where entries keep their SEO fields, when not the flat defaults. Drives the `noindex` read,
|
|
223
|
+
* so pass the same object as `createNextCanopyContext({ seo })`, or the two can disagree.
|
|
224
|
+
*/
|
|
225
|
+
seo?: SeoFieldLocation;
|
|
214
226
|
/** Custom bundles — filtered content subsets */
|
|
215
227
|
bundles?: BundleConfig[];
|
|
216
228
|
/** Per-entry-type, per-field markdown overrides */
|
package/dist/api/branch.js
CHANGED
|
@@ -202,7 +202,7 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
202
202
|
// is BOTH this deployment's local git origin AND a mirror of GitHub's
|
|
203
203
|
// view of the repo: worker/git-sync.ts's syncGit() fetches GitHub into it
|
|
204
204
|
// (see GITHUB_TRACKING_REF_PREFIX's doc comment in git-manager.ts) and
|
|
205
|
-
// then reconciles refs/heads/*
|
|
205
|
+
// then reconciles refs/heads/* without touching editor work, so it carries both
|
|
206
206
|
// this deployment's local heads and GitHub's view -- readable offline by
|
|
207
207
|
// this same Lambda, since it resolves to the same EFS inode the worker
|
|
208
208
|
// uses. Reading it here catches a sanitized-name collision with a branch
|
|
@@ -264,14 +264,14 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
264
264
|
// deleteBranchHandler's cleanup (below) handles the common case, but
|
|
265
265
|
// cannot cover every ordering: when the branch still existed on
|
|
266
266
|
// GitHub at editor-delete time, the sync loop's reconcile re-creates
|
|
267
|
-
// the local head from
|
|
268
|
-
// GitHub side is later deleted
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
267
|
+
// the local head from GitHub's branch within a cycle, and once the
|
|
268
|
+
// GitHub side is later deleted that re-created head stays until the
|
|
269
|
+
// sync loop's next prune, with no registry entry left for the delete
|
|
270
|
+
// path to run against. Healing at REUSE time covers every ordering,
|
|
271
|
+
// however the head got orphaned: the registry check above already
|
|
272
|
+
// proved no live branch owns this name, and the tracking check just
|
|
273
|
+
// proved GitHub doesn't either, so a remaining local head is stale by
|
|
274
|
+
// definition. Best-effort (old-value-guarded
|
|
275
275
|
// against a concurrent push): on failure, creation still proceeds --
|
|
276
276
|
// that is today's status quo, and the publish-time 409 message names
|
|
277
277
|
// this cause.
|
|
@@ -532,10 +532,10 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
532
532
|
if (deletingPath)
|
|
533
533
|
await removeLeftoverDir(deletingPath);
|
|
534
534
|
// Also delete the branch's local head from the remote.git mirror (the
|
|
535
|
-
// deployment's local origin). The sync loop
|
|
536
|
-
//
|
|
537
|
-
// path that removes it -- without this, the head outlives
|
|
538
|
-
//
|
|
535
|
+
// deployment's local origin). The sync loop's reconcileTrackedBranches
|
|
536
|
+
// removes a head only once GitHub no longer has its branch, so while GitHub
|
|
537
|
+
// does, THIS is the path that removes it -- without this, the head outlives
|
|
538
|
+
// the branch, and reusing the name after the GitHub side is gone (e.g. a
|
|
539
539
|
// squash-merged PR with auto-delete) has the reused branch's first publish
|
|
540
540
|
// rejected non-fast-forward against the stale old tip: a permanent,
|
|
541
541
|
// misleading 409 -- and a retried submit would then ship the STALE head to
|
package/dist/auth/cache.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Server-only: these use node:fs/promises and must NOT reach a client bundle.
|
|
3
3
|
* Import them as 'canopycms/auth/cache'.
|
|
4
4
|
*/
|
|
5
|
-
export { FileBasedAuthCache, writeAuthCacheSnapshot } from './file-based-auth-cache.js';
|
|
5
|
+
export { FileBasedAuthCache, writeAuthCacheSnapshot, writeAuthSnapshot, type AuthCacheSnapshot, } from './file-based-auth-cache.js';
|
|
6
6
|
export { CachingAuthPlugin } from './caching-auth-plugin.js';
|
package/dist/auth/cache.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Server-only: these use node:fs/promises and must NOT reach a client bundle.
|
|
3
3
|
* Import them as 'canopycms/auth/cache'.
|
|
4
4
|
*/
|
|
5
|
-
export { FileBasedAuthCache, writeAuthCacheSnapshot } from './file-based-auth-cache.js';
|
|
5
|
+
export { FileBasedAuthCache, writeAuthCacheSnapshot, writeAuthSnapshot, } from './file-based-auth-cache.js';
|
|
6
6
|
export { CachingAuthPlugin } from './caching-auth-plugin.js';
|
|
@@ -32,3 +32,11 @@ export declare class FileBasedAuthCache implements AuthCacheProvider {
|
|
|
32
32
|
* recent snapshot directories are kept.
|
|
33
33
|
*/
|
|
34
34
|
export declare function writeAuthCacheSnapshot(cachePath: string, files: Record<string, unknown>): Promise<string>;
|
|
35
|
+
/** One auth provider's users, groups and memberships: what an auth-cache refresh writes. */
|
|
36
|
+
export interface AuthCacheSnapshot {
|
|
37
|
+
users: UserSearchResult[];
|
|
38
|
+
groups: GroupMetadata[];
|
|
39
|
+
memberships: Record<CanopyUserId, CanopyGroupId[]>;
|
|
40
|
+
}
|
|
41
|
+
/** Write `snapshot` as the three files FileBasedAuthCache reads (users.json, orgs.json, memberships.json). */
|
|
42
|
+
export declare function writeAuthSnapshot(cachePath: string, snapshot: AuthCacheSnapshot): Promise<string>;
|
|
@@ -182,6 +182,17 @@ export async function writeAuthCacheSnapshot(cachePath, files) {
|
|
|
182
182
|
await cleanupOldSnapshots(cachePath, 2);
|
|
183
183
|
return snapshotDir;
|
|
184
184
|
}
|
|
185
|
+
/** Write `snapshot` as the three files FileBasedAuthCache reads (users.json, orgs.json, memberships.json). */
|
|
186
|
+
export async function writeAuthSnapshot(cachePath, snapshot) {
|
|
187
|
+
const users = { users: snapshot.users };
|
|
188
|
+
const groups = { groups: snapshot.groups };
|
|
189
|
+
const memberships = { memberships: snapshot.memberships };
|
|
190
|
+
return writeAuthCacheSnapshot(cachePath, {
|
|
191
|
+
'users.json': users,
|
|
192
|
+
'orgs.json': groups,
|
|
193
|
+
'memberships.json': memberships,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
185
196
|
async function cleanupOldSnapshots(cachePath, keepCount) {
|
|
186
197
|
let entries;
|
|
187
198
|
try {
|
|
@@ -50,7 +50,7 @@ export declare function createCheckBranchAccess(defaultAccess?: DefaultBranchAcc
|
|
|
50
50
|
export interface WorkflowActionOptions {
|
|
51
51
|
/**
|
|
52
52
|
* When true, disables the system-branch grant below. The protected base
|
|
53
|
-
* branch is auto-provisioned with `createdBy:
|
|
53
|
+
* branch is auto-provisioned with `createdBy: SYSTEM_USER_ID`, which
|
|
54
54
|
* would otherwise let anyone with general branch access submit/withdraw it
|
|
55
55
|
* -- pass `getBranchProtection(...).isProtected` here so only
|
|
56
56
|
* admins/reviewers/explicit-ACL users retain workflow rights on it.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAdmin, isReviewer } from './helpers.js';
|
|
2
2
|
import { getBranchProtection } from './protected-branch.js';
|
|
3
|
+
import { SYSTEM_USER_ID } from '../system-users.js';
|
|
3
4
|
/**
|
|
4
5
|
* Check if user has access to a branch with explicit default behavior.
|
|
5
6
|
*
|
|
@@ -80,7 +81,7 @@ export function canPerformWorkflowAction(context, user, defaultAccess = 'deny',
|
|
|
80
81
|
return false;
|
|
81
82
|
}
|
|
82
83
|
const userIsCreator = context.branch.createdBy === user.userId;
|
|
83
|
-
const isSystemBranch = !options?.isProtectedBranch && context.branch.createdBy ===
|
|
84
|
+
const isSystemBranch = !options?.isProtectedBranch && context.branch.createdBy === SYSTEM_USER_ID;
|
|
84
85
|
return (userIsCreator ||
|
|
85
86
|
accessResult.reason === 'privileged' ||
|
|
86
87
|
accessResult.reason === 'allowed_by_acl' ||
|