@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":"access.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/access.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,91 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { boot, title, withChild } from './endpoints.test-kit.js';
3
+ const emailOf = (req) => String(req.user.email);
4
+ let h;
5
+ let tokens;
6
+ let path;
7
+ beforeAll(async () => {
8
+ h = await boot('access', {
9
+ access: {
10
+ edit: ({ req }) => !emailOf(req).startsWith('viewer'),
11
+ publish: ({ req }) => emailOf(req).startsWith('publisher'),
12
+ unlockTemplates: ({ req }) => emailOf(req).startsWith('publisher'),
13
+ },
14
+ });
15
+ tokens = {
16
+ viewer: await h.login('viewer@example.com'),
17
+ author: await h.login('author@example.com'),
18
+ publisher: await h.login('publisher@example.com'),
19
+ };
20
+ const doc = await h.payload.create({
21
+ collection: 'pages',
22
+ data: { title: 'Home', slug: 'home' },
23
+ draft: true,
24
+ });
25
+ path = `/buildr/documents/pages/${doc.id}`;
26
+ });
27
+ afterAll(() => h.close());
28
+ const layout = withChild(title('Hi'));
29
+ const save = (token, revision) => h.call('PUT', path, {
30
+ token,
31
+ body: { document: layout, baseRevision: revision, autosave: false },
32
+ });
33
+ const publish = (token, revision) => h.call('POST', `${path}/publish`, { token, body: { baseRevision: revision } });
34
+ describe('role x endpoint matrix', () => {
35
+ it('answers 401 without a user, whatever the endpoint', async () => {
36
+ const statuses = [
37
+ (await h.call('GET', path, { token: null })).status,
38
+ (await save(null, 0)).status,
39
+ (await publish(null, 0)).status,
40
+ ];
41
+ expect(statuses).toEqual([401, 401, 401]);
42
+ });
43
+ it('keeps a viewer out of the builder altogether', async () => {
44
+ expect((await h.call('GET', path, { token: tokens.viewer })).status).toBe(403);
45
+ expect((await save(tokens.viewer, 0)).status).toBe(403);
46
+ expect((await publish(tokens.viewer, 0)).status).toBe(403);
47
+ });
48
+ it('lets an author save but not publish, and a publisher do both', async () => {
49
+ expect((await save(tokens.author, 0)).status).toBe(200);
50
+ expect((await publish(tokens.author, 1)).status).toBe(403);
51
+ expect((await publish(tokens.publisher, 1)).status).toBe(200);
52
+ });
53
+ it('reports each role its permissions in the session', async () => {
54
+ const permissions = async (token) => (await h.call('GET', '/buildr/session', { token })).body.permissions;
55
+ expect(await permissions(tokens.viewer)).toEqual({
56
+ canEdit: false,
57
+ canPublish: false,
58
+ canUnlockTemplates: false,
59
+ });
60
+ expect(await permissions(tokens.author)).toEqual({
61
+ canEdit: true,
62
+ canPublish: false,
63
+ canUnlockTemplates: false,
64
+ });
65
+ expect(await permissions(tokens.publisher)).toEqual({
66
+ canEdit: true,
67
+ canPublish: true,
68
+ canUnlockTemplates: true,
69
+ });
70
+ });
71
+ });
72
+ describe('CSRF guards on mutating requests', () => {
73
+ const send = (headers, method = 'PUT') => h.call(method, method === 'PUT' ? path : `${path}/publish`, {
74
+ token: tokens.publisher,
75
+ body: method === 'PUT'
76
+ ? { document: layout, baseRevision: 99, autosave: false }
77
+ : { baseRevision: 99 },
78
+ headers,
79
+ });
80
+ it('refuses a foreign Origin, but accepts the own one and none', async () => {
81
+ expect((await send({ origin: 'https://evil.example' })).status).toBe(403);
82
+ expect((await send({ origin: 'https://evil.example' }, 'POST')).status).toBe(403);
83
+ expect((await send({ origin: 'null' })).status).toBe(403);
84
+ expect((await send({ origin: 'http://localhost' })).status).toBe(409);
85
+ expect((await send({})).status).toBe(409);
86
+ });
87
+ it('refuses a body that is not declared as JSON', async () => {
88
+ expect((await send({ 'content-type': 'text/plain' })).status).toBe(415);
89
+ expect((await send({ 'content-type': 'application/x-www-form-urlencoded' })).status).toBe(415);
90
+ });
91
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter-contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-contract.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/adapter-contract.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ import { handleEndpoints } from 'payload';
2
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
3
+ import { createPayloadAdapter } from '../../adapter/index.js';
4
+ import { boot } from './endpoints.test-kit.js';
5
+ let h;
6
+ let pageId;
7
+ beforeAll(async () => {
8
+ h = await boot('adapter-server');
9
+ const page = await h.payload.create({
10
+ collection: 'pages',
11
+ data: { title: 'Home', slug: 'home' },
12
+ draft: true,
13
+ });
14
+ pageId = String(page.id);
15
+ });
16
+ afterAll(() => h.close());
17
+ /** The adapter's `fetch`, answered by the real endpoints (no network). */
18
+ const serverFetch = ((input, init = {}) => handleEndpoints({
19
+ config: h.payload.config,
20
+ request: new Request(new URL(input, 'http://localhost'), {
21
+ ...init,
22
+ headers: { ...init.headers, authorization: `JWT ${h.token}` },
23
+ }),
24
+ }));
25
+ describe('the adapter against the real endpoints', () => {
26
+ const adapter = () => createPayloadAdapter({ baseUrl: '/api', fetch: serverFetch });
27
+ it('reads the session and the data schema', async () => {
28
+ const ref = { collection: 'pages', id: pageId };
29
+ expect(await adapter().getSession(ref)).toMatchObject({ canEdit: true, canPublish: true });
30
+ const schema = await adapter().getDataSchema(ref);
31
+ expect(Object.keys(schema.scopes)).toContain('page');
32
+ });
33
+ it('lists samples and builds a context', async () => {
34
+ const ref = { collection: 'pages', id: pageId };
35
+ expect(await adapter().listSamples?.(ref)).toEqual([{ id: `pages:${pageId}`, label: 'Home' }]);
36
+ const context = await adapter().getContext(ref);
37
+ expect(context.scopes['page']).toMatchObject({ title: 'Home' });
38
+ expect(context.mode).toBe('canvas');
39
+ });
40
+ it('rejects when the server refuses (a collection that is not configured)', async () => {
41
+ await expect(adapter().getDataSchema({ collection: 'users', id: '1' })).rejects.toMatchObject({
42
+ status: 404,
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,65 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { SchemaOptions, SchemaSource } from '../../data/index.ts';
3
+ import { type Action } from '../access.ts';
4
+ import type { RateLimiter } from '../forms/rate-limit.ts';
5
+ import type { LocaleArgs } from '../locales.ts';
6
+ import type { ResolvedOptions } from '../options.ts';
7
+ export interface DocumentTarget {
8
+ readonly collection: string;
9
+ readonly id: string;
10
+ }
11
+ /** Who may do what; decided per request. */
12
+ export interface EndpointEnv {
13
+ readonly options: ResolvedOptions;
14
+ /** Limits form submissions (present when forms are enabled). */
15
+ readonly rateLimiter?: RateLimiter | undefined;
16
+ /** Limits the writes of API-key requests (present when `mcp.enabled`). */
17
+ readonly writeLimiter?: RateLimiter | undefined;
18
+ }
19
+ export type Guarded<T> = {
20
+ readonly ok: true;
21
+ readonly value: T;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly response: Response;
25
+ };
26
+ /** Whether an API-key user may use the builder collection (`mcp.collections`; default: all of them). */
27
+ export declare const mcpMayUse: (env: EndpointEnv, collection: string) => boolean;
28
+ /**
29
+ * The write rate limit of API-key requests (browser sessions are not limited): the response to send
30
+ * instead (`429`), or `undefined`.
31
+ */
32
+ export declare function writeLimit(env: EndpointEnv, req: PayloadRequest): Promise<Response | undefined>;
33
+ /** The context of every builder write: the guard flag, and whether an API key made the request. */
34
+ export declare const writeContext: (req: PayloadRequest) => Record<string, unknown>;
35
+ /**
36
+ * The user a write is attributed to (`buildrUpdatedBy`, added with `mcp.enabled`); it lands in the
37
+ * document and in every version, so history shows the agent's user.
38
+ */
39
+ export declare function updatedBy(env: EndpointEnv, req: PayloadRequest): Record<string, unknown>;
40
+ /** An authenticated user and a configured collection (the `:collection` param). */
41
+ export declare function collectionOf(env: EndpointEnv, req: PayloadRequest): Guarded<string>;
42
+ /** An authenticated user and a configured collection with an id: what every document endpoint needs first. */
43
+ export declare function targetOf(env: EndpointEnv, req: PayloadRequest): Guarded<DocumentTarget>;
44
+ /** The global that becomes the `site` scope. */
45
+ export declare const SITE_GLOBAL = "site-settings";
46
+ /** What the schema and the context derive from: the live Payload config and the plugin names. */
47
+ export declare function schemaEnv(env: EndpointEnv, req: PayloadRequest): {
48
+ source: SchemaSource;
49
+ options: SchemaOptions;
50
+ };
51
+ /** The JSON body of a request, or a `400` for anything else. */
52
+ export declare function bodyOf(req: PayloadRequest): Promise<Guarded<unknown>>;
53
+ /** Whether the user of `req` may `action` (see `plugin/access.ts`). */
54
+ export declare const allowed: (env: EndpointEnv, action: Action, req: PayloadRequest) => Promise<boolean>;
55
+ /** Loads the latest draft of the target, or the response to send instead (`404`). */
56
+ export declare function latestOf(req: PayloadRequest, target: DocumentTarget, options?: {
57
+ readonly localeArgs?: LocaleArgs | undefined;
58
+ readonly depth?: number;
59
+ readonly draft?: boolean;
60
+ /** Only these fields are read (Payload's `select`). */
61
+ readonly select?: Record<string, true>;
62
+ }): Promise<Guarded<Record<string, unknown>>>;
63
+ /** The stored revision of a document: 0 until the builder saved it. */
64
+ export declare const revisionOf: (doc: Record<string, unknown>) => number;
65
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import { allowed as isAllowed, isApiKeyRequest } from '../access.js';
2
+ import { BUILDR_WRITE } from '../write-guard.js';
3
+ import { fail, notFound, unauthorized } from './respond.js';
4
+ /** Whether an API-key user may use the builder collection (`mcp.collections`; default: all of them). */
5
+ export const mcpMayUse = (env, collection) => env.options.collections[collection] !== undefined &&
6
+ (env.options.mcp.collections === undefined || env.options.mcp.collections.includes(collection));
7
+ /**
8
+ * The write rate limit of API-key requests (browser sessions are not limited): the response to send
9
+ * instead (`429`), or `undefined`.
10
+ */
11
+ export async function writeLimit(env, req) {
12
+ if (!isApiKeyRequest(req) || env.writeLimiter === undefined)
13
+ return undefined;
14
+ const user = req.user;
15
+ const result = await env.writeLimiter.hit(`${user.collection ?? ''}:${String(user.id)}`);
16
+ if (result.allowed)
17
+ return undefined;
18
+ const response = fail(429, 'Too many writes. Try again later.');
19
+ response.headers.set('retry-after', String(result.retryAfterSeconds));
20
+ return response;
21
+ }
22
+ /** The context of every builder write: the guard flag, and whether an API key made the request. */
23
+ export const writeContext = (req) => ({
24
+ [BUILDR_WRITE]: true,
25
+ ...(isApiKeyRequest(req) ? { buildrApiKey: true } : {}),
26
+ });
27
+ /**
28
+ * The user a write is attributed to (`buildrUpdatedBy`, added with `mcp.enabled`); it lands in the
29
+ * document and in every version, so history shows the agent's user.
30
+ */
31
+ export function updatedBy(env, req) {
32
+ const user = req.user;
33
+ if (!env.options.mcp.enabled || user?.id === undefined || user.collection === undefined) {
34
+ return {};
35
+ }
36
+ return { buildrUpdatedBy: { relationTo: user.collection, value: user.id } };
37
+ }
38
+ /** An authenticated user and a configured collection (the `:collection` param). */
39
+ export function collectionOf(env, req) {
40
+ if (req.user === null || req.user === undefined)
41
+ return { ok: false, response: unauthorized() };
42
+ const collection = req.routeParams?.['collection'];
43
+ if (typeof collection !== 'string' || collection === '') {
44
+ return { ok: false, response: fail(400, 'A collection is required.') };
45
+ }
46
+ if (env.options.collections[collection] === undefined) {
47
+ return { ok: false, response: notFound(`The collection "${collection}"`) };
48
+ }
49
+ if (isApiKeyRequest(req) && !mcpMayUse(env, collection)) {
50
+ return { ok: false, response: fail(403, `Agents may not use the collection "${collection}".`) };
51
+ }
52
+ return { ok: true, value: collection };
53
+ }
54
+ /** An authenticated user and a configured collection with an id: what every document endpoint needs first. */
55
+ export function targetOf(env, req) {
56
+ const collection = collectionOf(env, req);
57
+ if (!collection.ok)
58
+ return collection;
59
+ const id = req.routeParams?.['id'];
60
+ if (typeof id !== 'string' || id === '') {
61
+ return { ok: false, response: fail(400, 'A collection and an id are required.') };
62
+ }
63
+ return { ok: true, value: { collection: collection.value, id } };
64
+ }
65
+ /** The global that becomes the `site` scope. */
66
+ export const SITE_GLOBAL = 'site-settings';
67
+ /** What the schema and the context derive from: the live Payload config and the plugin names. */
68
+ export function schemaEnv(env, req) {
69
+ const config = req.payload.config;
70
+ const contextNames = {};
71
+ for (const [slug, own] of Object.entries(env.options.collections)) {
72
+ contextNames[slug] = own.context;
73
+ }
74
+ return {
75
+ source: {
76
+ collections: config.collections,
77
+ globals: (config.globals ?? []),
78
+ },
79
+ options: { contextNames, siteGlobal: SITE_GLOBAL },
80
+ };
81
+ }
82
+ /** The JSON body of a request, or a `400` for anything else. */
83
+ export async function bodyOf(req) {
84
+ try {
85
+ return { ok: true, value: await req.json?.() };
86
+ }
87
+ catch {
88
+ return { ok: false, response: fail(400, 'The request body must be JSON.') };
89
+ }
90
+ }
91
+ /** Whether the user of `req` may `action` (see `plugin/access.ts`). */
92
+ export const allowed = (env, action, req) => isAllowed(env.options, action, req);
93
+ /** Loads the latest draft of the target, or the response to send instead (`404`). */
94
+ export async function latestOf(req, target, options = {}) {
95
+ try {
96
+ const doc = await req.payload.findByID({
97
+ collection: target.collection,
98
+ id: target.id,
99
+ draft: options.draft ?? true,
100
+ depth: options.depth ?? 0,
101
+ ...(options.select === undefined ? {} : { select: options.select }),
102
+ req,
103
+ overrideAccess: false,
104
+ ...options.localeArgs,
105
+ });
106
+ return { ok: true, value: doc };
107
+ }
108
+ catch {
109
+ return { ok: false, response: notFound(`The document "${target.collection}/${target.id}"`) };
110
+ }
111
+ }
112
+ /** The stored revision of a document: 0 until the builder saved it. */
113
+ export const revisionOf = (doc) => typeof doc['buildrRevision'] === 'number' ? doc['buildrRevision'] : 0;
@@ -0,0 +1,8 @@
1
+ import type { Endpoint } from 'payload';
2
+ import { type EndpointEnv } from './context.ts';
3
+ /**
4
+ * `GET /api/buildr/data/context?collection&id&draft&locale`: the scopes the canvas binds against,
5
+ * built with the access of the user who asks (`overrideAccess: false`), relations and uploads populated.
6
+ */
7
+ export declare const dataContextEndpoint: (env: EndpointEnv) => Endpoint;
8
+ //# sourceMappingURL=data-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-context.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/data-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,EAAW,KAAK,WAAW,EAAoC,MAAM,cAAc,CAAC;AAG3F;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAS,WAAW,KAAG,QAwDrD,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { dataContextQuerySchema, dataContextResponseSchema } from '../../contract.js';
2
+ import { buildContext } from '../../data/index.js';
3
+ import { chooseLocale } from '../locales.js';
4
+ import { allowed, latestOf, SITE_GLOBAL, schemaEnv } from './context.js';
5
+ import { fail, json, notFound, unauthorized } from './respond.js';
6
+ /**
7
+ * `GET /api/buildr/data/context?collection&id&draft&locale`: the scopes the canvas binds against,
8
+ * built with the access of the user who asks (`overrideAccess: false`), relations and uploads populated.
9
+ */
10
+ export const dataContextEndpoint = (env) => ({
11
+ path: '/buildr/data/context',
12
+ method: 'get',
13
+ handler: async (req) => {
14
+ if (req.user === null || req.user === undefined)
15
+ return unauthorized();
16
+ if (!(await allowed(env, 'edit', req)))
17
+ return fail(403, 'You may not edit with the builder.');
18
+ const parsed = dataContextQuerySchema.safeParse(Object.fromEntries(req.searchParams?.entries() ?? []));
19
+ if (!parsed.success)
20
+ return fail(400, 'The query does not match the contract.');
21
+ const { collection, id } = parsed.data;
22
+ const locale = chooseLocale(req, parsed.data.locale);
23
+ if (!locale.ok)
24
+ return locale.response;
25
+ const own = env.options.collections[collection];
26
+ if (own === undefined)
27
+ return notFound(`The collection "${collection}"`);
28
+ const found = await latestOf(req, { collection, id }, {
29
+ depth: Math.max(1, own.depth),
30
+ draft: parsed.data.draft !== '0',
31
+ localeArgs: locale.args,
32
+ });
33
+ if (!found.ok)
34
+ return found.response;
35
+ const { source, options } = schemaEnv(env, req);
36
+ let site;
37
+ if (source.globals?.some((global) => global.slug === SITE_GLOBAL) === true) {
38
+ try {
39
+ site = (await req.payload.findGlobal({
40
+ slug: SITE_GLOBAL,
41
+ depth: 1,
42
+ req,
43
+ overrideAccess: false,
44
+ ...locale.args,
45
+ }));
46
+ }
47
+ catch {
48
+ site = undefined; // the user may not read it: the scope is then null, not an error
49
+ }
50
+ }
51
+ const path = own.path?.(found.value) ?? '';
52
+ return json(dataContextResponseSchema.parse({
53
+ scopes: buildContext({
54
+ source,
55
+ options,
56
+ collection,
57
+ doc: found.value,
58
+ site,
59
+ route: { path, locale: locale.locale ?? req.locale ?? '' },
60
+ }),
61
+ }));
62
+ },
63
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data-endpoints.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-endpoints.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/data-endpoints.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { dataSchemaSchema } from '@next-buildr/core';
2
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
3
+ import { dataContextResponseSchema, samplesResponseSchema } from '../../contract.js';
4
+ import { boot } from './endpoints.test-kit.js';
5
+ let h;
6
+ let authorId;
7
+ let pageId;
8
+ beforeAll(async () => {
9
+ h = await boot('data-endpoints', {
10
+ collections: {
11
+ pages: { context: 'page', depth: 1, path: (doc) => `/${String(doc['slug'])}` },
12
+ },
13
+ }, {
14
+ pageFields: [
15
+ { name: 'author', type: 'relationship', relationTo: 'authors' },
16
+ { name: 'owner', type: 'relationship', relationTo: 'users' },
17
+ { name: 'internalNote', type: 'text', hidden: true },
18
+ { name: 'publishedOn', type: 'date' },
19
+ ],
20
+ collections: [
21
+ {
22
+ slug: 'authors',
23
+ fields: [
24
+ { name: 'name', type: 'text' },
25
+ { name: 'apiKey', type: 'text' },
26
+ ],
27
+ },
28
+ ],
29
+ globals: [
30
+ {
31
+ slug: 'site-settings',
32
+ fields: [
33
+ { name: 'siteName', type: 'text' },
34
+ { name: 'token', type: 'text' },
35
+ ],
36
+ },
37
+ ],
38
+ });
39
+ const author = await h.payload.create({
40
+ collection: 'authors',
41
+ data: { name: 'Ada', apiKey: 'secret-key' },
42
+ });
43
+ authorId = Number(author.id);
44
+ await h.payload.updateGlobal({
45
+ slug: 'site-settings',
46
+ data: { siteName: 'Buildr', token: 'global-secret' },
47
+ });
48
+ const users = await h.payload.find({ collection: 'users' });
49
+ const page = await h.payload.create({
50
+ collection: 'pages',
51
+ data: {
52
+ title: 'Home',
53
+ slug: 'home',
54
+ author: authorId,
55
+ owner: users.docs[0]?.id,
56
+ internalNote: 'private',
57
+ publishedOn: '2026-03-04T00:00:00.000Z',
58
+ },
59
+ draft: true,
60
+ });
61
+ pageId = Number(page.id);
62
+ });
63
+ afterAll(() => h.close());
64
+ describe('GET /buildr/data-schema/:collection', () => {
65
+ it('describes the collection and never the users', async () => {
66
+ const { status, body } = await h.call('GET', '/buildr/data-schema/pages');
67
+ expect(status).toBe(200);
68
+ const schema = dataSchemaSchema.parse(body);
69
+ expect(Object.keys(schema.scopes).sort()).toEqual(['page', 'route', 'site']);
70
+ expect(Object.keys(schema.entities)).toEqual(['authors']);
71
+ const json = JSON.stringify(body);
72
+ for (const leaked of ['users', 'email', 'apiKey', 'token', 'internalNote', 'owner', 'layout']) {
73
+ expect(json).not.toContain(`"${leaked}"`);
74
+ }
75
+ expect(json).toContain('"publishedOn"');
76
+ });
77
+ it('is for the builder only: 401, 404', async () => {
78
+ expect((await h.call('GET', '/buildr/data-schema/pages', { token: null })).status).toBe(401);
79
+ expect((await h.call('GET', '/buildr/data-schema/authors')).status).toBe(404);
80
+ });
81
+ });
82
+ describe('GET /buildr/data/context', () => {
83
+ it('builds the scopes of a document, its author included', async () => {
84
+ const { status, body } = await h.call('GET', `/buildr/data/context?collection=pages&id=${pageId}&locale=pl`);
85
+ expect(status).toBe(200);
86
+ const { scopes } = dataContextResponseSchema.parse(body);
87
+ expect(scopes['page']).toMatchObject({
88
+ id: String(pageId),
89
+ title: 'Home',
90
+ slug: 'home',
91
+ publishedOn: '2026-03-04T00:00:00.000Z',
92
+ author: { id: String(authorId), name: 'Ada' },
93
+ });
94
+ expect(scopes['site']).toMatchObject({ siteName: 'Buildr' });
95
+ expect(scopes['route']).toEqual({ path: '/home', locale: 'pl', params: { page: null } });
96
+ const json = JSON.stringify(scopes);
97
+ for (const leaked of ['secret-key', 'global-secret', 'private', 'editor@example.com']) {
98
+ expect(json).not.toContain(leaked);
99
+ }
100
+ });
101
+ it('answers 400, 401 and 404 to a bad, anonymous or unknown request', async () => {
102
+ expect((await h.call('GET', '/buildr/data/context?collection=pages')).status).toBe(400);
103
+ expect((await h.call('GET', `/buildr/data/context?collection=pages&id=${pageId}`, { token: null }))
104
+ .status).toBe(401);
105
+ expect((await h.call('GET', '/buildr/data/context?collection=pages&id=99999')).status).toBe(404);
106
+ expect((await h.call('GET', `/buildr/data/context?collection=users&id=${pageId}`)).status).toBe(404);
107
+ });
108
+ });
109
+ describe('GET /buildr/samples/:collection', () => {
110
+ it('lists documents by their title and filters by search', async () => {
111
+ await h.payload.create({
112
+ collection: 'pages',
113
+ data: { title: 'About us', slug: 'about' },
114
+ draft: true,
115
+ });
116
+ const all = samplesResponseSchema.parse((await h.call('GET', '/buildr/samples/pages')).body);
117
+ expect(all.items.map((item) => item.title).sort()).toEqual(['About us', 'Home']);
118
+ const found = samplesResponseSchema.parse((await h.call('GET', '/buildr/samples/pages?search=about')).body);
119
+ expect(found.items.map((item) => item.title)).toEqual(['About us']);
120
+ });
121
+ it('answers 401 and 404', async () => {
122
+ expect((await h.call('GET', '/buildr/samples/pages', { token: null })).status).toBe(401);
123
+ expect((await h.call('GET', '/buildr/samples/users')).status).toBe(404);
124
+ });
125
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data-query.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-query.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/data-query.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,96 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { dataMediaResponseSchema, dataQueryResponseSchema } from '../../contract.js';
3
+ import { boot } from './endpoints.test-kit.js';
4
+ let h;
5
+ beforeAll(async () => {
6
+ h = await boot('data-query', {
7
+ queryable: {
8
+ articles: { fields: ['title', 'views'], sort: ['views'] },
9
+ pages: { fields: ['title'], sort: [] },
10
+ },
11
+ media: { collection: 'assets' },
12
+ }, {
13
+ collections: [
14
+ {
15
+ slug: 'articles',
16
+ access: { read: () => true },
17
+ fields: [
18
+ { name: 'title', type: 'text', required: true },
19
+ { name: 'views', type: 'number', required: true },
20
+ { name: 'secret', type: 'text' },
21
+ ],
22
+ },
23
+ {
24
+ slug: 'assets',
25
+ fields: [
26
+ { name: 'url', type: 'text' },
27
+ { name: 'alt', type: 'text' },
28
+ ],
29
+ },
30
+ ],
31
+ });
32
+ for (const [title, views] of [
33
+ ['One', 1],
34
+ ['Two', 2],
35
+ ['Three', 3],
36
+ ]) {
37
+ await h.payload.create({
38
+ collection: 'articles',
39
+ data: { title, views, secret: 'hidden' },
40
+ });
41
+ }
42
+ await h.payload.create({
43
+ collection: 'assets',
44
+ data: { url: 'https://cdn.test/a.png', alt: 'A' },
45
+ });
46
+ });
47
+ afterAll(() => h.close());
48
+ const query = (spec, init = {}) => h.call('POST', '/buildr/data/query', { body: { spec }, ...init });
49
+ const base = { source: 'articles', limit: 10, page: 1 };
50
+ describe('POST /buildr/data/query', () => {
51
+ it('answers an allowlisted query with the shared QueryResult', async () => {
52
+ const { status, body } = await query({
53
+ ...base,
54
+ where: { field: 'views', op: 'gte', value: 2 },
55
+ sort: [{ field: 'views', dir: 'desc' }],
56
+ });
57
+ expect(status).toBe(200);
58
+ const result = dataQueryResponseSchema.parse(body);
59
+ expect(result.total).toBe(2);
60
+ expect(result.items.map((item) => item.title)).toEqual(['Three', 'Two']);
61
+ expect(JSON.stringify(body)).not.toContain('hidden');
62
+ });
63
+ it('refuses what was not allowlisted: 422', async () => {
64
+ expect((await query({ ...base, where: { field: 'secret', op: 'eq', value: 'x' } })).status).toBe(422);
65
+ expect((await query({ ...base, source: 'users' })).status).toBe(422);
66
+ expect((await query({ ...base, sort: [{ field: 'title', dir: 'asc' }] })).status).toBe(422);
67
+ });
68
+ it('validates the request and the caller: 400, 401, 403, 415', async () => {
69
+ expect((await query({ source: 'articles' })).status).toBe(400);
70
+ expect((await query(base, { token: null })).status).toBe(401);
71
+ const viewer = await h.login('viewer@example.com');
72
+ expect((await query(base, { token: viewer })).status).toBeLessThan(500);
73
+ const noJson = await h.call('POST', '/buildr/data/query', {
74
+ body: { spec: base },
75
+ headers: { 'content-type': 'text/plain' },
76
+ });
77
+ expect(noJson.status).toBe(415);
78
+ });
79
+ });
80
+ describe('POST /buildr/data/media', () => {
81
+ it('returns the assets by id and leaves unknown ids out', async () => {
82
+ const found = await h.payload.find({ collection: 'assets' });
83
+ const id = String(found.docs[0].id);
84
+ const { status, body } = await h.call('POST', '/buildr/data/media', {
85
+ body: { ids: [id, '9999'] },
86
+ });
87
+ expect(status).toBe(200);
88
+ const assets = dataMediaResponseSchema.parse(body);
89
+ expect(Object.keys(assets)).toEqual([id]);
90
+ expect(assets[id]).toMatchObject({ url: 'https://cdn.test/a.png', alt: 'A' });
91
+ });
92
+ it('is for the builder only, and validates ids', async () => {
93
+ expect((await h.call('POST', '/buildr/data/media', { body: { ids: [] }, token: null })).status).toBe(401);
94
+ expect((await h.call('POST', '/buildr/data/media', { body: { ids: 'x' } })).status).toBe(400);
95
+ });
96
+ });
@@ -0,0 +1,5 @@
1
+ import type { Endpoint } from 'payload';
2
+ import { type EndpointEnv } from './context.ts';
3
+ /** `GET /api/buildr/data-schema/:collection`: what the bindings of this collection's documents can reach. */
4
+ export declare const dataSchemaEndpoint: (env: EndpointEnv) => Endpoint;
5
+ //# sourceMappingURL=data-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-schema.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/data-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAyB,KAAK,WAAW,EAAa,MAAM,cAAc,CAAC;AAGlF,6GAA6G;AAC7G,eAAO,MAAM,kBAAkB,QAAS,WAAW,KAAG,QAYpD,CAAC"}