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.
Files changed (73) hide show
  1. package/dist/ai/index.d.ts +9 -0
  2. package/dist/ai/index.d.ts.map +1 -1
  3. package/dist/ai/index.js +9 -0
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/to-plain-text.d.ts +57 -0
  6. package/dist/ai/to-plain-text.d.ts.map +1 -0
  7. package/dist/ai/to-plain-text.js +255 -0
  8. package/dist/ai/to-plain-text.js.map +1 -0
  9. package/dist/ai/transform-components.d.ts.map +1 -1
  10. package/dist/ai/transform-components.js +70 -5
  11. package/dist/ai/transform-components.js.map +1 -1
  12. package/dist/api/comments.d.ts +2 -2
  13. package/dist/authorization/branch.d.ts +55 -3
  14. package/dist/authorization/branch.d.ts.map +1 -1
  15. package/dist/authorization/branch.js +55 -9
  16. package/dist/authorization/branch.js.map +1 -1
  17. package/dist/authorization/types.d.ts +1 -1
  18. package/dist/authorization/types.d.ts.map +1 -1
  19. package/dist/build-canopy.d.ts +49 -0
  20. package/dist/build-canopy.d.ts.map +1 -0
  21. package/dist/build-canopy.js +51 -0
  22. package/dist/build-canopy.js.map +1 -0
  23. package/dist/cli/cli.js +78 -26
  24. package/dist/cli/generate-ai-content.js +77 -25
  25. package/dist/cli/template-files/canopycms.config.ts.template +18 -1
  26. package/dist/content-listing.d.ts +110 -5
  27. package/dist/content-listing.d.ts.map +1 -1
  28. package/dist/content-listing.js +164 -23
  29. package/dist/content-listing.js.map +1 -1
  30. package/dist/content-reader.d.ts +41 -4
  31. package/dist/content-reader.d.ts.map +1 -1
  32. package/dist/content-reader.js +17 -4
  33. package/dist/content-reader.js.map +1 -1
  34. package/dist/content-store.d.ts +10 -1
  35. package/dist/content-store.d.ts.map +1 -1
  36. package/dist/content-store.js.map +1 -1
  37. package/dist/content-tree.d.ts +31 -2
  38. package/dist/content-tree.d.ts.map +1 -1
  39. package/dist/content-tree.js +22 -5
  40. package/dist/content-tree.js.map +1 -1
  41. package/dist/context.d.ts +30 -10
  42. package/dist/context.d.ts.map +1 -1
  43. package/dist/context.js +55 -4
  44. package/dist/context.js.map +1 -1
  45. package/dist/entry-schema.d.ts +228 -8
  46. package/dist/entry-schema.d.ts.map +1 -1
  47. package/dist/entry-schema.js +124 -0
  48. package/dist/entry-schema.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +7 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/server.d.ts +51 -0
  54. package/dist/server.d.ts.map +1 -1
  55. package/dist/server.js +49 -0
  56. package/dist/server.js.map +1 -1
  57. package/dist/services.d.ts +1 -2
  58. package/dist/services.d.ts.map +1 -1
  59. package/dist/services.js +2 -6
  60. package/dist/services.js.map +1 -1
  61. package/dist/static/index.d.ts +65 -5
  62. package/dist/static/index.d.ts.map +1 -1
  63. package/dist/static/index.js +65 -12
  64. package/dist/static/index.js.map +1 -1
  65. package/dist/static/seo.d.ts +150 -0
  66. package/dist/static/seo.d.ts.map +1 -0
  67. package/dist/static/seo.js +189 -0
  68. package/dist/static/seo.js.map +1 -0
  69. package/dist/utils/sanitize-href.d.ts +46 -0
  70. package/dist/utils/sanitize-href.d.ts.map +1 -1
  71. package/dist/utils/sanitize-href.js +63 -2
  72. package/dist/utils/sanitize-href.js.map +1 -1
  73. 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
- if (managerOrAdminAllowed) {
21
- return { allowed: false, reason: 'denied_by_acl' };
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
- const allowed = defaultAccess === 'allow';
24
- return { allowed, reason: 'no_acl' };
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;AAI/C;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAC1C,OAAsB,EACtB,IAAgB,EAChB,gBAAqC,MAAM;IAE3C,+CAA+C;IAC/C,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAA;IAChD,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAA;IACpC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAA;IACvD,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAA;IACzD,MAAM,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,IAAI,KAAK,CAAA;IAEnE,IAAI,CAAC,iBAAiB,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC9C,IAAI,qBAAqB,EAAE,CAAC;YAC1B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;QACpD,CAAC;QACD,MAAM,OAAO,GAAG,aAAa,KAAK,OAAO,CAAA;QACzC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IACtC,CAAC;IAED,MAAM,WAAW,GAAG,iBAAiB,IAAI,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACnF,MAAM,YAAY,GAChB,kBAAkB,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAEnF,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,IAAI,YAAY,CAAC,CAAA;IACpD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,eAAe,EAAE,CAAA;AAC1E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,gBAAqC,MAAM;IACjF,OAAO,CAAC,OAAsB,EAAE,IAAgB,EAAsB,EAAE,CACtE,4BAA4B,CAAC,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,CAAA;AAC9D,CAAC;AAcD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAsB,EACtB,IAAgB,EAChB,gBAAqC,MAAM,EAC3C,OAA+B;IAE/B,4EAA4E;IAC5E,MAAM,YAAY,GAAG,4BAA4B,CAAC,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,CAAA;IAE/E,6DAA6D;IAC7D,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,sCAAsC;IACtC,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,CAAC,MAAM,CAAA;IAE9D,kFAAkF;IAClF,MAAM,cAAc,GAClB,CAAC,OAAO,EAAE,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,kBAAkB,CAAA;IAEhF,YAAY;IACZ,6BAA6B;IAC7B,wEAAwE;IACxE,uCAAuC;IACvC,OAAO,CACL,aAAa;QACb,YAAY,CAAC,MAAM,KAAK,YAAY;QACpC,YAAY,CAAC,MAAM,KAAK,gBAAgB;QACxC,CAAC,cAAc,IAAI,YAAY,CAAC,OAAO,CAAC,CACzC,CAAA;AACH,CAAC"}
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;CACrE;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"}
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.replace(/^(```|~~~).*\n[\s\S]*?\n\1\s*$/gm, (block) => {
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
- return entries.map((entry) => ({ entry, collection }));
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 >= 3) {
7855
- const potentialType = parts[0];
7856
- const matchingType = entryTypes.find((e) => e.name === potentialType);
7857
- if (matchingType) {
7858
- const id = parts[parts.length - 1];
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
- return null;
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
- listCollectionEntries = async (root, collection) => {
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.replace(/^(```|~~~).*\n[\s\S]*?\n\1\s*$/gm, (block) => {
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 >= 3) {
5781
- const potentialType = parts[0];
5782
- const matchingType = entryTypes.find((e) => e.name === potentialType);
5783
- if (matchingType) {
5784
- const id = parts[parts.length - 1];
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
- return null;
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
- return entries.map((entry) => ({ entry, collection }));
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 listCollectionEntries = async (root, collection) => {
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
- defaultBranchAccess: 'allow',
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