@lowdefy/server-dev 5.6.0 → 6.1.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.
- package/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/clientBuildImports.test.mjs +61 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +22 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +9 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +128 -0
- package/lib/server/createLowdefyContext.test.mjs +117 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/lib/server/log/createLogger.test.mjs +62 -0
- package/lib/server/scrubSecrets.js +23 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +62 -49
- package/package.original.json +65 -52
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +247 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
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 '../../../lib/docs/getPageConfig.js';
|
|
18
|
+
|
|
19
|
+
// 404 when the pageId isn't registered at all. 200 with { buildError, errors }
|
|
20
|
+
// when the pageId is registered but its JIT build failed — agents need the
|
|
21
|
+
// errors in the body, not just a failing status code.
|
|
22
|
+
async function docsPageConfigHandler(c) {
|
|
23
|
+
const pageId = c.req.param('pageId');
|
|
24
|
+
const result = await getPageConfig({ pageId });
|
|
25
|
+
if (result === null) {
|
|
26
|
+
return c.json(
|
|
27
|
+
{
|
|
28
|
+
error: `No page found for pageId "${pageId}". Check the pageId in your lowdefy.yaml pages config.`,
|
|
29
|
+
},
|
|
30
|
+
404
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return c.json(result);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default docsPageConfigHandler;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
// The docs routes take the headless caller as a JSON string on the GET routes
|
|
20
|
+
// (query params are always strings) and as an object in the POST bodies, so both
|
|
21
|
+
// arrive here and every route answers a bad `user` with the same message.
|
|
22
|
+
// Returns { user } or { error }.
|
|
23
|
+
function parseUserParam({ value }) {
|
|
24
|
+
if (type.isNone(value)) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let user = value;
|
|
29
|
+
if (type.isString(value)) {
|
|
30
|
+
try {
|
|
31
|
+
user = JSON.parse(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return {
|
|
34
|
+
error: `The "user" param must be JSON, e.g. {"roles":["admin"]}. Received ${JSON.stringify(
|
|
35
|
+
value
|
|
36
|
+
)}.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!type.isObject(user)) {
|
|
42
|
+
return {
|
|
43
|
+
error: `The "user" param must be an object, e.g. {"roles":["admin"]}. Received ${JSON.stringify(
|
|
44
|
+
user
|
|
45
|
+
)}.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { user };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default parseUserParam;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 parseUserParam from './parseUserParam.js';
|
|
18
|
+
|
|
19
|
+
test('parseUserParam returns nothing when the param is absent', () => {
|
|
20
|
+
expect(parseUserParam({ value: undefined })).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('parseUserParam parses a JSON string from a query param', () => {
|
|
24
|
+
expect(parseUserParam({ value: '{"roles":["admin"]}' })).toEqual({
|
|
25
|
+
user: { roles: ['admin'] },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('parseUserParam passes an object from a JSON body through', () => {
|
|
30
|
+
expect(parseUserParam({ value: { roles: ['admin'] } })).toEqual({
|
|
31
|
+
user: { roles: ['admin'] },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('parseUserParam returns an error when the query param is not JSON', () => {
|
|
36
|
+
expect(parseUserParam({ value: 'admin' }).error).toMatch(/must be JSON/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('parseUserParam returns an error when the param parses to a non-object', () => {
|
|
40
|
+
expect(parseUserParam({ value: '["admin"]' }).error).toMatch(/must be an object/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('parseUserParam returns an error when a body value is not an object', () => {
|
|
44
|
+
expect(parseUserParam({ value: 42 }).error).toMatch(/must be an object/);
|
|
45
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
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 getPluginDoc from '../../../lib/docs/getPluginDoc.js';
|
|
18
|
+
|
|
19
|
+
function docsPluginDocHandler(c) {
|
|
20
|
+
const packageName = c.req.param('package');
|
|
21
|
+
const doc = getPluginDoc({ packageName });
|
|
22
|
+
if (doc === null) {
|
|
23
|
+
return c.json(
|
|
24
|
+
{
|
|
25
|
+
error: `Package "${packageName}" ships no markdown docs. See GET /lowdefy-docs/plugins for its types.`,
|
|
26
|
+
},
|
|
27
|
+
404
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return c.text(doc.markdown, 200, { 'Content-Type': 'text/markdown; charset=utf-8' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default docsPluginDocHandler;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 listPlugins from '../../../lib/docs/listPlugins.js';
|
|
18
|
+
|
|
19
|
+
function docsPluginsHandler(c) {
|
|
20
|
+
return c.json(listPlugins());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default docsPluginsHandler;
|
|
@@ -0,0 +1,36 @@
|
|
|
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 runRequest from '../../../lib/docs/runRequest.js';
|
|
18
|
+
|
|
19
|
+
// Refusals and request errors are returned as 200 data (see runRequest.js) —
|
|
20
|
+
// only malformed input (missing pageId/requestId) is a 400.
|
|
21
|
+
async function docsRunRequestHandler(c) {
|
|
22
|
+
// Parse the body from a clone: runRequest builds a Lowdefy context whose
|
|
23
|
+
// getSession(c) → getAuthUser(c) reconstructs a Request from c.req.raw,
|
|
24
|
+
// which throws ("body ... disturbed or locked") if the body was already
|
|
25
|
+
// consumed. The normal /api/request path never hits this because its
|
|
26
|
+
// session is read in middleware before the handler touches the body.
|
|
27
|
+
const { pageId, requestId, payload } = await c.req.raw.clone().json();
|
|
28
|
+
try {
|
|
29
|
+
const result = await runRequest({ pageId, requestId, payload, honoContext: c });
|
|
30
|
+
return c.json(result);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return c.json({ error: error.message }, 400);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default docsRunRequestHandler;
|
|
@@ -14,22 +14,24 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
17
|
+
import getSchema from '../../../lib/docs/getSchema.js';
|
|
18
|
+
|
|
19
|
+
function docsSchemaHandler(c) {
|
|
20
|
+
const kind = c.req.param('kind');
|
|
21
|
+
const type = c.req.param('type');
|
|
22
|
+
let schema;
|
|
23
|
+
try {
|
|
24
|
+
schema = getSchema({ kind, type });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return c.json({ error: error.message }, 400);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
if (schema === null) {
|
|
29
|
+
return c.json(
|
|
30
|
+
{ error: `No schema found for ${kind} type "${type}". See GET /lowdefy-docs/${kind} for available types.` },
|
|
31
|
+
404
|
|
32
|
+
);
|
|
31
33
|
}
|
|
32
|
-
return
|
|
34
|
+
return c.json(schema);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export default
|
|
37
|
+
export default docsSchemaHandler;
|
|
@@ -0,0 +1,72 @@
|
|
|
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 parseUserParam from './parseUserParam.js';
|
|
20
|
+
import screenshotPage from '../../../lib/docs/screenshotPage.js';
|
|
21
|
+
|
|
22
|
+
// Parses a query param as a number, or undefined if absent/not a valid
|
|
23
|
+
// number — undefined (not NaN/0) so callers can tell "not provided" apart
|
|
24
|
+
// from an explicit 0.
|
|
25
|
+
function queryNumber(c, key) {
|
|
26
|
+
const raw = c.req.query(key);
|
|
27
|
+
if (type.isNone(raw)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const value = Number(raw);
|
|
31
|
+
return Number.isFinite(value) ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function docsScreenshotHandler(c) {
|
|
35
|
+
const pageId = c.req.param('pageId');
|
|
36
|
+
const fullPage = c.req.query('fullPage') === 'true';
|
|
37
|
+
// Derived from the incoming request rather than a config value — this is
|
|
38
|
+
// the origin an agent can actually reach the dev server on (host/port it
|
|
39
|
+
// just connected to), regardless of how the server is bound.
|
|
40
|
+
const origin = new URL(c.req.url).origin;
|
|
41
|
+
|
|
42
|
+
const clipX = queryNumber(c, 'x');
|
|
43
|
+
const clipY = queryNumber(c, 'y');
|
|
44
|
+
const clipWidth = queryNumber(c, 'width');
|
|
45
|
+
const clipHeight = queryNumber(c, 'height');
|
|
46
|
+
// Only build a clip when all four dimensions are present — a partial
|
|
47
|
+
// clip is ambiguous, so screenshotPage falls back to a normal viewport
|
|
48
|
+
// screenshot in that case (same as an invalid clip).
|
|
49
|
+
const clip =
|
|
50
|
+
!type.isNone(clipX) &&
|
|
51
|
+
!type.isNone(clipY) &&
|
|
52
|
+
!type.isNone(clipWidth) &&
|
|
53
|
+
!type.isNone(clipHeight)
|
|
54
|
+
? { x: clipX, y: clipY, width: clipWidth, height: clipHeight }
|
|
55
|
+
: undefined;
|
|
56
|
+
const scrollX = queryNumber(c, 'scrollX') ?? 0;
|
|
57
|
+
const scrollY = queryNumber(c, 'scrollY') ?? 0;
|
|
58
|
+
|
|
59
|
+
const { user, error: userError } = parseUserParam({ value: c.req.query('user') });
|
|
60
|
+
if (userError) {
|
|
61
|
+
return c.json({ error: userError }, 400);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await screenshotPage({ origin, pageId, fullPage, clip, scrollX, scrollY, user });
|
|
65
|
+
if (result.error) {
|
|
66
|
+
return c.json({ error: result.error }, 502);
|
|
67
|
+
}
|
|
68
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
69
|
+
return c.body(buffer, 200, { 'Content-Type': result.mimeType });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default docsScreenshotHandler;
|
|
@@ -0,0 +1,27 @@
|
|
|
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;
|
|
@@ -0,0 +1,36 @@
|
|
|
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;
|
|
@@ -0,0 +1,26 @@
|
|
|
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;
|
|
@@ -0,0 +1,33 @@
|
|
|
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;
|
|
@@ -0,0 +1,71 @@
|
|
|
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` are third-party webhook receivers
|
|
29
|
+
// (SNS, Event Grid, Stripe, ...): they take the request RAW — body in the
|
|
30
|
+
// caller's own format (not the { payload } envelope), plus query + headers —
|
|
31
|
+
// and their return value is sent back verbatim, because webhook handshakes
|
|
32
|
+
// (e.g. Event Grid's validationResponse) require exact response shapes.
|
|
33
|
+
// The config lookup is a cached file read; an unknown endpoint falls through
|
|
34
|
+
// to the standard path so its error shape is unchanged. Everything about
|
|
35
|
+
// 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 === true) {
|
|
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
|
+
if (!result.success) {
|
|
60
|
+
return c.json({ error: 'Webhook failed.' }, 500);
|
|
61
|
+
}
|
|
62
|
+
return c.json(result.response ?? { ok: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { blockId, payload, pageId } = await c.req.json();
|
|
66
|
+
context.logger.info({ event: 'call_api_endpoint', blockId, endpointId, pageId });
|
|
67
|
+
const response = await callEndpoint(context, { blockId, endpointId, pageId, payload });
|
|
68
|
+
return c.json(response);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default endpointsHandler;
|
|
@@ -0,0 +1,110 @@
|
|
|
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;
|