@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,235 @@
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
+ import createErrorHandler from './errorHandler.js';
21
+
22
+ // This is the only test coverage of the hono 500 redaction path: the file here is
23
+ // byte-identical to its `server` and `server-e2e` counterparts, and `server` has no
24
+ // test script at all. Keep the three in sync when changing any of them.
25
+
26
+ // A string that must never reach a client. It is put on `received` at every
27
+ // level of the cause chain, so a single JSON.stringify search over the body
28
+ // proves the redaction reached that level.
29
+ const SECRET = 'SECRET-RECEIVED-VALUE';
30
+
31
+ function createLogger() {
32
+ return { error: jest.fn(), warn: jest.fn() };
33
+ }
34
+
35
+ // The error handler is only reachable through hono's app-level onError, so the
36
+ // handler is driven the way production drives it: a route that throws, with the
37
+ // lowdefyContext set by upstream middleware when there is one.
38
+ function createApp({ basePath = '', context, error, logger }) {
39
+ const app = new Hono();
40
+ app.use('*', async (c, next) => {
41
+ if (context) c.set('lowdefyContext', context);
42
+ await next();
43
+ });
44
+ app.all('*', () => {
45
+ throw error;
46
+ });
47
+ app.onError(createErrorHandler({ basePath, logger }));
48
+ return app;
49
+ }
50
+
51
+ function createAuthenticationError(message) {
52
+ const error = new Error(message);
53
+ error.name = 'AuthenticationError';
54
+ error.received = SECRET;
55
+ return error;
56
+ }
57
+
58
+ // Two Error levels, each with a stack (every Error has one) and a recognisable
59
+ // `received`, plus a configKey the policy keeps.
60
+ function createErrorWithCause() {
61
+ const cause = new Error('Cause message.');
62
+ cause.received = `${SECRET}-cause`;
63
+ const error = new Error('Top message.', { cause });
64
+ error.received = `${SECRET}-top`;
65
+ error.configKey = 'page:home.blocks.0';
66
+ return error;
67
+ }
68
+
69
+ test('errorHandler returns 401 with only name and message for an AuthenticationError on an api path', async () => {
70
+ const logger = createLogger();
71
+ const context = { handleError: jest.fn() };
72
+ const res = await createApp({
73
+ context,
74
+ error: createAuthenticationError('Unauthenticated.'),
75
+ logger,
76
+ }).request('/api/request/getUsers');
77
+
78
+ expect(res.status).toEqual(401);
79
+ expect(await res.json()).toEqual({ name: 'AuthenticationError', message: 'Unauthenticated.' });
80
+ expect(logger.warn).toHaveBeenCalledTimes(1);
81
+ expect(logger.warn.mock.calls[0][0]).toMatch(/Unauthenticated request/);
82
+ expect(logger.error).not.toHaveBeenCalled();
83
+ expect(context.handleError).not.toHaveBeenCalled();
84
+ });
85
+
86
+ test('errorHandler does not send an AuthenticationError through the redactor', async () => {
87
+ const res = await createApp({
88
+ error: createAuthenticationError('Unauthenticated.'),
89
+ logger: createLogger(),
90
+ }).request('/api/request/getUsers');
91
+
92
+ const body = await res.json();
93
+ expect(body['~e']).toBeUndefined();
94
+ });
95
+
96
+ test('errorHandler returns text Unauthorized at 401 for an AuthenticationError on a page path', async () => {
97
+ const logger = createLogger();
98
+ const res = await createApp({
99
+ error: createAuthenticationError('Unauthenticated.'),
100
+ logger,
101
+ }).request('/home');
102
+
103
+ expect(res.status).toEqual(401);
104
+ expect(await res.text()).toEqual('Unauthorized');
105
+ expect(logger.warn).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ test('errorHandler returns 500 with the serialized error envelope on an api path', async () => {
109
+ const res = await createApp({
110
+ error: createErrorWithCause(),
111
+ logger: createLogger(),
112
+ }).request('/api/request/getUsers');
113
+
114
+ expect(res.status).toEqual(500);
115
+ const body = await res.json();
116
+ expect(body['~e'].name).toEqual('Error');
117
+ expect(body['~e'].message).toEqual('Top message.');
118
+ expect(body['~e'].cause.message).toEqual('Cause message.');
119
+ });
120
+
121
+ test('errorHandler omits stack and received from the 500 envelope at the top level', async () => {
122
+ const res = await createApp({
123
+ error: createErrorWithCause(),
124
+ logger: createLogger(),
125
+ }).request('/api/request/getUsers');
126
+
127
+ const body = await res.json();
128
+ expect(body['~e'].stack).toBeUndefined();
129
+ expect(body['~e'].received).toBeUndefined();
130
+ });
131
+
132
+ // The regression test for the real bug: the four inline `delete` statements this
133
+ // replaces only ever reached depth 0, so a `received` or `stack` on the cause
134
+ // still crossed the wire. The policy is now applied at every error node.
135
+ test('errorHandler omits stack and received from the 500 envelope on the cause at depth 1', async () => {
136
+ const res = await createApp({
137
+ error: createErrorWithCause(),
138
+ logger: createLogger(),
139
+ }).request('/api/request/getUsers');
140
+
141
+ const body = await res.json();
142
+ expect(body['~e'].cause.stack).toBeUndefined();
143
+ expect(body['~e'].cause.received).toBeUndefined();
144
+ expect(JSON.stringify(body)).not.toContain(SECRET);
145
+ });
146
+
147
+ test('errorHandler keeps configKey on the 500 envelope so the fault can be located in config', async () => {
148
+ const res = await createApp({
149
+ error: createErrorWithCause(),
150
+ logger: createLogger(),
151
+ }).request('/api/request/getUsers');
152
+
153
+ const body = await res.json();
154
+ expect(body['~e'].configKey).toEqual('page:home.blocks.0');
155
+ });
156
+
157
+ test('errorHandler returns text Internal Server Error at 500 on a page path with no payload', async () => {
158
+ const res = await createApp({
159
+ error: createErrorWithCause(),
160
+ logger: createLogger(),
161
+ }).request('/home');
162
+
163
+ expect(res.status).toEqual(500);
164
+ expect(await res.text()).toEqual('Internal Server Error');
165
+ });
166
+
167
+ test('errorHandler awaits context.handleError before serializing the response', async () => {
168
+ const context = {
169
+ handleError: jest.fn(async (error) => {
170
+ await new Promise((resolve) => setTimeout(resolve, 10));
171
+ error.awaited = true;
172
+ }),
173
+ };
174
+ const error = createErrorWithCause();
175
+ const res = await createApp({ context, error, logger: createLogger() }).request(
176
+ '/api/request/getUsers'
177
+ );
178
+
179
+ expect(context.handleError).toHaveBeenCalledTimes(1);
180
+ expect(context.handleError).toHaveBeenCalledWith(error);
181
+ const body = await res.json();
182
+ expect(body['~e'].awaited).toBe(true);
183
+ });
184
+
185
+ // createHandleError sets error.handled once it has logged. The client reads it
186
+ // to decide whether to POST the error back for logging, so it has to survive
187
+ // serialization or the error is logged a second time.
188
+ test('errorHandler returns handled true on the 500 envelope after handleError sets it', async () => {
189
+ const context = {
190
+ handleError: jest.fn(async (error) => {
191
+ error.handled = true;
192
+ }),
193
+ };
194
+ const res = await createApp({
195
+ context,
196
+ error: createErrorWithCause(),
197
+ logger: createLogger(),
198
+ }).request('/api/request/getUsers');
199
+
200
+ const body = await res.json();
201
+ expect(body['~e'].handled).toBe(true);
202
+ });
203
+
204
+ test('errorHandler logs with logger.error when the request has no lowdefyContext', async () => {
205
+ const logger = createLogger();
206
+ const error = createErrorWithCause();
207
+ const res = await createApp({ error, logger }).request('/api/request/getUsers');
208
+
209
+ expect(res.status).toEqual(500);
210
+ expect(logger.error).toHaveBeenCalledTimes(1);
211
+ expect(logger.error).toHaveBeenCalledWith(error);
212
+ });
213
+
214
+ test('errorHandler treats a basePath prefixed api path as an api path', async () => {
215
+ const res = await createApp({
216
+ basePath: '/base',
217
+ error: createErrorWithCause(),
218
+ logger: createLogger(),
219
+ }).request('/base/api/request/getUsers');
220
+
221
+ expect(res.status).toEqual(500);
222
+ const body = await res.json();
223
+ expect(body['~e'].message).toEqual('Top message.');
224
+ });
225
+
226
+ test('errorHandler treats a basePath prefixed page path as a page path', async () => {
227
+ const res = await createApp({
228
+ basePath: '/base',
229
+ error: createErrorWithCause(),
230
+ logger: createLogger(),
231
+ }).request('/base/home');
232
+
233
+ expect(res.status).toEqual(500);
234
+ expect(await res.text()).toEqual('Internal Server Error');
235
+ });
@@ -0,0 +1,64 @@
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 { callAgent } from '@lowdefy/api';
18
+ import { translate, type } from '@lowdefy/helpers';
19
+
20
+ import getPathSegments from '../lib/getPathSegments.js';
21
+
22
+ async function agentHandler(c) {
23
+ const context = c.get('lowdefyContext');
24
+ const t = (key, values) => translate({ key, values, i18n: context.i18n });
25
+ if (c.req.method !== 'POST') {
26
+ throw new Error(t('agent.runtime.methodNotAllowed'));
27
+ }
28
+ const segments = getPathSegments(c, '/api/agent/');
29
+ if (segments.length < 2) {
30
+ return c.json({ error: t('agent.runtime.invalidPath') }, 400);
31
+ }
32
+ const agentId = segments[segments.length - 1];
33
+ const pageId = segments.slice(0, -1).join('/');
34
+ context.logger.info({ event: 'call_agent', agentId, pageId });
35
+ const { conversationId } = c.req.query();
36
+ const { messages, urlQuery, sharedState } = await c.req.json();
37
+ if (!Array.isArray(messages)) {
38
+ return c.json({ error: t('agent.runtime.messagesMustBeArray') }, 400);
39
+ }
40
+ if (urlQuery != null && (typeof urlQuery !== 'object' || Array.isArray(urlQuery))) {
41
+ return c.json({ error: t('agent.runtime.urlQueryMustBeObject') }, 400);
42
+ }
43
+ if (sharedState != null && !type.isObject(sharedState)) {
44
+ return c.json({ error: t('agent.runtime.sharedStateMustBeObject') }, 400);
45
+ }
46
+ const { response: webResponse } = await callAgent(context, {
47
+ agentId,
48
+ pageId,
49
+ messages,
50
+ conversationId: conversationId ?? undefined,
51
+ sharedState: sharedState ?? undefined,
52
+ urlQuery: urlQuery ?? undefined,
53
+ });
54
+
55
+ // callAgent returns a Web Response — stream its body straight through.
56
+ return c.body(webResponse.body, 200, {
57
+ 'Content-Type': 'text/event-stream',
58
+ 'Cache-Control': 'no-cache',
59
+ Connection: 'keep-alive',
60
+ 'Content-Encoding': 'none',
61
+ });
62
+ }
63
+
64
+ export default agentHandler;
@@ -0,0 +1,37 @@
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 getPathSegments from '../lib/getPathSegments.js';
20
+
21
+ // Page config as JSON for client-side SPA navigation. The first page load is
22
+ // served embedded in the HTML; subsequent navigations fetch from here.
23
+ async function apiPageHandler(c) {
24
+ const context = c.get('lowdefyContext');
25
+ const pageId = getPathSegments(c, '/api/page/').join('/');
26
+ // The client forwards its current query string on the fetch so Dynamic block
27
+ // resolution sees the same urlQuery as an initial HTML load.
28
+ const pageConfig = await getPageConfig(context, { pageId, urlQuery: c.req.query() });
29
+ if (!pageConfig) {
30
+ context.logger.info({ event: 'api_page_not_found', pageId });
31
+ return c.json({ pageConfig: null }, 404);
32
+ }
33
+ context.logger.info({ event: 'api_page_view', pageId });
34
+ return c.json({ pageConfig });
35
+ }
36
+
37
+ export default apiPageHandler;
@@ -0,0 +1,49 @@
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 { authHandler } from '@hono/auth-js';
18
+
19
+ import authJson from '../../lib/build/auth.js';
20
+ import getDevSession from '../../lib/server/auth/getDevSession.js';
21
+
22
+ // Replaces pages/api/auth/[...nextauth].js. Hono routes HEAD requests through
23
+ // GET handlers, so the corporate-email pre-check branch must live inside the
24
+ // middleware, before delegating to the Auth.js handler. See:
25
+ // https://next-auth.js.org/tutorials/avoid-corporate-link-checking-email-provider
26
+ function authMiddleware() {
27
+ const handler = authJson.configured === true ? authHandler() : null;
28
+ return async function auth(c, next) {
29
+ if (authJson.configured !== true) {
30
+ return c.json({ message: 'Auth not configured' }, 404);
31
+ }
32
+ if (c.req.method === 'HEAD') {
33
+ return c.body(null, 200);
34
+ }
35
+ // Dev sessions (mock user, headless renderer cookie) exist outside
36
+ // Auth.js. getDevSession is the same function the server request
37
+ // context uses (lib/server/auth/session.js), so the session the browser
38
+ // client sees here is identical to the one requests authenticate with.
39
+ if (c.req.method === 'GET' && c.req.path.endsWith('/api/auth/session')) {
40
+ const devSession = await getDevSession(c);
41
+ if (devSession) {
42
+ return c.json(devSession);
43
+ }
44
+ }
45
+ return handler(c, next);
46
+ };
47
+ }
48
+
49
+ export default authMiddleware;
@@ -0,0 +1,84 @@
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
+ // Only the route wiring is under test here — getDevSession's own behaviour
21
+ // (mock/headless resolution, session-callback parity) is covered in
22
+ // lib/server/auth/getDevSession.test.mjs, and the real Auth.js engine is
23
+ // stubbed with a sentinel handler so "delegated to Auth.js" is observable.
24
+ const mockAuthHandler = jest.fn(() => (c) => c.json({ delegated: true }));
25
+ jest.unstable_mockModule('@hono/auth-js', () => ({
26
+ authHandler: mockAuthHandler,
27
+ }));
28
+ jest.unstable_mockModule('../../lib/build/auth.js', () => ({
29
+ default: { configured: true },
30
+ }));
31
+ const mockGetDevSession = jest.fn(async () => undefined);
32
+ jest.unstable_mockModule('../../lib/server/auth/getDevSession.js', () => ({
33
+ default: mockGetDevSession,
34
+ }));
35
+
36
+ const { default: authMiddleware } = await import('./auth.js');
37
+
38
+ function createApp() {
39
+ const app = new Hono();
40
+ app.use('/api/auth/*', authMiddleware());
41
+ return app;
42
+ }
43
+
44
+ afterEach(() => {
45
+ mockGetDevSession.mockReset();
46
+ mockGetDevSession.mockResolvedValue(undefined);
47
+ });
48
+
49
+ test('GET /api/auth/session returns the dev session so the client matches the server', async () => {
50
+ const devSession = {
51
+ user: {
52
+ id: 'dev-mock-user',
53
+ roles: ['admin'],
54
+ global_attributes: { company_ids: ['C-1'] },
55
+ },
56
+ hashed_id: 'abc',
57
+ expires: '2100-01-01T00:00:00.000Z',
58
+ };
59
+ mockGetDevSession.mockResolvedValue(devSession);
60
+
61
+ const res = await createApp().request('/api/auth/session');
62
+ expect(res.status).toEqual(200);
63
+ expect(await res.json()).toEqual(devSession);
64
+ });
65
+
66
+ test('GET /api/auth/session delegates to Auth.js when no dev session exists', async () => {
67
+ const res = await createApp().request('/api/auth/session');
68
+ expect(res.status).toEqual(200);
69
+ expect(await res.json()).toEqual({ delegated: true });
70
+ });
71
+
72
+ test('other auth routes delegate to Auth.js even when a dev session exists', async () => {
73
+ mockGetDevSession.mockResolvedValue({ user: { id: 'mock' } });
74
+
75
+ const res = await createApp().request('/api/auth/csrf');
76
+ expect(await res.json()).toEqual({ delegated: true });
77
+ });
78
+
79
+ test('POST /api/auth/session delegates to Auth.js even when a dev session exists', async () => {
80
+ mockGetDevSession.mockResolvedValue({ user: { id: 'mock' } });
81
+
82
+ const res = await createApp().request('/api/auth/session', { method: 'POST' });
83
+ expect(await res.json()).toEqual({ delegated: true });
84
+ });
@@ -0,0 +1,57 @@
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 { logClientError } from '@lowdefy/api';
18
+
19
+ import clientErrorStore from '../../lib/docs/clientErrorStore.js';
20
+
21
+ async function clientErrorHandler(c) {
22
+ if (c.req.method !== 'POST') {
23
+ throw new Error('Only POST requests are supported.');
24
+ }
25
+ const context = c.get('lowdefyContext');
26
+
27
+ const origin = c.req.header('origin');
28
+ if (!origin) {
29
+ return c.json({ error: 'Forbidden' }, 403);
30
+ }
31
+ try {
32
+ if (new URL(origin).host !== c.req.header('host')) {
33
+ return c.json({ error: 'Forbidden' }, 403);
34
+ }
35
+ } catch {
36
+ return c.json({ error: 'Forbidden' }, 403);
37
+ }
38
+
39
+ // Dev keeps `received` in the payload — it powers error tracing in dev tools.
40
+ const body = await c.req.json();
41
+ const { error, ...response } = await logClientError(context, body);
42
+
43
+ // Feed the agent-facing feedback loop (GET /lowdefy-docs/build-status) with a
44
+ // serializable summary — not the full error object, which may carry
45
+ // non-serializable `received` values.
46
+ clientErrorStore.push({
47
+ timestamp: new Date().toISOString(),
48
+ name: error?.name ?? 'Error',
49
+ message: error?.message ?? null,
50
+ source: response.source ?? null,
51
+ config: response.config ?? null,
52
+ });
53
+
54
+ return c.json(response);
55
+ }
56
+
57
+ export default clientErrorHandler;
@@ -0,0 +1,47 @@
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 { runScheduledEndpoint } from '@lowdefy/api';
18
+
19
+ import getPathSegments from '../lib/getPathSegments.js';
20
+
21
+ // Triggered by Vercel Cron in production: an HTTP GET to /api/cron/<endpointId>. Available in dev so
22
+ // scheduled endpoints can be triggered locally with curl. Vercel auto-sends the value of the project
23
+ // env var named exactly CRON_SECRET as `Authorization: Bearer <value>`, and the firing cron
24
+ // expression in the `x-vercel-cron-schedule` header. Fails closed if CRON_SECRET is unset. A request
25
+ // forwarded from the production deployment of another environment (see cronForward.js) also names
26
+ // the environment whose schedules apply in `x-lowdefy-cron-environment`.
27
+ async function cronHandler(c) {
28
+ if (c.req.method !== 'GET') {
29
+ throw new Error('Only GET requests are supported.');
30
+ }
31
+ const context = c.get('lowdefyContext');
32
+
33
+ const secret = process.env.CRON_SECRET;
34
+ if (!secret || c.req.header('authorization') !== `Bearer ${secret}`) {
35
+ context.logger.warn({ event: 'cron_unauthorized' });
36
+ return c.json({ error: 'Unauthorized' }, 401);
37
+ }
38
+
39
+ const endpointId = getPathSegments(c, '/api/cron/').join('/');
40
+ const cron = c.req.header('x-vercel-cron-schedule');
41
+ const environment = c.req.header('x-lowdefy-cron-environment');
42
+ context.logger.info({ event: 'call_cron_endpoint', endpointId, cron, environment });
43
+ const response = await runScheduledEndpoint(context, { endpointId, cron, environment });
44
+ return c.json(response);
45
+ }
46
+
47
+ export default cronHandler;
@@ -0,0 +1,47 @@
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 { forwardScheduledEndpoint } from '@lowdefy/api';
18
+
19
+ import getPathSegments from '../lib/getPathSegments.js';
20
+
21
+ // Triggered by Vercel Cron on the production deployment: an HTTP GET to
22
+ // /api/cron-forward/<environment>/<endpointId>, registered by `lowdefy vercel-output` for the
23
+ // schedules of every environment declared with a url in config.cron.environments. Vercel fires crons
24
+ // only on production, so production pings that environment's own /api/cron/<endpointId> (with the
25
+ // environment's CRON_SECRET) and answers immediately. Same transport auth as cron (CRON_SECRET,
26
+ // fails closed).
27
+ async function cronForwardHandler(c) {
28
+ if (c.req.method !== 'GET') {
29
+ throw new Error('Only GET requests are supported.');
30
+ }
31
+ const context = c.get('lowdefyContext');
32
+
33
+ const secret = process.env.CRON_SECRET;
34
+ if (!secret || c.req.header('authorization') !== `Bearer ${secret}`) {
35
+ context.logger.warn({ event: 'cron_forward_unauthorized' });
36
+ return c.json({ error: 'Unauthorized' }, 401);
37
+ }
38
+
39
+ const [environment, ...endpointSegments] = getPathSegments(c, '/api/cron-forward/');
40
+ const endpointId = endpointSegments.join('/');
41
+ const cron = c.req.header('x-vercel-cron-schedule');
42
+ context.logger.info({ event: 'call_cron_forward', environment, endpointId, cron });
43
+ const response = await forwardScheduledEndpoint(context, { environment, endpointId, cron });
44
+ return c.json(response);
45
+ }
46
+
47
+ export default cronForwardHandler;
@@ -0,0 +1,47 @@
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 { runDetachedEndpoint } from '@lowdefy/api';
18
+
19
+ import getPathSegments from '../lib/getPathSegments.js';
20
+
21
+ // Target of a CallApi step with `detached: true`: the calling invocation
22
+ // fire-and-forgets a POST here, so the endpoint runs in its OWN function
23
+ // invocation with a fresh duration budget (chainable bounded work without a
24
+ // queue). Same transport auth as cron (CRON_SECRET, fails closed) and the same
25
+ // system execution context — InternalApi endpoints are callable, `_user` is
26
+ // undefined. Delivery is at-most-once with no retry: targets must be
27
+ // idempotent.
28
+ async function detachedHandler(c) {
29
+ if (c.req.method !== 'POST') {
30
+ throw new Error('Only POST requests are supported.');
31
+ }
32
+ const context = c.get('lowdefyContext');
33
+
34
+ const secret = process.env.CRON_SECRET;
35
+ if (!secret || c.req.header('authorization') !== `Bearer ${secret}`) {
36
+ context.logger.warn({ event: 'detached_unauthorized' });
37
+ return c.json({ error: 'Unauthorized' }, 401);
38
+ }
39
+
40
+ const endpointId = getPathSegments(c, '/api/detached/').join('/');
41
+ const { payload } = await c.req.json();
42
+ context.logger.info({ event: 'call_detached_endpoint', endpointId });
43
+ const response = await runDetachedEndpoint(context, { endpointId, payload });
44
+ return c.json(response);
45
+ }
46
+
47
+ export default detachedHandler;