@lowdefy/server-dev 0.0.0-experimental-20260720072521 → 0.0.0-experimental-20260720135014

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 (196) hide show
  1. package/.eslintrc.yaml +1 -0
  2. package/lib/build/app.js +1 -8
  3. package/lib/build/appMeta.js +1 -8
  4. package/lib/build/auth.js +1 -8
  5. package/lib/build/config.js +1 -8
  6. package/lib/build/i18n.js +1 -8
  7. package/lib/build/logger.js +1 -8
  8. package/lib/build/theme.js +1 -8
  9. package/lib/client/App.js +80 -0
  10. package/{client/Page.jsx → lib/client/Page.js} +5 -5
  11. package/{client/Reload.jsx → lib/client/Reload.js} +7 -5
  12. package/lib/client/auth/{Auth.jsx → Auth.js} +5 -12
  13. package/lib/client/auth/AuthConfigured.js +61 -0
  14. package/lib/client/auth/AuthNotConfigured.js +2 -22
  15. package/lib/client/setPageId.js +5 -6
  16. package/lib/client/utils/useMutateCache.js +1 -14
  17. package/lib/client/utils/usePageConfig.js +11 -31
  18. package/lib/server/apiWrapper.js +116 -0
  19. package/lib/server/auth/{getStrategies.js → getAuthOptions.js} +9 -14
  20. package/lib/server/auth/{getMockUser.js → getMockSession.js} +19 -10
  21. package/{src/routes/docs/checkpointsRevert.js → lib/server/auth/getServerSession.js} +13 -11
  22. package/lib/server/compileCss.js +39 -0
  23. package/lib/server/jitPageBuilder.js +11 -36
  24. package/lib/server/log/logRequest.js +3 -1
  25. package/manager/getContext.mjs +7 -3
  26. package/manager/processes/compileCss.mjs +54 -0
  27. package/manager/processes/initialBuild.mjs +2 -0
  28. package/manager/processes/installPlugins.mjs +11 -19
  29. package/manager/processes/lowdefyBuild.mjs +7 -29
  30. package/manager/processes/nextBuild.mjs +50 -0
  31. package/manager/processes/readDotEnv.mjs +1 -13
  32. package/manager/processes/shutdownServer.mjs +6 -6
  33. package/manager/processes/startServer.mjs +7 -8
  34. package/manager/processes/startWatchers.mjs +2 -2
  35. package/manager/run.mjs +0 -41
  36. package/manager/utils/createCustomPluginMessagesMap.mjs +1 -8
  37. package/manager/utils/createCustomPluginTypesMap.mjs +2 -3
  38. package/manager/utils/{getViteBin.mjs → getNextBin.mjs} +10 -7
  39. package/manager/utils/updatePageTailwindCss.mjs +1 -7
  40. package/manager/watchers/lowdefyBuildWatcher.mjs +1 -0
  41. package/manager/watchers/moduleBuildWatcher.mjs +1 -0
  42. package/manager/watchers/{serverArtifactWatcher.mjs → nextBuildWatcher.mjs} +22 -18
  43. package/next.config.js +25 -0
  44. package/package.json +47 -63
  45. package/package.original.json +55 -66
  46. package/{src/routes/devTools.js → pages/404.js} +2 -4
  47. package/{src/routes/docs/appMap.js → pages/[[...pageId]].js} +2 -6
  48. package/{client/App.jsx → pages/_app.js} +33 -21
  49. package/pages/_document.js +107 -0
  50. package/pages/api/agent/[...path].js +86 -0
  51. package/pages/api/auth/[...nextauth].js +48 -0
  52. package/pages/api/client-error.js +45 -0
  53. package/{src/routes/docs/buildStatus.js → pages/api/dev-tools.js} +4 -5
  54. package/{lib/docs/clientErrorStore.js → pages/api/endpoints/[...endpointId].js} +11 -15
  55. package/{src/lib/serveBuildJs.js → pages/api/icons/dynamic.js} +12 -8
  56. package/{src/routes/jsEnv.js → pages/api/js/[env].js} +20 -8
  57. package/pages/api/page/[...pageId].js +74 -0
  58. package/{src/routes → pages/api}/ping.js +2 -4
  59. package/pages/api/reload.js +53 -0
  60. package/pages/api/request/[...path].js +41 -0
  61. package/{src/routes → pages/api}/root.js +4 -4
  62. package/client/Inspector.jsx +0 -206
  63. package/client/Routing.jsx +0 -106
  64. package/client/feedback/FeedbackMount.jsx +0 -83
  65. package/client/feedback/FeedbackOverlay.jsx +0 -901
  66. package/client/feedback/captureTabScreenshot.js +0 -84
  67. package/client/feedback/copyToClipboard.js +0 -45
  68. package/client/feedback/drawAnnotationsSvg.js +0 -119
  69. package/client/feedback/elementInspect.js +0 -99
  70. package/client/feedback/feedbackStyles.js +0 -301
  71. package/client/feedback/pinResponsiveImages.js +0 -59
  72. package/client/feedback/sendFeedback.js +0 -39
  73. package/client/feedback/useFeedbackToggle.js +0 -70
  74. package/client/main.jsx +0 -42
  75. package/client/openInEditor/OpenInEditorListener.jsx +0 -261
  76. package/client/openInEditor/findBlockLocation.js +0 -51
  77. package/lib/client/auth/AuthConfigured.jsx +0 -155
  78. package/lib/docs/captureAnnotatedScreenshot.js +0 -88
  79. package/lib/docs/checkpointPaths.js +0 -91
  80. package/lib/docs/checkpointStore.js +0 -234
  81. package/lib/docs/checkpointToMocks.js +0 -65
  82. package/lib/docs/configCheckpoints.test.mjs +0 -156
  83. package/lib/docs/createConfigCheckpoint.js +0 -92
  84. package/lib/docs/createDocsMcpServer.js +0 -506
  85. package/lib/docs/createDocsMcpServer.test.mjs +0 -123
  86. package/lib/docs/devMockRegistry.js +0 -68
  87. package/lib/docs/docs.test.mjs +0 -448
  88. package/lib/docs/enrichFeedback.js +0 -71
  89. package/lib/docs/evalOperator.js +0 -39
  90. package/lib/docs/evalOperator.test.mjs +0 -112
  91. package/lib/docs/evalOperatorHeadless.js +0 -87
  92. package/lib/docs/evalOperatorHeadless.test.mjs +0 -72
  93. package/lib/docs/evalOperatorInTab.js +0 -48
  94. package/lib/docs/findConfig.js +0 -260
  95. package/lib/docs/formatFeedback.js +0 -121
  96. package/lib/docs/formatFeedback.test.mjs +0 -204
  97. package/lib/docs/getAppMap.js +0 -180
  98. package/lib/docs/getBrowser.js +0 -115
  99. package/lib/docs/getBuildStatus.js +0 -37
  100. package/lib/docs/getCoreDoc.js +0 -63
  101. package/lib/docs/getDocsManifest.js +0 -40
  102. package/lib/docs/getExamples.js +0 -58
  103. package/lib/docs/getOverview.js +0 -115
  104. package/lib/docs/getPageConfig.js +0 -53
  105. package/lib/docs/getPluginDoc.js +0 -55
  106. package/lib/docs/getSchema.js +0 -65
  107. package/lib/docs/inspectState.js +0 -41
  108. package/lib/docs/inspectState.test.mjs +0 -91
  109. package/lib/docs/inspectStateFromTab.js +0 -40
  110. package/lib/docs/inspectStateHeadless.js +0 -84
  111. package/lib/docs/inspectStateHeadless.test.mjs +0 -60
  112. package/lib/docs/isWriteRequestsAllowed.js +0 -50
  113. package/lib/docs/listConfigCheckpoints.js +0 -55
  114. package/lib/docs/listPlugins.js +0 -80
  115. package/lib/docs/listTypes.js +0 -105
  116. package/lib/docs/loadState.js +0 -139
  117. package/lib/docs/mapPageBuildErrors.js +0 -28
  118. package/lib/docs/normalizeTypeKind.js +0 -40
  119. package/lib/docs/readBuildArtifact.js +0 -35
  120. package/lib/docs/readPageArtifact.js +0 -26
  121. package/lib/docs/resolvePluginDir.js +0 -49
  122. package/lib/docs/revertConfigCheckpoint.js +0 -73
  123. package/lib/docs/runRequest.js +0 -141
  124. package/lib/docs/saveAnnotatedScreenshot.js +0 -43
  125. package/lib/docs/scaffoldPage.js +0 -75
  126. package/lib/docs/scaffoldPage.test.mjs +0 -97
  127. package/lib/docs/screenshotPage.js +0 -109
  128. package/lib/docs/screenshotPage.test.mjs +0 -59
  129. package/lib/docs/screenshotPageClip.test.mjs +0 -102
  130. package/lib/docs/searchDocs.js +0 -67
  131. package/lib/docs/setupTestFixtures.mjs +0 -335
  132. package/lib/docs/snapshotState.js +0 -59
  133. package/lib/docs/stateCheckpoints.test.mjs +0 -348
  134. package/lib/docs/tabAvailable.js +0 -28
  135. package/lib/docs/tabChannel.js +0 -119
  136. package/lib/docs/tabChannel.test.mjs +0 -130
  137. package/lib/server/auth/createSystemContext.js +0 -70
  138. package/lib/server/auth/getAuth.js +0 -46
  139. package/lib/server/auth/getHeadlessUser.js +0 -39
  140. package/lib/server/auth/headlessUser.js +0 -30
  141. package/lib/server/createLowdefyContext.js +0 -133
  142. package/lib/server/loadDynamicJsMap.js +0 -47
  143. package/manager/processes/lowdefyBuild.test.mjs +0 -102
  144. package/manager/utils/formatNoticeBox.mjs +0 -30
  145. package/manager/utils/writeBuildStatus.mjs +0 -39
  146. package/manager/utils/writeBuildStatus.test.mjs +0 -76
  147. package/postcss.config.cjs +0 -1
  148. package/src/app.js +0 -188
  149. package/src/html/renderDevPage.js +0 -86
  150. package/src/lib/getPathSegments.js +0 -25
  151. package/src/lib/safeScriptJson.js +0 -38
  152. package/src/middleware/apiContext.js +0 -32
  153. package/src/middleware/errorHandler.js +0 -55
  154. package/src/routes/agent.js +0 -64
  155. package/src/routes/auth.js +0 -43
  156. package/src/routes/clientError.js +0 -57
  157. package/src/routes/cron.js +0 -44
  158. package/src/routes/detached.js +0 -51
  159. package/src/routes/devInspect.js +0 -94
  160. package/src/routes/docs/checkpointsCreate.js +0 -28
  161. package/src/routes/docs/checkpointsList.js +0 -23
  162. package/src/routes/docs/content.js +0 -31
  163. package/src/routes/docs/evalOperator.js +0 -46
  164. package/src/routes/docs/examples.js +0 -33
  165. package/src/routes/docs/find.js +0 -30
  166. package/src/routes/docs/index.js +0 -23
  167. package/src/routes/docs/inspectState.js +0 -34
  168. package/src/routes/docs/loadState.js +0 -38
  169. package/src/routes/docs/mcp.js +0 -30
  170. package/src/routes/docs/pageConfig.js +0 -36
  171. package/src/routes/docs/pluginDoc.js +0 -33
  172. package/src/routes/docs/plugins.js +0 -23
  173. package/src/routes/docs/runRequest.js +0 -34
  174. package/src/routes/docs/schema.js +0 -37
  175. package/src/routes/docs/screenshot.js +0 -66
  176. package/src/routes/docs/search.js +0 -27
  177. package/src/routes/docs/snapshotState.js +0 -36
  178. package/src/routes/docs/stateCheckpointsList.js +0 -26
  179. package/src/routes/docs/types.js +0 -33
  180. package/src/routes/endpoints.js +0 -76
  181. package/src/routes/feedback.js +0 -110
  182. package/src/routes/feedback.test.mjs +0 -204
  183. package/src/routes/iconsDynamic.js +0 -25
  184. package/src/routes/jitPage.js +0 -99
  185. package/src/routes/reload.js +0 -81
  186. package/src/routes/request.js +0 -66
  187. package/src/routes/usage.js +0 -46
  188. package/src/routes/user.js +0 -27
  189. package/src/routes/websocket.js +0 -33
  190. package/src/websocket/devWebSocket.js +0 -78
  191. package/vite.config.js +0 -111
  192. /package/lib/client/{BuildErrorPage.jsx → BuildErrorPage.js} +0 -0
  193. /package/lib/client/{BuildingPage.jsx → BuildingPage.js} +0 -0
  194. /package/lib/client/{ErrorBar.jsx → ErrorBar.js} +0 -0
  195. /package/lib/client/{InstallingPluginsPage.jsx → InstallingPluginsPage.js} +0 -0
  196. /package/lib/client/{RestartingPage.jsx → RestartingPage.js} +0 -0
@@ -1,66 +0,0 @@
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 { type } from '@lowdefy/helpers';
18
-
19
- import screenshotPage from '../../../lib/docs/screenshotPage.js';
20
-
21
- // Parses a query param as a number, or undefined if absent/not a valid
22
- // number — undefined (not NaN/0) so callers can tell "not provided" apart
23
- // from an explicit 0.
24
- function queryNumber(c, key) {
25
- const raw = c.req.query(key);
26
- if (type.isNone(raw)) {
27
- return undefined;
28
- }
29
- const value = Number(raw);
30
- return Number.isFinite(value) ? value : undefined;
31
- }
32
-
33
- async function docsScreenshotHandler(c) {
34
- const pageId = c.req.param('pageId');
35
- const fullPage = c.req.query('fullPage') === 'true';
36
- // Derived from the incoming request rather than a config value — this is
37
- // the origin an agent can actually reach the dev server on (host/port it
38
- // just connected to), regardless of how the server is bound.
39
- const origin = new URL(c.req.url).origin;
40
-
41
- const clipX = queryNumber(c, 'x');
42
- const clipY = queryNumber(c, 'y');
43
- const clipWidth = queryNumber(c, 'width');
44
- const clipHeight = queryNumber(c, 'height');
45
- // Only build a clip when all four dimensions are present — a partial
46
- // clip is ambiguous, so screenshotPage falls back to a normal viewport
47
- // screenshot in that case (same as an invalid clip).
48
- const clip =
49
- !type.isNone(clipX) &&
50
- !type.isNone(clipY) &&
51
- !type.isNone(clipWidth) &&
52
- !type.isNone(clipHeight)
53
- ? { x: clipX, y: clipY, width: clipWidth, height: clipHeight }
54
- : undefined;
55
- const scrollX = queryNumber(c, 'scrollX') ?? 0;
56
- const scrollY = queryNumber(c, 'scrollY') ?? 0;
57
-
58
- const result = await screenshotPage({ origin, pageId, fullPage, clip, scrollX, scrollY });
59
- if (result.error) {
60
- return c.json({ error: result.error }, 502);
61
- }
62
- const buffer = Buffer.from(result.data, 'base64');
63
- return c.body(buffer, 200, { 'Content-Type': result.mimeType });
64
- }
65
-
66
- export default docsScreenshotHandler;
@@ -1,27 +0,0 @@
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 searchDocs from '../../../lib/docs/searchDocs.js';
18
-
19
- function docsSearchHandler(c) {
20
- const query = c.req.query('q');
21
- if (!query || query.trim() === '') {
22
- return c.json({ error: 'Missing search query. Use GET /lowdefy-docs/search?q=your+keywords.' }, 400);
23
- }
24
- return c.json(searchDocs({ query }));
25
- }
26
-
27
- export default docsSearchHandler;
@@ -1,36 +0,0 @@
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 snapshotState from '../../../lib/docs/snapshotState.js';
18
-
19
- // Lets an agent (or a developer) snapshot the running app's page state and
20
- // recorded request/api responses into a checkpoint folder under .lowdefy,
21
- // for later replay via POST /lowdefy-docs/state/load.
22
- async function docsSnapshotStateHandler(c) {
23
- const { pageId, name, notes, source, overwrite } = await c.req.json();
24
- // Derived from the incoming request rather than a config value — this is
25
- // the origin an agent can actually reach the dev server on (host/port it
26
- // just connected to), regardless of how the server is bound.
27
- const origin = new URL(c.req.url).origin;
28
-
29
- const result = await snapshotState({ origin, pageId, name, notes, source, overwrite });
30
- if (result.error) {
31
- return c.json({ error: result.error }, 502);
32
- }
33
- return c.json(result);
34
- }
35
-
36
- export default docsSnapshotStateHandler;
@@ -1,26 +0,0 @@
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 { listStateCheckpoints } from '../../../lib/docs/checkpointStore.js';
18
-
19
- // Lists state checkpoints (page state + recorded request/api
20
- // responses) — distinct from GET /lowdefy-docs/checkpoints, which lists
21
- // config-directory-snapshot checkpoints (see checkpointStore.js).
22
- function docsStateCheckpointsListHandler(c) {
23
- return c.json(listStateCheckpoints());
24
- }
25
-
26
- export default docsStateCheckpointsListHandler;
@@ -1,33 +0,0 @@
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 listTypes from '../../../lib/docs/listTypes.js';
18
- import normalizeTypeKind from '../../../lib/docs/normalizeTypeKind.js';
19
-
20
- function docsTypesHandler(c) {
21
- const kind = c.req.param('kind');
22
- if (normalizeTypeKind({ kind }) === null) {
23
- return c.json(
24
- {
25
- error: `Unknown docs kind "${kind}". Use one of: blocks, operators, actions, connections, requests, agents, notifications, websockets. See GET /lowdefy-docs for all routes.`,
26
- },
27
- 404
28
- );
29
- }
30
- return c.json(listTypes({ kind }));
31
- }
32
-
33
- export default docsTypesHandler;
@@ -1,76 +0,0 @@
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 { callEndpoint, getEndpointConfig, runWebhookEndpoint } from '@lowdefy/api';
18
-
19
- import getPathSegments from '../lib/getPathSegments.js';
20
-
21
- async function endpointsHandler(c) {
22
- if (c.req.method !== 'POST') {
23
- throw new Error('Only POST requests are supported.');
24
- }
25
- const context = c.get('lowdefyContext');
26
- const endpointId = getPathSegments(c, '/api/endpoints/').join('/');
27
-
28
- // Endpoints opting in with `webhook` (true or a { verify } object) are
29
- // third-party webhook receivers (SNS, Event Grid, Stripe, ...): they take the
30
- // request RAW — body in the caller's own format (not the { payload }
31
- // envelope), plus query + headers — and their return value is sent back
32
- // verbatim, because webhook handshakes (e.g. Event Grid's validationResponse)
33
- // require exact response shapes. The config lookup is a cached file read; an
34
- // unknown endpoint falls through to the standard path so its error shape is
35
- // unchanged. Everything about non-webhook endpoints is untouched.
36
- let endpointConfig = null;
37
- try {
38
- endpointConfig = await getEndpointConfig(context, { endpointId });
39
- } catch {
40
- /* unknown endpoint — the standard path below reports it exactly as before */
41
- }
42
- if (endpointConfig?.webhook) {
43
- // SNS posts JSON as text/plain — parse regardless of content-type; an
44
- // unparseable body arrives as the raw string.
45
- const raw = await c.req.text();
46
- let body = raw;
47
- try {
48
- body = JSON.parse(raw);
49
- } catch {
50
- /* non-JSON body — raw string passthrough */
51
- }
52
- context.logger.info({ event: 'call_webhook_endpoint', endpointId });
53
- const result = await runWebhookEndpoint(context, {
54
- endpointId,
55
- body,
56
- query: c.req.query(),
57
- headers: c.req.header(),
58
- });
59
- // A failed verify gate is a rejected caller (401), distinct from a routine
60
- // error (500).
61
- if (result.status === 'unauthorized') {
62
- return c.json({ error: 'Webhook verification failed.' }, 401);
63
- }
64
- if (!result.success) {
65
- return c.json({ error: 'Webhook failed.' }, 500);
66
- }
67
- return c.json(result.response ?? { ok: true });
68
- }
69
-
70
- const { blockId, payload, pageId } = await c.req.json();
71
- context.logger.info({ event: 'call_api_endpoint', blockId, endpointId, pageId });
72
- const response = await callEndpoint(context, { blockId, endpointId, pageId, payload });
73
- return c.json(response);
74
- }
75
-
76
- export default endpointsHandler;
@@ -1,110 +0,0 @@
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 enrichFeedback from '../../lib/docs/enrichFeedback.js';
18
- import formatFeedback from '../../lib/docs/formatFeedback.js';
19
- import createLogger from '../../lib/server/log/createLogger.js';
20
-
21
- const logger = createLogger({ server: 'lowdefy-dev-feedback' });
22
-
23
- // Receives annotation batches from the dev browser overlay (a developer
24
- // draws on a live page and comments), enriches each annotation with its
25
- // yaml file:line via findConfig, and returns the formatted agent-readable
26
- // text. The overlay copies that text to the clipboard — the developer
27
- // pastes it into whichever agent session they choose, which is also what
28
- // makes delivery unambiguous when several sessions share one dev server.
29
- // Same-origin check cloned from routes/clientError.js: only a page served
30
- // by this dev server (not an arbitrary site) should be able to use it.
31
- async function feedbackHandler(c) {
32
- const origin = c.req.header('origin');
33
- if (!origin) {
34
- return c.json({ error: 'Forbidden' }, 403);
35
- }
36
- try {
37
- if (new URL(origin).host !== c.req.header('host')) {
38
- return c.json({ error: 'Forbidden' }, 403);
39
- }
40
- } catch {
41
- return c.json({ error: 'Forbidden' }, 403);
42
- }
43
-
44
- const batch = await c.req.json();
45
- if (!Array.isArray(batch?.annotations)) {
46
- return c.json(
47
- {
48
- error:
49
- 'Feedback batch requires an "annotations" array. See GET /lowdefy-docs for the ' +
50
- 'feedback batch shape.',
51
- },
52
- 400
53
- );
54
- }
55
-
56
- // The tab screenshot rides in as a multi-MB data URL — lift it off the
57
- // batch before enrichment so it never reaches the formatted text or the
58
- // headless capture path.
59
- const tabScreenshot = batch.screenshot;
60
- delete batch.screenshot;
61
- let enriched = await enrichFeedback({ batch });
62
- delete enriched.screenshot;
63
-
64
- // Default on — the overlay sends includeScreenshot: false when the
65
- // developer unticks it.
66
- if (batch.includeScreenshot !== false) {
67
- const fileName = `${batch.pageId}-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
68
- if (typeof tabScreenshot === 'string') {
69
- // Preferred path: the overlay captured the developer's actual tab —
70
- // theme, loaded data, exact pixels — with annotations already drawn.
71
- const { default: saveAnnotatedScreenshot } = await import(
72
- '../../lib/docs/saveAnnotatedScreenshot.js'
73
- );
74
- const result = saveAnnotatedScreenshot({ dataUrl: tabScreenshot, fileName });
75
- if (result.path) {
76
- enriched = { ...enriched, screenshotPath: result.path };
77
- } else {
78
- logger.warn(result.error);
79
- }
80
- } else {
81
- // Fallback: re-render the page headless (batches from agents or older
82
- // clients, or when the in-tab capture failed). The response must NOT
83
- // wait for this capture: the overlay writes the clipboard when this
84
- // returns, and Chrome's transient user-activation (required for
85
- // clipboard writes) expires within ~5s — a headless browser launch can
86
- // take longer. So the path is pre-assigned, the formatted text returns
87
- // immediately, and the PNG lands on disk a moment later — well before
88
- // a human pastes it anywhere.
89
- // Deferred import: the capture module pulls in the browser singleton,
90
- // which reads build artifacts at import time — only load it when a
91
- // screenshot is actually wanted.
92
- const { default: captureAnnotatedScreenshot } = await import(
93
- '../../lib/docs/captureAnnotatedScreenshot.js'
94
- );
95
- const serverOrigin = new URL(c.req.url).origin;
96
- enriched = { ...enriched, screenshotPath: `.lowdefy/annotations/${fileName}` };
97
- captureAnnotatedScreenshot({ origin: serverOrigin, batch: enriched, fileName }).then(
98
- (result) => {
99
- if (result.error) {
100
- logger.warn(result.error);
101
- }
102
- }
103
- );
104
- }
105
- }
106
-
107
- return c.json({ ok: true, formatted: formatFeedback({ items: [enriched] }) });
108
- }
109
-
110
- export default feedbackHandler;
@@ -1,204 +0,0 @@
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
- });
@@ -1,25 +0,0 @@
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 serveBuildJs from '../lib/serveBuildJs.js';
18
-
19
- function iconsDynamicHandler(c) {
20
- return c.body(serveBuildJs(['plugins', 'iconsDynamic.js']), 200, {
21
- 'Content-Type': 'application/javascript',
22
- });
23
- }
24
-
25
- export default iconsDynamicHandler;