canopycms 0.0.67 → 0.0.68-int.93
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/handler.js +8 -0
- package/dist/api/admin-branch-health.js +12 -7
- package/dist/api/admin.d.ts +8 -8
- package/dist/api/branch-status.js +6 -2
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +35 -11
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +38 -39
- package/dist/api/entries.js +6 -8
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/reference-options.js +1 -1
- package/dist/api/request-url.d.ts +8 -0
- package/dist/api/request-url.js +15 -0
- package/dist/api/resolve-references.js +2 -2
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.js +3 -3
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-workspace.js +2 -2
- package/dist/cli/cli.js +285 -168
- package/dist/cli/generate-ai-content.js +190 -109
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +15 -10
- package/dist/cli/sync.js +15 -13
- package/dist/config/schemas/config.d.ts +6 -3
- package/dist/config/schemas/config.js +3 -1
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/types.d.ts +11 -2
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +2 -2
- package/dist/content-listing.js +3 -3
- package/dist/content-reader.js +4 -4
- package/dist/content-store.d.ts +2 -0
- package/dist/content-store.js +2 -1
- package/dist/content-tree.js +1 -1
- package/dist/context.d.ts +16 -7
- package/dist/context.js +116 -67
- package/dist/editor/Editor.js +4 -4
- package/dist/editor/FormRenderer.js +20 -5
- package/dist/editor/admin/SystemHealthPanel.js +37 -6
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/hooks/useDraftManager.d.ts +1 -1
- package/dist/editor/hooks/useDraftManager.js +2 -1
- package/dist/editor/hooks/useEntriesData.js +2 -2
- package/dist/editor/hooks/useEntryManager.js +17 -11
- package/dist/entry-schema-registry.js +2 -1
- package/dist/git-manager.d.ts +20 -0
- package/dist/git-manager.js +50 -4
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +24 -9
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/worker-not-ready.d.ts +9 -0
- package/dist/http/worker-not-ready.js +17 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/reference-resolver.d.ts +3 -3
- package/dist/reference-resolver.js +3 -2
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/services.d.ts +12 -5
- package/dist/services.js +56 -56
- package/dist/settings-workspace.js +36 -0
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.js +7 -8
- package/dist/types.d.ts +23 -2
- package/dist/utils/content-write-lock.d.ts +3 -7
- package/dist/utils/content-write-lock.js +6 -4
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/provisioning-lock.d.ts +15 -5
- package/dist/utils/provisioning-lock.js +31 -11
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/url-prefix.d.ts +15 -0
- package/dist/utils/url-prefix.js +26 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/git-sync.d.ts +4 -2
- package/dist/worker/git-sync.js +111 -24
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.js +75 -12
- package/dist/worker/task-runner.js +39 -4
- package/package.json +1 -1
|
@@ -24,9 +24,10 @@ export type PermissionSet = Readonly<Record<string, PermissionLevel>>;
|
|
|
24
24
|
* - `metadata: read` — implied by any repository permission; declared so
|
|
25
25
|
* this object states the whole surface, not just the non-automatic part.
|
|
26
26
|
* Nothing else: no `issues`, `administration`, `actions`, or organisation
|
|
27
|
-
* permissions. No `workflows` either
|
|
28
|
-
*
|
|
29
|
-
* .
|
|
27
|
+
* permissions. No `workflows` either: GitHub refuses only workflow content it
|
|
28
|
+
* does not already hold, so rebasing onto a base that changed a workflow is
|
|
29
|
+
* accepted. Content that is new to GitHub fails the push task with the file
|
|
30
|
+
* named (`throwIfWorkflowRefusal` in worker/task-runner.ts).
|
|
30
31
|
*/
|
|
31
32
|
export declare const CANOPY_APP_PERMISSIONS: PermissionSet;
|
|
32
33
|
/**
|
|
@@ -22,9 +22,10 @@ const PERMISSION_LEVELS = ['read', 'write', 'admin'];
|
|
|
22
22
|
* - `metadata: read` — implied by any repository permission; declared so
|
|
23
23
|
* this object states the whole surface, not just the non-automatic part.
|
|
24
24
|
* Nothing else: no `issues`, `administration`, `actions`, or organisation
|
|
25
|
-
* permissions. No `workflows` either
|
|
26
|
-
*
|
|
27
|
-
* .
|
|
25
|
+
* permissions. No `workflows` either: GitHub refuses only workflow content it
|
|
26
|
+
* does not already hold, so rebasing onto a base that changed a workflow is
|
|
27
|
+
* accepted. Content that is new to GitHub fails the push task with the file
|
|
28
|
+
* named (`throwIfWorkflowRefusal` in worker/task-runner.ts).
|
|
28
29
|
*/
|
|
29
30
|
export const CANOPY_APP_PERMISSIONS = {
|
|
30
31
|
contents: 'write',
|
package/dist/cli/init.js
CHANGED
|
@@ -115,7 +115,8 @@ var init_field = __esm({
|
|
|
115
115
|
});
|
|
116
116
|
objectFieldSchema = fieldBaseSchema.extend({
|
|
117
117
|
type: z.literal("object"),
|
|
118
|
-
fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
|
|
118
|
+
fields: z.array(z.lazy(() => fieldHolder[0])).min(1),
|
|
119
|
+
itemTitleField: z.string().min(1).optional()
|
|
119
120
|
});
|
|
120
121
|
inlineGroupFieldSchema = z.object({
|
|
121
122
|
type: z.literal("group"),
|
|
@@ -357,6 +358,9 @@ var init_config = __esm({
|
|
|
357
358
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
358
359
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
359
360
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
361
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
362
|
+
gitEditedByTrailers: z5.boolean().optional(),
|
|
363
|
+
gitCoAuthoredByTrailers: z5.boolean().optional(),
|
|
360
364
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
361
365
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
362
366
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -368,7 +372,6 @@ var init_config = __esm({
|
|
|
368
372
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
369
373
|
allowNetworkRemoteInProd: z5.boolean().optional(),
|
|
370
374
|
settingsBranch: z5.string().optional(),
|
|
371
|
-
autoCreateSettingsPR: z5.boolean().optional(),
|
|
372
375
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
373
376
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
374
377
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -889,6 +892,10 @@ var init_debug = __esm({
|
|
|
889
892
|
throw new Error(errorMsg);
|
|
890
893
|
}
|
|
891
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
897
|
+
* anything that can run concurrently uses {@link timed}.
|
|
898
|
+
*/
|
|
892
899
|
time(label) {
|
|
893
900
|
this.timers.set(label, Date.now());
|
|
894
901
|
}
|
|
@@ -903,12 +910,16 @@ var init_debug = __esm({
|
|
|
903
910
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
904
911
|
return duration;
|
|
905
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
915
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
916
|
+
*/
|
|
906
917
|
async timed(category, label, fn) {
|
|
907
|
-
|
|
918
|
+
const start = Date.now();
|
|
908
919
|
try {
|
|
909
920
|
return await fn();
|
|
910
921
|
} finally {
|
|
911
|
-
this.
|
|
922
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
912
923
|
}
|
|
913
924
|
}
|
|
914
925
|
};
|
|
@@ -1525,9 +1536,6 @@ var ProdStrategy = class extends ProdClientSafeStrategy {
|
|
|
1525
1536
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
1526
1537
|
}
|
|
1527
1538
|
}
|
|
1528
|
-
shouldCreateSettingsPR(config) {
|
|
1529
|
-
return config.autoCreateSettingsPR ?? true;
|
|
1530
|
-
}
|
|
1531
1539
|
};
|
|
1532
1540
|
var DevStrategy = class extends DevClientSafeStrategy {
|
|
1533
1541
|
getWorkspaceRoot(sourceRoot) {
|
|
@@ -1574,9 +1582,6 @@ var DevStrategy = class extends DevClientSafeStrategy {
|
|
|
1574
1582
|
}
|
|
1575
1583
|
validateConfig(_config) {
|
|
1576
1584
|
}
|
|
1577
|
-
shouldCreateSettingsPR(_config) {
|
|
1578
|
-
return false;
|
|
1579
|
-
}
|
|
1580
1585
|
};
|
|
1581
1586
|
var strategyCache = /* @__PURE__ */ new Map();
|
|
1582
1587
|
function operatingStrategy(mode) {
|
package/dist/cli/sync.js
CHANGED
|
@@ -17,6 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import { simpleGit } from 'simple-git';
|
|
18
18
|
import * as p from '@clack/prompts';
|
|
19
19
|
import { filePathExists } from '../utils/fs.js';
|
|
20
|
+
import { isCanopyInternalPath, stageAllExceptCanopyState } from '../utils/git.js';
|
|
20
21
|
import { assertWithinDir, copyDir, listFilesRecursive, pushContentToWorkspace, safeReplaceDir, SYNC_BASE_TAG, } from '../sync-core.js';
|
|
21
22
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
22
23
|
import { ensureGitExcludePattern } from '../git-manager.js';
|
|
@@ -149,13 +150,14 @@ async function syncPush(options) {
|
|
|
149
150
|
}
|
|
150
151
|
return { fileCount: 0 };
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const editorChanges = status.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
154
|
+
if (editorChanges.length > 0 && !options.force) {
|
|
155
|
+
p.log.warn(`Branch workspace has ${editorChanges.length} uncommitted change(s) that will be committed to history then overwritten:`);
|
|
156
|
+
for (const file of editorChanges.slice(0, 10)) {
|
|
155
157
|
p.log.warn(` ${file.path}`);
|
|
156
158
|
}
|
|
157
|
-
if (
|
|
158
|
-
p.log.warn(` ... and ${
|
|
159
|
+
if (editorChanges.length > 10) {
|
|
160
|
+
p.log.warn(` ... and ${editorChanges.length - 10} more`);
|
|
159
161
|
}
|
|
160
162
|
const confirm = await p.confirm({
|
|
161
163
|
message: 'Continue? Editor changes will be preserved in git history.',
|
|
@@ -167,8 +169,8 @@ async function syncPush(options) {
|
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
// Auto-commit uncommitted workspace changes to preserve in history
|
|
170
|
-
if (
|
|
171
|
-
await wsGit
|
|
172
|
+
if (editorChanges.length > 0) {
|
|
173
|
+
await stageAllExceptCanopyState(wsGit);
|
|
172
174
|
await wsGit.commit('sync: save editor state before push');
|
|
173
175
|
p.log.info('Committed editor changes to history before push');
|
|
174
176
|
}
|
|
@@ -329,8 +331,8 @@ async function syncBoth(options) {
|
|
|
329
331
|
return { pushed: 0, pulled: 0 };
|
|
330
332
|
}
|
|
331
333
|
// Auto-commit uncommitted workspace changes (preserves editor work for the merge)
|
|
332
|
-
if (status.files.
|
|
333
|
-
await wsGit
|
|
334
|
+
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
335
|
+
await stageAllExceptCanopyState(wsGit);
|
|
334
336
|
await wsGit.commit('sync: save editor state before merge');
|
|
335
337
|
p.log.info('Committed editor changes before merge');
|
|
336
338
|
}
|
|
@@ -365,9 +367,9 @@ async function syncBoth(options) {
|
|
|
365
367
|
await wsGit.raw(['branch', '-D', incomingBranch]).catch(() => { });
|
|
366
368
|
throw err;
|
|
367
369
|
}
|
|
368
|
-
await wsGit
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
370
|
+
await stageAllExceptCanopyState(wsGit);
|
|
371
|
+
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
372
|
+
if (incomingFiles.length === 0) {
|
|
371
373
|
await wsGit.checkout(currentBranch);
|
|
372
374
|
await wsGit.raw(['branch', '-D', incomingBranch]);
|
|
373
375
|
p.log.info('No working-tree changes to merge — pulling editor changes only');
|
|
@@ -410,7 +412,7 @@ async function syncBoth(options) {
|
|
|
410
412
|
await wsGit.tag(['-f', SYNC_BASE_TAG]);
|
|
411
413
|
p.log.success('Merged working-tree changes with editor changes');
|
|
412
414
|
const pullResult = await syncPull({ ...options, branch: branchName, force: true });
|
|
413
|
-
return { pushed:
|
|
415
|
+
return { pushed: incomingFiles.length, pulled: pullResult.fileCount };
|
|
414
416
|
}
|
|
415
417
|
finally {
|
|
416
418
|
await invalidateBranchContentCaches(branchPath);
|
|
@@ -69,12 +69,13 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
69
69
|
defaultRemoteUrl: z.ZodOptional<z.ZodString>;
|
|
70
70
|
gitBotAuthorName: z.ZodString;
|
|
71
71
|
gitBotAuthorEmail: z.ZodString;
|
|
72
|
+
gitEditedByTrailers: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
+
gitCoAuthoredByTrailers: z.ZodOptional<z.ZodBoolean>;
|
|
72
74
|
githubTokenEnvVar: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
73
75
|
mode: z.ZodEnum<["prod", "dev"]>;
|
|
74
76
|
deployedAs: z.ZodDefault<z.ZodEnum<["static", "server"]>>;
|
|
75
77
|
allowNetworkRemoteInProd: z.ZodOptional<z.ZodBoolean>;
|
|
76
78
|
settingsBranch: z.ZodOptional<z.ZodString>;
|
|
77
|
-
autoCreateSettingsPR: z.ZodOptional<z.ZodBoolean>;
|
|
78
79
|
deploymentName: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
79
80
|
contentRoot: z.ZodDefault<z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>>>;
|
|
80
81
|
sourceRoot: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
@@ -152,9 +153,10 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
152
153
|
defaultActiveBranch?: string | undefined;
|
|
153
154
|
defaultRemoteName?: string | undefined;
|
|
154
155
|
defaultRemoteUrl?: string | undefined;
|
|
156
|
+
gitEditedByTrailers?: boolean | undefined;
|
|
157
|
+
gitCoAuthoredByTrailers?: boolean | undefined;
|
|
155
158
|
githubTokenEnvVar?: string | undefined;
|
|
156
159
|
allowNetworkRemoteInProd?: boolean | undefined;
|
|
157
|
-
autoCreateSettingsPR?: boolean | undefined;
|
|
158
160
|
deploymentName?: string | undefined;
|
|
159
161
|
sourceRoot?: string | undefined;
|
|
160
162
|
basePath?: string | undefined;
|
|
@@ -204,10 +206,11 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
204
206
|
defaultActiveBranch?: string | undefined;
|
|
205
207
|
defaultRemoteName?: string | undefined;
|
|
206
208
|
defaultRemoteUrl?: string | undefined;
|
|
209
|
+
gitEditedByTrailers?: boolean | undefined;
|
|
210
|
+
gitCoAuthoredByTrailers?: boolean | undefined;
|
|
207
211
|
githubTokenEnvVar?: string | undefined;
|
|
208
212
|
deployedAs?: "static" | "server" | undefined;
|
|
209
213
|
allowNetworkRemoteInProd?: boolean | undefined;
|
|
210
|
-
autoCreateSettingsPR?: boolean | undefined;
|
|
211
214
|
deploymentName?: string | undefined;
|
|
212
215
|
contentRoot?: string | undefined;
|
|
213
216
|
sourceRoot?: string | undefined;
|
|
@@ -79,6 +79,9 @@ export const CanopyConfigSchema = z
|
|
|
79
79
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
80
80
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
81
81
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
82
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
83
|
+
gitEditedByTrailers: z.boolean().optional(),
|
|
84
|
+
gitCoAuthoredByTrailers: z.boolean().optional(),
|
|
82
85
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
83
86
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
84
87
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -89,7 +92,6 @@ export const CanopyConfigSchema = z
|
|
|
89
92
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
90
93
|
allowNetworkRemoteInProd: z.boolean().optional(),
|
|
91
94
|
settingsBranch: z.string().optional(),
|
|
92
|
-
autoCreateSettingsPR: z.boolean().optional(),
|
|
93
95
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
94
96
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
95
97
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -81,6 +81,7 @@ const blockFieldSchema = fieldBaseSchema.extend({
|
|
|
81
81
|
const objectFieldSchema = fieldBaseSchema.extend({
|
|
82
82
|
type: z.literal('object'),
|
|
83
83
|
fields: z.array(z.lazy(() => fieldHolder[0])).min(1),
|
|
84
|
+
itemTitleField: z.string().min(1).optional(),
|
|
84
85
|
});
|
|
85
86
|
// Inline group field: visual grouping only, no data nesting
|
|
86
87
|
const inlineGroupFieldSchema = z.object({
|
package/dist/config/types.d.ts
CHANGED
|
@@ -132,6 +132,11 @@ export interface BlockFieldConfig extends BaseFieldConfig {
|
|
|
132
132
|
export interface ObjectFieldConfig extends BaseFieldConfig {
|
|
133
133
|
type: 'object';
|
|
134
134
|
fields: FieldConfig[];
|
|
135
|
+
/**
|
|
136
|
+
* On a `list: true` object, a direct `string` or `number` child whose value titles each
|
|
137
|
+
* item's card; a blank or non-finite value falls back to "<label> #N".
|
|
138
|
+
*/
|
|
139
|
+
itemTitleField?: string;
|
|
135
140
|
}
|
|
136
141
|
/**
|
|
137
142
|
* Inline group field config: visually groups fields in the editor without creating
|
|
@@ -346,6 +351,8 @@ export interface CanopyConfig {
|
|
|
346
351
|
defaultRemoteUrl?: DefaultRemoteUrl;
|
|
347
352
|
gitBotAuthorName: GitBotAuthorName;
|
|
348
353
|
gitBotAuthorEmail: GitBotAuthorEmail;
|
|
354
|
+
gitEditedByTrailers?: boolean;
|
|
355
|
+
gitCoAuthoredByTrailers?: boolean;
|
|
349
356
|
githubTokenEnvVar?: GithubTokenEnvVar;
|
|
350
357
|
mode: CanopyOperatingMode;
|
|
351
358
|
/** How this build is deployed — see {@link CanopyConfigInput.deployedAs}. */
|
|
@@ -353,7 +360,6 @@ export interface CanopyConfig {
|
|
|
353
360
|
/** Escape hatch for a prod host with real internet access — see {@link CanopyConfigInput.allowNetworkRemoteInProd}. */
|
|
354
361
|
allowNetworkRemoteInProd?: boolean;
|
|
355
362
|
settingsBranch?: string;
|
|
356
|
-
autoCreateSettingsPR?: boolean;
|
|
357
363
|
deploymentName?: string;
|
|
358
364
|
contentRoot: ContentRoot;
|
|
359
365
|
sourceRoot?: SourceRoot;
|
|
@@ -382,6 +388,10 @@ export interface CanopyConfigInput {
|
|
|
382
388
|
defaultRemoteUrl?: string;
|
|
383
389
|
gitBotAuthorName: string;
|
|
384
390
|
gitBotAuthorEmail: string;
|
|
391
|
+
/** Add `Edited-by: Name (user id)` for the submitter to submit commits. Default true. */
|
|
392
|
+
gitEditedByTrailers?: boolean;
|
|
393
|
+
/** Also add `Co-authored-by: Name <email>`. Default false: emails in public history are public. */
|
|
394
|
+
gitCoAuthoredByTrailers?: boolean;
|
|
385
395
|
githubTokenEnvVar?: string;
|
|
386
396
|
/**
|
|
387
397
|
* Operating mode: 'prod' or 'dev'. Required — no default (SEC-C1). A prod deploy that
|
|
@@ -402,7 +412,6 @@ export interface CanopyConfigInput {
|
|
|
402
412
|
*/
|
|
403
413
|
allowNetworkRemoteInProd?: boolean;
|
|
404
414
|
settingsBranch?: string;
|
|
405
|
-
autoCreateSettingsPR?: boolean;
|
|
406
415
|
deploymentName?: string;
|
|
407
416
|
contentRoot?: string;
|
|
408
417
|
sourceRoot?: string;
|
|
@@ -25,6 +25,12 @@ export declare const ensureReferenceFieldsHaveScope: (fields: unknown) => void;
|
|
|
25
25
|
* can run at config-validation and schema-load time, before any entry exists.
|
|
26
26
|
*/
|
|
27
27
|
export declare const forEachReferenceField: (fields: unknown, visit: (field: Record<string, unknown>) => void) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Check that every `itemTitleField` sits on a `list: true` object and names a direct child
|
|
30
|
+
* of type `string` or `number`, the only values the card heading shows as typed (a reference
|
|
31
|
+
* would show its raw id, a select its stored value). Throws an error when it does not.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ensureItemTitleFieldsExist: (fields: unknown) => void;
|
|
28
34
|
/**
|
|
29
35
|
* Validate that inline groups don't cause field name collisions within the same scope.
|
|
30
36
|
* Because inline groups flatten their children into the parent scope, a field name used
|
|
@@ -77,6 +77,43 @@ export const forEachReferenceField = (fields, visit) => {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Check that every `itemTitleField` sits on a `list: true` object and names a direct child
|
|
82
|
+
* of type `string` or `number`, the only values the card heading shows as typed (a reference
|
|
83
|
+
* would show its raw id, a select its stored value). Throws an error when it does not.
|
|
84
|
+
*/
|
|
85
|
+
export const ensureItemTitleFieldsExist = (fields) => {
|
|
86
|
+
if (!Array.isArray(fields))
|
|
87
|
+
return;
|
|
88
|
+
for (const field of fields) {
|
|
89
|
+
const f = field;
|
|
90
|
+
if (f?.type === 'object') {
|
|
91
|
+
const children = Array.isArray(f.fields) ? f.fields : [];
|
|
92
|
+
if (f.itemTitleField !== undefined) {
|
|
93
|
+
const label = `Object field "${f.name ?? 'unknown'}" has itemTitleField "${String(f.itemTitleField)}"`;
|
|
94
|
+
if (f.list !== true) {
|
|
95
|
+
throw new Error(`${label}, but itemTitleField applies only to list: true objects`);
|
|
96
|
+
}
|
|
97
|
+
const child = children.find((c) => c?.name === f.itemTitleField);
|
|
98
|
+
if (typeof f.itemTitleField !== 'string' || !child) {
|
|
99
|
+
throw new Error(`${label}, which is not one of its child fields`);
|
|
100
|
+
}
|
|
101
|
+
if (child.type !== 'string' && child.type !== 'number') {
|
|
102
|
+
throw new Error(`${label}, which must name a string or number field`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
ensureItemTitleFieldsExist(children);
|
|
106
|
+
}
|
|
107
|
+
else if (f?.type === 'group') {
|
|
108
|
+
ensureItemTitleFieldsExist(f.fields);
|
|
109
|
+
}
|
|
110
|
+
else if (f?.type === 'block' && Array.isArray(f.templates)) {
|
|
111
|
+
for (const template of f.templates) {
|
|
112
|
+
ensureItemTitleFieldsExist(template.fields);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
80
117
|
/**
|
|
81
118
|
* Validate that inline groups don't cause field name collisions within the same scope.
|
|
82
119
|
* Because inline groups flatten their children into the parent scope, a field name used
|
|
@@ -123,8 +123,8 @@ export interface ListEntriesOptions<T = Record<string, unknown>> {
|
|
|
123
123
|
* unfiltered exactly as build-time callers want.
|
|
124
124
|
*/
|
|
125
125
|
export interface ContentVisibilityOptions {
|
|
126
|
-
/** Return false to drop an entry. Receives the entry's
|
|
127
|
-
shouldInclude?: (
|
|
126
|
+
/** Return false to drop an entry. Receives the entry's logical path (see `entryLogicalPath`). */
|
|
127
|
+
shouldInclude?: (logicalPath: LogicalPath) => boolean;
|
|
128
128
|
}
|
|
129
129
|
/** A collection node from the flattened schema. */
|
|
130
130
|
export type CollectionSchemaItem = Extract<FlatSchemaItem, {
|
package/dist/content-listing.js
CHANGED
|
@@ -9,7 +9,7 @@ import { findBodyFieldName } from './utils/body-field.js';
|
|
|
9
9
|
import { computeEntryUrl } from './utils/entry-url.js';
|
|
10
10
|
import { asRecord, getFormatExtension } from './utils/format.js';
|
|
11
11
|
import { resolveCollectionPath } from './content-id-index.js';
|
|
12
|
-
import { validateAndNormalizePath } from './paths/index.js';
|
|
12
|
+
import { entryLogicalPath, validateAndNormalizePath } from './paths/index.js';
|
|
13
13
|
import { isNotFoundError, getErrorMessage } from './utils/error.js';
|
|
14
14
|
import { createDebugLogger } from './utils/debug.js';
|
|
15
15
|
import { ContentStore, ContentStoreError, createReferenceResolveCache, } from './content-store.js';
|
|
@@ -139,7 +139,7 @@ export async function listEntries(branchRoot, flatSchema, contentRootName, optio
|
|
|
139
139
|
: null;
|
|
140
140
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
141
141
|
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
142
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
142
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
143
143
|
// Resolve AFTER the visibility filter (a denied entry is never resolved, so its
|
|
144
144
|
// references cost nothing and leak nothing) and BEFORE the mapping below, so
|
|
145
145
|
// `extract` and `filter` both see resolved data — which is the entire point.
|
|
@@ -330,7 +330,7 @@ export const listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
330
330
|
readEntryData(absolutePath, format, bodyField),
|
|
331
331
|
]);
|
|
332
332
|
const item = {
|
|
333
|
-
logicalPath:
|
|
333
|
+
logicalPath: entryLogicalPath(collection.logicalPath, slug),
|
|
334
334
|
contentId,
|
|
335
335
|
slug,
|
|
336
336
|
collectionPath: collection.logicalPath,
|
package/dist/content-reader.js
CHANGED
|
@@ -128,10 +128,10 @@ export const createContentReader = (options) => {
|
|
|
128
128
|
throw new ContentStoreError(`Path is not a collection: ${entryPath}`, 'NO_SCHEMA_ITEM');
|
|
129
129
|
}
|
|
130
130
|
// Get the path WITHOUT reading the file
|
|
131
|
-
let
|
|
131
|
+
let logicalPath;
|
|
132
132
|
// Absolute filesystem path to the entry file. Surfaced on read() / readByUrlPath()
|
|
133
133
|
// for server-side colocated-artifact reads (e.g. a sibling profile.json). Already
|
|
134
|
-
// computed here
|
|
134
|
+
// computed here by path resolution; just carried through.
|
|
135
135
|
let physicalPath;
|
|
136
136
|
// Entry type + content ID come out of path resolution (buildPaths() derives both from the
|
|
137
137
|
// schema item / filename), surfaced on read() / readByUrlPath() so callers can route or
|
|
@@ -140,7 +140,7 @@ export const createContentReader = (options) => {
|
|
|
140
140
|
let entryId;
|
|
141
141
|
try {
|
|
142
142
|
const resolved = await store.resolveDocumentPath(entryPath, slug ?? '');
|
|
143
|
-
|
|
143
|
+
logicalPath = resolved.logicalPath;
|
|
144
144
|
physicalPath = resolved.absolutePath;
|
|
145
145
|
// resolveDocumentPath() always sets entryTypeName for a valid schema item, so no
|
|
146
146
|
// further fallback is needed here; `id` is the one field that can be legitimately
|
|
@@ -164,7 +164,7 @@ export const createContentReader = (options) => {
|
|
|
164
164
|
// Check permissions BEFORE reading the file (security)
|
|
165
165
|
const shouldCheckPermissions = !(isDeployedStatic(services.config) || isBuildMode());
|
|
166
166
|
if (shouldCheckPermissions) {
|
|
167
|
-
const access = await services.checkContentAccess(context, branchRoot,
|
|
167
|
+
const access = await services.checkContentAccess(context, branchRoot, logicalPath, user, 'read');
|
|
168
168
|
if (!access.allowed) {
|
|
169
169
|
if (services.config.mode !== 'prod') {
|
|
170
170
|
const reasons = [];
|
package/dist/content-store.d.ts
CHANGED
|
@@ -347,6 +347,8 @@ export declare class ContentStore {
|
|
|
347
347
|
resolveDocumentPath(schemaPath: LogicalPath, slug?: string): Promise<{
|
|
348
348
|
absolutePath: string;
|
|
349
349
|
relativePath: PhysicalPath;
|
|
350
|
+
/** The entry's logical path, the form path-permission rules match (`entryLogicalPath`). */
|
|
351
|
+
logicalPath: LogicalPath;
|
|
350
352
|
id?: string;
|
|
351
353
|
/**
|
|
352
354
|
* Always populated for a valid schema item: the collection branch below resolves a name
|
package/dist/content-store.js
CHANGED
|
@@ -30,7 +30,7 @@ import { generateId } from './id.js';
|
|
|
30
30
|
import { isNodeError } from './utils/error.js';
|
|
31
31
|
import { filePathExists, readFileIfExists } from './utils/fs.js';
|
|
32
32
|
import { asRecord, getFormatExtension } from './utils/format.js';
|
|
33
|
-
import { normalizeFilesystemPath, parseSlug, } from './paths/index.js';
|
|
33
|
+
import { entryLogicalPath, normalizeFilesystemPath, parseSlug, } from './paths/index.js';
|
|
34
34
|
/**
|
|
35
35
|
* Acquire `keys` in canonical (sorted) order before running `fn`, ruling out AB-BA
|
|
36
36
|
* deadlocks between callers with overlapping key sets. Only renameEntry() needs two
|
|
@@ -602,6 +602,7 @@ export class ContentStore {
|
|
|
602
602
|
return {
|
|
603
603
|
absolutePath: resolved,
|
|
604
604
|
relativePath: path.relative(this.root, resolved),
|
|
605
|
+
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
605
606
|
id,
|
|
606
607
|
entryTypeName: finalEntryTypeName,
|
|
607
608
|
existed,
|
package/dist/content-tree.js
CHANGED
|
@@ -74,7 +74,7 @@ export async function buildContentTree(branchRoot, flatSchema, contentRootName,
|
|
|
74
74
|
: null;
|
|
75
75
|
const listVisibleEntries = async (collection) => {
|
|
76
76
|
const entries = await listCollectionEntries(branchRoot, collection);
|
|
77
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
77
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
78
78
|
// Resolved here rather than at the two node-building sites so BOTH inherit it — the same
|
|
79
79
|
// reason the ACL filter lives here. A denied entry is filtered above and never resolved.
|
|
80
80
|
return resolver ? resolveCollectionItemReferences(visible, collection, resolver) : visible;
|
package/dist/context.d.ts
CHANGED
|
@@ -58,8 +58,12 @@ export interface CanopyBuildContext {
|
|
|
58
58
|
* `meta.indexEntry` handed to `extract`, and a collection whose children are
|
|
59
59
|
* all filtered out is pruned. On the build context and on static deployments
|
|
60
60
|
* nothing is filtered — the synthetic admin sees everything.
|
|
61
|
+
*
|
|
62
|
+
* Branch: the `branch` option behaves as on `listEntries`.
|
|
61
63
|
*/
|
|
62
|
-
buildContentTree: <T = unknown, TEntryTypes = DefaultEntryTypes>(options?: BuildContentTreeOptions<T, TEntryTypes>
|
|
64
|
+
buildContentTree: <T = unknown, TEntryTypes = DefaultEntryTypes>(options?: BuildContentTreeOptions<T, TEntryTypes> & {
|
|
65
|
+
branch?: string;
|
|
66
|
+
}) => Promise<ContentTreeNode<T>[]>;
|
|
63
67
|
/**
|
|
64
68
|
* Every content entry as a flat array.
|
|
65
69
|
*
|
|
@@ -67,13 +71,18 @@ export interface CanopyBuildContext {
|
|
|
67
71
|
* cannot `read` are omitted before `extract` runs; on the build context and
|
|
68
72
|
* static deployments nothing is filtered.
|
|
69
73
|
*
|
|
70
|
-
* Branch:
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
74
|
+
* Branch: with no `branch` it lists the active branch
|
|
75
|
+
* (`defaultActiveBranch ?? defaultBaseBranch`): git HEAD in `dev` when that is
|
|
76
|
+
* unset, otherwise usually the base branch in `prod`. Pass the preview
|
|
77
|
+
* iframe's `?branch=` as `branch`, as for `read`, so an index page previewed
|
|
78
|
+
* on a content branch lists that branch. Any other branch must already exist
|
|
79
|
+
* and, on the request-scoped context, be readable by the user; otherwise, or
|
|
80
|
+
* for a non-string value, the result is empty. It selects nothing at build
|
|
81
|
+
* time or on static deployments.
|
|
75
82
|
*/
|
|
76
|
-
listEntries: <T = Record<string, unknown>>(options?: ListEntriesOptions<T>
|
|
83
|
+
listEntries: <T = Record<string, unknown>>(options?: ListEntriesOptions<T> & {
|
|
84
|
+
branch?: string;
|
|
85
|
+
}) => Promise<ListEntriesItem<T>[]>;
|
|
77
86
|
/**
|
|
78
87
|
* Content reader, with the auth context applied automatically (admin at build
|
|
79
88
|
* time).
|