@lowdefy/server-e2e 0.0.0-experimental-20260909201552 → 0.0.0-experimental-20260910082708
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/renderPage.js +4 -6
- package/src/middleware/apiContext.js +3 -17
- package/src/middleware/errorHandler.js +11 -23
- package/src/middleware/errorHandler.test.mjs +0 -73
- package/src/routes/apiPage.js +3 -3
- package/src/routes/sessionMock.js +1 -4
- 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-20260910082708",
|
|
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-20260910082708",
|
|
47
|
+
"@lowdefy/api": "0.0.0-experimental-20260910082708",
|
|
48
|
+
"@lowdefy/block-utils": "0.0.0-experimental-20260910082708",
|
|
49
|
+
"@lowdefy/blocks-aggrid": "0.0.0-experimental-20260910082708",
|
|
50
|
+
"@lowdefy/blocks-antd": "0.0.0-experimental-20260910082708",
|
|
51
|
+
"@lowdefy/blocks-antd-x": "0.0.0-experimental-20260910082708",
|
|
52
|
+
"@lowdefy/blocks-basic": "0.0.0-experimental-20260910082708",
|
|
53
|
+
"@lowdefy/blocks-echarts": "0.0.0-experimental-20260910082708",
|
|
54
|
+
"@lowdefy/blocks-loaders": "0.0.0-experimental-20260910082708",
|
|
55
|
+
"@lowdefy/blocks-markdown": "0.0.0-experimental-20260910082708",
|
|
56
|
+
"@lowdefy/blocks-tiptap": "0.0.0-experimental-20260910082708",
|
|
57
|
+
"@lowdefy/client": "0.0.0-experimental-20260910082708",
|
|
58
|
+
"@lowdefy/connection-axios-http": "0.0.0-experimental-20260910082708",
|
|
59
|
+
"@lowdefy/connection-mongodb": "0.0.0-experimental-20260910082708",
|
|
60
|
+
"@lowdefy/errors": "0.0.0-experimental-20260910082708",
|
|
61
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260910082708",
|
|
62
|
+
"@lowdefy/layout": "0.0.0-experimental-20260910082708",
|
|
63
|
+
"@lowdefy/logger": "0.0.0-experimental-20260910082708",
|
|
64
|
+
"@lowdefy/node-utils": "0.0.0-experimental-20260910082708",
|
|
65
|
+
"@lowdefy/operators-js": "0.0.0-experimental-20260910082708",
|
|
66
|
+
"@lowdefy/operators-nunjucks": "0.0.0-experimental-20260910082708",
|
|
67
|
+
"@lowdefy/operators-uuid": "0.0.0-experimental-20260910082708",
|
|
68
|
+
"@lowdefy/websockets-core": "0.0.0-experimental-20260910082708",
|
|
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-20260910082708",
|
|
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-20260910082708",
|
|
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-20260910082708",
|
|
55
|
+
"@lowdefy/api": "0.0.0-experimental-20260910082708",
|
|
56
|
+
"@lowdefy/block-utils": "0.0.0-experimental-20260910082708",
|
|
57
|
+
"@lowdefy/blocks-aggrid": "0.0.0-experimental-20260910082708",
|
|
58
|
+
"@lowdefy/blocks-antd": "0.0.0-experimental-20260910082708",
|
|
59
|
+
"@lowdefy/blocks-antd-x": "0.0.0-experimental-20260910082708",
|
|
60
|
+
"@lowdefy/blocks-basic": "0.0.0-experimental-20260910082708",
|
|
61
|
+
"@lowdefy/blocks-echarts": "0.0.0-experimental-20260910082708",
|
|
62
|
+
"@lowdefy/blocks-loaders": "0.0.0-experimental-20260910082708",
|
|
63
|
+
"@lowdefy/blocks-markdown": "0.0.0-experimental-20260910082708",
|
|
64
|
+
"@lowdefy/blocks-tiptap": "0.0.0-experimental-20260910082708",
|
|
65
|
+
"@lowdefy/client": "0.0.0-experimental-20260910082708",
|
|
66
|
+
"@lowdefy/connection-axios-http": "0.0.0-experimental-20260910082708",
|
|
67
|
+
"@lowdefy/connection-mongodb": "0.0.0-experimental-20260910082708",
|
|
68
|
+
"@lowdefy/errors": "0.0.0-experimental-20260910082708",
|
|
69
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260910082708",
|
|
70
|
+
"@lowdefy/layout": "0.0.0-experimental-20260910082708",
|
|
71
|
+
"@lowdefy/logger": "0.0.0-experimental-20260910082708",
|
|
72
|
+
"@lowdefy/node-utils": "0.0.0-experimental-20260910082708",
|
|
73
|
+
"@lowdefy/operators-js": "0.0.0-experimental-20260910082708",
|
|
74
|
+
"@lowdefy/operators-nunjucks": "0.0.0-experimental-20260910082708",
|
|
75
|
+
"@lowdefy/operators-uuid": "0.0.0-experimental-20260910082708",
|
|
76
|
+
"@lowdefy/websockets-core": "0.0.0-experimental-20260910082708",
|
|
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-20260910082708",
|
|
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/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,22 @@
|
|
|
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 steps from '../../build/plugins/steps.js';
|
|
36
34
|
import websockets from '../../build/plugins/websockets.js';
|
|
37
35
|
|
|
38
36
|
const secrets = getE2eSecrets();
|
|
@@ -69,23 +67,11 @@ function apiContext() {
|
|
|
69
67
|
hostname: c.req.header('host'),
|
|
70
68
|
},
|
|
71
69
|
secrets,
|
|
72
|
-
steps,
|
|
73
70
|
websockets,
|
|
74
71
|
};
|
|
75
72
|
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;
|
|
73
|
+
context.session = getSession(c);
|
|
81
74
|
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
75
|
logRequest({ context });
|
|
90
76
|
c.set('lowdefyContext', context);
|
|
91
77
|
return next();
|
|
@@ -22,6 +22,17 @@ import { HTTPException } from 'hono/http-exception';
|
|
|
22
22
|
// JSON the old apiWrapper returned; page routes get a plain 500.
|
|
23
23
|
function createErrorHandler({ basePath = '', logger }) {
|
|
24
24
|
return async function errorHandler(error, c) {
|
|
25
|
+
// A route or transport that has already decided its own HTTP answer - hono's
|
|
26
|
+
// HTTPException carries the response to send. The MCP transport refuses an
|
|
27
|
+
// unsupported protocol-version header this way (a 404 with a JSON-RPC body
|
|
28
|
+
// that the client SDK falls back on to negotiate a version both sides
|
|
29
|
+
// know). Not a fault: send its response and log one warning line, no
|
|
30
|
+
// structured error log and no Sentry capture. Turning it into a 500 would
|
|
31
|
+
// hide the status the client acts on.
|
|
32
|
+
if (error instanceof HTTPException) {
|
|
33
|
+
logger.warn(`${error.status} answered by the route: ${c.req.method} ${c.req.path}`);
|
|
34
|
+
return error.getResponse();
|
|
35
|
+
}
|
|
25
36
|
const path = basePath ? c.req.path.replace(basePath, '') : c.req.path;
|
|
26
37
|
// Unauthenticated requests to protected endpoints are expected traffic -
|
|
27
38
|
// one warning line and a 401, skipping the structured error log so
|
|
@@ -44,29 +55,6 @@ function createErrorHandler({ basePath = '', logger }) {
|
|
|
44
55
|
}
|
|
45
56
|
return c.text('Forbidden', 403);
|
|
46
57
|
}
|
|
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
58
|
const context = c.get('lowdefyContext');
|
|
71
59
|
if (context) {
|
|
72
60
|
await context.handleError(error);
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { Hono } from 'hono';
|
|
18
|
-
import { HTTPException } from 'hono/http-exception';
|
|
19
18
|
import { jest } from '@jest/globals';
|
|
20
19
|
|
|
21
20
|
import createErrorHandler from './errorHandler.js';
|
|
@@ -63,15 +62,6 @@ function createAuthorizationError(message) {
|
|
|
63
62
|
return error;
|
|
64
63
|
}
|
|
65
64
|
|
|
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
65
|
function createErrorWithCause() {
|
|
76
66
|
const cause = new Error('Cause message.');
|
|
77
67
|
cause.received = `${SECRET}-cause`;
|
|
@@ -161,69 +151,6 @@ test('errorHandler returns text Forbidden at 403 for an AuthorizationError on a
|
|
|
161
151
|
expect(logger.error).not.toHaveBeenCalled();
|
|
162
152
|
});
|
|
163
153
|
|
|
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
154
|
test('errorHandler returns 500 with the serialized error envelope on an api path', async () => {
|
|
228
155
|
const res = await createApp({
|
|
229
156
|
error: createErrorWithCause(),
|
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;
|
|
@@ -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/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;
|