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