canopycms 0.0.62 → 0.0.63
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/index.d.ts +9 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +9 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/to-plain-text.d.ts +57 -0
- package/dist/ai/to-plain-text.d.ts.map +1 -0
- package/dist/ai/to-plain-text.js +255 -0
- package/dist/ai/to-plain-text.js.map +1 -0
- package/dist/ai/transform-components.d.ts.map +1 -1
- package/dist/ai/transform-components.js +70 -5
- package/dist/ai/transform-components.js.map +1 -1
- package/dist/api/comments.d.ts +2 -2
- package/dist/authorization/branch.d.ts +55 -3
- package/dist/authorization/branch.d.ts.map +1 -1
- package/dist/authorization/branch.js +55 -9
- package/dist/authorization/branch.js.map +1 -1
- package/dist/authorization/types.d.ts +1 -1
- package/dist/authorization/types.d.ts.map +1 -1
- package/dist/build-canopy.d.ts +49 -0
- package/dist/build-canopy.d.ts.map +1 -0
- package/dist/build-canopy.js +51 -0
- package/dist/build-canopy.js.map +1 -0
- package/dist/cli/cli.js +78 -26
- package/dist/cli/generate-ai-content.js +77 -25
- package/dist/cli/template-files/canopycms.config.ts.template +18 -1
- package/dist/content-listing.d.ts +110 -5
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +164 -23
- package/dist/content-listing.js.map +1 -1
- package/dist/content-reader.d.ts +41 -4
- package/dist/content-reader.d.ts.map +1 -1
- package/dist/content-reader.js +17 -4
- package/dist/content-reader.js.map +1 -1
- package/dist/content-store.d.ts +10 -1
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js.map +1 -1
- package/dist/content-tree.d.ts +31 -2
- package/dist/content-tree.d.ts.map +1 -1
- package/dist/content-tree.js +22 -5
- package/dist/content-tree.js.map +1 -1
- package/dist/context.d.ts +30 -10
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +55 -4
- package/dist/context.js.map +1 -1
- package/dist/entry-schema.d.ts +228 -8
- package/dist/entry-schema.d.ts.map +1 -1
- package/dist/entry-schema.js +124 -0
- package/dist/entry-schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +51 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +49 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts +1 -2
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +2 -6
- package/dist/services.js.map +1 -1
- package/dist/static/index.d.ts +65 -5
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +65 -12
- package/dist/static/index.js.map +1 -1
- package/dist/static/seo.d.ts +150 -0
- package/dist/static/seo.d.ts.map +1 -0
- package/dist/static/seo.js +189 -0
- package/dist/static/seo.js.map +1 -0
- package/dist/utils/sanitize-href.d.ts +46 -0
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +63 -2
- package/dist/utils/sanitize-href.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
* Handles checking if a user can access a branch based on ACLs.
|
|
5
5
|
*/
|
|
6
6
|
import { isAdmin, isReviewer } from './helpers.js';
|
|
7
|
+
import { getBranchProtection } from './protected-branch.js';
|
|
7
8
|
/**
|
|
8
9
|
* Check if user has access to a branch with explicit default behavior.
|
|
10
|
+
*
|
|
11
|
+
* Precedence, highest first:
|
|
12
|
+
* 1. Admins and Reviewers
|
|
13
|
+
* 2. `managerOrAdminAllowed` lockdown
|
|
14
|
+
* 3. An explicit user/group ACL -- an allowlist omitting the creator still
|
|
15
|
+
* denies them, so an admin can lock down a branch someone else created
|
|
16
|
+
* 4. With no ACL: the branch creator, then `defaultAccess`, and only where that
|
|
17
|
+
* would deny, the protected base branch grant
|
|
18
|
+
* (see {@link BranchAccessOptions.isProtectedBranch})
|
|
9
19
|
*/
|
|
10
|
-
export function checkBranchAccessWithDefault(context, user, defaultAccess = 'deny') {
|
|
20
|
+
export function checkBranchAccessWithDefault(context, user, defaultAccess = 'deny', options) {
|
|
11
21
|
// Admins and Reviewers have full branch access
|
|
12
22
|
if (isAdmin(user.groups) || isReviewer(user.groups)) {
|
|
13
23
|
return { allowed: true, reason: 'privileged' };
|
|
@@ -16,12 +26,34 @@ export function checkBranchAccessWithDefault(context, user, defaultAccess = 'den
|
|
|
16
26
|
const hasUserConstraint = !!access.allowedUsers?.length;
|
|
17
27
|
const hasGroupConstraint = !!access.allowedGroups?.length;
|
|
18
28
|
const managerOrAdminAllowed = access.managerOrAdminAllowed ?? false;
|
|
29
|
+
if (!hasUserConstraint && !hasGroupConstraint && managerOrAdminAllowed) {
|
|
30
|
+
return { allowed: false, reason: 'denied_by_acl' };
|
|
31
|
+
}
|
|
19
32
|
if (!hasUserConstraint && !hasGroupConstraint) {
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
// The branch creator owns their own un-ACL'd branch. This matches the three
|
|
34
|
+
// places the server already grants on creator-ownership independently of
|
|
35
|
+
// branch access (listBranchesHandler, canDeleteBranch, canModifyBranchAccess)
|
|
36
|
+
// -- without it, under 'deny' a creator could delete their branch and rewrite
|
|
37
|
+
// its ACL but not read a single file on it.
|
|
38
|
+
//
|
|
39
|
+
// Deliberately scoped to the no-ACL case: an EXPLICIT allowlist that omits
|
|
40
|
+
// the creator still denies them, because that is how an admin locks down a
|
|
41
|
+
// branch someone else created. Granting creator ahead of the ACL would make
|
|
42
|
+
// that lockdown silently ineffective.
|
|
43
|
+
if (context.branch.createdBy === user.userId) {
|
|
44
|
+
return { allowed: true, reason: 'creator' };
|
|
45
|
+
}
|
|
46
|
+
if (defaultAccess === 'allow') {
|
|
47
|
+
return { allowed: true, reason: 'no_acl' };
|
|
22
48
|
}
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
// Same scoping rationale for the base branch: only where the bare default
|
|
50
|
+
// would otherwise decide. A short-circuit higher up would replace
|
|
51
|
+
// 'allowed_by_acl' with 'base_branch' and silently strip Withdraw rights
|
|
52
|
+
// from ACL-listed users, and would override an explicit 'denied_by_acl'.
|
|
53
|
+
if (options?.isProtectedBranch) {
|
|
54
|
+
return { allowed: true, reason: 'base_branch' };
|
|
55
|
+
}
|
|
56
|
+
return { allowed: false, reason: 'no_acl' };
|
|
25
57
|
}
|
|
26
58
|
const userAllowed = hasUserConstraint && access.allowedUsers?.includes(user.userId);
|
|
27
59
|
const groupAllowed = hasGroupConstraint && user.groups?.some((g) => access.allowedGroups?.includes(g));
|
|
@@ -30,9 +62,17 @@ export function checkBranchAccessWithDefault(context, user, defaultAccess = 'den
|
|
|
30
62
|
}
|
|
31
63
|
/**
|
|
32
64
|
* Create a branch access checker with bound default access.
|
|
65
|
+
*
|
|
66
|
+
* `config` is needed to resolve whether a branch is the protected base branch;
|
|
67
|
+
* it routes through `getBranchProtection`, the single source of truth for that
|
|
68
|
+
* question, rather than repeating the base-branch test here.
|
|
33
69
|
*/
|
|
34
|
-
export function createCheckBranchAccess(defaultAccess = 'deny') {
|
|
35
|
-
return (context, user) => checkBranchAccessWithDefault(context, user, defaultAccess
|
|
70
|
+
export function createCheckBranchAccess(defaultAccess = 'deny', config) {
|
|
71
|
+
return (context, user) => checkBranchAccessWithDefault(context, user, defaultAccess, {
|
|
72
|
+
isProtectedBranch: config
|
|
73
|
+
? getBranchProtection(config, context.branch.name, context.branch.baseBranch).isProtected
|
|
74
|
+
: false,
|
|
75
|
+
});
|
|
36
76
|
}
|
|
37
77
|
/**
|
|
38
78
|
* Check if user can perform workflow actions (submit/withdraw) on a branch.
|
|
@@ -45,10 +85,16 @@ export function createCheckBranchAccess(defaultAccess = 'deny') {
|
|
|
45
85
|
* - Admins and Reviewers always have access (via checkBranchAccess)
|
|
46
86
|
* - Unless `options.isProtectedBranch` is set, in which case the system-branch
|
|
47
87
|
* grant above is disabled (see {@link WorkflowActionOptions})
|
|
88
|
+
*
|
|
89
|
+
* The creator grant is enforced twice over: `checkBranchAccessWithDefault` now
|
|
90
|
+
* admits the creator (so the gate below no longer swallows them under
|
|
91
|
+
* `defaultBranchAccess: 'deny'`), and `userIsCreator` still decides the result.
|
|
48
92
|
*/
|
|
49
93
|
export function canPerformWorkflowAction(context, user, defaultAccess = 'deny', options) {
|
|
50
|
-
// Check if user has general branch access (handles admins, reviewers, ACLs)
|
|
51
|
-
const accessResult = checkBranchAccessWithDefault(context, user, defaultAccess
|
|
94
|
+
// Check if user has general branch access (handles admins, reviewers, creator, ACLs)
|
|
95
|
+
const accessResult = checkBranchAccessWithDefault(context, user, defaultAccess, {
|
|
96
|
+
isProtectedBranch: options?.isProtectedBranch,
|
|
97
|
+
});
|
|
52
98
|
// If user doesn't have basic branch access, deny immediately
|
|
53
99
|
if (!accessResult.allowed) {
|
|
54
100
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../../src/authorization/branch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../../src/authorization/branch.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ export type PermissionPath = string & {
|
|
|
19
19
|
*/
|
|
20
20
|
export interface BranchAccessResult {
|
|
21
21
|
allowed: boolean;
|
|
22
|
-
reason: 'privileged' | 'allowed_by_acl' | 'denied_by_acl' | 'no_acl';
|
|
22
|
+
reason: 'privileged' | 'base_branch' | 'creator' | 'allowed_by_acl' | 'denied_by_acl' | 'no_acl';
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Result of checking path-level permissions
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/authorization/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG3D,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAE3C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE5E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,YAAY,GAAG,gBAAgB,GAAG,eAAe,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/authorization/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG3D,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAE3C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE5E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,YAAY,GAAG,aAAa,GAAG,SAAS,GAAG,gBAAgB,GAAG,eAAe,GAAG,QAAQ,CAAA;CACjG;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,WAAW,EAAE,cAAc,CAAA;IAChD,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iBAAiB,EAAE,CACjB,OAAO,EAAE,OAAO,UAAU,EAAE,aAAa,EACzC,IAAI,EAAE,OAAO,SAAS,EAAE,UAAU,KAC/B,kBAAkB,CAAA;IACvB,mBAAmB,EAAE,CACnB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,mBAAmB,EAAE,aAAa,KAC5C,OAAO,CAAC,OAAO,WAAW,EAAE,cAAc,EAAE,CAAC,CAAA;IAClD,iBAAiB,EAAE,OAAO,WAAW,EAAE,iBAAiB,CAAA;IACxD,IAAI,EAAE,OAAO,mBAAmB,EAAE,aAAa,CAAA;IAC/C;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CAC9C"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CanopyConfig } from './config';
|
|
2
|
+
import type { EntrySchemaRegistry } from './schema/types';
|
|
3
|
+
import { type CanopyBuildContext } from './context';
|
|
4
|
+
export interface CreateBuildCanopyOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Entry schema registry for resolving `.collection.json` references.
|
|
7
|
+
* Same shape as `createCanopyServices`'s option of the same name.
|
|
8
|
+
*/
|
|
9
|
+
entrySchemaRegistry?: EntrySchemaRegistry;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One-call factory for a **build/admin** Canopy context — for standalone
|
|
13
|
+
* scripts that run entirely outside a Next.js request or build phase (index
|
|
14
|
+
* builders, content audits, codegen, ad hoc reports).
|
|
15
|
+
*
|
|
16
|
+
* **This is a build/admin context: it reads the filesystem directly as a
|
|
17
|
+
* synthetic admin user (`STATIC_DEPLOY_USER`) and bypasses ALL branch and
|
|
18
|
+
* path ACLs.** There is no per-request auth to enforce because there is no
|
|
19
|
+
* request. Do not reach for this from request-handling code (an API route,
|
|
20
|
+
* a server component rendering for a specific visitor, anything with a real
|
|
21
|
+
* user on the other end) — there, use `createNextCanopyContext(...)`'s
|
|
22
|
+
* `getCanopy()` (or its phase-selecting `read`/`readByUrlPath`), which
|
|
23
|
+
* enforce branch/path permissions per user. This factory mirrors the same
|
|
24
|
+
* `createCanopyServices` + `createCanopyContext` + `STATIC_DEPLOY_USER` boot
|
|
25
|
+
* sequence that `createNextCanopyContext`'s own `getCanopyForBuild()` uses
|
|
26
|
+
* internally, minus the Next.js pieces (no `next/headers`, no React
|
|
27
|
+
* `cache()`, no request-time misuse guard — there is no request phase for
|
|
28
|
+
* it to guard against) — so it works in a plain script run with `tsx`/`node`.
|
|
29
|
+
*
|
|
30
|
+
* Trivially stubbable by design: the boot sequence is one function call
|
|
31
|
+
* taking a plain config object and returning a plain context, so a script
|
|
32
|
+
* that calls it can be imported and exercised from a test with the config
|
|
33
|
+
* (or this function itself) swapped out. Contrast with a hand-rolled
|
|
34
|
+
* top-level-`await` boot block, which can never be imported by a test at
|
|
35
|
+
* all — that's the concrete problem this factory removes.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // scripts/build-search-index.ts
|
|
40
|
+
* import { createBuildCanopy } from 'canopycms/server'
|
|
41
|
+
* import config from '../canopycms.config'
|
|
42
|
+
* import { entrySchemaRegistry } from '../src/schemas'
|
|
43
|
+
*
|
|
44
|
+
* const canopy = await createBuildCanopy(config.server, { entrySchemaRegistry })
|
|
45
|
+
* const entries = await canopy.listEntries()
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function createBuildCanopy(config: CanopyConfig, options?: CreateBuildCanopyOptions): Promise<CanopyBuildContext>;
|
|
49
|
+
//# sourceMappingURL=build-canopy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-canopy.d.ts","sourceRoot":"","sources":["../src/build-canopy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEzD,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAGxE,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,kBAAkB,CAAC,CAiB7B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createCanopyServices } from './services.js';
|
|
2
|
+
import { createCanopyContext } from './context.js';
|
|
3
|
+
import { STATIC_DEPLOY_USER } from './build-mode.js';
|
|
4
|
+
/**
|
|
5
|
+
* One-call factory for a **build/admin** Canopy context — for standalone
|
|
6
|
+
* scripts that run entirely outside a Next.js request or build phase (index
|
|
7
|
+
* builders, content audits, codegen, ad hoc reports).
|
|
8
|
+
*
|
|
9
|
+
* **This is a build/admin context: it reads the filesystem directly as a
|
|
10
|
+
* synthetic admin user (`STATIC_DEPLOY_USER`) and bypasses ALL branch and
|
|
11
|
+
* path ACLs.** There is no per-request auth to enforce because there is no
|
|
12
|
+
* request. Do not reach for this from request-handling code (an API route,
|
|
13
|
+
* a server component rendering for a specific visitor, anything with a real
|
|
14
|
+
* user on the other end) — there, use `createNextCanopyContext(...)`'s
|
|
15
|
+
* `getCanopy()` (or its phase-selecting `read`/`readByUrlPath`), which
|
|
16
|
+
* enforce branch/path permissions per user. This factory mirrors the same
|
|
17
|
+
* `createCanopyServices` + `createCanopyContext` + `STATIC_DEPLOY_USER` boot
|
|
18
|
+
* sequence that `createNextCanopyContext`'s own `getCanopyForBuild()` uses
|
|
19
|
+
* internally, minus the Next.js pieces (no `next/headers`, no React
|
|
20
|
+
* `cache()`, no request-time misuse guard — there is no request phase for
|
|
21
|
+
* it to guard against) — so it works in a plain script run with `tsx`/`node`.
|
|
22
|
+
*
|
|
23
|
+
* Trivially stubbable by design: the boot sequence is one function call
|
|
24
|
+
* taking a plain config object and returning a plain context, so a script
|
|
25
|
+
* that calls it can be imported and exercised from a test with the config
|
|
26
|
+
* (or this function itself) swapped out. Contrast with a hand-rolled
|
|
27
|
+
* top-level-`await` boot block, which can never be imported by a test at
|
|
28
|
+
* all — that's the concrete problem this factory removes.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // scripts/build-search-index.ts
|
|
33
|
+
* import { createBuildCanopy } from 'canopycms/server'
|
|
34
|
+
* import config from '../canopycms.config.js'
|
|
35
|
+
* import { entrySchemaRegistry } from '../src/schemas.js'
|
|
36
|
+
*
|
|
37
|
+
* const canopy = await createBuildCanopy(config.server, { entrySchemaRegistry })
|
|
38
|
+
* const entries = await canopy.listEntries()
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export async function createBuildCanopy(config, options = {}) {
|
|
42
|
+
const services = await createCanopyServices(config, {
|
|
43
|
+
entrySchemaRegistry: options.entrySchemaRegistry,
|
|
44
|
+
});
|
|
45
|
+
const { buildContentTree, listEntries, read, readByUrlPath, services: contextServices, } = await createCanopyContext({
|
|
46
|
+
services,
|
|
47
|
+
extractUser: async () => STATIC_DEPLOY_USER,
|
|
48
|
+
}).getContext();
|
|
49
|
+
return { buildContentTree, listEntries, read, readByUrlPath, services: contextServices };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=build-canopy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-canopy.js","sourceRoot":"","sources":["../src/build-canopy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,mBAAmB,EAA2B,MAAM,WAAW,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAUjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAoB,EACpB,UAAoC,EAAE;IAEtC,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE;QAClD,mBAAmB,EAAE,OAAO,CAAC,mBAAmB;KACjD,CAAC,CAAA;IAEF,MAAM,EACJ,gBAAgB,EAChB,WAAW,EACX,IAAI,EACJ,aAAa,EACb,QAAQ,EAAE,eAAe,GAC1B,GAAG,MAAM,mBAAmB,CAAC;QAC5B,QAAQ;QACR,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,kBAAkB;KAC5C,CAAC,CAAC,UAAU,EAAE,CAAA;IAEf,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAA;AAC1F,CAAC"}
|
package/dist/cli/cli.js
CHANGED
|
@@ -2590,14 +2590,44 @@ function parseComponentProps(attrString) {
|
|
|
2590
2590
|
}
|
|
2591
2591
|
return props;
|
|
2592
2592
|
}
|
|
2593
|
+
function isFenceCloseLine(line, marker) {
|
|
2594
|
+
return line.startsWith(marker) && /^\s*$/.test(line.slice(marker.length));
|
|
2595
|
+
}
|
|
2596
|
+
function maskFencedCodeBlocks(body, blocks) {
|
|
2597
|
+
const lines = body.split("\n");
|
|
2598
|
+
const lineCount = lines.length;
|
|
2599
|
+
const nextCloseLine = {
|
|
2600
|
+
"```": new Array(lineCount + 1).fill(-1),
|
|
2601
|
+
"~~~": new Array(lineCount + 1).fill(-1)
|
|
2602
|
+
};
|
|
2603
|
+
for (let i2 = lineCount - 1; i2 >= 0; i2--) {
|
|
2604
|
+
nextCloseLine["```"][i2] = isFenceCloseLine(lines[i2], "```") ? i2 : nextCloseLine["```"][i2 + 1];
|
|
2605
|
+
nextCloseLine["~~~"][i2] = isFenceCloseLine(lines[i2], "~~~") ? i2 : nextCloseLine["~~~"][i2 + 1];
|
|
2606
|
+
}
|
|
2607
|
+
const outLines = [];
|
|
2608
|
+
let i = 0;
|
|
2609
|
+
while (i < lineCount) {
|
|
2610
|
+
const line = lines[i];
|
|
2611
|
+
const marker = line.startsWith("```") ? "```" : line.startsWith("~~~") ? "~~~" : null;
|
|
2612
|
+
const searchFrom = i + 2;
|
|
2613
|
+
const closeIndex = marker && searchFrom < lineCount ? nextCloseLine[marker][searchFrom] : -1;
|
|
2614
|
+
if (!marker || closeIndex === -1) {
|
|
2615
|
+
outLines.push(line);
|
|
2616
|
+
i++;
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
const block = lines.slice(i, closeIndex + 1).join("\n");
|
|
2620
|
+
const idx = blocks.length;
|
|
2621
|
+
blocks.push(block);
|
|
2622
|
+
outLines.push(`${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`);
|
|
2623
|
+
i = closeIndex + 1;
|
|
2624
|
+
}
|
|
2625
|
+
return outLines.join("\n");
|
|
2626
|
+
}
|
|
2593
2627
|
function maskCodeBlocks(body) {
|
|
2594
2628
|
const blocks = [];
|
|
2595
2629
|
const inlines = [];
|
|
2596
|
-
let masked = body
|
|
2597
|
-
const idx = blocks.length;
|
|
2598
|
-
blocks.push(block);
|
|
2599
|
-
return `${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`;
|
|
2600
|
-
});
|
|
2630
|
+
let masked = maskFencedCodeBlocks(body, blocks);
|
|
2601
2631
|
masked = masked.replace(/``[^`]+``|`[^`]+`/g, (span) => {
|
|
2602
2632
|
const idx = inlines.length;
|
|
2603
2633
|
inlines.push(span);
|
|
@@ -3631,16 +3661,25 @@ async function resolveBranchRoot(config) {
|
|
|
3631
3661
|
});
|
|
3632
3662
|
return context.branchRoot;
|
|
3633
3663
|
}
|
|
3634
|
-
async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
3664
|
+
async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
|
|
3635
3665
|
const rootPath = options?.rootPath ?? contentRootName;
|
|
3636
3666
|
const extract = options?.extract;
|
|
3637
3667
|
const filter = options?.filter;
|
|
3638
3668
|
const customSort = options?.sort;
|
|
3639
3669
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
3670
|
+
const shouldInclude = visibility?.shouldInclude;
|
|
3671
|
+
const skippedFiles = [];
|
|
3640
3672
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
3641
|
-
const entries = await listCollectionEntries(branchRoot, collection);
|
|
3642
|
-
|
|
3673
|
+
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
3674
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.physicalPath)) : entries;
|
|
3675
|
+
return visible.map((entry) => ({ entry, collection }));
|
|
3643
3676
|
}));
|
|
3677
|
+
if (isBuildMode() && skippedFiles.length > 0) {
|
|
3678
|
+
const lines = skippedFiles.map(({ filename, collectionPath }) => ` - ${collectionPath}/${filename}`);
|
|
3679
|
+
throw new Error(`CanopyCMS static build: found ${skippedFiles.length} file(s) that look like malformed content entries inside a content collection:
|
|
3680
|
+
${lines.join("\n")}
|
|
3681
|
+
Expected {type}.{slug}.{id}.{ext} with a known entry type (a name declared in that collection's entries config) and a valid 12-char Base58 ID. This usually means a schema rename without a matching file rename, an entry type declared in one collection but not another, or a stray file placed directly in a collection directory. If this is meant to be a colocated sibling artifact (e.g. an entry's contentId-prefixed data file read via an entryTransforms readSibling call), keep its name to three or fewer dot-separated segments (id.suffix.ext) so it is never mistaken for an entry attempt. Otherwise rename or move the file, or fix the schema, then rebuild.`);
|
|
3682
|
+
}
|
|
3644
3683
|
const contentPrefix = contentRootName ? `${contentRootName}/` : "";
|
|
3645
3684
|
const items = [];
|
|
3646
3685
|
for (const results of collectionResults) {
|
|
@@ -3667,7 +3706,8 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
|
3667
3706
|
entryType: entry.entryType,
|
|
3668
3707
|
format: entry.format,
|
|
3669
3708
|
data,
|
|
3670
|
-
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
|
|
3709
|
+
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema,
|
|
3710
|
+
updatedAt: entry.updatedAt
|
|
3671
3711
|
};
|
|
3672
3712
|
if (filter && !filter(item))
|
|
3673
3713
|
continue;
|
|
@@ -4022,7 +4062,7 @@ async function generateAIContentCLI(options) {
|
|
|
4022
4062
|
console.log(` Output: ${result.outputDir}
|
|
4023
4063
|
`);
|
|
4024
4064
|
}
|
|
4025
|
-
var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2, init_logger2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env2, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned22, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
|
|
4065
|
+
var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2, init_logger2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env2, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned22, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
|
|
4026
4066
|
var init_generate_ai_content = __esm({
|
|
4027
4067
|
"dist/cli/generate-ai-content.js"() {
|
|
4028
4068
|
"use strict";
|
|
@@ -7846,29 +7886,38 @@ var init_generate_ai_content = __esm({
|
|
|
7846
7886
|
}
|
|
7847
7887
|
};
|
|
7848
7888
|
parseTypedFilename = (filename, entryTypes) => {
|
|
7889
|
+
if (filename.startsWith("."))
|
|
7890
|
+
return null;
|
|
7849
7891
|
const lastDot = filename.lastIndexOf(".");
|
|
7850
7892
|
if (lastDot === -1)
|
|
7851
7893
|
return null;
|
|
7852
7894
|
const nameWithoutExt = filename.slice(0, lastDot);
|
|
7853
7895
|
const parts = nameWithoutExt.split(".");
|
|
7854
|
-
if (parts.length
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
if (!isValidId(id))
|
|
7860
|
-
return null;
|
|
7861
|
-
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
7862
|
-
return {
|
|
7863
|
-
type: potentialType,
|
|
7864
|
-
slug,
|
|
7865
|
-
id
|
|
7866
|
-
};
|
|
7867
|
-
}
|
|
7896
|
+
if (parts.length < 3)
|
|
7897
|
+
return null;
|
|
7898
|
+
const potentialType = parts[0];
|
|
7899
|
+
if (entryTypes && !entryTypes.some((e) => e.name === potentialType)) {
|
|
7900
|
+
return null;
|
|
7868
7901
|
}
|
|
7869
|
-
|
|
7902
|
+
const id = parts[parts.length - 1];
|
|
7903
|
+
if (!isValidId(id))
|
|
7904
|
+
return null;
|
|
7905
|
+
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
7906
|
+
return {
|
|
7907
|
+
type: potentialType,
|
|
7908
|
+
slug,
|
|
7909
|
+
id
|
|
7910
|
+
};
|
|
7870
7911
|
};
|
|
7871
|
-
|
|
7912
|
+
looksLikeMalformedEntry = (filename, entryTypes) => {
|
|
7913
|
+
if (filename.startsWith(".") || filename.startsWith("_"))
|
|
7914
|
+
return false;
|
|
7915
|
+
const segments = filename.split(".");
|
|
7916
|
+
if (segments.length >= 4)
|
|
7917
|
+
return true;
|
|
7918
|
+
return segments.length === 3 && entryTypes.some((e) => e.name === segments[0]);
|
|
7919
|
+
};
|
|
7920
|
+
listCollectionEntries = async (root, collection, onSkip) => {
|
|
7872
7921
|
if (collection.type !== "collection" || !collection.entries) {
|
|
7873
7922
|
return [];
|
|
7874
7923
|
}
|
|
@@ -7901,6 +7950,9 @@ var init_generate_ai_content = __esm({
|
|
|
7901
7950
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
7902
7951
|
if (!parsed) {
|
|
7903
7952
|
log7.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
|
|
7953
|
+
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
7954
|
+
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
7955
|
+
}
|
|
7904
7956
|
return null;
|
|
7905
7957
|
}
|
|
7906
7958
|
const { type: entryTypeName, slug, id: contentId } = parsed;
|
|
@@ -3298,14 +3298,44 @@ var BLOCK_RESTORE_RE = /<<CODEBLOCK(\d+)>>/g;
|
|
|
3298
3298
|
var INLINE_PREFIX = "<<INLINECODE";
|
|
3299
3299
|
var INLINE_SUFFIX = ">>";
|
|
3300
3300
|
var INLINE_RESTORE_RE = /<<INLINECODE(\d+)>>/g;
|
|
3301
|
+
function isFenceCloseLine(line, marker) {
|
|
3302
|
+
return line.startsWith(marker) && /^\s*$/.test(line.slice(marker.length));
|
|
3303
|
+
}
|
|
3304
|
+
function maskFencedCodeBlocks(body, blocks) {
|
|
3305
|
+
const lines = body.split("\n");
|
|
3306
|
+
const lineCount = lines.length;
|
|
3307
|
+
const nextCloseLine = {
|
|
3308
|
+
"```": new Array(lineCount + 1).fill(-1),
|
|
3309
|
+
"~~~": new Array(lineCount + 1).fill(-1)
|
|
3310
|
+
};
|
|
3311
|
+
for (let i2 = lineCount - 1; i2 >= 0; i2--) {
|
|
3312
|
+
nextCloseLine["```"][i2] = isFenceCloseLine(lines[i2], "```") ? i2 : nextCloseLine["```"][i2 + 1];
|
|
3313
|
+
nextCloseLine["~~~"][i2] = isFenceCloseLine(lines[i2], "~~~") ? i2 : nextCloseLine["~~~"][i2 + 1];
|
|
3314
|
+
}
|
|
3315
|
+
const outLines = [];
|
|
3316
|
+
let i = 0;
|
|
3317
|
+
while (i < lineCount) {
|
|
3318
|
+
const line = lines[i];
|
|
3319
|
+
const marker = line.startsWith("```") ? "```" : line.startsWith("~~~") ? "~~~" : null;
|
|
3320
|
+
const searchFrom = i + 2;
|
|
3321
|
+
const closeIndex = marker && searchFrom < lineCount ? nextCloseLine[marker][searchFrom] : -1;
|
|
3322
|
+
if (!marker || closeIndex === -1) {
|
|
3323
|
+
outLines.push(line);
|
|
3324
|
+
i++;
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
const block = lines.slice(i, closeIndex + 1).join("\n");
|
|
3328
|
+
const idx = blocks.length;
|
|
3329
|
+
blocks.push(block);
|
|
3330
|
+
outLines.push(`${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`);
|
|
3331
|
+
i = closeIndex + 1;
|
|
3332
|
+
}
|
|
3333
|
+
return outLines.join("\n");
|
|
3334
|
+
}
|
|
3301
3335
|
function maskCodeBlocks(body) {
|
|
3302
3336
|
const blocks = [];
|
|
3303
3337
|
const inlines = [];
|
|
3304
|
-
let masked = body
|
|
3305
|
-
const idx = blocks.length;
|
|
3306
|
-
blocks.push(block);
|
|
3307
|
-
return `${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`;
|
|
3308
|
-
});
|
|
3338
|
+
let masked = maskFencedCodeBlocks(body, blocks);
|
|
3309
3339
|
masked = masked.replace(/``[^`]+``|`[^`]+`/g, (span) => {
|
|
3310
3340
|
const idx = inlines.length;
|
|
3311
3341
|
inlines.push(span);
|
|
@@ -5772,38 +5802,48 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
|
5772
5802
|
}
|
|
5773
5803
|
};
|
|
5774
5804
|
var parseTypedFilename = (filename, entryTypes) => {
|
|
5805
|
+
if (filename.startsWith("."))
|
|
5806
|
+
return null;
|
|
5775
5807
|
const lastDot = filename.lastIndexOf(".");
|
|
5776
5808
|
if (lastDot === -1)
|
|
5777
5809
|
return null;
|
|
5778
5810
|
const nameWithoutExt = filename.slice(0, lastDot);
|
|
5779
5811
|
const parts = nameWithoutExt.split(".");
|
|
5780
|
-
if (parts.length
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
if (!isValidId(id))
|
|
5786
|
-
return null;
|
|
5787
|
-
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
5788
|
-
return {
|
|
5789
|
-
type: potentialType,
|
|
5790
|
-
slug,
|
|
5791
|
-
id
|
|
5792
|
-
};
|
|
5793
|
-
}
|
|
5812
|
+
if (parts.length < 3)
|
|
5813
|
+
return null;
|
|
5814
|
+
const potentialType = parts[0];
|
|
5815
|
+
if (entryTypes && !entryTypes.some((e) => e.name === potentialType)) {
|
|
5816
|
+
return null;
|
|
5794
5817
|
}
|
|
5795
|
-
|
|
5818
|
+
const id = parts[parts.length - 1];
|
|
5819
|
+
if (!isValidId(id))
|
|
5820
|
+
return null;
|
|
5821
|
+
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
5822
|
+
return {
|
|
5823
|
+
type: potentialType,
|
|
5824
|
+
slug,
|
|
5825
|
+
id
|
|
5826
|
+
};
|
|
5796
5827
|
};
|
|
5797
|
-
async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
5828
|
+
async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
|
|
5798
5829
|
const rootPath = options?.rootPath ?? contentRootName;
|
|
5799
5830
|
const extract = options?.extract;
|
|
5800
5831
|
const filter = options?.filter;
|
|
5801
5832
|
const customSort = options?.sort;
|
|
5802
5833
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
5834
|
+
const shouldInclude = visibility?.shouldInclude;
|
|
5835
|
+
const skippedFiles = [];
|
|
5803
5836
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
5804
|
-
const entries = await listCollectionEntries(branchRoot, collection);
|
|
5805
|
-
|
|
5837
|
+
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
5838
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.physicalPath)) : entries;
|
|
5839
|
+
return visible.map((entry) => ({ entry, collection }));
|
|
5806
5840
|
}));
|
|
5841
|
+
if (isBuildMode() && skippedFiles.length > 0) {
|
|
5842
|
+
const lines = skippedFiles.map(({ filename, collectionPath }) => ` - ${collectionPath}/${filename}`);
|
|
5843
|
+
throw new Error(`CanopyCMS static build: found ${skippedFiles.length} file(s) that look like malformed content entries inside a content collection:
|
|
5844
|
+
${lines.join("\n")}
|
|
5845
|
+
Expected {type}.{slug}.{id}.{ext} with a known entry type (a name declared in that collection's entries config) and a valid 12-char Base58 ID. This usually means a schema rename without a matching file rename, an entry type declared in one collection but not another, or a stray file placed directly in a collection directory. If this is meant to be a colocated sibling artifact (e.g. an entry's contentId-prefixed data file read via an entryTransforms readSibling call), keep its name to three or fewer dot-separated segments (id.suffix.ext) so it is never mistaken for an entry attempt. Otherwise rename or move the file, or fix the schema, then rebuild.`);
|
|
5846
|
+
}
|
|
5807
5847
|
const contentPrefix = contentRootName ? `${contentRootName}/` : "";
|
|
5808
5848
|
const items = [];
|
|
5809
5849
|
for (const results of collectionResults) {
|
|
@@ -5830,7 +5870,8 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
|
5830
5870
|
entryType: entry.entryType,
|
|
5831
5871
|
format: entry.format,
|
|
5832
5872
|
data,
|
|
5833
|
-
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
|
|
5873
|
+
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema,
|
|
5874
|
+
updatedAt: entry.updatedAt
|
|
5834
5875
|
};
|
|
5835
5876
|
if (filter && !filter(item))
|
|
5836
5877
|
continue;
|
|
@@ -5844,7 +5885,15 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
|
5844
5885
|
}
|
|
5845
5886
|
return items;
|
|
5846
5887
|
}
|
|
5847
|
-
var
|
|
5888
|
+
var looksLikeMalformedEntry = (filename, entryTypes) => {
|
|
5889
|
+
if (filename.startsWith(".") || filename.startsWith("_"))
|
|
5890
|
+
return false;
|
|
5891
|
+
const segments = filename.split(".");
|
|
5892
|
+
if (segments.length >= 4)
|
|
5893
|
+
return true;
|
|
5894
|
+
return segments.length === 3 && entryTypes.some((e) => e.name === segments[0]);
|
|
5895
|
+
};
|
|
5896
|
+
var listCollectionEntries = async (root, collection, onSkip) => {
|
|
5848
5897
|
if (collection.type !== "collection" || !collection.entries) {
|
|
5849
5898
|
return [];
|
|
5850
5899
|
}
|
|
@@ -5877,6 +5926,9 @@ var listCollectionEntries = async (root, collection) => {
|
|
|
5877
5926
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
5878
5927
|
if (!parsed) {
|
|
5879
5928
|
log7.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
|
|
5929
|
+
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
5930
|
+
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
5931
|
+
}
|
|
5880
5932
|
return null;
|
|
5881
5933
|
}
|
|
5882
5934
|
const { type: entryTypeName, slug, id: contentId } = parsed;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { defineCanopyConfig } from 'canopycms'
|
|
2
2
|
|
|
3
3
|
export default defineCanopyConfig({
|
|
4
|
-
|
|
4
|
+
// Access defaults are fail-closed. Both layers are checked on every content
|
|
5
|
+
// read or write, and BOTH must allow it.
|
|
6
|
+
//
|
|
7
|
+
// 'deny' means "branches you neither created nor were invited to". You still
|
|
8
|
+
// get your own branches (the creator of an un-ACL'd branch always has access)
|
|
9
|
+
// and the base branch everyone lands on, so this is not a lockout -- it stops
|
|
10
|
+
// one editor from wandering into another's in-progress work.
|
|
11
|
+
//
|
|
12
|
+
// The PATH layer defaults to public read rather than 'deny': without this, a
|
|
13
|
+
// non-admin visitor (including the dev-auth default user this scaffold uses
|
|
14
|
+
// out of the box) gets "Forbidden: path access denied" on every route the
|
|
15
|
+
// moment you run `npm run dev`. Edit and review stay closed -- this is not
|
|
16
|
+
// a security walk-back, just the posture README.md's "Public read on
|
|
17
|
+
// server deployments" section recommends, applied from the start so the
|
|
18
|
+
// first run isn't a wall. Tighten to 'deny' yourself if you don't want
|
|
19
|
+
// public read (e.g. an internal-only site).
|
|
20
|
+
defaultBranchAccess: 'deny',
|
|
21
|
+
defaultPathAccess: { read: 'allow' },
|
|
5
22
|
// Leave this as 'dev'. This file is loaded by `next dev`, by `next build`
|
|
6
23
|
// inside the deployment image, and by the deployed server -- and the first
|
|
7
24
|
// two must be 'dev' (a prod-mode build read would look for a branch
|