@lowdefy/server-e2e 0.0.0-experimental-20260909201552 → 0.0.0-experimental-20260911113654
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/client/App.jsx +3 -3
- package/lib/client/auth/Auth.jsx +4 -31
- package/lib/server/auth/{getUser.js → session.js} +7 -8
- package/lib/server/log/logRequest.js +3 -1
- package/lowdefy/createCustomPluginTypesMap.mjs +2 -2
- package/package.json +25 -26
- package/package.original.json +25 -26
- package/src/app.js +0 -2
- package/src/html/getAssets.js +1 -3
- package/src/html/renderPage.js +4 -6
- package/src/middleware/apiContext.js +6 -17
- package/src/middleware/errorHandler.js +4 -48
- package/src/routes/apiPage.js +3 -3
- package/src/routes/clientError.js +1 -3
- package/src/routes/endpoints.js +1 -3
- package/src/routes/request.js +1 -3
- package/src/routes/sessionMock.js +1 -4
- package/src/routes/usage.js +1 -3
- package/src/routes/websocket.js +1 -3
- package/src/middleware/errorHandler.test.mjs +0 -354
- package/src/routes/user.js +0 -27
package/client/App.jsx
CHANGED
|
@@ -39,7 +39,7 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function App({ config }) {
|
|
42
|
-
const { rootConfig,
|
|
42
|
+
const { rootConfig, session } = config;
|
|
43
43
|
|
|
44
44
|
const usageDataRef = useRef({});
|
|
45
45
|
const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
|
|
@@ -111,9 +111,9 @@ function App({ config }) {
|
|
|
111
111
|
<AntdApp>
|
|
112
112
|
<ThemeTokenResolver lowdefyRef={lowdefyRef}>
|
|
113
113
|
<ErrorBoundary fullPage onError={handleError}>
|
|
114
|
-
<Auth
|
|
114
|
+
<Auth session={session}>
|
|
115
115
|
{(auth) => {
|
|
116
|
-
usageDataRef.current.user = auth.
|
|
116
|
+
usageDataRef.current.user = auth.session?.hashed_id;
|
|
117
117
|
return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
|
|
118
118
|
}}
|
|
119
119
|
</Auth>
|
package/lib/client/auth/Auth.jsx
CHANGED
|
@@ -24,46 +24,19 @@ function e2eNotSupported() {
|
|
|
24
24
|
throw new Error('Sign-in and sign-out are not supported in e2e testing.');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function Auth({ children,
|
|
27
|
+
function Auth({ children, session }) {
|
|
28
28
|
const auth = {
|
|
29
29
|
authConfig,
|
|
30
|
-
|
|
31
|
-
// Matches the BetterAuth client contract: { data, error }.
|
|
30
|
+
session,
|
|
32
31
|
getSession: async () => {
|
|
33
32
|
const res = await fetch('/api/auth/session');
|
|
34
|
-
if (res.ok) {
|
|
35
|
-
return { data: await res.json(), error: null };
|
|
36
|
-
}
|
|
37
|
-
return { data: null, error: null };
|
|
38
|
-
},
|
|
39
|
-
getResolvedUser: async () => {
|
|
40
|
-
const res = await fetch('/api/user');
|
|
41
33
|
if (res.ok) {
|
|
42
34
|
return res.json();
|
|
43
35
|
}
|
|
44
|
-
return
|
|
36
|
+
return null;
|
|
45
37
|
},
|
|
46
|
-
|
|
47
|
-
addPasskey: e2eNotSupported,
|
|
48
|
-
changePassword: e2eNotSupported,
|
|
49
|
-
deletePasskey: e2eNotSupported,
|
|
50
|
-
updatePasskey: e2eNotSupported,
|
|
51
|
-
requestPasswordReset: e2eNotSupported,
|
|
52
|
-
resetPassword: e2eNotSupported,
|
|
53
|
-
revokeOtherSessions: e2eNotSupported,
|
|
54
|
-
sendVerificationEmail: e2eNotSupported,
|
|
55
|
-
setActiveOrganization: e2eNotSupported,
|
|
56
|
-
signInEmail: e2eNotSupported,
|
|
57
|
-
signInMagicLink: e2eNotSupported,
|
|
58
|
-
signInOauth2: e2eNotSupported,
|
|
59
|
-
signInSocial: e2eNotSupported,
|
|
38
|
+
signIn: e2eNotSupported,
|
|
60
39
|
signOut: e2eNotSupported,
|
|
61
|
-
signUpEmail: e2eNotSupported,
|
|
62
|
-
twoFactorDisable: e2eNotSupported,
|
|
63
|
-
twoFactorEnable: e2eNotSupported,
|
|
64
|
-
twoFactorGenerateBackupCodes: e2eNotSupported,
|
|
65
|
-
twoFactorVerifyBackupCode: e2eNotSupported,
|
|
66
|
-
twoFactorVerifyTotp: e2eNotSupported,
|
|
67
40
|
};
|
|
68
41
|
return children(auth);
|
|
69
42
|
}
|
|
@@ -14,23 +14,22 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
// E2E test-user injection: the
|
|
17
|
+
// E2E test-user injection: the session is read from the lowdefy_e2e_user
|
|
18
18
|
// cookie (base64-encoded JSON user object) instead of a real auth engine.
|
|
19
|
-
|
|
20
|
-
// and its roles are authoritative.
|
|
21
|
-
function getUser(c) {
|
|
19
|
+
function getSession(c) {
|
|
22
20
|
const cookieHeader = c.req.header('cookie') ?? '';
|
|
23
21
|
const match = cookieHeader.match(/lowdefy_e2e_user=([^;]+)/);
|
|
24
22
|
if (!match) {
|
|
25
|
-
return
|
|
23
|
+
return undefined;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
try {
|
|
29
27
|
const decoded = Buffer.from(decodeURIComponent(match[1]), 'base64').toString();
|
|
30
|
-
|
|
28
|
+
const user = JSON.parse(decoded);
|
|
29
|
+
return { user };
|
|
31
30
|
} catch {
|
|
32
|
-
return
|
|
31
|
+
return undefined;
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export default
|
|
35
|
+
export default getSession;
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
function logRequest({ context }) {
|
|
18
|
-
const user = context.
|
|
18
|
+
const { user = {} } = context.session ?? {};
|
|
19
19
|
context.logger.debug({
|
|
20
20
|
user: {
|
|
21
21
|
id: user.id,
|
|
22
22
|
roles: user.roles,
|
|
23
|
+
sub: user.sub,
|
|
24
|
+
session_id: user.session_id,
|
|
23
25
|
},
|
|
24
26
|
url: context.req.url,
|
|
25
27
|
method: context.req.method,
|
|
@@ -42,11 +42,11 @@ async function createCustomPluginTypesMap({ directories }) {
|
|
|
42
42
|
agents: {},
|
|
43
43
|
auth: {
|
|
44
44
|
adapters: {},
|
|
45
|
+
callbacks: {},
|
|
46
|
+
events: {},
|
|
45
47
|
providers: {},
|
|
46
|
-
strategies: {},
|
|
47
48
|
},
|
|
48
49
|
blockMetas: {},
|
|
49
|
-
connectionMetas: {},
|
|
50
50
|
blocks: {},
|
|
51
51
|
connections: {},
|
|
52
52
|
icons: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server-e2e",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-20260911113654",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lowdefy e2e testing server with cookie-based user injection",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -43,30 +43,29 @@
|
|
|
43
43
|
"@ant-design/cssinjs": "2.1.2",
|
|
44
44
|
"@ant-design/x": "2.7.0",
|
|
45
45
|
"@hono/node-server": "2.0.4",
|
|
46
|
-
"@lowdefy/actions-core": "0.0.0-experimental-
|
|
47
|
-
"@lowdefy/api": "0.0.0-experimental-
|
|
48
|
-
"@lowdefy/block-utils": "0.0.0-experimental-
|
|
49
|
-
"@lowdefy/blocks-aggrid": "0.0.0-experimental-
|
|
50
|
-
"@lowdefy/blocks-antd": "0.0.0-experimental-
|
|
51
|
-
"@lowdefy/blocks-antd-x": "0.0.0-experimental-
|
|
52
|
-
"@lowdefy/blocks-basic": "0.0.0-experimental-
|
|
53
|
-
"@lowdefy/blocks-
|
|
54
|
-
"@lowdefy/blocks-
|
|
55
|
-
"@lowdefy/blocks-
|
|
56
|
-
"@lowdefy/blocks-
|
|
57
|
-
"@lowdefy/
|
|
58
|
-
"@lowdefy/
|
|
59
|
-
"@lowdefy/connection-
|
|
60
|
-
"@lowdefy/
|
|
61
|
-
"@lowdefy/
|
|
62
|
-
"@lowdefy/
|
|
63
|
-
"@lowdefy/
|
|
64
|
-
"@lowdefy/
|
|
65
|
-
"@lowdefy/
|
|
66
|
-
"@lowdefy/operators-
|
|
67
|
-
"@lowdefy/operators-
|
|
68
|
-
"@lowdefy/
|
|
69
|
-
"@lowdefy/websockets-core": "0.0.0-experimental-20260909201552",
|
|
46
|
+
"@lowdefy/actions-core": "0.0.0-experimental-20260911113654",
|
|
47
|
+
"@lowdefy/api": "0.0.0-experimental-20260911113654",
|
|
48
|
+
"@lowdefy/block-utils": "0.0.0-experimental-20260911113654",
|
|
49
|
+
"@lowdefy/blocks-aggrid": "0.0.0-experimental-20260911113654",
|
|
50
|
+
"@lowdefy/blocks-antd": "0.0.0-experimental-20260911113654",
|
|
51
|
+
"@lowdefy/blocks-antd-x": "0.0.0-experimental-20260911113654",
|
|
52
|
+
"@lowdefy/blocks-basic": "0.0.0-experimental-20260911113654",
|
|
53
|
+
"@lowdefy/blocks-echarts": "0.0.0-experimental-20260911113654",
|
|
54
|
+
"@lowdefy/blocks-loaders": "0.0.0-experimental-20260911113654",
|
|
55
|
+
"@lowdefy/blocks-markdown": "0.0.0-experimental-20260911113654",
|
|
56
|
+
"@lowdefy/blocks-tiptap": "0.0.0-experimental-20260911113654",
|
|
57
|
+
"@lowdefy/client": "0.0.0-experimental-20260911113654",
|
|
58
|
+
"@lowdefy/connection-axios-http": "0.0.0-experimental-20260911113654",
|
|
59
|
+
"@lowdefy/connection-mongodb": "0.0.0-experimental-20260911113654",
|
|
60
|
+
"@lowdefy/errors": "0.0.0-experimental-20260911113654",
|
|
61
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260911113654",
|
|
62
|
+
"@lowdefy/layout": "0.0.0-experimental-20260911113654",
|
|
63
|
+
"@lowdefy/logger": "0.0.0-experimental-20260911113654",
|
|
64
|
+
"@lowdefy/node-utils": "0.0.0-experimental-20260911113654",
|
|
65
|
+
"@lowdefy/operators-js": "0.0.0-experimental-20260911113654",
|
|
66
|
+
"@lowdefy/operators-nunjucks": "0.0.0-experimental-20260911113654",
|
|
67
|
+
"@lowdefy/operators-uuid": "0.0.0-experimental-20260911113654",
|
|
68
|
+
"@lowdefy/websockets-core": "0.0.0-experimental-20260911113654",
|
|
70
69
|
"@tailwindcss/postcss": "4.3.1",
|
|
71
70
|
"@vitejs/plugin-react": "6.0.2",
|
|
72
71
|
"antd": "6.3.1",
|
|
@@ -83,7 +82,7 @@
|
|
|
83
82
|
},
|
|
84
83
|
"devDependencies": {
|
|
85
84
|
"@jest/globals": "28.1.3",
|
|
86
|
-
"@lowdefy/build": "0.0.0-experimental-
|
|
85
|
+
"@lowdefy/build": "0.0.0-experimental-20260911113654",
|
|
87
86
|
"@next/eslint-plugin-next": "16.1.6",
|
|
88
87
|
"@tailwindcss/postcss": "4.3.1",
|
|
89
88
|
"jest": "28.1.3",
|
package/package.original.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server-e2e",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-20260911113654",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lowdefy e2e testing server with cookie-based user injection",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -51,30 +51,29 @@
|
|
|
51
51
|
"@ant-design/cssinjs": "2.1.2",
|
|
52
52
|
"@ant-design/x": "2.7.0",
|
|
53
53
|
"@hono/node-server": "2.0.4",
|
|
54
|
-
"@lowdefy/actions-core": "0.0.0-experimental-
|
|
55
|
-
"@lowdefy/api": "0.0.0-experimental-
|
|
56
|
-
"@lowdefy/block-utils": "0.0.0-experimental-
|
|
57
|
-
"@lowdefy/blocks-aggrid": "0.0.0-experimental-
|
|
58
|
-
"@lowdefy/blocks-antd": "0.0.0-experimental-
|
|
59
|
-
"@lowdefy/blocks-antd-x": "0.0.0-experimental-
|
|
60
|
-
"@lowdefy/blocks-basic": "0.0.0-experimental-
|
|
61
|
-
"@lowdefy/blocks-
|
|
62
|
-
"@lowdefy/blocks-
|
|
63
|
-
"@lowdefy/blocks-
|
|
64
|
-
"@lowdefy/blocks-
|
|
65
|
-
"@lowdefy/
|
|
66
|
-
"@lowdefy/
|
|
67
|
-
"@lowdefy/connection-
|
|
68
|
-
"@lowdefy/
|
|
69
|
-
"@lowdefy/
|
|
70
|
-
"@lowdefy/
|
|
71
|
-
"@lowdefy/
|
|
72
|
-
"@lowdefy/
|
|
73
|
-
"@lowdefy/
|
|
74
|
-
"@lowdefy/operators-
|
|
75
|
-
"@lowdefy/operators-
|
|
76
|
-
"@lowdefy/
|
|
77
|
-
"@lowdefy/websockets-core": "0.0.0-experimental-20260909201552",
|
|
54
|
+
"@lowdefy/actions-core": "0.0.0-experimental-20260911113654",
|
|
55
|
+
"@lowdefy/api": "0.0.0-experimental-20260911113654",
|
|
56
|
+
"@lowdefy/block-utils": "0.0.0-experimental-20260911113654",
|
|
57
|
+
"@lowdefy/blocks-aggrid": "0.0.0-experimental-20260911113654",
|
|
58
|
+
"@lowdefy/blocks-antd": "0.0.0-experimental-20260911113654",
|
|
59
|
+
"@lowdefy/blocks-antd-x": "0.0.0-experimental-20260911113654",
|
|
60
|
+
"@lowdefy/blocks-basic": "0.0.0-experimental-20260911113654",
|
|
61
|
+
"@lowdefy/blocks-echarts": "0.0.0-experimental-20260911113654",
|
|
62
|
+
"@lowdefy/blocks-loaders": "0.0.0-experimental-20260911113654",
|
|
63
|
+
"@lowdefy/blocks-markdown": "0.0.0-experimental-20260911113654",
|
|
64
|
+
"@lowdefy/blocks-tiptap": "0.0.0-experimental-20260911113654",
|
|
65
|
+
"@lowdefy/client": "0.0.0-experimental-20260911113654",
|
|
66
|
+
"@lowdefy/connection-axios-http": "0.0.0-experimental-20260911113654",
|
|
67
|
+
"@lowdefy/connection-mongodb": "0.0.0-experimental-20260911113654",
|
|
68
|
+
"@lowdefy/errors": "0.0.0-experimental-20260911113654",
|
|
69
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260911113654",
|
|
70
|
+
"@lowdefy/layout": "0.0.0-experimental-20260911113654",
|
|
71
|
+
"@lowdefy/logger": "0.0.0-experimental-20260911113654",
|
|
72
|
+
"@lowdefy/node-utils": "0.0.0-experimental-20260911113654",
|
|
73
|
+
"@lowdefy/operators-js": "0.0.0-experimental-20260911113654",
|
|
74
|
+
"@lowdefy/operators-nunjucks": "0.0.0-experimental-20260911113654",
|
|
75
|
+
"@lowdefy/operators-uuid": "0.0.0-experimental-20260911113654",
|
|
76
|
+
"@lowdefy/websockets-core": "0.0.0-experimental-20260911113654",
|
|
78
77
|
"@tailwindcss/postcss": "4.3.1",
|
|
79
78
|
"@vitejs/plugin-react": "6.0.2",
|
|
80
79
|
"antd": "6.3.1",
|
|
@@ -91,7 +90,7 @@
|
|
|
91
90
|
},
|
|
92
91
|
"devDependencies": {
|
|
93
92
|
"@jest/globals": "28.1.3",
|
|
94
|
-
"@lowdefy/build": "0.0.0-experimental-
|
|
93
|
+
"@lowdefy/build": "0.0.0-experimental-20260911113654",
|
|
95
94
|
"@next/eslint-plugin-next": "16.1.6",
|
|
96
95
|
"@tailwindcss/postcss": "4.3.1",
|
|
97
96
|
"jest": "28.1.3",
|
package/src/app.js
CHANGED
|
@@ -29,7 +29,6 @@ import renderPage from './html/renderPage.js';
|
|
|
29
29
|
import requestHandler from './routes/request.js';
|
|
30
30
|
import sessionMockHandler from './routes/sessionMock.js';
|
|
31
31
|
import usageHandler from './routes/usage.js';
|
|
32
|
-
import userHandler from './routes/user.js';
|
|
33
32
|
import websocketHandler from './routes/websocket.js';
|
|
34
33
|
|
|
35
34
|
const basePath = lowdefyConfig.basePath ?? '';
|
|
@@ -55,7 +54,6 @@ function createApp() {
|
|
|
55
54
|
app.all('/api/usage', usageHandler);
|
|
56
55
|
app.get('/api/websocket', websocketHandler);
|
|
57
56
|
app.get('/api/page/*', apiPageHandler);
|
|
58
|
-
app.get('/api/user', userHandler);
|
|
59
57
|
|
|
60
58
|
// Vite build output (includes public/ via Vite's publicDir copy). Falls
|
|
61
59
|
// through to the page routes when no file matches.
|
package/src/html/getAssets.js
CHANGED
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
20
|
-
import { LowdefyInternalError } from '@lowdefy/errors';
|
|
21
|
-
|
|
22
20
|
// The Vite manifest is read once at startup — deploys start a new server
|
|
23
21
|
// process after `vite build` completes, so assets never change at runtime.
|
|
24
22
|
// In-place deploys must build before restarting (documented).
|
|
@@ -32,7 +30,7 @@ function getAssets() {
|
|
|
32
30
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
33
31
|
const entry = manifest['client/main.jsx'];
|
|
34
32
|
if (!entry) {
|
|
35
|
-
throw new
|
|
33
|
+
throw new Error(
|
|
36
34
|
'Vite manifest has no "client/main.jsx" entry. Run the client build before starting the server.'
|
|
37
35
|
);
|
|
38
36
|
}
|
package/src/html/renderPage.js
CHANGED
|
@@ -28,7 +28,7 @@ const basePath = lowdefyConfig.basePath ?? '';
|
|
|
28
28
|
// getStaticProps. The home redirect logic lives here, not at the route level.
|
|
29
29
|
async function renderPage(c, { pageId, status = 200 }) {
|
|
30
30
|
const context = c.get('lowdefyContext');
|
|
31
|
-
const { logger,
|
|
31
|
+
const { logger, session } = context;
|
|
32
32
|
|
|
33
33
|
let resolvedPageId = pageId;
|
|
34
34
|
const rootConfig = await getRootConfig(context);
|
|
@@ -42,12 +42,12 @@ async function renderPage(c, { pageId, status = 200 }) {
|
|
|
42
42
|
resolvedPageId = home.pageId;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const pageConfig = await getPageConfig(context, {
|
|
46
46
|
pageId: resolvedPageId,
|
|
47
47
|
urlQuery: c.req.query(),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
if (
|
|
50
|
+
if (!pageConfig) {
|
|
51
51
|
if (resolvedPageId === '404') {
|
|
52
52
|
// No 404 page in the build — return a plain 404 rather than redirecting in a loop.
|
|
53
53
|
return c.text('Page not found.', 404);
|
|
@@ -56,8 +56,6 @@ async function renderPage(c, { pageId, status = 200 }) {
|
|
|
56
56
|
return c.redirect(`${basePath}/404`, 302);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const { pageConfig } = result;
|
|
60
|
-
|
|
61
59
|
logger.info({ event: 'page_view', pageId: resolvedPageId });
|
|
62
60
|
|
|
63
61
|
const html = template({
|
|
@@ -69,7 +67,7 @@ async function renderPage(c, { pageId, status = 200 }) {
|
|
|
69
67
|
basePath,
|
|
70
68
|
pageConfig,
|
|
71
69
|
rootConfig,
|
|
72
|
-
|
|
70
|
+
session: session ?? null,
|
|
73
71
|
},
|
|
74
72
|
themeConfig,
|
|
75
73
|
title: pageConfig.properties?.title ?? resolvedPageId,
|
|
@@ -15,24 +15,23 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import { createApiContext
|
|
18
|
+
import { createApiContext } from '@lowdefy/api';
|
|
19
19
|
import { v4 as uuid } from 'uuid';
|
|
20
20
|
|
|
21
21
|
import agents from '../../build/plugins/agents.js';
|
|
22
22
|
import appMeta from '../../lib/build/appMeta.js';
|
|
23
|
-
import authJson from '../../lib/build/auth.js';
|
|
24
23
|
import config from '../../lib/build/config.js';
|
|
25
24
|
import connections from '../../build/plugins/connections.js';
|
|
26
25
|
import createHandleError from '../../lib/server/log/createHandleError.js';
|
|
27
26
|
import createLogger from '../../lib/server/log/createLogger.js';
|
|
28
27
|
import fileCache from '../../lib/server/fileCache.js';
|
|
29
28
|
import getE2eSecrets from '../../lib/server/getE2eSecrets.js';
|
|
30
|
-
import
|
|
29
|
+
import getSession from '../../lib/server/auth/session.js';
|
|
31
30
|
import i18nConfig from '../../lib/build/i18n.js';
|
|
32
31
|
import jsMap from '../../build/plugins/operators/serverJsMap.js';
|
|
33
32
|
import logRequest from '../../lib/server/log/logRequest.js';
|
|
34
33
|
import operators from '../../build/plugins/operators/server.js';
|
|
35
|
-
import
|
|
34
|
+
import reportsRuntime from '../../build/plugins/reportsRuntime.js';
|
|
36
35
|
import websockets from '../../build/plugins/websockets.js';
|
|
37
36
|
|
|
38
37
|
const secrets = getE2eSecrets();
|
|
@@ -53,6 +52,8 @@ function apiContext() {
|
|
|
53
52
|
rid,
|
|
54
53
|
agents,
|
|
55
54
|
appMeta,
|
|
55
|
+
reportsRuntime,
|
|
56
|
+
publicDirectory: path.join(process.cwd(), 'public'),
|
|
56
57
|
buildDirectory: path.join(process.cwd(), 'build'),
|
|
57
58
|
config,
|
|
58
59
|
configDirectory: process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd(),
|
|
@@ -69,23 +70,11 @@ function apiContext() {
|
|
|
69
70
|
hostname: c.req.header('host'),
|
|
70
71
|
},
|
|
71
72
|
secrets,
|
|
72
|
-
steps,
|
|
73
73
|
websockets,
|
|
74
74
|
};
|
|
75
75
|
context.handleError = createHandleError({ context });
|
|
76
|
-
|
|
77
|
-
// resolveAuthentication - normalizeInjectedCaller floors it to the
|
|
78
|
-
// resolved-caller shape. An absent cookie stays a logged-out (null) caller.
|
|
79
|
-
const user = getUser(c);
|
|
80
|
-
context.user = user ? normalizeInjectedCaller(user) : null;
|
|
76
|
+
context.session = getSession(c);
|
|
81
77
|
createApiContext(context);
|
|
82
|
-
// No auth engine runs in the e2e server, so createApiContext retains no
|
|
83
|
-
// organization binding. Derive the policy from the built auth config so
|
|
84
|
-
// the tenant wall's policy gate still engages for a tenant app; pinned
|
|
85
|
-
// stays null (no engine means no seeded organization row).
|
|
86
|
-
if (authJson.organizations) {
|
|
87
|
-
context.organization = { policy: authJson.organizations.policy, pinned: null };
|
|
88
|
-
}
|
|
89
78
|
logRequest({ context });
|
|
90
79
|
c.set('lowdefyContext', context);
|
|
91
80
|
return next();
|
|
@@ -15,64 +15,20 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { redactErrorResponse } from '@lowdefy/api';
|
|
18
|
-
import { HTTPException } from 'hono/http-exception';
|
|
19
18
|
|
|
20
19
|
// Hono routes every handler error to the app-level error handler — upstream
|
|
21
|
-
// middleware try/catch never sees them
|
|
22
|
-
// JSON the old apiWrapper returned;
|
|
20
|
+
// middleware try/catch never sees them (compose catches per dispatch level).
|
|
21
|
+
// API routes get the serialized error JSON the old apiWrapper returned;
|
|
22
|
+
// page routes get a plain 500.
|
|
23
23
|
function createErrorHandler({ basePath = '', logger }) {
|
|
24
24
|
return async function errorHandler(error, c) {
|
|
25
|
-
const path = basePath ? c.req.path.replace(basePath, '') : c.req.path;
|
|
26
|
-
// Unauthenticated requests to protected endpoints are expected traffic -
|
|
27
|
-
// one warning line and a 401, skipping the structured error log so
|
|
28
|
-
// probing cannot flood it. Keys strictly on the error name; the 500 path
|
|
29
|
-
// below is untouched.
|
|
30
|
-
if (error.name === 'AuthenticationError') {
|
|
31
|
-
logger.warn(`Unauthenticated request: ${c.req.method} ${c.req.path}`);
|
|
32
|
-
if (path.startsWith('/api/')) {
|
|
33
|
-
return c.json({ name: error.name, message: error.message }, 401);
|
|
34
|
-
}
|
|
35
|
-
return c.text('Unauthorized', 401);
|
|
36
|
-
}
|
|
37
|
-
// An authenticated caller whose roles do not permit the resource. Expected
|
|
38
|
-
// traffic, not a fault: one warning line, no structured error log and no
|
|
39
|
-
// Sentry capture.
|
|
40
|
-
if (error.name === 'AuthorizationError') {
|
|
41
|
-
logger.warn(`Forbidden: ${c.req.method} ${c.req.path}`);
|
|
42
|
-
if (path.startsWith('/api/')) {
|
|
43
|
-
return c.json({ name: error.name, message: error.message }, 403);
|
|
44
|
-
}
|
|
45
|
-
return c.text('Forbidden', 403);
|
|
46
|
-
}
|
|
47
|
-
// An authorized caller who has not enrolled a second factor under
|
|
48
|
-
// auth.twoFactor.required. 403, not 401 - a 401 reads to the client as a dead
|
|
49
|
-
// session and bounces the user to sign-in, which is the loop the gate exists to
|
|
50
|
-
// avoid. Expected traffic under required: true, so one warning line and no
|
|
51
|
-
// Sentry capture, exactly as for AuthenticationError.
|
|
52
|
-
if (error.name === 'TwoFactorEnrolmentRequiredError') {
|
|
53
|
-
logger.warn(`Two-factor enrolment required: ${c.req.method} ${c.req.path}`);
|
|
54
|
-
if (path.startsWith('/api/')) {
|
|
55
|
-
return c.json({ name: error.name, message: error.message }, 403);
|
|
56
|
-
}
|
|
57
|
-
return c.text('Two-factor enrolment required', 403);
|
|
58
|
-
}
|
|
59
|
-
// A route or transport that has already decided its own HTTP answer - hono's
|
|
60
|
-
// HTTPException carries the response to send. The MCP transport refuses an
|
|
61
|
-
// unsupported protocol-version header this way (a 404 with a JSON-RPC body
|
|
62
|
-
// that the client SDK falls back on to negotiate a version both sides
|
|
63
|
-
// know). Not a fault: send its response and log one warning line, no
|
|
64
|
-
// structured error log and no Sentry capture. Turning it into a 500 would
|
|
65
|
-
// hide the status the client acts on.
|
|
66
|
-
if (error instanceof HTTPException) {
|
|
67
|
-
logger.warn(`${error.status} answered by the route: ${c.req.method} ${c.req.path}`);
|
|
68
|
-
return error.getResponse();
|
|
69
|
-
}
|
|
70
25
|
const context = c.get('lowdefyContext');
|
|
71
26
|
if (context) {
|
|
72
27
|
await context.handleError(error);
|
|
73
28
|
} else {
|
|
74
29
|
logger.error(error);
|
|
75
30
|
}
|
|
31
|
+
const path = basePath ? c.req.path.replace(basePath, '') : c.req.path;
|
|
76
32
|
if (path.startsWith('/api/')) {
|
|
77
33
|
return c.json(redactErrorResponse(context, error), 500);
|
|
78
34
|
}
|
package/src/routes/apiPage.js
CHANGED
|
@@ -25,13 +25,13 @@ async function apiPageHandler(c) {
|
|
|
25
25
|
const pageId = getPathSegments(c, '/api/page/').join('/');
|
|
26
26
|
// The client forwards its current query string on the fetch so Dynamic block
|
|
27
27
|
// resolution sees the same urlQuery as an initial HTML load.
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
28
|
+
const pageConfig = await getPageConfig(context, { pageId, urlQuery: c.req.query() });
|
|
29
|
+
if (!pageConfig) {
|
|
30
30
|
context.logger.info({ event: 'api_page_not_found', pageId });
|
|
31
31
|
return c.json({ pageConfig: null }, 404);
|
|
32
32
|
}
|
|
33
33
|
context.logger.info({ event: 'api_page_view', pageId });
|
|
34
|
-
return c.json({ pageConfig
|
|
34
|
+
return c.json({ pageConfig });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export default apiPageHandler;
|
|
@@ -18,9 +18,7 @@ import { logClientError } from '@lowdefy/api';
|
|
|
18
18
|
|
|
19
19
|
async function clientErrorHandler(c) {
|
|
20
20
|
if (c.req.method !== 'POST') {
|
|
21
|
-
|
|
22
|
-
// fault that would be logged at error level and answered with a 500.
|
|
23
|
-
return c.json({ error: 'Method not allowed.' }, 405);
|
|
21
|
+
throw new Error('Only POST requests are supported.');
|
|
24
22
|
}
|
|
25
23
|
const context = c.get('lowdefyContext');
|
|
26
24
|
|
package/src/routes/endpoints.js
CHANGED
|
@@ -20,9 +20,7 @@ import getPathSegments from '../lib/getPathSegments.js';
|
|
|
20
20
|
|
|
21
21
|
async function endpointsHandler(c) {
|
|
22
22
|
if (c.req.method !== 'POST') {
|
|
23
|
-
|
|
24
|
-
// fault that would be logged at error level and answered with a 500.
|
|
25
|
-
return c.json({ error: 'Method not allowed.' }, 405);
|
|
23
|
+
throw new Error('Only POST requests are supported.');
|
|
26
24
|
}
|
|
27
25
|
const context = c.get('lowdefyContext');
|
|
28
26
|
const endpointId = getPathSegments(c, '/api/endpoints/').join('/');
|
package/src/routes/request.js
CHANGED
|
@@ -20,9 +20,7 @@ import getPathSegments from '../lib/getPathSegments.js';
|
|
|
20
20
|
|
|
21
21
|
async function requestHandler(c) {
|
|
22
22
|
if (c.req.method !== 'POST') {
|
|
23
|
-
|
|
24
|
-
// fault that would be logged at error level and answered with a 500.
|
|
25
|
-
return c.json({ error: 'Method not allowed.' }, 405);
|
|
23
|
+
throw new Error('Only POST requests are supported.');
|
|
26
24
|
}
|
|
27
25
|
const context = c.get('lowdefyContext');
|
|
28
26
|
const segments = getPathSegments(c, '/api/request/');
|
|
@@ -16,10 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
async function sessionMockHandler(c) {
|
|
18
18
|
const context = c.get('lowdefyContext');
|
|
19
|
-
|
|
20
|
-
return c.json(null);
|
|
21
|
-
}
|
|
22
|
-
return c.json({ session: { id: 'e2e' }, user: context.user });
|
|
19
|
+
return c.json(context.session ?? {});
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
export default sessionMockHandler;
|
package/src/routes/usage.js
CHANGED
|
@@ -21,9 +21,7 @@ const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package
|
|
|
21
21
|
|
|
22
22
|
async function usageHandler(c) {
|
|
23
23
|
if (c.req.method !== 'POST') {
|
|
24
|
-
|
|
25
|
-
// fault that would be logged at error level and answered with a 500.
|
|
26
|
-
return c.json({ error: 'Method not allowed.' }, 405);
|
|
24
|
+
throw new Error('Only POST requests are supported.');
|
|
27
25
|
}
|
|
28
26
|
const context = c.get('lowdefyContext');
|
|
29
27
|
const { user, machine } = await c.req.json();
|
package/src/routes/websocket.js
CHANGED
|
@@ -38,9 +38,7 @@ const websocketHandler = upgradeWebSocket((c) => {
|
|
|
38
38
|
connection?.close();
|
|
39
39
|
},
|
|
40
40
|
onError(event) {
|
|
41
|
-
|
|
42
|
-
// the error object attached so the cause is not reduced to a string.
|
|
43
|
-
context.logger.warn({ event: 'ws_error', err: event?.error }, 'WebSocket error.');
|
|
41
|
+
context.logger.debug({ event: 'ws_error' }, String(event?.error ?? 'WebSocket error'));
|
|
44
42
|
},
|
|
45
43
|
};
|
|
46
44
|
});
|
|
@@ -1,354 +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 { Hono } from 'hono';
|
|
18
|
-
import { HTTPException } from 'hono/http-exception';
|
|
19
|
-
import { jest } from '@jest/globals';
|
|
20
|
-
|
|
21
|
-
import createErrorHandler from './errorHandler.js';
|
|
22
|
-
|
|
23
|
-
// The handler here is byte-identical to its `server-e2e` counterpart (and to
|
|
24
|
-
// `server`'s apart from the Sentry capture, which `server` tests separately).
|
|
25
|
-
// Keep the three in sync when changing any of them.
|
|
26
|
-
|
|
27
|
-
// A string that must never reach a client. It is put on `received` at every
|
|
28
|
-
// level of the cause chain, so a single JSON.stringify search over the body
|
|
29
|
-
// proves the redaction reached that level.
|
|
30
|
-
const SECRET = 'SECRET-RECEIVED-VALUE';
|
|
31
|
-
|
|
32
|
-
function createLogger() {
|
|
33
|
-
return { error: jest.fn(), warn: jest.fn() };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// The error handler is only reachable through hono's app-level onError, so the
|
|
37
|
-
// handler is driven the way production drives it: a route that throws, with the
|
|
38
|
-
// lowdefyContext set by upstream middleware when there is one.
|
|
39
|
-
function createApp({ basePath = '', context, error, logger }) {
|
|
40
|
-
const app = new Hono();
|
|
41
|
-
app.use('*', async (c, next) => {
|
|
42
|
-
if (context) c.set('lowdefyContext', context);
|
|
43
|
-
await next();
|
|
44
|
-
});
|
|
45
|
-
app.all('*', () => {
|
|
46
|
-
throw error;
|
|
47
|
-
});
|
|
48
|
-
app.onError(createErrorHandler({ basePath, logger }));
|
|
49
|
-
return app;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createAuthenticationError(message) {
|
|
53
|
-
const error = new Error(message);
|
|
54
|
-
error.name = 'AuthenticationError';
|
|
55
|
-
error.received = SECRET;
|
|
56
|
-
return error;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function createAuthorizationError(message) {
|
|
60
|
-
const error = new Error(message);
|
|
61
|
-
error.name = 'AuthorizationError';
|
|
62
|
-
error.received = SECRET;
|
|
63
|
-
return error;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function createTwoFactorEnrolmentRequiredError(message) {
|
|
67
|
-
const error = new Error(message);
|
|
68
|
-
error.name = 'TwoFactorEnrolmentRequiredError';
|
|
69
|
-
error.received = SECRET;
|
|
70
|
-
return error;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Two Error levels, each with a stack (every Error has one) and a recognisable
|
|
74
|
-
// `received`, plus a configKey the policy keeps.
|
|
75
|
-
function createErrorWithCause() {
|
|
76
|
-
const cause = new Error('Cause message.');
|
|
77
|
-
cause.received = `${SECRET}-cause`;
|
|
78
|
-
const error = new Error('Top message.', { cause });
|
|
79
|
-
error.received = `${SECRET}-top`;
|
|
80
|
-
error.configKey = 'page:home.blocks.0';
|
|
81
|
-
return error;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
test('errorHandler returns 401 with only name and message for an AuthenticationError on an api path', async () => {
|
|
85
|
-
const logger = createLogger();
|
|
86
|
-
const context = { handleError: jest.fn() };
|
|
87
|
-
const res = await createApp({
|
|
88
|
-
context,
|
|
89
|
-
error: createAuthenticationError('Unauthenticated.'),
|
|
90
|
-
logger,
|
|
91
|
-
}).request('/api/request/getUsers');
|
|
92
|
-
|
|
93
|
-
expect(res.status).toEqual(401);
|
|
94
|
-
expect(await res.json()).toEqual({ name: 'AuthenticationError', message: 'Unauthenticated.' });
|
|
95
|
-
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
96
|
-
expect(logger.warn.mock.calls[0][0]).toMatch(/Unauthenticated request/);
|
|
97
|
-
expect(logger.error).not.toHaveBeenCalled();
|
|
98
|
-
expect(context.handleError).not.toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('errorHandler does not send an AuthenticationError through the redactor', async () => {
|
|
102
|
-
const res = await createApp({
|
|
103
|
-
error: createAuthenticationError('Unauthenticated.'),
|
|
104
|
-
logger: createLogger(),
|
|
105
|
-
}).request('/api/request/getUsers');
|
|
106
|
-
|
|
107
|
-
const body = await res.json();
|
|
108
|
-
expect(body['~e']).toBeUndefined();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('errorHandler returns text Unauthorized at 401 for an AuthenticationError on a page path', async () => {
|
|
112
|
-
const logger = createLogger();
|
|
113
|
-
const res = await createApp({
|
|
114
|
-
error: createAuthenticationError('Unauthenticated.'),
|
|
115
|
-
logger,
|
|
116
|
-
}).request('/home');
|
|
117
|
-
|
|
118
|
-
expect(res.status).toEqual(401);
|
|
119
|
-
expect(await res.text()).toEqual('Unauthorized');
|
|
120
|
-
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('errorHandler returns 403 with only name and message for an AuthorizationError on an api path', async () => {
|
|
124
|
-
const logger = createLogger();
|
|
125
|
-
const context = { handleError: jest.fn() };
|
|
126
|
-
const res = await createApp({
|
|
127
|
-
context,
|
|
128
|
-
error: createAuthorizationError('Forbidden.'),
|
|
129
|
-
logger,
|
|
130
|
-
}).request('/api/request/getUsers');
|
|
131
|
-
|
|
132
|
-
expect(res.status).toEqual(403);
|
|
133
|
-
expect(await res.json()).toEqual({ name: 'AuthorizationError', message: 'Forbidden.' });
|
|
134
|
-
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
135
|
-
expect(logger.warn.mock.calls[0][0]).toMatch(/Forbidden: GET \/api\/request\/getUsers/);
|
|
136
|
-
expect(logger.error).not.toHaveBeenCalled();
|
|
137
|
-
expect(context.handleError).not.toHaveBeenCalled();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('errorHandler does not send an AuthorizationError through the redactor', async () => {
|
|
141
|
-
const res = await createApp({
|
|
142
|
-
error: createAuthorizationError('Forbidden.'),
|
|
143
|
-
logger: createLogger(),
|
|
144
|
-
}).request('/api/request/getUsers');
|
|
145
|
-
|
|
146
|
-
const body = await res.json();
|
|
147
|
-
expect(body['~e']).toBeUndefined();
|
|
148
|
-
expect(JSON.stringify(body)).not.toContain(SECRET);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('errorHandler returns text Forbidden at 403 for an AuthorizationError on a page path', async () => {
|
|
152
|
-
const logger = createLogger();
|
|
153
|
-
const res = await createApp({
|
|
154
|
-
error: createAuthorizationError('Forbidden.'),
|
|
155
|
-
logger,
|
|
156
|
-
}).request('/home');
|
|
157
|
-
|
|
158
|
-
expect(res.status).toEqual(403);
|
|
159
|
-
expect(await res.text()).toEqual('Forbidden');
|
|
160
|
-
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
161
|
-
expect(logger.error).not.toHaveBeenCalled();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('errorHandler returns 403 with only name and message for a TwoFactorEnrolmentRequiredError on an api path', async () => {
|
|
165
|
-
const logger = createLogger();
|
|
166
|
-
const context = { handleError: jest.fn() };
|
|
167
|
-
const res = await createApp({
|
|
168
|
-
context,
|
|
169
|
-
error: createTwoFactorEnrolmentRequiredError('Two-factor enrolment required.'),
|
|
170
|
-
logger,
|
|
171
|
-
}).request('/api/request/getUsers');
|
|
172
|
-
|
|
173
|
-
expect(res.status).toEqual(403);
|
|
174
|
-
expect(await res.json()).toEqual({
|
|
175
|
-
name: 'TwoFactorEnrolmentRequiredError',
|
|
176
|
-
message: 'Two-factor enrolment required.',
|
|
177
|
-
});
|
|
178
|
-
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
179
|
-
expect(logger.warn.mock.calls[0][0]).toMatch(/Two-factor enrolment required/);
|
|
180
|
-
expect(logger.error).not.toHaveBeenCalled();
|
|
181
|
-
expect(context.handleError).not.toHaveBeenCalled();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('errorHandler does not send a TwoFactorEnrolmentRequiredError through the redactor', async () => {
|
|
185
|
-
const res = await createApp({
|
|
186
|
-
error: createTwoFactorEnrolmentRequiredError('Two-factor enrolment required.'),
|
|
187
|
-
logger: createLogger(),
|
|
188
|
-
}).request('/api/request/getUsers');
|
|
189
|
-
|
|
190
|
-
const body = await res.json();
|
|
191
|
-
expect(body['~e']).toBeUndefined();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('errorHandler returns text at 403 for a TwoFactorEnrolmentRequiredError on a page path', async () => {
|
|
195
|
-
const logger = createLogger();
|
|
196
|
-
const context = { handleError: jest.fn() };
|
|
197
|
-
const res = await createApp({
|
|
198
|
-
context,
|
|
199
|
-
error: createTwoFactorEnrolmentRequiredError('Two-factor enrolment required.'),
|
|
200
|
-
logger,
|
|
201
|
-
}).request('/home');
|
|
202
|
-
|
|
203
|
-
expect(res.status).toEqual(403);
|
|
204
|
-
expect(await res.text()).toEqual('Two-factor enrolment required');
|
|
205
|
-
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
206
|
-
expect(context.handleError).not.toHaveBeenCalled();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('errorHandler sends an HTTPException response as-is with one warning and no error log', async () => {
|
|
210
|
-
const logger = createLogger();
|
|
211
|
-
const error = new HTTPException(404, {
|
|
212
|
-
res: Response.json(
|
|
213
|
-
{ jsonrpc: '2.0', error: { code: -32000, message: 'Unsupported protocol version' } },
|
|
214
|
-
{ status: 404 }
|
|
215
|
-
),
|
|
216
|
-
});
|
|
217
|
-
const res = await createApp({ error, logger }).request('/api/mcp', { method: 'POST' });
|
|
218
|
-
expect(res.status).toEqual(404);
|
|
219
|
-
expect(await res.json()).toEqual({
|
|
220
|
-
jsonrpc: '2.0',
|
|
221
|
-
error: { code: -32000, message: 'Unsupported protocol version' },
|
|
222
|
-
});
|
|
223
|
-
expect(logger.warn).toHaveBeenCalledWith('404 answered by the route: POST /api/mcp');
|
|
224
|
-
expect(logger.error).not.toHaveBeenCalled();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('errorHandler returns 500 with the serialized error envelope on an api path', async () => {
|
|
228
|
-
const res = await createApp({
|
|
229
|
-
error: createErrorWithCause(),
|
|
230
|
-
logger: createLogger(),
|
|
231
|
-
}).request('/api/request/getUsers');
|
|
232
|
-
|
|
233
|
-
expect(res.status).toEqual(500);
|
|
234
|
-
const body = await res.json();
|
|
235
|
-
expect(body['~e'].name).toEqual('Error');
|
|
236
|
-
expect(body['~e'].message).toEqual('Top message.');
|
|
237
|
-
expect(body['~e'].cause.message).toEqual('Cause message.');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('errorHandler omits stack and received from the 500 envelope at the top level', async () => {
|
|
241
|
-
const res = await createApp({
|
|
242
|
-
error: createErrorWithCause(),
|
|
243
|
-
logger: createLogger(),
|
|
244
|
-
}).request('/api/request/getUsers');
|
|
245
|
-
|
|
246
|
-
const body = await res.json();
|
|
247
|
-
expect(body['~e'].stack).toBeUndefined();
|
|
248
|
-
expect(body['~e'].received).toBeUndefined();
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// The regression test for the real bug: the four inline `delete` statements this
|
|
252
|
-
// replaces only ever reached depth 0, so a `received` or `stack` on the cause
|
|
253
|
-
// still crossed the wire. The policy is now applied at every error node.
|
|
254
|
-
test('errorHandler omits stack and received from the 500 envelope on the cause at depth 1', async () => {
|
|
255
|
-
const res = await createApp({
|
|
256
|
-
error: createErrorWithCause(),
|
|
257
|
-
logger: createLogger(),
|
|
258
|
-
}).request('/api/request/getUsers');
|
|
259
|
-
|
|
260
|
-
const body = await res.json();
|
|
261
|
-
expect(body['~e'].cause.stack).toBeUndefined();
|
|
262
|
-
expect(body['~e'].cause.received).toBeUndefined();
|
|
263
|
-
expect(JSON.stringify(body)).not.toContain(SECRET);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test('errorHandler keeps configKey on the 500 envelope so the fault can be located in config', async () => {
|
|
267
|
-
const res = await createApp({
|
|
268
|
-
error: createErrorWithCause(),
|
|
269
|
-
logger: createLogger(),
|
|
270
|
-
}).request('/api/request/getUsers');
|
|
271
|
-
|
|
272
|
-
const body = await res.json();
|
|
273
|
-
expect(body['~e'].configKey).toEqual('page:home.blocks.0');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('errorHandler returns text Internal Server Error at 500 on a page path with no payload', async () => {
|
|
277
|
-
const res = await createApp({
|
|
278
|
-
error: createErrorWithCause(),
|
|
279
|
-
logger: createLogger(),
|
|
280
|
-
}).request('/home');
|
|
281
|
-
|
|
282
|
-
expect(res.status).toEqual(500);
|
|
283
|
-
expect(await res.text()).toEqual('Internal Server Error');
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test('errorHandler awaits context.handleError before serializing the response', async () => {
|
|
287
|
-
const context = {
|
|
288
|
-
handleError: jest.fn(async (error) => {
|
|
289
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
290
|
-
error.awaited = true;
|
|
291
|
-
}),
|
|
292
|
-
};
|
|
293
|
-
const error = createErrorWithCause();
|
|
294
|
-
const res = await createApp({ context, error, logger: createLogger() }).request(
|
|
295
|
-
'/api/request/getUsers'
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
expect(context.handleError).toHaveBeenCalledTimes(1);
|
|
299
|
-
expect(context.handleError).toHaveBeenCalledWith(error);
|
|
300
|
-
const body = await res.json();
|
|
301
|
-
expect(body['~e'].awaited).toBe(true);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// createHandleError sets error.handled once it has logged. The client reads it
|
|
305
|
-
// to decide whether to POST the error back for logging, so it has to survive
|
|
306
|
-
// serialization or the error is logged a second time.
|
|
307
|
-
test('errorHandler returns handled true on the 500 envelope after handleError sets it', async () => {
|
|
308
|
-
const context = {
|
|
309
|
-
handleError: jest.fn(async (error) => {
|
|
310
|
-
error.handled = true;
|
|
311
|
-
}),
|
|
312
|
-
};
|
|
313
|
-
const res = await createApp({
|
|
314
|
-
context,
|
|
315
|
-
error: createErrorWithCause(),
|
|
316
|
-
logger: createLogger(),
|
|
317
|
-
}).request('/api/request/getUsers');
|
|
318
|
-
|
|
319
|
-
const body = await res.json();
|
|
320
|
-
expect(body['~e'].handled).toBe(true);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test('errorHandler logs with logger.error when the request has no lowdefyContext', async () => {
|
|
324
|
-
const logger = createLogger();
|
|
325
|
-
const error = createErrorWithCause();
|
|
326
|
-
const res = await createApp({ error, logger }).request('/api/request/getUsers');
|
|
327
|
-
|
|
328
|
-
expect(res.status).toEqual(500);
|
|
329
|
-
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
330
|
-
expect(logger.error).toHaveBeenCalledWith(error);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test('errorHandler treats a basePath prefixed api path as an api path', async () => {
|
|
334
|
-
const res = await createApp({
|
|
335
|
-
basePath: '/base',
|
|
336
|
-
error: createErrorWithCause(),
|
|
337
|
-
logger: createLogger(),
|
|
338
|
-
}).request('/base/api/request/getUsers');
|
|
339
|
-
|
|
340
|
-
expect(res.status).toEqual(500);
|
|
341
|
-
const body = await res.json();
|
|
342
|
-
expect(body['~e'].message).toEqual('Top message.');
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('errorHandler treats a basePath prefixed page path as a page path', async () => {
|
|
346
|
-
const res = await createApp({
|
|
347
|
-
basePath: '/base',
|
|
348
|
-
error: createErrorWithCause(),
|
|
349
|
-
logger: createLogger(),
|
|
350
|
-
}).request('/base/home');
|
|
351
|
-
|
|
352
|
-
expect(res.status).toEqual(500);
|
|
353
|
-
expect(await res.text()).toEqual('Internal Server Error');
|
|
354
|
-
});
|
package/src/routes/user.js
DELETED
|
@@ -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
|
-
// The resolved caller for the client - the same resolveAuthentication output
|
|
18
|
-
// the first page load embeds. The base session carries neither roles (read
|
|
19
|
-
// live from the active member row) nor the merged attributes bag, so the
|
|
20
|
-
// client re-syncs from here (UpdateSession) after a session change such as
|
|
21
|
-
// SetActiveOrganization.
|
|
22
|
-
async function userHandler(c) {
|
|
23
|
-
const context = c.get('lowdefyContext');
|
|
24
|
-
return c.json({ user: context.user ?? null });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default userHandler;
|