@lowdefy/server-dev 5.5.1 → 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/{apiWrapper.js → createLowdefyContext.js} +63 -57
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.js +9 -0
- 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 -55
- 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/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 -107
- 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,70 @@
|
|
|
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 { createSessionCallback } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import authJson from '../../build/auth.js';
|
|
20
|
+
import callbacks from '../../../build/plugins/auth/callbacks.js';
|
|
21
|
+
import getHeadlessUser from './getHeadlessUser.js';
|
|
22
|
+
import getMockUser from './getMockUser.js';
|
|
23
|
+
|
|
24
|
+
// THE single source of dev sessions (mock user, headless renderer). Two
|
|
25
|
+
// invariants live here, by construction rather than by convention:
|
|
26
|
+
//
|
|
27
|
+
// 1. Prod parity: the dev user runs through the identical Auth.js session
|
|
28
|
+
// callback a real sign-in uses (OIDC claim copy, userFields mapping,
|
|
29
|
+
// custom session-callback plugins, roles validation, hashed_id) — so what
|
|
30
|
+
// a mock or headless session sees is exactly what a real authenticated
|
|
31
|
+
// user would see in production, and dev validates prod behaviour.
|
|
32
|
+
// 2. Client/server parity: every consumer — the server request context
|
|
33
|
+
// (session.js) and the browser client's GET /api/auth/session
|
|
34
|
+
// (routes/auth.js) — obtains the session from this one function, so the
|
|
35
|
+
// two can never diverge.
|
|
36
|
+
//
|
|
37
|
+
// Auth.js sessions carry an expires timestamp (the client SessionProvider
|
|
38
|
+
// schedules refetches off it); dev sessions never expire while the server
|
|
39
|
+
// runs, a day keeps polling calm.
|
|
40
|
+
const DEV_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
41
|
+
|
|
42
|
+
async function getDevSession(c) {
|
|
43
|
+
const user = getMockUser() ?? getHeadlessUser(c);
|
|
44
|
+
if (!user) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
// Without auth configured nothing is protected and there is no session
|
|
48
|
+
// callback pipeline to run — a dev session would not match any prod
|
|
49
|
+
// behaviour. (A configured mock user already threw in getMockUser.)
|
|
50
|
+
if (authJson.configured !== true) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sessionCallback = createSessionCallback({
|
|
55
|
+
authConfig: authJson,
|
|
56
|
+
plugins: { callbacks },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// The dev user acts as both token and user, exactly how a decoded JWT
|
|
60
|
+
// reaches the session callback on a real request.
|
|
61
|
+
const session = await sessionCallback({
|
|
62
|
+
session: { user: {} },
|
|
63
|
+
token: user,
|
|
64
|
+
user,
|
|
65
|
+
});
|
|
66
|
+
session.expires = new Date(Date.now() + DEV_SESSION_MAX_AGE_MS).toISOString();
|
|
67
|
+
return session;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default getDevSession;
|
|
@@ -0,0 +1,138 @@
|
|
|
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 path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
import { jest } from '@jest/globals';
|
|
22
|
+
|
|
23
|
+
// Only the auth build artifact is mocked — createSessionCallback is the REAL
|
|
24
|
+
// @lowdefy/api pipeline, because prod parity is exactly what these tests
|
|
25
|
+
// exist to prove: a dev session must be shaped by the same userFields
|
|
26
|
+
// mapping, roles validation, and hashed_id stamping as a real sign-in.
|
|
27
|
+
jest.unstable_mockModule('../../build/auth.js', () => ({
|
|
28
|
+
default: {
|
|
29
|
+
configured: true,
|
|
30
|
+
callbacks: [],
|
|
31
|
+
userFields: {
|
|
32
|
+
id: 'user.id',
|
|
33
|
+
roles: 'user.roles',
|
|
34
|
+
global_attributes: 'user.global_attributes',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// getDevSession statically imports the app build's auth callbacks module,
|
|
40
|
+
// which only exists in a built app (build/** is gitignored). Materialize an
|
|
41
|
+
// empty one — exactly what a build without custom auth callback plugins
|
|
42
|
+
// writes — so the import resolves; clean up only if this test created it.
|
|
43
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const callbacksDir = path.resolve(dirname, '../../../build/plugins/auth');
|
|
45
|
+
const callbacksPath = path.join(callbacksDir, 'callbacks.js');
|
|
46
|
+
const createdCallbacksFixture = !fs.existsSync(callbacksPath);
|
|
47
|
+
if (createdCallbacksFixture) {
|
|
48
|
+
fs.mkdirSync(callbacksDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(callbacksPath, 'export default {};\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
if (createdCallbacksFixture) {
|
|
54
|
+
fs.rmSync(callbacksPath, { force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const { default: getDevSession } = await import('./getDevSession.js');
|
|
59
|
+
const { HEADLESS_USER_COOKIE, headlessUser } = await import('./headlessUser.js');
|
|
60
|
+
|
|
61
|
+
function createContext({ cookie } = {}) {
|
|
62
|
+
return { req: { header: (name) => (name === 'cookie' ? (cookie ?? '') : undefined) } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function headlessCookie(user = headlessUser) {
|
|
66
|
+
return `${HEADLESS_USER_COOKIE}=${Buffer.from(JSON.stringify(user)).toString('base64')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const originalDevUser = process.env.LOWDEFY_DEV_USER;
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
if (originalDevUser === undefined) {
|
|
73
|
+
delete process.env.LOWDEFY_DEV_USER;
|
|
74
|
+
} else {
|
|
75
|
+
process.env.LOWDEFY_DEV_USER = originalDevUser;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('getDevSession returns undefined with no mock user and no headless cookie', async () => {
|
|
80
|
+
delete process.env.LOWDEFY_DEV_USER;
|
|
81
|
+
expect(await getDevSession(createContext())).toBe(undefined);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('mock user session is built by the real session callback — userFields, hashed_id, expires', async () => {
|
|
85
|
+
// The claim shape a consuming app's tenant convention uses (e.g.
|
|
86
|
+
// global_attributes.company_ids) must survive to session.user on BOTH the
|
|
87
|
+
// server request path and the client session endpoint — this session is
|
|
88
|
+
// what both serve.
|
|
89
|
+
process.env.LOWDEFY_DEV_USER = JSON.stringify({
|
|
90
|
+
id: 'dev-user',
|
|
91
|
+
sub: 'dev-user',
|
|
92
|
+
name: 'Dev User',
|
|
93
|
+
roles: ['admin'],
|
|
94
|
+
global_attributes: { company_ids: ['C-1'] },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const session = await getDevSession(createContext());
|
|
98
|
+
expect(session.user.id).toEqual('dev-user');
|
|
99
|
+
expect(session.user.name).toEqual('Dev User');
|
|
100
|
+
expect(session.user.roles).toEqual(['admin']);
|
|
101
|
+
expect(session.user.global_attributes).toEqual({ company_ids: ['C-1'] });
|
|
102
|
+
// Prod-session markers from the real pipeline:
|
|
103
|
+
expect(typeof session.hashed_id).toBe('string');
|
|
104
|
+
expect(session.hashed_id.length).toBeGreaterThan(0);
|
|
105
|
+
expect(new Date(session.expires).getTime()).toBeGreaterThan(Date.now());
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('headless cookie session runs through the same pipeline as a real sign-in', async () => {
|
|
109
|
+
delete process.env.LOWDEFY_DEV_USER;
|
|
110
|
+
|
|
111
|
+
const session = await getDevSession(createContext({ cookie: headlessCookie() }));
|
|
112
|
+
expect(session.user.id).toEqual(headlessUser.id);
|
|
113
|
+
expect(session.user.roles).toEqual([]);
|
|
114
|
+
expect(typeof session.hashed_id).toBe('string');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('mock user takes precedence over the headless cookie', async () => {
|
|
118
|
+
process.env.LOWDEFY_DEV_USER = JSON.stringify({ id: 'mock-wins', roles: [] });
|
|
119
|
+
|
|
120
|
+
const session = await getDevSession(createContext({ cookie: headlessCookie() }));
|
|
121
|
+
expect(session.user.id).toEqual('mock-wins');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('invalid roles are rejected by the real pipeline, exactly as in prod', async () => {
|
|
125
|
+
process.env.LOWDEFY_DEV_USER = JSON.stringify({ id: 'bad', roles: 'admin' });
|
|
126
|
+
|
|
127
|
+
await expect(getDevSession(createContext())).rejects.toThrow(
|
|
128
|
+
'session.user.roles must be an array of strings.'
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('invalid JSON in LOWDEFY_DEV_USER throws a clear error', async () => {
|
|
133
|
+
process.env.LOWDEFY_DEV_USER = 'not json';
|
|
134
|
+
|
|
135
|
+
await expect(getDevSession(createContext())).rejects.toThrow(
|
|
136
|
+
'Invalid JSON in LOWDEFY_DEV_USER environment variable.'
|
|
137
|
+
);
|
|
138
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { HEADLESS_USER_COOKIE } from './headlessUser.js';
|
|
18
|
+
|
|
19
|
+
// Decodes the headless renderer's user cookie (set by getBrowser.js openPage
|
|
20
|
+
// on its own browser context, base64-encoded JSON) into the raw user object.
|
|
21
|
+
// Returns just the user; getDevSession.js turns it into a session through
|
|
22
|
+
// the same pipeline a real sign-in uses, so headless requests carry a
|
|
23
|
+
// prod-shaped session while the developer's real browser (no cookie) is
|
|
24
|
+
// unaffected.
|
|
25
|
+
function getHeadlessUser(c) {
|
|
26
|
+
const cookieHeader = c.req.header('cookie') ?? '';
|
|
27
|
+
const match = cookieHeader.match(new RegExp(`${HEADLESS_USER_COOKIE}=([^;]+)`));
|
|
28
|
+
if (!match) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const decoded = Buffer.from(decodeURIComponent(match[1]), 'base64').toString();
|
|
34
|
+
return JSON.parse(decoded);
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default getHeadlessUser;
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { createSessionCallback } from '@lowdefy/api';
|
|
18
17
|
import { serializer } from '@lowdefy/helpers';
|
|
19
18
|
|
|
20
19
|
import authJson from '../../build/auth.js';
|
|
21
|
-
import callbacks from '../../../build/plugins/auth/callbacks.js';
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
// Resolves the raw mock user object — LOWDEFY_DEV_USER env var first (the
|
|
22
|
+
// `lowdefy dev --mock-user` flag sets it), then auth.dev.mockUser from the
|
|
23
|
+
// build config. Returns just the user object; getDevSession.js turns it into
|
|
24
|
+
// a session through the same pipeline a real sign-in uses.
|
|
25
|
+
function getMockUser() {
|
|
24
26
|
const mockUserJson = process.env.LOWDEFY_DEV_USER;
|
|
25
27
|
let mockUser;
|
|
26
28
|
|
|
@@ -38,9 +40,6 @@ async function getMockSession() {
|
|
|
38
40
|
return undefined;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
// Deserialize to restore arrays from ~arr markers and remove other build markers
|
|
42
|
-
mockUser = serializer.deserialize(mockUser);
|
|
43
|
-
|
|
44
43
|
if (authJson.configured !== true) {
|
|
45
44
|
throw new Error(
|
|
46
45
|
'Mock user configured but auth is not configured in lowdefy.yaml. ' +
|
|
@@ -48,20 +47,8 @@ async function getMockSession() {
|
|
|
48
47
|
);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
authConfig: authJson,
|
|
54
|
-
plugins: { callbacks },
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Transform mock user through session callback (mock user acts as token)
|
|
58
|
-
const session = await sessionCallback({
|
|
59
|
-
session: { user: {} },
|
|
60
|
-
token: mockUser,
|
|
61
|
-
user: mockUser,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return session;
|
|
50
|
+
// Deserialize to restore arrays from ~arr markers and remove other build markers
|
|
51
|
+
return serializer.deserialize(mockUser);
|
|
65
52
|
}
|
|
66
53
|
|
|
67
|
-
export default
|
|
54
|
+
export default getMockUser;
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
// Shared between the headless renderer (getBrowser.js, which sets the cookie on
|
|
18
|
+
// its browser context) and the dev session resolution (getHeadlessUser.js, which
|
|
19
|
+
// decodes it) so the cookie name and injected user shape live in one place.
|
|
20
|
+
const HEADLESS_USER_COOKIE = 'lowdefy_headless_user';
|
|
21
|
+
|
|
22
|
+
const headlessUser = {
|
|
23
|
+
sub: 'lowdefy-headless',
|
|
24
|
+
id: 'lowdefy-headless',
|
|
25
|
+
name: 'Lowdefy Headless',
|
|
26
|
+
roles: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { HEADLESS_USER_COOKIE, headlessUser };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import { headlessUser } from './headlessUser.js';
|
|
20
|
+
|
|
21
|
+
// The default headless caller is roleless, so role-gated pages and requests are
|
|
22
|
+
// refused for it. A per-call `user` merges over that default, which keeps the
|
|
23
|
+
// default id/name for the common `{ roles: [...] }` case while still allowing a
|
|
24
|
+
// full caller (email, profile, attributes) for config that reads those - nothing
|
|
25
|
+
// derives them for an injected caller, since no auth engine runs. Each headless
|
|
26
|
+
// call gets its own browser context and cookie jar, so callers do not share an
|
|
27
|
+
// identity.
|
|
28
|
+
function resolveHeadlessUser({ user }) {
|
|
29
|
+
if (type.isNone(user)) {
|
|
30
|
+
return headlessUser;
|
|
31
|
+
}
|
|
32
|
+
if (!type.isObject(user)) {
|
|
33
|
+
throw new Error(`Headless "user" must be an object. Received ${JSON.stringify(user)}.`);
|
|
34
|
+
}
|
|
35
|
+
return { ...headlessUser, ...user };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default resolveHeadlessUser;
|
|
@@ -0,0 +1,78 @@
|
|
|
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 resolveHeadlessUser from './resolveHeadlessUser.js';
|
|
18
|
+
import { headlessUser } from './headlessUser.js';
|
|
19
|
+
|
|
20
|
+
test('resolveHeadlessUser returns the default roleless user when no user is given', () => {
|
|
21
|
+
expect(resolveHeadlessUser({})).toEqual({
|
|
22
|
+
sub: 'lowdefy-headless',
|
|
23
|
+
id: 'lowdefy-headless',
|
|
24
|
+
name: 'Lowdefy Headless',
|
|
25
|
+
roles: [],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('resolveHeadlessUser returns the default roleless user when user is null', () => {
|
|
30
|
+
expect(resolveHeadlessUser({ user: null })).toEqual(headlessUser);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('resolveHeadlessUser merges roles over the default, keeping the default id and name', () => {
|
|
34
|
+
expect(resolveHeadlessUser({ user: { roles: ['user-admin'] } })).toEqual({
|
|
35
|
+
sub: 'lowdefy-headless',
|
|
36
|
+
id: 'lowdefy-headless',
|
|
37
|
+
name: 'Lowdefy Headless',
|
|
38
|
+
roles: ['user-admin'],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('resolveHeadlessUser keeps caller fields the app reads, like email and profile', () => {
|
|
43
|
+
expect(
|
|
44
|
+
resolveHeadlessUser({
|
|
45
|
+
user: { id: 'agent', email: 'agent@example.com', profile: { name: 'Agent' } },
|
|
46
|
+
})
|
|
47
|
+
).toEqual({
|
|
48
|
+
sub: 'lowdefy-headless',
|
|
49
|
+
id: 'agent',
|
|
50
|
+
name: 'Lowdefy Headless',
|
|
51
|
+
email: 'agent@example.com',
|
|
52
|
+
profile: { name: 'Agent' },
|
|
53
|
+
roles: [],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('resolveHeadlessUser does not mutate the default user between calls', () => {
|
|
58
|
+
resolveHeadlessUser({ user: { id: 'first', roles: ['admin'] } });
|
|
59
|
+
|
|
60
|
+
expect(resolveHeadlessUser({})).toEqual({
|
|
61
|
+
sub: 'lowdefy-headless',
|
|
62
|
+
id: 'lowdefy-headless',
|
|
63
|
+
name: 'Lowdefy Headless',
|
|
64
|
+
roles: [],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('resolveHeadlessUser throws when user is not an object', () => {
|
|
69
|
+
expect(() => resolveHeadlessUser({ user: 'admin' })).toThrow(
|
|
70
|
+
'Headless "user" must be an object. Received "admin".'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('resolveHeadlessUser throws when user is an array', () => {
|
|
75
|
+
expect(() => resolveHeadlessUser({ user: ['admin'] })).toThrow(
|
|
76
|
+
'Headless "user" must be an object. Received ["admin"].'
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { getAuthUser } from '@hono/auth-js';
|
|
18
|
+
|
|
19
|
+
import authJson from '../../build/auth.js';
|
|
20
|
+
import getDevSession from './getDevSession.js';
|
|
21
|
+
|
|
22
|
+
// Replaces getServerSession.js — dev sessions (mock user, headless renderer)
|
|
23
|
+
// first, then the session from the Hono context populated by initAuthConfig.
|
|
24
|
+
// getDevSession is the same function the client's GET /api/auth/session is
|
|
25
|
+
// answered from (routes/auth.js), so server and client sessions match by
|
|
26
|
+
// construction.
|
|
27
|
+
async function getSession(c) {
|
|
28
|
+
const devSession = await getDevSession(c);
|
|
29
|
+
if (devSession) {
|
|
30
|
+
return devSession;
|
|
31
|
+
}
|
|
32
|
+
if (authJson.configured !== true) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const authUser = await getAuthUser(c);
|
|
36
|
+
return authUser?.session ?? undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default getSession;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { getAuthStrategies, resolveStrategyCaller } from '@lowdefy/api';
|
|
18
|
+
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
19
|
+
|
|
20
|
+
import appMeta from '../../build/appMeta.js';
|
|
21
|
+
import authJson from '../../build/auth.js';
|
|
22
|
+
import strategies from '../../../build/plugins/auth/strategies.js';
|
|
23
|
+
|
|
24
|
+
// Secrets are process-static env, so read once at module load rather than
|
|
25
|
+
// on every sessionless request.
|
|
26
|
+
const secrets = getSecretsFromEnv();
|
|
27
|
+
|
|
28
|
+
// API auth strategies (apiKey/jwt headers) let MCP and service clients that
|
|
29
|
+
// cannot hold a session cookie authenticate. Tried only when no session
|
|
30
|
+
// resolved; a match is synthesized as session.user so createAuthorize and
|
|
31
|
+
// _user work unchanged.
|
|
32
|
+
async function getStrategyCaller(c, logger) {
|
|
33
|
+
const authStrategies = getAuthStrategies({
|
|
34
|
+
appMeta,
|
|
35
|
+
authJson,
|
|
36
|
+
logger,
|
|
37
|
+
plugins: { strategies },
|
|
38
|
+
secrets,
|
|
39
|
+
});
|
|
40
|
+
return resolveStrategyCaller({
|
|
41
|
+
headers: c.req.raw.headers,
|
|
42
|
+
logger,
|
|
43
|
+
strategies: authStrategies,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default getStrategyCaller;
|