@next-buildr/payload 0.0.0-stage → 2.0.1

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 (250) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/adapter/adapter.test.d.ts +2 -0
  5. package/dist/adapter/adapter.test.d.ts.map +1 -0
  6. package/dist/adapter/adapter.test.js +282 -0
  7. package/dist/adapter/canvas-data-source.d.ts +9 -0
  8. package/dist/adapter/canvas-data-source.d.ts.map +1 -0
  9. package/dist/adapter/canvas-data-source.js +24 -0
  10. package/dist/adapter/document-adapter.d.ts +20 -0
  11. package/dist/adapter/document-adapter.d.ts.map +1 -0
  12. package/dist/adapter/document-adapter.js +183 -0
  13. package/dist/adapter/http.d.ts +34 -0
  14. package/dist/adapter/http.d.ts.map +1 -0
  15. package/dist/adapter/http.js +72 -0
  16. package/dist/adapter/index.d.ts +4 -0
  17. package/dist/adapter/index.d.ts.map +1 -0
  18. package/dist/adapter/index.js +5 -0
  19. package/dist/admin/index.d.ts +4 -0
  20. package/dist/admin/index.d.ts.map +1 -0
  21. package/dist/admin/index.js +2 -0
  22. package/dist/admin/layout-field-connected.d.ts +6 -0
  23. package/dist/admin/layout-field-connected.d.ts.map +1 -0
  24. package/dist/admin/layout-field-connected.js +16 -0
  25. package/dist/admin/layout-field-view.d.ts +16 -0
  26. package/dist/admin/layout-field-view.d.ts.map +1 -0
  27. package/dist/admin/layout-field-view.js +14 -0
  28. package/dist/admin/layout-field.d.ts +6 -0
  29. package/dist/admin/layout-field.d.ts.map +1 -0
  30. package/dist/admin/layout-field.js +13 -0
  31. package/dist/admin/layout-field.test.d.ts +2 -0
  32. package/dist/admin/layout-field.test.d.ts.map +1 -0
  33. package/dist/admin/layout-field.test.js +41 -0
  34. package/dist/contract.d.ts +289 -0
  35. package/dist/contract.d.ts.map +1 -0
  36. package/dist/contract.js +176 -0
  37. package/dist/data/build-context.d.ts +20 -0
  38. package/dist/data/build-context.d.ts.map +1 -0
  39. package/dist/data/build-context.js +25 -0
  40. package/dist/data/data.test.d.ts +2 -0
  41. package/dist/data/data.test.d.ts.map +1 -0
  42. package/dist/data/data.test.js +261 -0
  43. package/dist/data/index.d.ts +10 -0
  44. package/dist/data/index.d.ts.map +1 -0
  45. package/dist/data/index.js +5 -0
  46. package/dist/data/normalize.d.ts +14 -0
  47. package/dist/data/normalize.d.ts.map +1 -0
  48. package/dist/data/normalize.js +128 -0
  49. package/dist/data/payload-data-source.d.ts +42 -0
  50. package/dist/data/payload-data-source.d.ts.map +1 -0
  51. package/dist/data/payload-data-source.js +179 -0
  52. package/dist/data/payload-data-source.test.d.ts +2 -0
  53. package/dist/data/payload-data-source.test.d.ts.map +1 -0
  54. package/dist/data/payload-data-source.test.js +160 -0
  55. package/dist/data/resolve-layout.d.ts +45 -0
  56. package/dist/data/resolve-layout.d.ts.map +1 -0
  57. package/dist/data/resolve-layout.js +111 -0
  58. package/dist/data/schema-from-fields.d.ts +63 -0
  59. package/dist/data/schema-from-fields.d.ts.map +1 -0
  60. package/dist/data/schema-from-fields.js +178 -0
  61. package/dist/data/where.d.ts +53 -0
  62. package/dist/data/where.d.ts.map +1 -0
  63. package/dist/data/where.js +210 -0
  64. package/dist/mcp/http-backend.d.ts +35 -0
  65. package/dist/mcp/http-backend.d.ts.map +1 -0
  66. package/dist/mcp/http-backend.js +337 -0
  67. package/dist/mcp/http-backend.test.d.ts +2 -0
  68. package/dist/mcp/http-backend.test.d.ts.map +1 -0
  69. package/dist/mcp/http-backend.test.js +178 -0
  70. package/dist/mcp/index.d.ts +2 -0
  71. package/dist/mcp/index.d.ts.map +1 -0
  72. package/dist/mcp/index.js +2 -0
  73. package/dist/mcp/route.d.ts +63 -0
  74. package/dist/mcp/route.d.ts.map +1 -0
  75. package/dist/mcp/route.js +230 -0
  76. package/dist/next/get-document.d.ts +64 -0
  77. package/dist/next/get-document.d.ts.map +1 -0
  78. package/dist/next/get-document.js +208 -0
  79. package/dist/next/get-document.test.d.ts +2 -0
  80. package/dist/next/get-document.test.d.ts.map +1 -0
  81. package/dist/next/get-document.test.js +155 -0
  82. package/dist/next/index.d.ts +9 -0
  83. package/dist/next/index.d.ts.map +1 -0
  84. package/dist/next/index.js +4 -0
  85. package/dist/next/next-helpers.test.d.ts +2 -0
  86. package/dist/next/next-helpers.test.d.ts.map +1 -0
  87. package/dist/next/next-helpers.test.js +122 -0
  88. package/dist/next/revalidate.d.ts +44 -0
  89. package/dist/next/revalidate.d.ts.map +1 -0
  90. package/dist/next/revalidate.js +45 -0
  91. package/dist/next/seo.d.ts +43 -0
  92. package/dist/next/seo.d.ts.map +1 -0
  93. package/dist/next/seo.js +53 -0
  94. package/dist/next/tags.d.ts +23 -0
  95. package/dist/next/tags.d.ts.map +1 -0
  96. package/dist/next/tags.js +31 -0
  97. package/dist/plugin/access.d.ts +19 -0
  98. package/dist/plugin/access.d.ts.map +1 -0
  99. package/dist/plugin/access.js +32 -0
  100. package/dist/plugin/collections/form-submissions.d.ts +10 -0
  101. package/dist/plugin/collections/form-submissions.d.ts.map +1 -0
  102. package/dist/plugin/collections/form-submissions.js +37 -0
  103. package/dist/plugin/collections/templates.d.ts +13 -0
  104. package/dist/plugin/collections/templates.d.ts.map +1 -0
  105. package/dist/plugin/collections/templates.js +118 -0
  106. package/dist/plugin/endpoints/access.test.d.ts +2 -0
  107. package/dist/plugin/endpoints/access.test.d.ts.map +1 -0
  108. package/dist/plugin/endpoints/access.test.js +91 -0
  109. package/dist/plugin/endpoints/adapter-contract.test.d.ts +2 -0
  110. package/dist/plugin/endpoints/adapter-contract.test.d.ts.map +1 -0
  111. package/dist/plugin/endpoints/adapter-contract.test.js +45 -0
  112. package/dist/plugin/endpoints/context.d.ts +65 -0
  113. package/dist/plugin/endpoints/context.d.ts.map +1 -0
  114. package/dist/plugin/endpoints/context.js +113 -0
  115. package/dist/plugin/endpoints/data-context.d.ts +8 -0
  116. package/dist/plugin/endpoints/data-context.d.ts.map +1 -0
  117. package/dist/plugin/endpoints/data-context.js +63 -0
  118. package/dist/plugin/endpoints/data-endpoints.test.d.ts +2 -0
  119. package/dist/plugin/endpoints/data-endpoints.test.d.ts.map +1 -0
  120. package/dist/plugin/endpoints/data-endpoints.test.js +125 -0
  121. package/dist/plugin/endpoints/data-query.test.d.ts +2 -0
  122. package/dist/plugin/endpoints/data-query.test.d.ts.map +1 -0
  123. package/dist/plugin/endpoints/data-query.test.js +96 -0
  124. package/dist/plugin/endpoints/data-schema.d.ts +5 -0
  125. package/dist/plugin/endpoints/data-schema.d.ts.map +1 -0
  126. package/dist/plugin/endpoints/data-schema.js +21 -0
  127. package/dist/plugin/endpoints/data.d.ts +10 -0
  128. package/dist/plugin/endpoints/data.d.ts.map +1 -0
  129. package/dist/plugin/endpoints/data.js +96 -0
  130. package/dist/plugin/endpoints/document.d.ts +11 -0
  131. package/dist/plugin/endpoints/document.d.ts.map +1 -0
  132. package/dist/plugin/endpoints/document.js +119 -0
  133. package/dist/plugin/endpoints/documents.d.ts +10 -0
  134. package/dist/plugin/endpoints/documents.d.ts.map +1 -0
  135. package/dist/plugin/endpoints/documents.js +145 -0
  136. package/dist/plugin/endpoints/endpoints.test-kit.d.ts +42 -0
  137. package/dist/plugin/endpoints/endpoints.test-kit.d.ts.map +1 -0
  138. package/dist/plugin/endpoints/endpoints.test-kit.js +141 -0
  139. package/dist/plugin/endpoints/endpoints.test.d.ts +2 -0
  140. package/dist/plugin/endpoints/endpoints.test.d.ts.map +1 -0
  141. package/dist/plugin/endpoints/endpoints.test.js +209 -0
  142. package/dist/plugin/endpoints/forms.d.ts +16 -0
  143. package/dist/plugin/endpoints/forms.d.ts.map +1 -0
  144. package/dist/plugin/endpoints/forms.js +199 -0
  145. package/dist/plugin/endpoints/forms.test.d.ts +2 -0
  146. package/dist/plugin/endpoints/forms.test.d.ts.map +1 -0
  147. package/dist/plugin/endpoints/forms.test.js +346 -0
  148. package/dist/plugin/endpoints/guards.d.ts +9 -0
  149. package/dist/plugin/endpoints/guards.d.ts.map +1 -0
  150. package/dist/plugin/endpoints/guards.js +35 -0
  151. package/dist/plugin/endpoints/index.d.ts +6 -0
  152. package/dist/plugin/endpoints/index.d.ts.map +1 -0
  153. package/dist/plugin/endpoints/index.js +32 -0
  154. package/dist/plugin/endpoints/locales-no-fallback.test.d.ts +2 -0
  155. package/dist/plugin/endpoints/locales-no-fallback.test.d.ts.map +1 -0
  156. package/dist/plugin/endpoints/locales-no-fallback.test.js +30 -0
  157. package/dist/plugin/endpoints/locales.test.d.ts +2 -0
  158. package/dist/plugin/endpoints/locales.test.d.ts.map +1 -0
  159. package/dist/plugin/endpoints/locales.test.js +128 -0
  160. package/dist/plugin/endpoints/manifest.d.ts +5 -0
  161. package/dist/plugin/endpoints/manifest.d.ts.map +1 -0
  162. package/dist/plugin/endpoints/manifest.js +18 -0
  163. package/dist/plugin/endpoints/mcp-http-backend.test.d.ts +2 -0
  164. package/dist/plugin/endpoints/mcp-http-backend.test.d.ts.map +1 -0
  165. package/dist/plugin/endpoints/mcp-http-backend.test.js +84 -0
  166. package/dist/plugin/endpoints/mcp-off.test.d.ts +2 -0
  167. package/dist/plugin/endpoints/mcp-off.test.d.ts.map +1 -0
  168. package/dist/plugin/endpoints/mcp-off.test.js +33 -0
  169. package/dist/plugin/endpoints/mcp-publish.test.d.ts +2 -0
  170. package/dist/plugin/endpoints/mcp-publish.test.d.ts.map +1 -0
  171. package/dist/plugin/endpoints/mcp-publish.test.js +42 -0
  172. package/dist/plugin/endpoints/mcp-route.test.d.ts +2 -0
  173. package/dist/plugin/endpoints/mcp-route.test.d.ts.map +1 -0
  174. package/dist/plugin/endpoints/mcp-route.test.js +132 -0
  175. package/dist/plugin/endpoints/mcp.test.d.ts +2 -0
  176. package/dist/plugin/endpoints/mcp.test.d.ts.map +1 -0
  177. package/dist/plugin/endpoints/mcp.test.js +231 -0
  178. package/dist/plugin/endpoints/media.d.ts +7 -0
  179. package/dist/plugin/endpoints/media.d.ts.map +1 -0
  180. package/dist/plugin/endpoints/media.js +115 -0
  181. package/dist/plugin/endpoints/media.test.d.ts +2 -0
  182. package/dist/plugin/endpoints/media.test.d.ts.map +1 -0
  183. package/dist/plugin/endpoints/media.test.js +95 -0
  184. package/dist/plugin/endpoints/publish-a11y.test.d.ts +2 -0
  185. package/dist/plugin/endpoints/publish-a11y.test.d.ts.map +1 -0
  186. package/dist/plugin/endpoints/publish-a11y.test.js +27 -0
  187. package/dist/plugin/endpoints/publish.d.ts +9 -0
  188. package/dist/plugin/endpoints/publish.d.ts.map +1 -0
  189. package/dist/plugin/endpoints/publish.js +81 -0
  190. package/dist/plugin/endpoints/rename.d.ts +9 -0
  191. package/dist/plugin/endpoints/rename.d.ts.map +1 -0
  192. package/dist/plugin/endpoints/rename.js +47 -0
  193. package/dist/plugin/endpoints/respond.d.ts +9 -0
  194. package/dist/plugin/endpoints/respond.d.ts.map +1 -0
  195. package/dist/plugin/endpoints/respond.js +10 -0
  196. package/dist/plugin/endpoints/samples.d.ts +5 -0
  197. package/dist/plugin/endpoints/samples.d.ts.map +1 -0
  198. package/dist/plugin/endpoints/samples.js +42 -0
  199. package/dist/plugin/endpoints/save.d.ts +9 -0
  200. package/dist/plugin/endpoints/save.d.ts.map +1 -0
  201. package/dist/plugin/endpoints/save.js +58 -0
  202. package/dist/plugin/endpoints/session.d.ts +5 -0
  203. package/dist/plugin/endpoints/session.d.ts.map +1 -0
  204. package/dist/plugin/endpoints/session.js +30 -0
  205. package/dist/plugin/endpoints/templates.test.d.ts +2 -0
  206. package/dist/plugin/endpoints/templates.test.d.ts.map +1 -0
  207. package/dist/plugin/endpoints/templates.test.js +191 -0
  208. package/dist/plugin/fields.d.ts +28 -0
  209. package/dist/plugin/fields.d.ts.map +1 -0
  210. package/dist/plugin/fields.js +62 -0
  211. package/dist/plugin/forms/forms.test.d.ts +2 -0
  212. package/dist/plugin/forms/forms.test.d.ts.map +1 -0
  213. package/dist/plugin/forms/forms.test.js +88 -0
  214. package/dist/plugin/forms/notify.d.ts +6 -0
  215. package/dist/plugin/forms/notify.d.ts.map +1 -0
  216. package/dist/plugin/forms/notify.js +13 -0
  217. package/dist/plugin/forms/rate-limit.d.ts +24 -0
  218. package/dist/plugin/forms/rate-limit.d.ts.map +1 -0
  219. package/dist/plugin/forms/rate-limit.js +36 -0
  220. package/dist/plugin/forms/validate.d.ts +22 -0
  221. package/dist/plugin/forms/validate.d.ts.map +1 -0
  222. package/dist/plugin/forms/validate.js +79 -0
  223. package/dist/plugin/hooks/layout-hook.d.ts +9 -0
  224. package/dist/plugin/hooks/layout-hook.d.ts.map +1 -0
  225. package/dist/plugin/hooks/layout-hook.js +33 -0
  226. package/dist/plugin/hooks/layout-hook.test.d.ts +2 -0
  227. package/dist/plugin/hooks/layout-hook.test.d.ts.map +1 -0
  228. package/dist/plugin/hooks/layout-hook.test.js +191 -0
  229. package/dist/plugin/hooks/process-layout.d.ts +32 -0
  230. package/dist/plugin/hooks/process-layout.d.ts.map +1 -0
  231. package/dist/plugin/hooks/process-layout.js +68 -0
  232. package/dist/plugin/hooks/read-layout.d.ts +17 -0
  233. package/dist/plugin/hooks/read-layout.d.ts.map +1 -0
  234. package/dist/plugin/hooks/read-layout.js +39 -0
  235. package/dist/plugin/index.d.ts +18 -0
  236. package/dist/plugin/index.d.ts.map +1 -0
  237. package/dist/plugin/index.js +105 -0
  238. package/dist/plugin/locales.d.ts +32 -0
  239. package/dist/plugin/locales.d.ts.map +1 -0
  240. package/dist/plugin/locales.js +56 -0
  241. package/dist/plugin/options.d.ts +77 -0
  242. package/dist/plugin/options.d.ts.map +1 -0
  243. package/dist/plugin/options.js +123 -0
  244. package/dist/plugin/plugin.test.d.ts +2 -0
  245. package/dist/plugin/plugin.test.d.ts.map +1 -0
  246. package/dist/plugin/plugin.test.js +186 -0
  247. package/dist/plugin/write-guard.d.ts +14 -0
  248. package/dist/plugin/write-guard.d.ts.map +1 -0
  249. package/dist/plugin/write-guard.js +18 -0
  250. package/package.json +108 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/plugin/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKzD,uDAAuD;AACvD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;CAC9B;AACD,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAExE,mGAAmG;AACnG,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACvD;AAoBD,eAAO,MAAM,aAAa;;;;;;;;;8CAdT,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqHjD,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE7D,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,eAAe,CAS1E"}
@@ -0,0 +1,123 @@
1
+ import { z } from 'zod';
2
+ /** A function the host supplies; Zod only checks that it is one, the plugin never runs it at startup. */
3
+ const fn = () => z.custom((value) => typeof value === 'function', 'must be a function');
4
+ const collectionOptions = z.object({
5
+ /** The name the collection document is bound under in dynamic values (`page`, `post`, ...). */
6
+ context: z.string().min(1),
7
+ /** The public path of a document; drives previews, revalidation and `route` in the context. */
8
+ path: fn().optional(),
9
+ /** Templates (`buildr-templates`) can be attached to this collection. */
10
+ templates: z.boolean().default(false),
11
+ /** How deep relations are resolved in the context. */
12
+ depth: z.number().int().min(0).max(5).default(0),
13
+ /** Whether the a11y rules expect exactly one `h1`. */
14
+ expectH1: z.boolean().default(false),
15
+ });
16
+ const queryableOptions = z.object({
17
+ fields: z.array(z.string().min(1)),
18
+ sort: z.array(z.string().min(1)).default([]),
19
+ });
20
+ export const optionsSchema = z.object({
21
+ /** Only its metadata and migrations are used, so `{ meta }` is enough (a React registry fits too). */
22
+ registry: z
23
+ .custom((value) => typeof value === 'object' &&
24
+ value !== null &&
25
+ typeof value.meta === 'object')
26
+ .optional(),
27
+ routes: z
28
+ .object({
29
+ editor: z.string().startsWith('/').default('/buildr/edit'),
30
+ canvas: z.string().startsWith('/').default('/buildr/canvas'),
31
+ preview: z.string().startsWith('/').default('/buildr/preview'),
32
+ })
33
+ .default({ editor: '/buildr/edit', canvas: '/buildr/canvas', preview: '/buildr/preview' }),
34
+ collections: z.record(z.string().min(1), collectionOptions),
35
+ globals: z.record(z.string().min(1), z.string().min(1)).default({}),
36
+ queryable: z.record(z.string().min(1), queryableOptions).default({}),
37
+ media: z.object({ collection: z.string().min(1) }).optional(),
38
+ forms: z
39
+ .object({
40
+ /** Adds `buildr-form-submissions` and the public submission endpoint (needs `registry`). */
41
+ enabled: z.boolean().default(false),
42
+ /** Who a notification may go to: exact addresses or domains (`@example.com`). */
43
+ notifyAllowlist: z.array(z.string()).default([]),
44
+ /** Notified about every submission; each must match `notifyAllowlist`. */
45
+ notifyTo: z.array(z.string()).default([]),
46
+ /** Attempts per visitor and form per window; used by the bundled in-memory limiter. */
47
+ rateLimit: z
48
+ .object({
49
+ limit: z.number().int().positive().default(5),
50
+ windowMs: z.number().int().positive().default(60_000),
51
+ })
52
+ .default({ limit: 5, windowMs: 60_000 }),
53
+ /** Replaces the in-memory limiter (which does not span serverless instances). */
54
+ rateLimiter: z
55
+ .custom((value) => typeof value === 'object' &&
56
+ value !== null &&
57
+ typeof value.hit === 'function', 'must be a rate limiter')
58
+ .optional(),
59
+ })
60
+ .default({
61
+ enabled: false,
62
+ notifyAllowlist: [],
63
+ notifyTo: [],
64
+ rateLimit: { limit: 5, windowMs: 60_000 },
65
+ }),
66
+ access: z
67
+ .object({
68
+ edit: fn().optional(),
69
+ publish: fn().optional(),
70
+ unlockTemplates: fn().optional(),
71
+ })
72
+ .default({}),
73
+ /**
74
+ * Access for AI agents (docs/mcp.md, ADR-024): Payload API-key requests, document listing and
75
+ * creation. Off by default; an API-key request is refused by the builder endpoints unless enabled.
76
+ */
77
+ mcp: z
78
+ .object({
79
+ enabled: z.boolean().default(false),
80
+ /** Lets an API-key user publish (they still need `access.publish`); off by default. */
81
+ allowPublish: z.boolean().default(false),
82
+ /** The collections an agent may list, create in and edit; default: all builder collections. */
83
+ collections: z.array(z.string().min(1)).optional(),
84
+ /** Writes per API-key user and window; used by the bundled in-memory limiter. */
85
+ rateLimit: z
86
+ .object({
87
+ limit: z.number().int().positive().default(60),
88
+ windowMs: z.number().int().positive().default(60_000),
89
+ })
90
+ .default({ limit: 60, windowMs: 60_000 }),
91
+ /** Replaces the in-memory limiter (which does not span serverless instances). */
92
+ rateLimiter: z
93
+ .custom((value) => typeof value === 'object' &&
94
+ value !== null &&
95
+ typeof value.hit === 'function', 'must be a rate limiter')
96
+ .optional(),
97
+ })
98
+ .default({
99
+ enabled: false,
100
+ allowPublish: false,
101
+ rateLimit: { limit: 60, windowMs: 60_000 },
102
+ }),
103
+ limits: z
104
+ .object({
105
+ maxNodes: z.number().int().positive().default(5000),
106
+ maxBytes: z.number().int().positive().default(2_000_000),
107
+ })
108
+ .default({ maxNodes: 5000, maxBytes: 2_000_000 }),
109
+ a11y: z
110
+ .object({ publish: z.enum(['warn', 'block']).default('warn') })
111
+ .default({ publish: 'warn' }),
112
+ });
113
+ /** Validates the options (a bad config fails at startup, with every problem listed). */
114
+ export function resolveOptions(input) {
115
+ const parsed = optionsSchema.safeParse(input);
116
+ if (!parsed.success) {
117
+ const problems = parsed.error.issues
118
+ .map((issue) => ` - ${issue.path.join('.') || '(options)'}: ${issue.message}`)
119
+ .join('\n');
120
+ throw new Error(`buildrPlugin: invalid options\n${problems}`);
121
+ }
122
+ return parsed.data;
123
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../src/plugin/plugin.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,186 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { createEmptyDocument } from '@next-buildr/core';
5
+ import { sqliteAdapter } from '@payloadcms/db-sqlite';
6
+ import { buildConfig, getPayload } from 'payload';
7
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8
+ import { buildrPlugin } from './index.js';
9
+ import { BUILDR_WRITE } from './write-guard.js';
10
+ const pages = {
11
+ slug: 'pages',
12
+ fields: [{ name: 'title', type: 'text' }],
13
+ versions: { drafts: { autosave: true } },
14
+ };
15
+ const layoutOf = (name) => ({
16
+ ...createEmptyDocument(),
17
+ nodes: { root: { id: 'root', type: 'buildr/page', name } },
18
+ });
19
+ let dir;
20
+ const configOf = (options, collections = [pages]) => buildConfig({
21
+ secret: 'test-secret',
22
+ collections,
23
+ db: sqliteAdapter({
24
+ client: { url: `file:${join(dir, `${Math.random().toString(36).slice(2)}.db`)}` },
25
+ }),
26
+ plugins: [buildrPlugin(options)],
27
+ typescript: { autoGenerate: false },
28
+ });
29
+ // Only the write-guard suite talks to a database: Payload pushes the schema through one shared
30
+ // drizzle-kit state, so a second live instance in the same process would find no tables.
31
+ const boot = async (options) => getPayload({ config: configOf(options), key: 'write-guard' });
32
+ beforeAll(() => {
33
+ dir = mkdtempSync(join(tmpdir(), 'buildr-payload-'));
34
+ });
35
+ afterAll(() => {
36
+ try {
37
+ rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ catch {
40
+ // Windows keeps the database file open a moment longer; the temp dir is cleaned by the OS.
41
+ }
42
+ });
43
+ describe('buildrPlugin', () => {
44
+ it('adds the fields to the configured collections only', async () => {
45
+ const config = await configOf({ collections: { pages: { context: 'page', templates: true } } });
46
+ const names = (slug) => config.collections
47
+ .find((collection) => collection.slug === slug)
48
+ ?.fields.map((field) => field.name);
49
+ expect(names('pages')).toEqual(expect.arrayContaining(['layout', 'buildrRevision', 'template']));
50
+ expect(names('buildr-templates')).toEqual(expect.arrayContaining(['title', 'targetCollection', 'isDefault', 'layout']));
51
+ const layout = config.collections[0]?.fields.find((field) => field.name === 'layout');
52
+ expect(layout.admin?.components?.Field).toEqual({
53
+ path: '@next-buildr/payload/admin#LayoutField',
54
+ clientProps: { editorRoute: '/buildr/edit' },
55
+ });
56
+ });
57
+ it('leaves out the template field unless the collection takes templates', async () => {
58
+ const config = await configOf({ collections: { pages: { context: 'page' } } });
59
+ const names = config.collections[0]?.fields.map((field) => field.name);
60
+ expect(names).not.toContain('template');
61
+ });
62
+ it('adds buildr-templates only when a collection takes templates, and never twice', async () => {
63
+ const without = await configOf({ collections: { pages: { context: 'page' } } });
64
+ expect(without.collections.map((collection) => collection.slug)).not.toContain('buildr-templates');
65
+ await expect(configOf({ collections: { pages: { context: 'page', templates: true } } }, [
66
+ pages,
67
+ { slug: 'buildr-templates', fields: [] },
68
+ ])).rejects.toThrow(/added by the plugin/);
69
+ });
70
+ it('rejects bad options, listing each problem', () => {
71
+ expect(() => buildrPlugin({ collections: { pages: { context: '' } }, limits: { maxNodes: -1 } })).toThrow(/collections\.pages\.context[\s\S]*limits\.maxNodes/);
72
+ });
73
+ it('requires an existing collection with drafts and free field names', async () => {
74
+ await expect(configOf({ collections: { nope: { context: 'x' } } })).rejects.toThrow(/"nope" does not exist/);
75
+ await expect(configOf({ collections: { plain: { context: 'x' } } }, [{ slug: 'plain', fields: [] }])).rejects.toThrow(/versions\.drafts/);
76
+ await expect(configOf({ collections: { pages: { context: 'x' } } }, [
77
+ { ...pages, fields: [{ name: 'layout', type: 'text' }] },
78
+ ])).rejects.toThrow(/already has a "layout" field/);
79
+ });
80
+ describe('write-guard', () => {
81
+ let payload;
82
+ beforeAll(async () => {
83
+ payload = await boot({ collections: { pages: { context: 'page' } } });
84
+ });
85
+ const create = () => payload.create({
86
+ collection: 'pages',
87
+ data: { title: 'One', layout: layoutOf('a'), buildrRevision: 1 },
88
+ draft: true,
89
+ });
90
+ const latest = async (id) => {
91
+ const found = await payload.findByID({ collection: 'pages', id, draft: true });
92
+ return found;
93
+ };
94
+ it('accepts the layout when the document is created', async () => {
95
+ const doc = await create();
96
+ expect((await latest(doc.id)).layout).toEqual(layoutOf('a'));
97
+ });
98
+ it('never changes the layout for an update that is not the builder', async () => {
99
+ const doc = await create();
100
+ // What the admin form does: it resubmits the layout it loaded, next to the edited title.
101
+ await payload.update({
102
+ collection: 'pages',
103
+ id: doc.id,
104
+ data: { title: 'Two', layout: layoutOf('stale'), buildrRevision: 99 },
105
+ draft: true,
106
+ });
107
+ const after = await latest(doc.id);
108
+ expect(after.title).toBe('Two');
109
+ expect(after.layout).toEqual(layoutOf('a'));
110
+ expect(after.buildrRevision).toBe(1);
111
+ });
112
+ it('keeps the builder edits when the admin saves afterwards (drafts and autosave)', async () => {
113
+ const doc = await create();
114
+ await payload.update({
115
+ collection: 'pages',
116
+ id: doc.id,
117
+ data: { layout: layoutOf('built'), buildrRevision: 2 },
118
+ draft: true,
119
+ autosave: true,
120
+ context: { [BUILDR_WRITE]: true },
121
+ });
122
+ await payload.update({
123
+ collection: 'pages',
124
+ id: doc.id,
125
+ data: { title: 'Renamed', layout: layoutOf('a'), buildrRevision: 1 },
126
+ draft: true,
127
+ autosave: true,
128
+ });
129
+ const after = await latest(doc.id);
130
+ expect(after.title).toBe('Renamed');
131
+ expect(after.layout).toEqual(layoutOf('built'));
132
+ expect(after.buildrRevision).toBe(2);
133
+ });
134
+ it('protects the layout when the admin publishes too', async () => {
135
+ const doc = await create();
136
+ await payload.update({
137
+ collection: 'pages',
138
+ id: doc.id,
139
+ data: { layout: layoutOf('built'), buildrRevision: 2 },
140
+ draft: true,
141
+ context: { [BUILDR_WRITE]: true },
142
+ });
143
+ await payload.update({
144
+ collection: 'pages',
145
+ id: doc.id,
146
+ data: { title: 'Live', layout: layoutOf('a'), _status: 'published' },
147
+ draft: false,
148
+ });
149
+ const published = (await payload.findByID({
150
+ collection: 'pages',
151
+ id: doc.id,
152
+ }));
153
+ expect(published.layout).toEqual(layoutOf('built'));
154
+ });
155
+ });
156
+ });
157
+ describe('buildrPlugin mcp option', () => {
158
+ const collections = { pages: { context: 'page' } };
159
+ const users = (auth) => ({
160
+ slug: 'users',
161
+ auth,
162
+ fields: [],
163
+ });
164
+ it('is off by default and needs no API-key collection then', async () => {
165
+ const config = await configOf({ collections }, [pages]);
166
+ const page = config.collections.find((c) => c.slug === 'pages');
167
+ expect(page?.fields.some((f) => 'name' in f && f.name === 'buildrUpdatedBy')).toBe(false);
168
+ });
169
+ it('requires an auth collection with useAPIKey when enabled', async () => {
170
+ await expect(configOf({ collections, mcp: { enabled: true } }, [pages, users(true)])).rejects.toThrow(/useAPIKey/);
171
+ });
172
+ it('adds buildrUpdatedBy when enabled', async () => {
173
+ const config = await configOf({ collections, mcp: { enabled: true } }, [
174
+ pages,
175
+ users({ useAPIKey: true }),
176
+ ]);
177
+ const page = config.collections.find((c) => c.slug === 'pages');
178
+ expect(page?.fields.some((f) => 'name' in f && f.name === 'buildrUpdatedBy')).toBe(true);
179
+ });
180
+ it('rejects mcp.collections that are not builder collections', async () => {
181
+ await expect(configOf({ collections, mcp: { enabled: true, collections: ['posts'] } }, [
182
+ pages,
183
+ users({ useAPIKey: true }),
184
+ ])).rejects.toThrow(/mcp\.collections "posts"/);
185
+ });
186
+ });
@@ -0,0 +1,14 @@
1
+ import type { FieldHook } from 'payload';
2
+ /**
3
+ * The context flag every write of the builder endpoints sets (docs/payload.md#save-autosave-drafts-publish-versions).
4
+ * Anything else that saves the document (Payload Admin form, the REST API, a script) is not allowed to change `layout`.
5
+ */
6
+ export declare const BUILDR_WRITE = "buildrWrite";
7
+ /**
8
+ * Payload Admin submits every field, including the `layout` it loaded when the form opened; taking
9
+ * that value would overwrite what the builder saved since. So a write only changes the guarded field
10
+ * when it comes from the builder (or creates the document); otherwise the stored value is kept.
11
+ * `originalDoc` is the latest draft when drafts are enabled, which is exactly the value to keep.
12
+ */
13
+ export declare const writeGuard: (name: string) => FieldHook;
14
+ //# sourceMappingURL=write-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-guard.d.ts","sourceRoot":"","sources":["../../src/plugin/write-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,YAAY,gBAAgB,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,UAAU,SACd,MAAM,KAAG,SAKf,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The context flag every write of the builder endpoints sets (docs/payload.md#save-autosave-drafts-publish-versions).
3
+ * Anything else that saves the document (Payload Admin form, the REST API, a script) is not allowed to change `layout`.
4
+ */
5
+ export const BUILDR_WRITE = 'buildrWrite';
6
+ /**
7
+ * Payload Admin submits every field, including the `layout` it loaded when the form opened; taking
8
+ * that value would overwrite what the builder saved since. So a write only changes the guarded field
9
+ * when it comes from the builder (or creates the document); otherwise the stored value is kept.
10
+ * `originalDoc` is the latest draft when drafts are enabled, which is exactly the value to keep.
11
+ */
12
+ export const writeGuard = (name) => ({ value, originalDoc, operation, req }) => {
13
+ if (operation === 'create')
14
+ return value;
15
+ if (req.context[BUILDR_WRITE] === true)
16
+ return value;
17
+ return originalDoc?.[name];
18
+ };
package/package.json CHANGED
@@ -1,6 +1,111 @@
1
1
  {
2
2
  "name": "@next-buildr/payload",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.0.1",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ "./plugin": {
10
+ "types": "./dist/plugin/index.d.ts",
11
+ "default": "./dist/plugin/index.js"
12
+ },
13
+ "./data": {
14
+ "types": "./dist/data/index.d.ts",
15
+ "default": "./dist/data/index.js"
16
+ },
17
+ "./admin": {
18
+ "types": "./dist/admin/index.d.ts",
19
+ "default": "./dist/admin/index.js"
20
+ },
21
+ "./adapter": {
22
+ "types": "./dist/adapter/index.d.ts",
23
+ "default": "./dist/adapter/index.js"
24
+ },
25
+ "./next": {
26
+ "types": "./dist/next/index.d.ts",
27
+ "default": "./dist/next/index.js"
28
+ },
29
+ "./mcp": {
30
+ "types": "./dist/mcp/index.d.ts",
31
+ "default": "./dist/mcp/index.js"
32
+ },
33
+ "./package.json": "./package.json",
34
+ "./mcp/route": {
35
+ "types": "./dist/mcp/route.d.ts",
36
+ "default": "./dist/mcp/route.js"
37
+ }
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "dependencies": {
46
+ "@next-buildr/core": "2.0.1",
47
+ "zod": "^4.6.5"
48
+ },
49
+ "peerDependencies": {
50
+ "@next-buildr/editor": "2.0.1",
51
+ "@next-buildr/mcp": "2.0.1",
52
+ "@next-buildr/next": "2.0.1",
53
+ "@modelcontextprotocol/sdk": "^1.30.1",
54
+ "@payloadcms/ui": "^3.0.0",
55
+ "next": "^15.0.0 || ^16.0.0",
56
+ "payload": "^3.0.0"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@next-buildr/editor": {
60
+ "optional": true
61
+ },
62
+ "@next-buildr/mcp": {
63
+ "optional": true
64
+ },
65
+ "@next-buildr/next": {
66
+ "optional": true
67
+ },
68
+ "@modelcontextprotocol/sdk": {
69
+ "optional": true
70
+ },
71
+ "@payloadcms/ui": {
72
+ "optional": true
73
+ },
74
+ "next": {
75
+ "optional": true
76
+ },
77
+ "payload": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@next-buildr/editor": "2.0.1",
83
+ "@next-buildr/mcp": "2.0.1",
84
+ "@next-buildr/next": "2.0.1",
85
+ "@next-buildr/test-utils": "2.0.1",
86
+ "@next-buildr/tsconfig": "2.0.1",
87
+ "@modelcontextprotocol/sdk": "^1.30.1",
88
+ "@payloadcms/db-sqlite": "3.90.1",
89
+ "@payloadcms/ui": "3.90.1",
90
+ "@types/node": "^22.0.0",
91
+ "@types/react": "^19.0.0",
92
+ "@types/react-dom": "^19.0.0",
93
+ "jsdom": "^30.1.1",
94
+ "next": "16.3.5",
95
+ "payload": "3.90.1",
96
+ "react": "^19.0.0",
97
+ "react-dom": "^19.0.0",
98
+ "typescript": "7.0.2",
99
+ "vitest": "5.0.1"
100
+ },
101
+ "repository": {
102
+ "type": "git",
103
+ "url": "git+https://github.com/czekanskyy/buildr.git",
104
+ "directory": "packages/payload"
105
+ },
106
+ "scripts": {
107
+ "build": "tsc -b",
108
+ "typecheck": "tsc --noEmit",
109
+ "test": "vitest run"
110
+ }
6
111
  }