@lowdefy/server-dev 5.5.1 → 6.0.0

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 (214) hide show
  1. package/{pages/_app.js → client/App.jsx} +21 -33
  2. package/client/DevStreamContext.js +30 -0
  3. package/client/Inspector.jsx +226 -0
  4. package/{lib/client/Page.js → client/Page.jsx} +4 -8
  5. package/client/Reload.jsx +85 -0
  6. package/client/Routing.jsx +117 -0
  7. package/client/feedback/FeedbackMount.jsx +83 -0
  8. package/client/feedback/FeedbackOverlay.jsx +901 -0
  9. package/client/feedback/captureTabScreenshot.js +84 -0
  10. package/client/feedback/copyToClipboard.js +45 -0
  11. package/client/feedback/drawAnnotationsSvg.js +119 -0
  12. package/client/feedback/elementInspect.js +99 -0
  13. package/client/feedback/feedbackStyles.js +301 -0
  14. package/client/feedback/pinResponsiveImages.js +59 -0
  15. package/client/feedback/sendFeedback.js +39 -0
  16. package/client/feedback/useFeedbackToggle.js +70 -0
  17. package/client/main.jsx +42 -0
  18. package/client/openInEditor/OpenInEditorListener.jsx +268 -0
  19. package/client/openInEditor/findBlockLocation.js +51 -0
  20. package/lib/build/app.js +8 -1
  21. package/lib/build/appMeta.js +8 -1
  22. package/lib/build/auth.js +8 -1
  23. package/lib/build/config.js +8 -1
  24. package/lib/build/i18n.js +8 -1
  25. package/lib/build/logger.js +8 -1
  26. package/lib/build/theme.js +8 -1
  27. package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
  28. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
  29. package/lib/client/setPageId.js +6 -5
  30. package/lib/client/utils/useMutateCache.js +14 -1
  31. package/lib/client/utils/usePageConfig.js +45 -43
  32. package/lib/client/utils/usePageConfig.test.mjs +62 -0
  33. package/lib/docs/captureAnnotatedScreenshot.js +88 -0
  34. package/lib/docs/checkpointPaths.js +91 -0
  35. package/lib/docs/checkpointStore.js +240 -0
  36. package/lib/docs/checkpointToMocks.js +65 -0
  37. package/lib/docs/clientErrorStore.js +36 -0
  38. package/lib/docs/configCheckpoints.test.mjs +156 -0
  39. package/lib/docs/createConfigCheckpoint.js +92 -0
  40. package/lib/docs/createDocsMcpServer.js +531 -0
  41. package/lib/docs/createDocsMcpServer.test.mjs +141 -0
  42. package/lib/docs/devMockRegistry.js +68 -0
  43. package/lib/docs/docs.test.mjs +448 -0
  44. package/lib/docs/enrichFeedback.js +71 -0
  45. package/lib/docs/evalOperator.js +47 -0
  46. package/lib/docs/evalOperator.test.mjs +157 -0
  47. package/lib/docs/evalOperatorHeadless.js +87 -0
  48. package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
  49. package/lib/docs/evalOperatorInTab.js +48 -0
  50. package/lib/docs/findConfig.js +260 -0
  51. package/lib/docs/formatFeedback.js +121 -0
  52. package/lib/docs/formatFeedback.test.mjs +204 -0
  53. package/lib/docs/getAppMap.js +180 -0
  54. package/lib/docs/getBrowser.js +127 -0
  55. package/lib/docs/getBrowser.test.mjs +122 -0
  56. package/lib/docs/getBuildStatus.js +37 -0
  57. package/lib/docs/getCoreDoc.js +63 -0
  58. package/lib/docs/getDocsManifest.js +40 -0
  59. package/lib/docs/getExamples.js +58 -0
  60. package/lib/docs/getOverview.js +115 -0
  61. package/lib/docs/getPageConfig.js +53 -0
  62. package/lib/docs/getPluginDoc.js +55 -0
  63. package/lib/docs/getSchema.js +65 -0
  64. package/lib/docs/inspectState.js +47 -0
  65. package/lib/docs/inspectState.test.mjs +140 -0
  66. package/lib/docs/inspectStateFromTab.js +40 -0
  67. package/lib/docs/inspectStateHeadless.js +84 -0
  68. package/lib/docs/inspectStateHeadless.test.mjs +60 -0
  69. package/lib/docs/isWriteRequestsAllowed.js +50 -0
  70. package/lib/docs/listConfigCheckpoints.js +55 -0
  71. package/lib/docs/listPlugins.js +80 -0
  72. package/lib/docs/listTypes.js +105 -0
  73. package/lib/docs/loadState.js +152 -0
  74. package/lib/docs/mapPageBuildErrors.js +28 -0
  75. package/lib/docs/normalizeTypeKind.js +40 -0
  76. package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
  77. package/lib/docs/readPageArtifact.js +26 -0
  78. package/lib/docs/resolvePluginDir.js +49 -0
  79. package/lib/docs/resolveSource.js +48 -0
  80. package/lib/docs/resolveSource.test.mjs +100 -0
  81. package/lib/docs/revertConfigCheckpoint.js +73 -0
  82. package/lib/docs/runRequest.js +141 -0
  83. package/lib/docs/saveAnnotatedScreenshot.js +43 -0
  84. package/lib/docs/scaffoldPage.js +75 -0
  85. package/lib/docs/scaffoldPage.test.mjs +97 -0
  86. package/lib/docs/screenshotPage.js +110 -0
  87. package/lib/docs/screenshotPage.test.mjs +59 -0
  88. package/lib/docs/screenshotPageClip.test.mjs +102 -0
  89. package/lib/docs/searchDocs.js +67 -0
  90. package/lib/docs/setupTestFixtures.mjs +335 -0
  91. package/lib/docs/snapshotState.js +59 -0
  92. package/lib/docs/stateCheckpoints.test.mjs +366 -0
  93. package/lib/docs/tabAvailable.js +28 -0
  94. package/lib/docs/tabChannel.js +119 -0
  95. package/lib/docs/tabChannel.test.mjs +130 -0
  96. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
  97. package/lib/server/auth/getDevSession.js +70 -0
  98. package/lib/server/auth/getDevSession.test.mjs +138 -0
  99. package/lib/server/auth/getHeadlessUser.js +40 -0
  100. package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
  101. package/lib/server/auth/headlessUser.js +29 -0
  102. package/lib/server/auth/resolveHeadlessUser.js +38 -0
  103. package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
  104. package/lib/server/auth/session.js +39 -0
  105. package/lib/server/auth/strategies.js +47 -0
  106. package/lib/server/{apiWrapper.js → createLowdefyContext.js} +63 -57
  107. package/lib/server/getPageJitEnrichment.test.mjs +105 -0
  108. package/lib/server/jitPageBuilder.js +125 -12
  109. package/lib/server/log/createHandleError.js +9 -0
  110. package/lib/server/log/createHandleError.test.mjs +126 -0
  111. package/manager/getContext.mjs +3 -7
  112. package/manager/processes/initialBuild.mjs +0 -2
  113. package/manager/processes/installPlugins.mjs +19 -11
  114. package/manager/processes/lowdefyBuild.mjs +29 -7
  115. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  116. package/manager/processes/readDotEnv.mjs +13 -1
  117. package/manager/processes/shutdownServer.mjs +6 -6
  118. package/manager/processes/startProxy.mjs +160 -0
  119. package/manager/processes/startProxy.test.mjs +102 -0
  120. package/manager/processes/startServer.mjs +33 -16
  121. package/manager/processes/startWatchers.mjs +2 -2
  122. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  123. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  124. package/manager/run.mjs +50 -0
  125. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  126. package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
  127. package/manager/utils/formatNoticeBox.mjs +30 -0
  128. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  129. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  130. package/manager/utils/writeBuildStatus.mjs +39 -0
  131. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  132. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  133. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  134. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
  135. package/package.json +61 -47
  136. package/package.original.json +64 -55
  137. package/postcss.config.cjs +1 -0
  138. package/src/app.js +175 -0
  139. package/src/html/renderDevPage.js +86 -0
  140. package/src/lib/getPathSegments.js +25 -0
  141. package/src/lib/safeScriptJson.js +38 -0
  142. package/src/middleware/apiContext.js +32 -0
  143. package/src/middleware/errorHandler.js +48 -0
  144. package/src/middleware/errorHandler.test.mjs +235 -0
  145. package/src/routes/agent.js +64 -0
  146. package/src/routes/apiPage.js +37 -0
  147. package/src/routes/auth.js +49 -0
  148. package/src/routes/auth.test.mjs +84 -0
  149. package/src/routes/clientError.js +57 -0
  150. package/src/routes/cron.js +47 -0
  151. package/src/routes/cronForward.js +47 -0
  152. package/src/routes/detached.js +47 -0
  153. package/src/routes/devInspect.js +111 -0
  154. package/src/routes/devInspect.test.mjs +119 -0
  155. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  156. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  157. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  158. package/src/routes/docs/checkpointsCreate.js +28 -0
  159. package/src/routes/docs/checkpointsList.js +23 -0
  160. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  161. package/src/routes/docs/content.js +31 -0
  162. package/src/routes/docs/evalOperator.js +56 -0
  163. package/src/routes/docs/examples.js +33 -0
  164. package/src/routes/docs/find.js +30 -0
  165. package/src/routes/docs/index.js +23 -0
  166. package/src/routes/docs/inspectState.js +44 -0
  167. package/src/routes/docs/loadState.js +49 -0
  168. package/src/routes/docs/mcp.js +30 -0
  169. package/src/routes/docs/pageConfig.js +36 -0
  170. package/src/routes/docs/parseUserParam.js +52 -0
  171. package/src/routes/docs/parseUserParam.test.mjs +45 -0
  172. package/src/routes/docs/pluginDoc.js +33 -0
  173. package/src/routes/docs/plugins.js +23 -0
  174. package/src/routes/docs/runRequest.js +36 -0
  175. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  176. package/src/routes/docs/screenshot.js +72 -0
  177. package/src/routes/docs/search.js +27 -0
  178. package/src/routes/docs/snapshotState.js +36 -0
  179. package/src/routes/docs/stateCheckpointsList.js +26 -0
  180. package/src/routes/docs/types.js +33 -0
  181. package/src/routes/endpoints.js +71 -0
  182. package/src/routes/feedback.js +110 -0
  183. package/src/routes/feedback.test.mjs +204 -0
  184. package/src/routes/jitPage.js +86 -0
  185. package/src/routes/mcp.js +34 -0
  186. package/{pages/api → src/routes}/ping.js +4 -2
  187. package/src/routes/reload.js +79 -0
  188. package/src/routes/request.js +74 -0
  189. package/src/routes/request.test.mjs +124 -0
  190. package/{pages/api → src/routes}/root.js +4 -4
  191. package/src/routes/usage.js +46 -0
  192. package/src/routes/websocket.js +33 -0
  193. package/src/websocket/devWebSocket.js +78 -0
  194. package/vite.config.js +111 -0
  195. package/.eslintrc.yaml +0 -1
  196. package/lib/client/App.js +0 -80
  197. package/lib/client/Reload.js +0 -60
  198. package/lib/server/compileCss.js +0 -39
  199. package/manager/processes/compileCss.mjs +0 -54
  200. package/manager/processes/nextBuild.mjs +0 -50
  201. package/next.config.js +0 -25
  202. package/pages/_document.js +0 -107
  203. package/pages/api/agent/[...path].js +0 -86
  204. package/pages/api/auth/[...nextauth].js +0 -48
  205. package/pages/api/client-error.js +0 -45
  206. package/pages/api/js/[env].js +0 -42
  207. package/pages/api/page/[...pageId].js +0 -74
  208. package/pages/api/reload.js +0 -53
  209. package/pages/api/request/[...path].js +0 -41
  210. /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
  211. /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
  212. /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
  213. /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
  214. /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
@@ -0,0 +1,204 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import os from 'os';
19
+ import path from 'path';
20
+
21
+ import { Hono } from 'hono';
22
+ import { jest } from '@jest/globals';
23
+
24
+ // enrichFeedback (findConfig → force-builds a page) is covered on its own in
25
+ // lib/docs/formatFeedback.test.mjs — here it's stubbed as a pass-through so
26
+ // this file only exercises the route: origin check, validation, and enrich +
27
+ // format wiring.
28
+ const mockEnrichFeedback = jest.fn(async ({ batch }) => batch);
29
+ jest.unstable_mockModule('../../lib/docs/enrichFeedback.js', () => ({
30
+ default: mockEnrichFeedback,
31
+ }));
32
+ const mockCaptureAnnotatedScreenshot = jest.fn(async () => ({
33
+ path: '.lowdefy/annotations/test.png',
34
+ }));
35
+ jest.unstable_mockModule('../../lib/docs/captureAnnotatedScreenshot.js', () => ({
36
+ default: mockCaptureAnnotatedScreenshot,
37
+ }));
38
+
39
+ const { default: feedbackHandler } = await import('./feedback.js');
40
+
41
+ function createApp() {
42
+ const app = new Hono();
43
+ app.all('/api/feedback', feedbackHandler);
44
+ return app;
45
+ }
46
+
47
+ let configDirectory;
48
+ const originalConfigDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG;
49
+
50
+ beforeEach(() => {
51
+ configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-feedback-test-'));
52
+ process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
53
+ });
54
+
55
+ afterEach(() => {
56
+ mockEnrichFeedback.mockClear();
57
+ mockCaptureAnnotatedScreenshot.mockClear();
58
+ fs.rmSync(configDirectory, { recursive: true, force: true });
59
+ if (originalConfigDirectory === undefined) {
60
+ delete process.env.LOWDEFY_DIRECTORY_CONFIG;
61
+ } else {
62
+ process.env.LOWDEFY_DIRECTORY_CONFIG = originalConfigDirectory;
63
+ }
64
+ });
65
+
66
+ test('feedbackHandler returns 403 when the Origin header is missing', async () => {
67
+ const res = await createApp().request('/api/feedback', {
68
+ method: 'POST',
69
+ headers: { host: 'localhost:3001', 'content-type': 'application/json' },
70
+ body: JSON.stringify({ annotations: [] }),
71
+ });
72
+ expect(res.status).toEqual(403);
73
+ });
74
+
75
+ test('feedbackHandler returns 403 when the Origin host does not match the request host', async () => {
76
+ const res = await createApp().request('/api/feedback', {
77
+ method: 'POST',
78
+ headers: {
79
+ host: 'localhost:3001',
80
+ origin: 'http://evil.example.com',
81
+ 'content-type': 'application/json',
82
+ },
83
+ body: JSON.stringify({ annotations: [] }),
84
+ });
85
+ expect(res.status).toEqual(403);
86
+ });
87
+
88
+ test('feedbackHandler returns 400 with a helpful message when annotations is missing', async () => {
89
+ const res = await createApp().request('/api/feedback', {
90
+ method: 'POST',
91
+ headers: {
92
+ host: 'localhost:3001',
93
+ origin: 'http://localhost:3001',
94
+ 'content-type': 'application/json',
95
+ },
96
+ body: JSON.stringify({ pageId: 'login' }),
97
+ });
98
+ expect(res.status).toEqual(400);
99
+ const body = await res.json();
100
+ expect(body.error).toMatch(/annotations/);
101
+ expect(body.error).toMatch(/\/lowdefy-docs/);
102
+ });
103
+
104
+ test('feedbackHandler enriches a valid batch and returns the formatted text', async () => {
105
+ const batch = { pageId: 'login', annotations: [{ id: '1', kind: 'region', comment: 'x' }] };
106
+ const res = await createApp().request('/api/feedback', {
107
+ method: 'POST',
108
+ headers: {
109
+ host: 'localhost:3001',
110
+ origin: 'http://localhost:3001',
111
+ 'content-type': 'application/json',
112
+ },
113
+ body: JSON.stringify(batch),
114
+ });
115
+
116
+ expect(res.status).toEqual(200);
117
+ const body = await res.json();
118
+ expect(body.ok).toBe(true);
119
+ expect(typeof body.formatted).toBe('string');
120
+ expect(body.formatted).toContain('login');
121
+ expect(mockEnrichFeedback).toHaveBeenCalledWith({ batch });
122
+ });
123
+
124
+ // A real 1x1 red PNG so the saved file is a valid image, not just bytes.
125
+ const PNG_1X1_BASE64 =
126
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
127
+
128
+ test('feedbackHandler saves a tab-captured screenshot and skips the headless capture', async () => {
129
+ const batch = {
130
+ pageId: 'dashboard',
131
+ annotations: [{ id: '1', kind: 'region', comment: 'x' }],
132
+ screenshot: `data:image/png;base64,${PNG_1X1_BASE64}`,
133
+ };
134
+ const res = await createApp().request('/api/feedback', {
135
+ method: 'POST',
136
+ headers: {
137
+ host: 'localhost:3001',
138
+ origin: 'http://localhost:3001',
139
+ 'content-type': 'application/json',
140
+ },
141
+ body: JSON.stringify(batch),
142
+ });
143
+
144
+ expect(res.status).toEqual(200);
145
+ const body = await res.json();
146
+ expect(body.ok).toBe(true);
147
+ expect(body.formatted).toContain('.lowdefy/annotations/dashboard-');
148
+ // The data URL must never leak into the agent-readable text.
149
+ expect(body.formatted).not.toContain(PNG_1X1_BASE64);
150
+ expect(mockCaptureAnnotatedScreenshot).not.toHaveBeenCalled();
151
+
152
+ const dir = path.join(configDirectory, '.lowdefy', 'annotations');
153
+ const files = fs.readdirSync(dir);
154
+ expect(files).toHaveLength(1);
155
+ expect(files[0]).toMatch(/^dashboard-.*\.png$/);
156
+ expect(fs.readFileSync(path.join(dir, files[0]))).toEqual(
157
+ Buffer.from(PNG_1X1_BASE64, 'base64')
158
+ );
159
+ });
160
+
161
+ test('feedbackHandler falls back to the headless capture when the screenshot is not a PNG data URL', async () => {
162
+ const batch = {
163
+ pageId: 'dashboard',
164
+ annotations: [{ id: '1', kind: 'region', comment: 'x' }],
165
+ screenshot: 'data:image/jpeg;base64,notpng',
166
+ };
167
+ const res = await createApp().request('/api/feedback', {
168
+ method: 'POST',
169
+ headers: {
170
+ host: 'localhost:3001',
171
+ origin: 'http://localhost:3001',
172
+ 'content-type': 'application/json',
173
+ },
174
+ body: JSON.stringify(batch),
175
+ });
176
+
177
+ expect(res.status).toEqual(200);
178
+ const body = await res.json();
179
+ expect(body.ok).toBe(true);
180
+ // Not a valid tab capture — no file saved, and no screenshotPath claimed
181
+ // for it (the invalid-string branch logs a warning instead).
182
+ expect(fs.existsSync(path.join(configDirectory, '.lowdefy', 'annotations'))).toBe(false);
183
+ });
184
+
185
+ test('feedbackHandler uses the headless capture when the batch has no screenshot', async () => {
186
+ const batch = {
187
+ pageId: 'dashboard',
188
+ annotations: [{ id: '1', kind: 'region', comment: 'x' }],
189
+ };
190
+ const res = await createApp().request('/api/feedback', {
191
+ method: 'POST',
192
+ headers: {
193
+ host: 'localhost:3001',
194
+ origin: 'http://localhost:3001',
195
+ 'content-type': 'application/json',
196
+ },
197
+ body: JSON.stringify(batch),
198
+ });
199
+
200
+ expect(res.status).toEqual(200);
201
+ const body = await res.json();
202
+ expect(body.formatted).toContain('.lowdefy/annotations/dashboard-');
203
+ expect(mockCaptureAnnotatedScreenshot).toHaveBeenCalledTimes(1);
204
+ });
@@ -0,0 +1,86 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { getPageConfig } from '@lowdefy/api';
18
+
19
+ import buildPageIfNeeded, { getPageJitEnrichment } from '../../lib/server/jitPageBuilder.js';
20
+ import getPathSegments from '../lib/getPathSegments.js';
21
+
22
+ // JIT page build + config response. The response shapes are a frozen contract
23
+ // with the dev client:
24
+ // 200 { installing: true, packages } — plugin install in progress, client polls
25
+ // 500 { buildError: true, errors, message, source } — build failed
26
+ // 404 'Page not found.'
27
+ // 200 pageConfig (+ _warnings, + _jsEntries module text, + _dynamicIcons data)
28
+ async function jitPageHandler(c) {
29
+ const context = c.get('lowdefyContext');
30
+ const pageId = getPathSegments(c, '/api/page/').join('/');
31
+
32
+ let buildResult;
33
+ try {
34
+ buildResult = await buildPageIfNeeded({
35
+ pageId,
36
+ buildDirectory: context.buildDirectory,
37
+ configDirectory: context.configDirectory,
38
+ });
39
+ } catch (error) {
40
+ const rawErrors = error.buildErrors ?? [error];
41
+ const errors = [];
42
+ for (const err of rawErrors) {
43
+ await context.handleError(err);
44
+ errors.push({
45
+ type: err.name ?? 'Error',
46
+ message: err.message,
47
+ source: err.source ?? null,
48
+ stack: err.stack ?? null,
49
+ });
50
+ }
51
+ return c.json(
52
+ {
53
+ buildError: true,
54
+ errors,
55
+ // Keep top-level message/source for backward compatibility
56
+ message: error.message,
57
+ source: error.source ?? null,
58
+ },
59
+ 500
60
+ );
61
+ }
62
+
63
+ if (buildResult && buildResult.installing) {
64
+ return c.json({
65
+ installing: true,
66
+ packages: buildResult.packages,
67
+ });
68
+ }
69
+
70
+ const pageConfig = await getPageConfig(context, { pageId, urlQuery: c.req.query() });
71
+ if (pageConfig === null) {
72
+ return c.text('Page not found.', 404);
73
+ }
74
+ if (buildResult?.warnings?.length > 0) {
75
+ pageConfig._warnings = buildResult.warnings;
76
+ }
77
+ // Fold this page's JIT-discovered _js entries and dynamic icons into the
78
+ // response the client already awaits, so first paint has everything it needs
79
+ // without the two secondary fetches that stalled in Vite's transform window.
80
+ const { jsEntries, dynamicIcons } = getPageJitEnrichment({ pageConfig });
81
+ if (jsEntries) pageConfig._jsEntries = jsEntries;
82
+ if (dynamicIcons) pageConfig._dynamicIcons = dynamicIcons;
83
+ return c.json(pageConfig);
84
+ }
85
+
86
+ export default jitPageHandler;
@@ -0,0 +1,34 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { StreamableHTTPTransport } from '@hono/mcp';
18
+ import { createMcpServer } from '@lowdefy/api';
19
+
20
+ // Stateless per-request MCP server over the app's configured endpoints. The
21
+ // apiContext middleware has already resolved the caller's session, so tool
22
+ // listing and calls are authorized per request.
23
+ async function mcpHandler(c) {
24
+ const context = c.get('lowdefyContext');
25
+ const server = await createMcpServer({ context });
26
+ if (!server) {
27
+ return c.json({ error: 'MCP is not configured.' }, 404);
28
+ }
29
+ const transport = new StreamableHTTPTransport();
30
+ await server.connect(transport);
31
+ return transport.handleRequest(c);
32
+ }
33
+
34
+ export default mcpHandler;
@@ -14,6 +14,8 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- export default async function handler(req, res) {
18
- res.status(200).json({ timestamp: new Date().toISOString() });
17
+ function pingHandler(c) {
18
+ return c.json({ timestamp: new Date().toISOString() });
19
19
  }
20
+
21
+ export default pingHandler;
@@ -0,0 +1,79 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { randomUUID } from 'node:crypto';
18
+
19
+ import chokidar from 'chokidar';
20
+ import { streamSSE } from 'hono/streaming';
21
+
22
+ import { registerTab, unregisterTab } from '../../lib/docs/tabChannel.js';
23
+
24
+ // SSE endpoint — notifies the client when build/reload is written so it can
25
+ // mutate the SWR cache and refetch config. Also doubles as the transport for
26
+ // the agent-state-xray tab channel (lib/docs/tabChannel.js): every
27
+ // connection is a browser tab (client/Reload.jsx opens exactly one per tab),
28
+ // so each is registered as an inspectable dev tab and told its id in a `tab`
29
+ // event, after which an agent can push it inspect-request/eval-request
30
+ // events on the same stream.
31
+ //
32
+ // One stream per tab is deliberate: dev serves over HTTP/1.1, where browsers
33
+ // allow six connections per host, and an open event stream holds one for the
34
+ // life of the tab. The tab's page is not carried on this connection — it
35
+ // would force a reconnect on every navigation — but posted by
36
+ // client/Inspector.jsx to /api/dev-inspect/page (routes/devInspect.js) as
37
+ // { tabId, pageId } whenever it changes.
38
+ async function reloadHandler(c) {
39
+ return streamSSE(c, async (stream) => {
40
+ const watcher = chokidar.watch(['./build/reload'], {
41
+ persistent: true,
42
+ ignoreInitial: true,
43
+ });
44
+
45
+ const tabId = randomUUID();
46
+ registerTab({
47
+ id: tabId,
48
+ send: (event, data) => stream.writeSSE({ event, data: JSON.stringify(data) }),
49
+ });
50
+
51
+ let open = true;
52
+ stream.onAbort(() => {
53
+ open = false;
54
+ watcher.close();
55
+ unregisterTab({ id: tabId });
56
+ });
57
+
58
+ await stream.writeSSE({ event: 'tab', data: JSON.stringify({ tabId }) });
59
+
60
+ const reload = () => {
61
+ stream.writeSSE({ event: 'reload', data: JSON.stringify({}) });
62
+ };
63
+ watcher.on('add', () => reload());
64
+ watcher.on('change', () => reload());
65
+ // Do not reload on unlink — cleanBuildDirectory deletes build/reload during
66
+ // skeleton rebuilds, which would send a premature SSE event before the new
67
+ // build artifacts are written. The real reload comes via add/change when
68
+ // reloadClients() creates the file after the build completes.
69
+
70
+ while (open) {
71
+ await stream.sleep(15000);
72
+ if (open) {
73
+ await stream.writeSSE({ event: 'ping', data: '' });
74
+ }
75
+ }
76
+ });
77
+ }
78
+
79
+ export default reloadHandler;
@@ -0,0 +1,74 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { callRequest, redactErrorResponse } from '@lowdefy/api';
18
+ import { serializer } from '@lowdefy/helpers';
19
+
20
+ import buildPageIfNeeded from '../../lib/server/jitPageBuilder.js';
21
+ import getPathSegments from '../lib/getPathSegments.js';
22
+ import { getMock } from '../../lib/docs/devMockRegistry.js';
23
+
24
+ async function requestHandler(c) {
25
+ if (c.req.method !== 'POST') {
26
+ throw new Error('Only POST requests are supported.');
27
+ }
28
+ const context = c.get('lowdefyContext');
29
+ const segments = getPathSegments(c, '/api/request/');
30
+ if (segments.length < 2) {
31
+ return c.json({ error: 'Invalid request path' }, 400);
32
+ }
33
+ const requestId = segments[segments.length - 1];
34
+ const pageId = segments.slice(0, -1).join('/');
35
+ const { actionId, blockId, payload } = await c.req.json();
36
+
37
+ // Dev-only agent tooling: once a state checkpoint has been loaded
38
+ // (lib/docs/loadState.js), its recorded request/response pairs live in
39
+ // devMockRegistry keyed by (pageId, requestId). Short-circuit here, before
40
+ // ever touching the real connection, so a headless verification page, a
41
+ // human tester's browser tab, or an e2e test replays exactly what was
42
+ // recorded. The response shape mirrors @lowdefy/api's callRequest contract
43
+ // (see callRequestResolver.js / callRequest.js success shape, and
44
+ // middleware/errorHandler.js for the error shape) so the client's
45
+ // Requests.js can't tell the difference from a real request.
46
+ const mock = getMock({ pageId, requestId });
47
+ if (mock) {
48
+ context.logger.info({ event: 'dev_mock_request', pageId, requestId, blockId, actionId });
49
+ if (mock.error) {
50
+ const error = mock.error instanceof Error ? mock.error : new Error(mock.error);
51
+ return c.json(redactErrorResponse(context, error), 500);
52
+ }
53
+ return c.json({ success: true, response: serializer.serialize(mock.response) });
54
+ }
55
+
56
+ // Page artifacts (including pages/{pageId}/requests/{requestId}.json) are
57
+ // built JIT by GET /api/page/* and thrown away on every page invalidation
58
+ // (lowdefyBuildWatcher / moduleBuildWatcher). A client that already holds
59
+ // the page config keeps firing requests across that window — and a request
60
+ // that arrived before the page was rebuilt failed with
61
+ // `Request "x" does not exist.` Run the same idempotent build the page route
62
+ // runs (a no-op once the page is compiled) so the request artifact is there.
63
+ await buildPageIfNeeded({
64
+ pageId,
65
+ buildDirectory: context.buildDirectory,
66
+ configDirectory: context.configDirectory,
67
+ });
68
+
69
+ context.logger.info({ event: 'call_request', pageId, requestId, blockId, actionId });
70
+ const response = await callRequest(context, { blockId, pageId, payload, requestId });
71
+ return c.json(response);
72
+ }
73
+
74
+ export default requestHandler;
@@ -0,0 +1,124 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { Hono } from 'hono';
18
+ import { jest } from '@jest/globals';
19
+
20
+ const mockCallRequest = jest.fn();
21
+ jest.unstable_mockModule('@lowdefy/api', () => ({
22
+ callRequest: mockCallRequest,
23
+ redactErrorResponse: jest.fn((context, error) => ({ error: error.message })),
24
+ }));
25
+
26
+ const mockBuildPageIfNeeded = jest.fn(async () => true);
27
+ jest.unstable_mockModule('../../lib/server/jitPageBuilder.js', () => ({
28
+ default: mockBuildPageIfNeeded,
29
+ getPageJitEnrichment: jest.fn(() => ({})),
30
+ }));
31
+
32
+ const mockGetMock = jest.fn(() => null);
33
+ jest.unstable_mockModule('../../lib/docs/devMockRegistry.js', () => ({
34
+ getMock: mockGetMock,
35
+ }));
36
+
37
+ const { default: requestHandler } = await import('./request.js');
38
+
39
+ const callOrder = [];
40
+
41
+ function createApp() {
42
+ const app = new Hono();
43
+ app.use('*', async (c, next) => {
44
+ c.set('lowdefyContext', {
45
+ buildDirectory: '/build',
46
+ configDirectory: '/config',
47
+ logger: { debug: jest.fn(), info: jest.fn(), error: jest.fn() },
48
+ });
49
+ await next();
50
+ });
51
+ app.all('/api/request/*', requestHandler);
52
+ return app;
53
+ }
54
+
55
+ function post(app, path, body = { actionId: 'a', blockId: 'b', payload: {} }) {
56
+ return app.request(path, {
57
+ method: 'POST',
58
+ headers: { 'content-type': 'application/json' },
59
+ body: JSON.stringify(body),
60
+ });
61
+ }
62
+
63
+ beforeEach(() => {
64
+ callOrder.length = 0;
65
+ mockBuildPageIfNeeded.mockImplementation(async () => {
66
+ callOrder.push('build');
67
+ return true;
68
+ });
69
+ mockCallRequest.mockImplementation(async () => {
70
+ callOrder.push('call');
71
+ return { success: true, response: { ok: true } };
72
+ });
73
+ });
74
+
75
+ afterEach(() => {
76
+ mockCallRequest.mockReset();
77
+ mockBuildPageIfNeeded.mockReset();
78
+ mockGetMock.mockReset();
79
+ mockGetMock.mockImplementation(() => null);
80
+ });
81
+
82
+ test('requestHandler builds the page JIT before calling the request', async () => {
83
+ const res = await post(createApp(), '/api/request/dashboard/get_rows');
84
+ expect(res.status).toEqual(200);
85
+ expect(await res.json()).toEqual({ success: true, response: { ok: true } });
86
+ expect(mockBuildPageIfNeeded).toHaveBeenCalledWith({
87
+ pageId: 'dashboard',
88
+ buildDirectory: '/build',
89
+ configDirectory: '/config',
90
+ });
91
+ expect(mockCallRequest).toHaveBeenCalledWith(
92
+ expect.objectContaining({ buildDirectory: '/build' }),
93
+ { blockId: 'b', pageId: 'dashboard', payload: {}, requestId: 'get_rows' }
94
+ );
95
+ expect(callOrder).toEqual(['build', 'call']);
96
+ });
97
+
98
+ test('requestHandler builds nested page ids as one page', async () => {
99
+ await post(createApp(), '/api/request/user-account/login/get_session');
100
+ expect(mockBuildPageIfNeeded).toHaveBeenCalledWith(
101
+ expect.objectContaining({ pageId: 'user-account/login' })
102
+ );
103
+ expect(mockCallRequest).toHaveBeenCalledWith(
104
+ expect.anything(),
105
+ expect.objectContaining({ pageId: 'user-account/login', requestId: 'get_session' })
106
+ );
107
+ });
108
+
109
+ test('requestHandler replays a dev mock without building or calling the request', async () => {
110
+ mockGetMock.mockImplementation(() => ({ response: { rows: [1] } }));
111
+ const res = await post(createApp(), '/api/request/dashboard/get_rows');
112
+ expect(res.status).toEqual(200);
113
+ expect(await res.json()).toEqual({ success: true, response: { rows: [1] } });
114
+ expect(mockBuildPageIfNeeded).not.toHaveBeenCalled();
115
+ expect(mockCallRequest).not.toHaveBeenCalled();
116
+ });
117
+
118
+ test('requestHandler rejects non-POST methods', async () => {
119
+ const app = createApp();
120
+ app.onError((err, c) => c.json({ error: err.message }, 500));
121
+ const res = await app.request('/api/request/dashboard/get_rows');
122
+ expect(res.status).toEqual(500);
123
+ expect(mockBuildPageIfNeeded).not.toHaveBeenCalled();
124
+ });
@@ -15,11 +15,11 @@
15
15
  */
16
16
 
17
17
  import { getRootConfig } from '@lowdefy/api';
18
- import apiWrapper from '../../lib/server/apiWrapper.js';
19
18
 
20
- async function handler({ context, res }) {
19
+ async function rootHandler(c) {
20
+ const context = c.get('lowdefyContext');
21
21
  const rootConfig = await getRootConfig(context);
22
- res.status(200).json(rootConfig);
22
+ return c.json(rootConfig);
23
23
  }
24
24
 
25
- export default apiWrapper(handler);
25
+ export default rootHandler;
@@ -0,0 +1,46 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
21
+
22
+ async function usageHandler(c) {
23
+ if (c.req.method !== 'POST') {
24
+ throw new Error('Only POST requests are supported.');
25
+ }
26
+ const context = c.get('lowdefyContext');
27
+ const { user, machine } = await c.req.json();
28
+ const host = c.req.header('host');
29
+ context.logger.info({ event: 'log_usage', user, machine });
30
+
31
+ const timestamp = Date.now();
32
+
33
+ return c.json({
34
+ offline: false,
35
+ data: {
36
+ gitSha: context.appMeta.gitSha,
37
+ host,
38
+ machine,
39
+ timestamp,
40
+ user,
41
+ version: packageJson.version,
42
+ },
43
+ });
44
+ }
45
+
46
+ export default usageHandler;