@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.
Files changed (214) hide show
  1. package/{pages/_app.js → client/App.jsx} +21 -33
  2. package/client/DevStreamContext.js +30 -0
  3. package/client/Inspector.jsx +226 -0
  4. package/{lib/client/Page.js → client/Page.jsx} +4 -8
  5. package/client/Reload.jsx +85 -0
  6. package/client/Routing.jsx +117 -0
  7. package/client/feedback/FeedbackMount.jsx +83 -0
  8. package/client/feedback/FeedbackOverlay.jsx +901 -0
  9. package/client/feedback/captureTabScreenshot.js +84 -0
  10. package/client/feedback/copyToClipboard.js +45 -0
  11. package/client/feedback/drawAnnotationsSvg.js +119 -0
  12. package/client/feedback/elementInspect.js +99 -0
  13. package/client/feedback/feedbackStyles.js +301 -0
  14. package/client/feedback/pinResponsiveImages.js +59 -0
  15. package/client/feedback/sendFeedback.js +39 -0
  16. package/client/feedback/useFeedbackToggle.js +70 -0
  17. package/client/main.jsx +42 -0
  18. package/client/openInEditor/OpenInEditorListener.jsx +268 -0
  19. package/client/openInEditor/findBlockLocation.js +51 -0
  20. package/lib/build/app.js +8 -1
  21. package/lib/build/appMeta.js +8 -1
  22. package/lib/build/auth.js +8 -1
  23. package/lib/build/config.js +8 -1
  24. package/lib/build/i18n.js +8 -1
  25. package/lib/build/logger.js +8 -1
  26. package/lib/build/theme.js +8 -1
  27. package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
  28. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
  29. package/lib/client/setPageId.js +6 -5
  30. package/lib/client/utils/useMutateCache.js +14 -1
  31. package/lib/client/utils/usePageConfig.js +45 -43
  32. package/lib/client/utils/usePageConfig.test.mjs +62 -0
  33. package/lib/docs/captureAnnotatedScreenshot.js +88 -0
  34. package/lib/docs/checkpointPaths.js +91 -0
  35. package/lib/docs/checkpointStore.js +240 -0
  36. package/lib/docs/checkpointToMocks.js +65 -0
  37. package/lib/docs/clientErrorStore.js +36 -0
  38. package/lib/docs/configCheckpoints.test.mjs +156 -0
  39. package/lib/docs/createConfigCheckpoint.js +92 -0
  40. package/lib/docs/createDocsMcpServer.js +531 -0
  41. package/lib/docs/createDocsMcpServer.test.mjs +141 -0
  42. package/lib/docs/devMockRegistry.js +68 -0
  43. package/lib/docs/docs.test.mjs +448 -0
  44. package/lib/docs/enrichFeedback.js +71 -0
  45. package/lib/docs/evalOperator.js +47 -0
  46. package/lib/docs/evalOperator.test.mjs +157 -0
  47. package/lib/docs/evalOperatorHeadless.js +87 -0
  48. package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
  49. package/lib/docs/evalOperatorInTab.js +48 -0
  50. package/lib/docs/findConfig.js +260 -0
  51. package/lib/docs/formatFeedback.js +121 -0
  52. package/lib/docs/formatFeedback.test.mjs +204 -0
  53. package/lib/docs/getAppMap.js +180 -0
  54. package/lib/docs/getBrowser.js +127 -0
  55. package/lib/docs/getBrowser.test.mjs +122 -0
  56. package/lib/docs/getBuildStatus.js +37 -0
  57. package/lib/docs/getCoreDoc.js +63 -0
  58. package/lib/docs/getDocsManifest.js +40 -0
  59. package/lib/docs/getExamples.js +58 -0
  60. package/lib/docs/getOverview.js +115 -0
  61. package/lib/docs/getPageConfig.js +53 -0
  62. package/lib/docs/getPluginDoc.js +55 -0
  63. package/lib/docs/getSchema.js +65 -0
  64. package/lib/docs/inspectState.js +47 -0
  65. package/lib/docs/inspectState.test.mjs +140 -0
  66. package/lib/docs/inspectStateFromTab.js +40 -0
  67. package/lib/docs/inspectStateHeadless.js +84 -0
  68. package/lib/docs/inspectStateHeadless.test.mjs +60 -0
  69. package/lib/docs/isWriteRequestsAllowed.js +50 -0
  70. package/lib/docs/listConfigCheckpoints.js +55 -0
  71. package/lib/docs/listPlugins.js +80 -0
  72. package/lib/docs/listTypes.js +105 -0
  73. package/lib/docs/loadState.js +152 -0
  74. package/lib/docs/mapPageBuildErrors.js +28 -0
  75. package/lib/docs/normalizeTypeKind.js +40 -0
  76. package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
  77. package/lib/docs/readPageArtifact.js +26 -0
  78. package/lib/docs/resolvePluginDir.js +49 -0
  79. package/lib/docs/resolveSource.js +48 -0
  80. package/lib/docs/resolveSource.test.mjs +100 -0
  81. package/lib/docs/revertConfigCheckpoint.js +73 -0
  82. package/lib/docs/runRequest.js +141 -0
  83. package/lib/docs/saveAnnotatedScreenshot.js +43 -0
  84. package/lib/docs/scaffoldPage.js +75 -0
  85. package/lib/docs/scaffoldPage.test.mjs +97 -0
  86. package/lib/docs/screenshotPage.js +110 -0
  87. package/lib/docs/screenshotPage.test.mjs +59 -0
  88. package/lib/docs/screenshotPageClip.test.mjs +102 -0
  89. package/lib/docs/searchDocs.js +67 -0
  90. package/lib/docs/setupTestFixtures.mjs +335 -0
  91. package/lib/docs/snapshotState.js +59 -0
  92. package/lib/docs/stateCheckpoints.test.mjs +366 -0
  93. package/lib/docs/tabAvailable.js +28 -0
  94. package/lib/docs/tabChannel.js +119 -0
  95. package/lib/docs/tabChannel.test.mjs +130 -0
  96. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
  97. package/lib/server/auth/getDevSession.js +70 -0
  98. package/lib/server/auth/getDevSession.test.mjs +138 -0
  99. package/lib/server/auth/getHeadlessUser.js +40 -0
  100. package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
  101. package/lib/server/auth/headlessUser.js +29 -0
  102. package/lib/server/auth/resolveHeadlessUser.js +38 -0
  103. package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
  104. package/lib/server/auth/session.js +39 -0
  105. package/lib/server/auth/strategies.js +47 -0
  106. package/lib/server/{apiWrapper.js → createLowdefyContext.js} +63 -57
  107. package/lib/server/getPageJitEnrichment.test.mjs +105 -0
  108. package/lib/server/jitPageBuilder.js +125 -12
  109. package/lib/server/log/createHandleError.js +9 -0
  110. package/lib/server/log/createHandleError.test.mjs +126 -0
  111. package/manager/getContext.mjs +3 -7
  112. package/manager/processes/initialBuild.mjs +0 -2
  113. package/manager/processes/installPlugins.mjs +19 -11
  114. package/manager/processes/lowdefyBuild.mjs +29 -7
  115. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  116. package/manager/processes/readDotEnv.mjs +13 -1
  117. package/manager/processes/shutdownServer.mjs +6 -6
  118. package/manager/processes/startProxy.mjs +160 -0
  119. package/manager/processes/startProxy.test.mjs +102 -0
  120. package/manager/processes/startServer.mjs +33 -16
  121. package/manager/processes/startWatchers.mjs +2 -2
  122. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  123. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  124. package/manager/run.mjs +50 -0
  125. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  126. package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
  127. package/manager/utils/formatNoticeBox.mjs +30 -0
  128. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  129. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  130. package/manager/utils/writeBuildStatus.mjs +39 -0
  131. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  132. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  133. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  134. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
  135. package/package.json +61 -47
  136. package/package.original.json +64 -55
  137. package/postcss.config.cjs +1 -0
  138. package/src/app.js +175 -0
  139. package/src/html/renderDevPage.js +86 -0
  140. package/src/lib/getPathSegments.js +25 -0
  141. package/src/lib/safeScriptJson.js +38 -0
  142. package/src/middleware/apiContext.js +32 -0
  143. package/src/middleware/errorHandler.js +48 -0
  144. package/src/middleware/errorHandler.test.mjs +235 -0
  145. package/src/routes/agent.js +64 -0
  146. package/src/routes/apiPage.js +37 -0
  147. package/src/routes/auth.js +49 -0
  148. package/src/routes/auth.test.mjs +84 -0
  149. package/src/routes/clientError.js +57 -0
  150. package/src/routes/cron.js +47 -0
  151. package/src/routes/cronForward.js +47 -0
  152. package/src/routes/detached.js +47 -0
  153. package/src/routes/devInspect.js +111 -0
  154. package/src/routes/devInspect.test.mjs +119 -0
  155. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  156. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  157. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  158. package/src/routes/docs/checkpointsCreate.js +28 -0
  159. package/src/routes/docs/checkpointsList.js +23 -0
  160. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  161. package/src/routes/docs/content.js +31 -0
  162. package/src/routes/docs/evalOperator.js +56 -0
  163. package/src/routes/docs/examples.js +33 -0
  164. package/src/routes/docs/find.js +30 -0
  165. package/src/routes/docs/index.js +23 -0
  166. package/src/routes/docs/inspectState.js +44 -0
  167. package/src/routes/docs/loadState.js +49 -0
  168. package/src/routes/docs/mcp.js +30 -0
  169. package/src/routes/docs/pageConfig.js +36 -0
  170. package/src/routes/docs/parseUserParam.js +52 -0
  171. package/src/routes/docs/parseUserParam.test.mjs +45 -0
  172. package/src/routes/docs/pluginDoc.js +33 -0
  173. package/src/routes/docs/plugins.js +23 -0
  174. package/src/routes/docs/runRequest.js +36 -0
  175. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  176. package/src/routes/docs/screenshot.js +72 -0
  177. package/src/routes/docs/search.js +27 -0
  178. package/src/routes/docs/snapshotState.js +36 -0
  179. package/src/routes/docs/stateCheckpointsList.js +26 -0
  180. package/src/routes/docs/types.js +33 -0
  181. package/src/routes/endpoints.js +71 -0
  182. package/src/routes/feedback.js +110 -0
  183. package/src/routes/feedback.test.mjs +204 -0
  184. package/src/routes/jitPage.js +86 -0
  185. package/src/routes/mcp.js +34 -0
  186. package/{pages/api → src/routes}/ping.js +4 -2
  187. package/src/routes/reload.js +79 -0
  188. package/src/routes/request.js +74 -0
  189. package/src/routes/request.test.mjs +124 -0
  190. package/{pages/api → src/routes}/root.js +4 -4
  191. package/src/routes/usage.js +46 -0
  192. package/src/routes/websocket.js +33 -0
  193. package/src/websocket/devWebSocket.js +78 -0
  194. package/vite.config.js +111 -0
  195. package/.eslintrc.yaml +0 -1
  196. package/lib/client/App.js +0 -80
  197. package/lib/client/Reload.js +0 -60
  198. package/lib/server/compileCss.js +0 -39
  199. package/manager/processes/compileCss.mjs +0 -54
  200. package/manager/processes/nextBuild.mjs +0 -50
  201. package/next.config.js +0 -25
  202. package/pages/_document.js +0 -107
  203. package/pages/api/agent/[...path].js +0 -86
  204. package/pages/api/auth/[...nextauth].js +0 -48
  205. package/pages/api/client-error.js +0 -45
  206. package/pages/api/js/[env].js +0 -42
  207. package/pages/api/page/[...pageId].js +0 -74
  208. package/pages/api/reload.js +0 -53
  209. package/pages/api/request/[...path].js +0 -41
  210. /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
  211. /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
  212. /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
  213. /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
  214. /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
- async function getMockSession() {
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
- // Create session callback to transform mock user
52
- const sessionCallback = createSessionCallback({
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 getMockSession;
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;