@lowdefy/server-dev 0.0.0-experimental-20260928105334 → 0.0.0-experimental-20260928120643

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 (258) hide show
  1. package/client/App.jsx +35 -24
  2. package/client/Page.jsx +18 -9
  3. package/client/clientBuildImports.test.mjs +2 -0
  4. package/lib/client/ErrorBar.jsx +17 -13
  5. package/lib/client/RedirectingPage.jsx +56 -0
  6. package/lib/client/auth/Auth.jsx +11 -6
  7. package/lib/client/auth/AuthConfigured.jsx +201 -36
  8. package/lib/client/auth/AuthNotConfigured.js +46 -4
  9. package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
  10. package/lib/client/auth/authTemplateSync.test.mjs +40 -0
  11. package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
  12. package/lib/client/utils/formatErrorsForCopy.js +30 -0
  13. package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
  14. package/lib/client/utils/getErrorBarColor.js +25 -0
  15. package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
  16. package/lib/client/utils/request.js +8 -1
  17. package/lib/client/utils/usePageConfig.js +22 -0
  18. package/lib/client/utils/usePageConfig.test.mjs +2 -2
  19. package/lib/docs/JourneyStepError.js +30 -0
  20. package/lib/docs/buildEditedPages.js +50 -0
  21. package/lib/docs/buildWaitHeader.js +21 -0
  22. package/lib/docs/clientErrorStore.js +7 -1
  23. package/lib/docs/clientErrorStore.test.mjs +46 -0
  24. package/lib/docs/createDocsMcpServer.js +236 -397
  25. package/lib/docs/createDocsMcpServer.test.mjs +263 -2
  26. package/lib/docs/createJourneyActors.js +73 -0
  27. package/lib/docs/devEventBus.js +136 -0
  28. package/lib/docs/devEventBus.test.mjs +225 -0
  29. package/lib/docs/devToolDefinitions.js +423 -0
  30. package/lib/docs/docs.test.mjs +124 -6
  31. package/lib/docs/evalOperatorHeadless.js +7 -1
  32. package/lib/docs/findConfig.js +84 -39
  33. package/lib/docs/fitResponse.js +81 -0
  34. package/lib/docs/fitResponse.test.mjs +128 -0
  35. package/lib/docs/getBrowser.js +92 -42
  36. package/lib/docs/getBrowser.test.mjs +186 -3
  37. package/lib/docs/getBuildId.js +61 -0
  38. package/lib/docs/getBuildId.test.mjs +84 -0
  39. package/lib/docs/getBuildStatus.js +35 -6
  40. package/lib/docs/getBuildStatus.test.mjs +66 -0
  41. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  42. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  43. package/lib/docs/getCoreDoc.js +9 -1
  44. package/lib/docs/getHazards.js +115 -0
  45. package/lib/docs/getHazards.test.mjs +175 -0
  46. package/lib/docs/getMailOutboxDirectory.js +26 -0
  47. package/lib/docs/getOverview.js +35 -10
  48. package/lib/docs/getOverviewBasePath.test.mjs +36 -0
  49. package/lib/docs/getPageBuildStatus.js +46 -0
  50. package/lib/docs/getSchema.js +5 -1
  51. package/lib/docs/getStaleStatus.js +36 -0
  52. package/lib/docs/getStaleStatus.test.mjs +74 -0
  53. package/lib/docs/inspectStateHeadless.js +7 -1
  54. package/lib/docs/isPageReady.js +63 -0
  55. package/lib/docs/isPageReady.test.mjs +160 -0
  56. package/lib/docs/listPlugins.js +15 -0
  57. package/lib/docs/loadFrameworkHazards.js +40 -0
  58. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  59. package/lib/docs/loadState.js +15 -3
  60. package/lib/docs/openJourneyEmail.js +41 -0
  61. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  62. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  63. package/lib/docs/readJourneyEmailMatch.js +57 -0
  64. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  65. package/lib/docs/readMailOutbox.js +37 -0
  66. package/lib/docs/readProxyBuildWait.js +38 -0
  67. package/lib/docs/requestRestart.js +30 -0
  68. package/lib/docs/requestRestart.test.mjs +49 -0
  69. package/lib/docs/reviewPage.js +70 -0
  70. package/lib/docs/reviewPageBuilds.js +46 -0
  71. package/lib/docs/reviewPageBuilds.test.mjs +169 -0
  72. package/lib/docs/runCheck.js +75 -0
  73. package/lib/docs/runCheck.test.mjs +91 -0
  74. package/lib/docs/runCheckChild.js +49 -0
  75. package/lib/docs/runEndpoint.js +141 -0
  76. package/lib/docs/runEndpoint.test.mjs +414 -0
  77. package/lib/docs/runJourney.js +911 -0
  78. package/lib/docs/runJourney.test.mjs +1524 -0
  79. package/lib/docs/runRequest.js +72 -43
  80. package/lib/docs/runRequest.test.mjs +185 -0
  81. package/lib/docs/screenshotPage.js +23 -3
  82. package/lib/docs/screenshotPage.test.mjs +16 -0
  83. package/lib/docs/searchIcons.js +90 -0
  84. package/lib/docs/searchIcons.test.mjs +127 -0
  85. package/lib/docs/selectFinalState.js +66 -0
  86. package/lib/docs/selectFinalState.test.mjs +51 -0
  87. package/lib/docs/serverErrorStore.js +43 -0
  88. package/lib/docs/serverErrorStore.test.mjs +50 -0
  89. package/lib/docs/setupTestFixtures.mjs +82 -6
  90. package/lib/docs/unsettledPageNote.js +24 -0
  91. package/lib/docs/validateJourneySteps.js +412 -0
  92. package/lib/docs/validateJourneySteps.test.mjs +276 -0
  93. package/lib/docs/validateJourneyTimeout.js +38 -0
  94. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  95. package/lib/docs/validateStateSelection.js +33 -0
  96. package/lib/docs/validateStateSelection.test.mjs +33 -0
  97. package/lib/docs/validateViewport.js +51 -0
  98. package/lib/docs/validateViewport.test.mjs +54 -0
  99. package/lib/docs/waitForBuild.js +47 -0
  100. package/lib/docs/waitForBuild.test.mjs +71 -0
  101. package/lib/docs/waitForJourneyEmail.js +77 -0
  102. package/lib/server/auth/createSystemContext.js +73 -0
  103. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  104. package/lib/server/auth/getHeadlessUser.js +7 -8
  105. package/lib/server/auth/getMockUser.js +18 -14
  106. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  107. package/lib/server/auth/headlessUser.js +3 -2
  108. package/lib/server/auth/journeyActor.js +37 -0
  109. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  110. package/lib/server/createLowdefyContext.js +95 -41
  111. package/lib/server/createLowdefyContext.test.mjs +42 -5
  112. package/lib/server/getClientAddress.js +70 -0
  113. package/lib/server/getClientAddress.test.mjs +53 -0
  114. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  115. package/lib/server/jitPageBuilder.js +45 -29
  116. package/lib/server/jitPageBuilder.test.mjs +56 -0
  117. package/lib/server/loadDynamicJsMap.js +47 -0
  118. package/lib/server/log/createHandleError.js +26 -0
  119. package/lib/server/log/createHandleError.test.mjs +74 -2
  120. package/lib/server/log/logRequest.js +1 -3
  121. package/lib/server/pageBuildRecords.js +84 -0
  122. package/lib/server/pageBuildRecords.test.mjs +157 -0
  123. package/lib/server/skipStaleMapWrites.js +41 -0
  124. package/lib/server/skipStaleMapWrites.test.mjs +69 -0
  125. package/lib/vite/createDevServerExclude.js +40 -0
  126. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  127. package/manager/getContext.mjs +19 -1
  128. package/manager/processes/lowdefyBuild.mjs +29 -7
  129. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  130. package/manager/processes/restartServer.mjs +20 -6
  131. package/manager/processes/shutdownServer.mjs +8 -0
  132. package/manager/processes/shutdownServer.test.mjs +45 -0
  133. package/manager/processes/startMailSink.mjs +135 -0
  134. package/manager/processes/startMailSink.test.mjs +146 -0
  135. package/manager/processes/startProxy.mjs +110 -12
  136. package/manager/processes/startProxy.test.mjs +207 -0
  137. package/manager/processes/startServer.mjs +17 -2
  138. package/manager/processes/startServer.test.mjs +62 -0
  139. package/manager/processes/startWatchers.mjs +6 -4
  140. package/manager/processes/syncServer.mjs +52 -0
  141. package/manager/processes/syncServer.test.mjs +84 -0
  142. package/manager/run.mjs +60 -34
  143. package/manager/utils/BatchChanges.mjs +21 -5
  144. package/manager/utils/BatchChanges.test.mjs +77 -0
  145. package/manager/utils/acquireDevInstance.mjs +99 -0
  146. package/manager/utils/acquireDevInstance.test.mjs +141 -0
  147. package/manager/utils/createBuildActivity.mjs +67 -0
  148. package/manager/utils/createBuildActivity.test.mjs +58 -0
  149. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  150. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  151. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  152. package/manager/utils/createDotPathIgnore.mjs +44 -0
  153. package/manager/utils/createDotPathIgnore.test.mjs +52 -0
  154. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  155. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  156. package/manager/utils/findBuildFilesOutsideWatch.mjs +41 -0
  157. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +77 -0
  158. package/manager/utils/importFresh.mjs +46 -0
  159. package/manager/utils/importFresh.test.mjs +72 -0
  160. package/manager/utils/importFreshWorker.mjs +20 -0
  161. package/manager/utils/publishBuildDirectory.mjs +102 -0
  162. package/manager/utils/publishBuildDirectory.order.test.mjs +56 -0
  163. package/manager/utils/publishBuildDirectory.test.mjs +201 -0
  164. package/manager/utils/readBasePath.mjs +27 -0
  165. package/manager/utils/readBasePath.test.mjs +43 -0
  166. package/manager/utils/readBuildStatusWait.mjs +67 -0
  167. package/manager/utils/readPluginDefinitions.mjs +36 -0
  168. package/manager/utils/resolvePorts.mjs +49 -0
  169. package/manager/utils/setupWatcher.mjs +5 -10
  170. package/manager/utils/waitForServer.mjs +43 -0
  171. package/manager/utils/writeBuildStatus.mjs +1 -1
  172. package/manager/watchers/envWatcher.mjs +9 -2
  173. package/manager/watchers/envWatcher.test.mjs +87 -0
  174. package/manager/watchers/listServerSidePackages.mjs +47 -0
  175. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  176. package/manager/watchers/lowdefyBuildWatcher.mjs +85 -18
  177. package/manager/watchers/lowdefyBuildWatcher.test.mjs +197 -0
  178. package/manager/watchers/pluginSourceWatcher.mjs +75 -0
  179. package/manager/watchers/pluginSourceWatcher.test.mjs +129 -0
  180. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  181. package/manager/watchers/restartRequestWatcher.test.mjs +102 -0
  182. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  183. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  184. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  185. package/package.json +48 -43
  186. package/package.original.json +48 -43
  187. package/src/app.js +74 -17
  188. package/src/html/renderDevPage.js +4 -1
  189. package/src/middleware/createSameOriginGuard.js +37 -0
  190. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  191. package/src/middleware/errorHandler.js +49 -1
  192. package/src/middleware/errorHandler.test.mjs +173 -3
  193. package/src/middleware/isRebindingSafeHost.js +44 -0
  194. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  195. package/src/middleware/localDevToolsOnly.js +38 -0
  196. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  197. package/src/middleware/staleFlag.js +76 -0
  198. package/src/middleware/staleFlag.test.mjs +127 -0
  199. package/src/routes/agent.js +5 -3
  200. package/src/routes/auth.js +22 -20
  201. package/src/routes/clientError.js +11 -13
  202. package/src/routes/cron.js +3 -1
  203. package/src/routes/cronForward.js +3 -1
  204. package/src/routes/detached.js +19 -9
  205. package/src/routes/devInspect.js +6 -3
  206. package/src/routes/docs/buildStatus.js +9 -1
  207. package/src/routes/docs/events.js +50 -0
  208. package/src/routes/docs/events.test.mjs +117 -0
  209. package/src/routes/docs/find.js +9 -1
  210. package/src/routes/docs/icons.js +30 -0
  211. package/src/routes/docs/journey.js +87 -0
  212. package/src/routes/docs/journey.test.mjs +195 -0
  213. package/src/routes/docs/loadState.js +3 -1
  214. package/src/routes/docs/mcp.js +28 -2
  215. package/src/routes/docs/mcp.test.mjs +130 -0
  216. package/src/routes/docs/restart.js +35 -0
  217. package/src/routes/docs/restart.test.mjs +64 -0
  218. package/src/routes/docs/runEndpoint.js +53 -0
  219. package/src/routes/docs/runEndpoint.test.mjs +155 -0
  220. package/src/routes/docs/runRequest.js +24 -8
  221. package/src/routes/docs/runRequest.test.mjs +82 -0
  222. package/src/routes/docs/screenshot.js +37 -1
  223. package/src/routes/docs/screenshot.test.mjs +117 -0
  224. package/src/routes/docs/snapshotState.js +3 -1
  225. package/src/routes/endpoints.js +24 -12
  226. package/src/routes/feedback.js +10 -13
  227. package/src/routes/jitPage.js +42 -2
  228. package/src/routes/jitPage.test.mjs +107 -0
  229. package/src/routes/mcp.js +71 -5
  230. package/src/routes/mcp.test.mjs +215 -0
  231. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  232. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  233. package/src/routes/mountDevPageRoutes.js +25 -0
  234. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  235. package/src/routes/mountOauthDiscovery.js +44 -0
  236. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  237. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  238. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  239. package/src/routes/request.js +6 -4
  240. package/src/routes/request.test.mjs +9 -5
  241. package/src/routes/requestBody.test.mjs +123 -0
  242. package/src/routes/usage.js +6 -2
  243. package/src/routes/user.js +27 -0
  244. package/src/routes/websocket.js +25 -8
  245. package/src/routes/websocket.test.mjs +51 -0
  246. package/src/routes/wellKnownFallback.js +30 -0
  247. package/src/routes/wellKnownFallback.test.mjs +32 -0
  248. package/src/websocket/devWebSocket.js +36 -26
  249. package/src/websocket/devWebSocket.test.mjs +127 -0
  250. package/vite.config.js +46 -23
  251. package/lib/server/auth/getDevSession.js +0 -70
  252. package/lib/server/auth/getDevSession.test.mjs +0 -138
  253. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  254. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  255. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  256. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  257. package/src/routes/apiPage.js +0 -37
  258. package/src/routes/auth.test.mjs +0 -84
package/client/App.jsx CHANGED
@@ -18,19 +18,25 @@ import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react
18
18
  import useSWR from 'swr';
19
19
 
20
20
  import { ErrorBoundary } from '@lowdefy/block-utils';
21
- import { useDarkMode, useLocale } from '@lowdefy/client';
21
+ import { IconProvider, useDarkMode, useLocale } from '@lowdefy/client';
22
22
  import { StyleProvider } from '@ant-design/cssinjs';
23
23
  import { App as AntdApp, theme as antdTheme } from 'antd';
24
24
  import { XProvider } from '@ant-design/x';
25
+ import { serializer } from '@lowdefy/helpers';
25
26
 
26
27
  import antdLocaleLoaders from '../build/i18n/antdLocales.js';
27
28
  import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
28
29
  import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
30
+ import rawTheme from '../build/theme.json';
29
31
  import Auth from '../lib/client/auth/Auth.jsx';
30
32
  import ErrorBar from '../lib/client/ErrorBar.jsx';
31
33
  import request from '../lib/client/utils/request.js';
32
34
  import Routing from './Routing.jsx';
33
35
 
36
+ // rootConfig arrives after the first render; the build-written theme gives
37
+ // the icon provider its settings until then.
38
+ const buildTheme = serializer.deserialize(rawTheme);
39
+
34
40
  function ThemeTokenResolver({ lowdefyRef, children }) {
35
41
  const { token } = antdTheme.useToken();
36
42
  if (!lowdefyRef.current.theme) {
@@ -40,7 +46,7 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
40
46
  return children;
41
47
  }
42
48
 
43
- function App({ router }) {
49
+ function App({ config, router }) {
44
50
  const lowdefyRef = useRef({});
45
51
  const [runtimeErrors, setRuntimeErrors] = useState([]);
46
52
  // Subscribe to rootConfig SWR cache — deduplicates with Routing's fetch.
@@ -88,6 +94,7 @@ function App({ router }) {
88
94
  message: error.message,
89
95
  source: error.source,
90
96
  stack: error.stack,
97
+ prodError: error.prodError === true,
91
98
  },
92
99
  ]);
93
100
  }, []);
@@ -130,29 +137,33 @@ function App({ router }) {
130
137
  algorithm,
131
138
  }}
132
139
  >
133
- <AntdApp>
134
- <ThemeTokenResolver lowdefyRef={lowdefyRef}>
135
- <ErrorBoundary fullPage onError={handleError}>
136
- <Suspense
137
- fallback={
138
- <div
139
- style={{
140
- minHeight: '100vh',
141
- background: 'var(--ant-color-bg-layout)',
140
+ {/* Message, Notification and ConfirmModal render in the App.useApp()
141
+ holders here, above the Client's icon provider. */}
142
+ <IconProvider icons={rootConfig?.theme.icons ?? buildTheme.icons}>
143
+ <AntdApp>
144
+ <ThemeTokenResolver lowdefyRef={lowdefyRef}>
145
+ <ErrorBoundary fullPage onError={handleError}>
146
+ <Suspense
147
+ fallback={
148
+ <div
149
+ style={{
150
+ minHeight: '100vh',
151
+ background: 'var(--ant-color-bg-layout)',
152
+ }}
153
+ />
154
+ }
155
+ >
156
+ <Auth user={config?.user}>
157
+ {(auth) => {
158
+ return <Routing auth={auth} lowdefy={lowdefyRef.current} router={router} />;
142
159
  }}
143
- />
144
- }
145
- >
146
- <Auth>
147
- {(auth) => {
148
- return <Routing auth={auth} lowdefy={lowdefyRef.current} router={router} />;
149
- }}
150
- </Auth>
151
- </Suspense>
152
- </ErrorBoundary>
153
- <ErrorBar errors={runtimeErrors} />
154
- </ThemeTokenResolver>
155
- </AntdApp>
160
+ </Auth>
161
+ </Suspense>
162
+ </ErrorBoundary>
163
+ <ErrorBar errors={runtimeErrors} />
164
+ </ThemeTokenResolver>
165
+ </AntdApp>
166
+ </IconProvider>
156
167
  </XProvider>
157
168
  </StyleProvider>
158
169
  );
package/client/Page.jsx CHANGED
@@ -15,11 +15,11 @@
15
15
  */
16
16
 
17
17
  import React, { useEffect, useRef } from 'react';
18
- import { GenIcon } from 'react-icons/lib';
19
18
  import Client from '@lowdefy/client';
20
19
 
21
20
  import BuildErrorPage from '../lib/client/BuildErrorPage.jsx';
22
21
  import InstallingPluginsPage from '../lib/client/InstallingPluginsPage.jsx';
22
+ import RedirectingPage from '../lib/client/RedirectingPage.jsx';
23
23
  import usePageConfig from '../lib/client/utils/usePageConfig.js';
24
24
 
25
25
  const Page = ({
@@ -46,10 +46,22 @@ const Page = ({
46
46
  }
47
47
  }, [pageConfig?._warnings, lowdefy]);
48
48
 
49
+ // Full load to the sign-in page so it can return here after sign-in — an
50
+ // effect, not a fetcher side effect, so the redirect re-fires if the same
51
+ // cached result renders again.
52
+ useEffect(() => {
53
+ if (pageConfig?.authRedirect) {
54
+ window.location.assign(pageConfig.authRedirect);
55
+ }
56
+ }, [pageConfig?.authRedirect]);
57
+
49
58
  if (!pageConfig) {
50
59
  router.replace({ pathname: '/404' });
51
60
  return '';
52
61
  }
62
+ if (pageConfig.authRedirect) {
63
+ return <RedirectingPage redirect={pageConfig.authRedirect} />;
64
+ }
53
65
  if (pageConfig.buildError) {
54
66
  return (
55
67
  <BuildErrorPage
@@ -66,15 +78,12 @@ const Page = ({
66
78
  // Merge dynamic JS entries fetched after JIT build with the static jsMap
67
79
  const mergedJsMap = pageConfig._jsEntries ? { ...jsMap, ...pageConfig._jsEntries } : jsMap;
68
80
 
69
- // Merge JIT-discovered icon data into the static icons object.
70
- // createIcon.js looks up Icons[name] on every render from the captured reference,
71
- // so mutating the original object makes new icons available immediately.
81
+ // Merge JIT-delivered icon data into the static icons map. Icons are data
82
+ // (IconData), and createIcon looks up Icons[name] on every render from the
83
+ // captured reference, so mutating the original object makes them available
84
+ // immediately.
72
85
  if (pageConfig._dynamicIcons) {
73
- for (const [name, data] of Object.entries(pageConfig._dynamicIcons)) {
74
- if (!types.icons[name]) {
75
- types.icons[name] = GenIcon(data);
76
- }
77
- }
86
+ Object.assign(types.icons, pageConfig._dynamicIcons);
78
87
  }
79
88
 
80
89
  return (
@@ -36,6 +36,8 @@ const allowed = new Set([
36
36
  'plugins/icons.js',
37
37
  'plugins/operators/client.js',
38
38
  'plugins/operators/clientJsMap.js',
39
+ // The whole theme already reaches the browser in the root config.
40
+ 'theme.json',
39
41
  ]);
40
42
 
41
43
  const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -16,15 +16,17 @@
16
16
 
17
17
  import React, { useCallback, useState } from 'react';
18
18
 
19
+ import formatErrorsForCopy from './utils/formatErrorsForCopy.js';
20
+ import getErrorBarColor from './utils/getErrorBarColor.js';
21
+
19
22
  function getBarStyle(errors) {
20
- const hasError = errors.some((e) => e.type !== 'ConfigWarning');
21
23
  return {
22
24
  position: 'fixed',
23
25
  bottom: 0,
24
26
  left: 0,
25
27
  right: 0,
26
28
  height: 28,
27
- backgroundColor: hasError ? '#cf1322' : '#d48806',
29
+ backgroundColor: getErrorBarColor(errors),
28
30
  color: '#fff',
29
31
  display: 'flex',
30
32
  alignItems: 'center',
@@ -38,17 +40,6 @@ function getBarStyle(errors) {
38
40
  };
39
41
  }
40
42
 
41
- function formatErrorsForCopy(errors) {
42
- return errors
43
- .map((e) => {
44
- let text = `[${e.type}] ${e.message}`;
45
- if (e.source) text += `\n Source: ${e.source}`;
46
- if (e.stack) text += `\n${e.stack}`;
47
- return text;
48
- })
49
- .join('\n\n');
50
- }
51
-
52
43
  function CopyIcon() {
53
44
  return (
54
45
  <svg
@@ -112,6 +103,19 @@ const ErrorBar = ({ errors }) => {
112
103
  }}
113
104
  >
114
105
  <span style={{ opacity: 0.8 }}>{latest.type}: </span>
106
+ {latest.prodError === true && (
107
+ <span
108
+ style={{
109
+ backgroundColor: 'rgba(255,255,255,0.25)',
110
+ borderRadius: 8,
111
+ padding: '1px 7px',
112
+ fontSize: 11,
113
+ marginRight: 6,
114
+ }}
115
+ >
116
+ fails in prod
117
+ </span>
118
+ )}
115
119
  <span>{latest.message}</span>
116
120
  {latest.source && <span style={{ opacity: 0.7, marginLeft: 8 }}>{latest.source}</span>}
117
121
  </div>
@@ -0,0 +1,56 @@
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 React from 'react';
18
+
19
+ // Shown while the 401 sign-in redirect navigates. The link is the escape
20
+ // hatch for a dropped navigation - the tab must never depend on a single
21
+ // window.location.assign to leave this screen.
22
+ const RedirectingPage = ({ redirect }) => {
23
+ return (
24
+ <div
25
+ style={{
26
+ height: '100vh',
27
+ textAlign: 'center',
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ backgroundColor: 'var(--ant-color-bg-layout)',
33
+ color: 'var(--ant-color-text)',
34
+ fontFamily:
35
+ "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
36
+ }}
37
+ >
38
+ <div
39
+ style={{
40
+ verticalAlign: 'middle',
41
+ display: 'inline-block',
42
+ }}
43
+ >
44
+ <h3>Redirecting to sign-in...</h3>
45
+ <p>
46
+ <a href={redirect} style={{ color: 'var(--ant-color-primary)' }}>
47
+ Continue to sign-in
48
+ </a>{' '}
49
+ if you are not redirected automatically.
50
+ </p>
51
+ </div>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ export default RedirectingPage;
@@ -14,8 +14,6 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- /* eslint-disable react/jsx-props-no-spreading */
18
-
19
17
  import React from 'react';
20
18
  import AuthConfigured from './AuthConfigured.jsx';
21
19
  import AuthNotConfigured from './AuthNotConfigured.js';
@@ -25,20 +23,27 @@ import { serializer } from '@lowdefy/helpers';
25
23
  // Client code imports the build JSON directly — Vite handles JSON imports;
26
24
  // the lib/build/*.js wrappers are server-only (they read from disk).
27
25
  // Deserialize to restore arrays from their ~arr build markers (providers
28
- // must be a real array for single-provider inference in createAuthMethods).
26
+ // must be a real array for provider lookups in createAuthMethods).
29
27
  import rawAuthConfig from '../../../build/auth.json';
30
28
 
31
29
  const authConfig = serializer.deserialize(rawAuthConfig);
32
30
 
33
- function Auth({ children, session }) {
31
+ function Auth({ children, user }) {
34
32
  if (authConfig.configured === true) {
35
33
  return (
36
- <AuthConfigured serverSession={session} authConfig={authConfig}>
34
+ <AuthConfigured serverUser={user} authConfig={authConfig}>
37
35
  {(auth) => children(auth)}
38
36
  </AuthConfigured>
39
37
  );
40
38
  }
41
- return <AuthNotConfigured authConfig={authConfig}>{(auth) => children(auth)}</AuthNotConfigured>;
39
+ // An app whose only auth key is auth.dev has no auth stack, but the dev
40
+ // server still resolves a caller for auth.dev.mockUser - pass it through so
41
+ // _user reads the same identity in the browser as on the server.
42
+ return (
43
+ <AuthNotConfigured authConfig={authConfig} user={user}>
44
+ {(auth) => children(auth)}
45
+ </AuthNotConfigured>
46
+ );
42
47
  }
43
48
 
44
49
  export default Auth;
@@ -14,59 +14,224 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- /* eslint-disable react/jsx-props-no-spreading */
18
-
19
- import React, { useEffect, useRef } from 'react';
17
+ import React, { useEffect, useRef, useState } from 'react';
18
+ import { createAuthClient } from 'better-auth/react';
20
19
  import {
21
- authConfigManager,
22
- getSession,
23
- SessionProvider,
24
- signIn,
25
- signOut,
26
- useSession,
27
- } from '@hono/auth-js/react';
20
+ adminClient,
21
+ emailOTPClient,
22
+ magicLinkClient,
23
+ organizationClient,
24
+ phoneNumberClient,
25
+ twoFactorClient,
26
+ } from 'better-auth/client/plugins';
27
+ import { oauthProviderClient } from '@better-auth/oauth-provider/client';
28
+ import { passkeyClient } from '@better-auth/passkey/client';
29
+
30
+ import { normalizeCaller } from '@lowdefy/helpers';
28
31
 
29
32
  // The app basePath, from Vite's BASE_URL (`${config.basePath}/`, set in
30
33
  // vite.config.js). build/config.json is server-only - it carries every
31
34
  // deployment environment's settings - so it is never imported into the client.
32
35
  const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
33
36
 
34
- // @hono/auth-js/react configures its fetch paths through a module-level
35
- // manager instead of SessionProvider props.
36
- if (basePath) {
37
- authConfigManager.setConfig({ basePath: `${basePath}/api/auth` });
38
- }
37
+ // GenericOAuth providers have no client plugin here. BetterAuth 1.7.0 dropped
38
+ // genericOAuthClient from better-auth/client/plugins, and nothing replaces it:
39
+ // through 1.6.x it was a marker plugin carrying only an id, a version, an empty
40
+ // $InferServerPlugin for TypeScript inference, and the generic-oauth error
41
+ // codes - no actions, atoms, fetch plugins or path methods. signIn.oauth2 is
42
+ // resolved by the client's dynamic path proxy from the call itself
43
+ // (signIn.oauth2 -> POST /sign-in/oauth2, POST because providerId is always
44
+ // sent), so the sign-in a Lowdefy GenericOAuth provider makes is unaffected.
45
+ const authClient = createAuthClient({
46
+ baseURL: `${window.location.origin}${basePath}/api/auth`,
47
+ plugins: [
48
+ adminClient(),
49
+ emailOTPClient(),
50
+ magicLinkClient(),
51
+ oauthProviderClient(),
52
+ organizationClient(),
53
+ passkeyClient(),
54
+ phoneNumberClient(),
55
+ twoFactorClient(),
56
+ ],
57
+ });
58
+
59
+ // UpdateSession owns session freshness - suppress BetterAuth's own
60
+ // signal-driven refetch on the calls that would otherwise fire one, so
61
+ // nothing competes with (and aborts) the refetch UpdateSession awaits.
62
+ // Only calls that mutate an existing session are suppressed - calls that
63
+ // can establish one (the sign-ins, and the two-factor, phone-number,
64
+ // email-otp and passkey verifies) keep their signal: its refetch is what makes the store
65
+ // notice a login when no navigation follows.
66
+ // disableSignal silences all atomListeners for the call, including the
67
+ // organization plugin's org atoms - none are exposed to app config, so a
68
+ // future design exposing one must refresh it through its own action.
69
+ const sessionScoped = (params) => ({
70
+ ...params,
71
+ fetchOptions: { ...params?.fetchOptions, disableSignal: true },
72
+ });
39
73
 
40
- function Session({ children }) {
41
- const wasAuthenticated = useRef(false);
42
- const { data: session, status } = useSession();
43
- wasAuthenticated.current = wasAuthenticated.current || status === 'authenticated';
74
+ // The server resolves the caller per request and embeds it in the page
75
+ // config, so the first render never flashes unauthenticated. The BetterAuth
76
+ // client store takes over once its session fetch settles.
77
+ //
78
+ // Roles and merged attributes resolve server-side from the active member row
79
+ // - the base session carries neither. The last server-resolved caller is
80
+ // held in state: while the session user is unchanged it stays authoritative
81
+ // for the fields the session does not carry, and UpdateSession refreshes it
82
+ // from /api/user after a change (e.g. SetActiveOrganization). Holding it in
83
+ // state, not a ref, re-renders Session when it changes so _user recomputes -
84
+ // the same reactive footing the session half already stands on.
85
+ //
86
+ // Only the session half is passed through normalizeCaller. BetterAuth's store
87
+ // returns camelCase keys whatever the columns are named, so untransformed it
88
+ // would sit emailVerified beside the resolved caller's email_verified on one
89
+ // object; anything arriving from the server is already snake_case.
90
+ function Session({ children, reloadSuppressedRef, serverUser }) {
91
+ const { data: session, isPending } = authClient.useSession();
92
+ const [resolvedUser, setResolvedUser] = useState(serverUser);
93
+ const wasAuthenticated = useRef(Boolean(serverUser));
44
94
 
45
95
  useEffect(() => {
46
- if (wasAuthenticated.current && status === 'unauthenticated') {
96
+ if (session) {
97
+ wasAuthenticated.current = true;
98
+ }
99
+ // Reload after sign-out (or session revocation) so the server can apply
100
+ // the page auth fork - a protected page redirects to the login page.
101
+ // A logout with a callbackUrl suppresses the reload - its own navigation
102
+ // would otherwise race this one.
103
+ if (wasAuthenticated.current && !isPending && !session && !reloadSuppressedRef.current) {
47
104
  window.location.reload();
48
105
  }
49
- }, [status]);
106
+ }, [session, isPending]);
50
107
 
51
- // If session is passed to SessionProvider from the server-rendered config
52
- // we won't have a loading state here, but unauthenticated first loads do.
53
- if (status === 'loading') {
54
- return '';
108
+ if (isPending) {
109
+ return children(resolvedUser, setResolvedUser);
110
+ }
111
+ if (!session?.user) {
112
+ return children(null, setResolvedUser);
113
+ }
114
+ const resolved = resolvedUser;
115
+ if (!resolved) {
116
+ return children({ roles: [], ...normalizeCaller(session.user) }, setResolvedUser);
117
+ }
118
+ if (resolved.id !== session.user.id) {
119
+ // The ref has not caught up with a changed session user - a sign-in as a
120
+ // different person lands between the session refetch and the /api/user
121
+ // result. Render the last resolved caller whole rather than mixing the new
122
+ // session user with the previous person's resolved fields, or falling back
123
+ // to a caller with no roles - blocks re-evaluate operators on render, so a
124
+ // role-guarded page would act on an empty role list. UpdateSession closes
125
+ // the window when it lands the new caller; a flow that changes the session
126
+ // user without chaining it leaves the previous caller rendered until the
127
+ // next page load.
128
+ return children(resolved, setResolvedUser);
55
129
  }
56
- return children(session);
130
+ // _user is one object with one meaning on both sides: the resolved caller is
131
+ // spread whole, so every field resolveAuthentication emits is readable in app
132
+ // config without this file naming it. session.user is the floor - the client
133
+ // store may hold a fresher one than the resolved state, and the resolved
134
+ // object wins wherever both carry a key.
135
+ const user = { ...normalizeCaller(session.user), ...resolved };
136
+ return children(user, setResolvedUser);
57
137
  }
58
138
 
59
- function AuthConfigured({ authConfig, children, serverSession }) {
60
- const auth = { authConfig, getSession, signIn, signOut };
139
+ function AuthConfigured({ authConfig, children, serverUser }) {
140
+ const reloadSuppressedRef = useRef(false);
141
+ const auth = {
142
+ authConfig,
143
+ getSession: ({ disableCookieCache } = {}) =>
144
+ authClient.getSession(disableCookieCache ? { query: { disableCookieCache: true } } : {}),
145
+ // Refetches the session through the session atom - the store's only
146
+ // writer - and resolves only after the store is written. fetchSession
147
+ // returns undefined and never rejects (failures are written into the
148
+ // atom), so the atom's own { data, error } is read back and returned,
149
+ // keeping unwrap working and surfacing a failed refresh to the action.
150
+ refreshSession: async ({ disableCookieCache } = {}) => {
151
+ const sessionAtom = authClient.$store.atoms.session;
152
+ await sessionAtom
153
+ .get()
154
+ .refetch(disableCookieCache ? { query: { disableCookieCache: true } } : undefined);
155
+ const { data, error } = sessionAtom.get();
156
+ return { data, error };
157
+ },
158
+ // The server-resolved caller - roles from the active member row and the
159
+ // merged attributes bag - for re-syncing after session changes.
160
+ getResolvedUser: async () => {
161
+ const response = await fetch(`${basePath}/api/user`, {
162
+ credentials: 'same-origin',
163
+ });
164
+ if (!response.ok) {
165
+ // /api/user itself always answers 200 { user } - user: null when no
166
+ // caller is admitted, which updateSession branches on. A non-OK
167
+ // response is a transport failure (gateway error page, 5xx) with no
168
+ // parsable body.
169
+ throw new Error(`Failed to fetch the resolved user (HTTP ${response.status}).`);
170
+ }
171
+ return response.json();
172
+ },
173
+ suppressSignOutReload: () => {
174
+ reloadSuppressedRef.current = true;
175
+ },
176
+ acceptInvitation: (params) => authClient.organization.acceptInvitation(sessionScoped(params)),
177
+ // addPasskey runs the WebAuthn browser ceremony itself - options fetch,
178
+ // authenticator prompt, verification.
179
+ addPasskey: (params) => authClient.passkey.addPasskey(params),
180
+ changePassword: (params) => authClient.changePassword(sessionScoped(params)),
181
+ deletePasskey: (params) => authClient.passkey.deletePasskey(params),
182
+ updatePasskey: (params) => authClient.passkey.updatePasskey(params),
183
+ leaveOrganization: (params) => authClient.organization.leave(sessionScoped(params)),
184
+ // The caller's memberships from the org plugin, keyed on the session
185
+ // alone - not the active organization. sessionScoped so the list read
186
+ // does not also fire the org-list atom's own refetch of the same route.
187
+ listOrganizations: () => authClient.organization.list(sessionScoped()),
188
+ // POST /oauth2/consent. The signed authorization query is not passed
189
+ // here - the oauthProviderClient fetch plugin stamps it onto the body as
190
+ // oauth_query from window.location.search, so the call must run while
191
+ // the page still holds the query the oauth-provider redirect arrived
192
+ // with.
193
+ oauth2Consent: (params) => authClient.oauth2.consent(params),
194
+ // POST /oauth2/continue - resumes the authorization after the post-login
195
+ // organization choice (the page runs SetActiveOrganization first). Same
196
+ // oauth_query contract as oauth2Consent: the signed query rides from
197
+ // window.location.search, so the call must run before any navigation.
198
+ oauth2Continue: (params) => authClient.oauth2.continue(params),
199
+ // The one-time email code: send, then sign in with it.
200
+ emailOtpSend: (params) => authClient.emailOtp.sendVerificationOtp(params),
201
+ emailOtpVerify: (params) => authClient.signIn.emailOtp(params),
202
+ phoneNumberRequestPasswordReset: (params) =>
203
+ authClient.phoneNumber.requestPasswordReset(params),
204
+ phoneNumberResetPassword: (params) => authClient.phoneNumber.resetPassword(params),
205
+ phoneNumberSendOtp: (params) => authClient.phoneNumber.sendOtp(params),
206
+ phoneNumberVerify: (params) => authClient.phoneNumber.verify(params),
207
+ requestPasswordReset: (params) => authClient.requestPasswordReset(params),
208
+ resetPassword: (params) => authClient.resetPassword(params),
209
+ revokeOtherSessions: () => authClient.revokeOtherSessions(sessionScoped()),
210
+ sendVerificationEmail: (params) => authClient.sendVerificationEmail(params),
211
+ setActiveOrganization: (params) => authClient.organization.setActive(sessionScoped(params)),
212
+ signInEmail: (params) => authClient.signIn.email(params),
213
+ signInMagicLink: (params) => authClient.signIn.magicLink(params),
214
+ signInOauth2: (params) => authClient.signIn.oauth2(params),
215
+ signInPasskey: (params) => authClient.signIn.passkey(params),
216
+ signInPhoneNumber: (params) => authClient.signIn.phoneNumber(params),
217
+ signInSocial: (params) => authClient.signIn.social(params),
218
+ signOut: () => authClient.signOut(),
219
+ signUpEmail: (params) => authClient.signUp.email(params),
220
+ twoFactorDisable: (params) => authClient.twoFactor.disable(sessionScoped(params)),
221
+ twoFactorEnable: (params) => authClient.twoFactor.enable(sessionScoped(params)),
222
+ twoFactorGenerateBackupCodes: (params) =>
223
+ authClient.twoFactor.generateBackupCodes(sessionScoped(params)),
224
+ twoFactorVerifyBackupCode: (params) => authClient.twoFactor.verifyBackupCode(params),
225
+ twoFactorVerifyTotp: (params) => authClient.twoFactor.verifyTotp(params),
226
+ };
61
227
  return (
62
- <SessionProvider session={serverSession}>
63
- <Session>
64
- {(session) => {
65
- auth.session = session;
66
- return children(auth);
67
- }}
68
- </Session>
69
- </SessionProvider>
228
+ <Session reloadSuppressedRef={reloadSuppressedRef} serverUser={serverUser}>
229
+ {(user, setResolvedUser) => {
230
+ auth.user = user;
231
+ auth.updateResolvedUser = (resolved) => setResolvedUser(resolved);
232
+ return children(auth);
233
+ }}
234
+ </Session>
70
235
  );
71
236
  }
72
237
 
@@ -13,18 +13,60 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
- /* eslint-disable react/jsx-props-no-spreading */
16
+
17
+ import { ConfigError } from '@lowdefy/errors';
17
18
 
18
19
  function authNotConfigured() {
19
- throw new Error('Auth not configured.');
20
+ throw new ConfigError(
21
+ 'Auth is not configured. Add an "auth" section to lowdefy.yaml to use auth actions.'
22
+ );
20
23
  }
21
24
 
22
- function AuthNotConfigured({ authConfig, children }) {
25
+ // Every key AuthConfigured exposes must be present here, or an auth action in an
26
+ // app without an auth section fails with "auth.x is not a function" instead of
27
+ // the ConfigError that names the missing config.
28
+ function AuthNotConfigured({ authConfig, children, user = null }) {
23
29
  const auth = {
24
30
  authConfig,
31
+ user,
32
+ acceptInvitation: authNotConfigured,
33
+ addPasskey: authNotConfigured,
34
+ changePassword: authNotConfigured,
35
+ deletePasskey: authNotConfigured,
36
+ getResolvedUser: authNotConfigured,
25
37
  getSession: authNotConfigured,
26
- signIn: authNotConfigured,
38
+ leaveOrganization: authNotConfigured,
39
+ listOrganizations: authNotConfigured,
40
+ oauth2Consent: authNotConfigured,
41
+ oauth2Continue: authNotConfigured,
42
+ emailOtpSend: authNotConfigured,
43
+ emailOtpVerify: authNotConfigured,
44
+ phoneNumberRequestPasswordReset: authNotConfigured,
45
+ phoneNumberResetPassword: authNotConfigured,
46
+ phoneNumberSendOtp: authNotConfigured,
47
+ phoneNumberVerify: authNotConfigured,
48
+ refreshSession: authNotConfigured,
49
+ requestPasswordReset: authNotConfigured,
50
+ resetPassword: authNotConfigured,
51
+ revokeOtherSessions: authNotConfigured,
52
+ sendVerificationEmail: authNotConfigured,
53
+ setActiveOrganization: authNotConfigured,
54
+ signInEmail: authNotConfigured,
55
+ signInMagicLink: authNotConfigured,
56
+ signInOauth2: authNotConfigured,
57
+ signInPasskey: authNotConfigured,
58
+ signInPhoneNumber: authNotConfigured,
59
+ signInSocial: authNotConfigured,
27
60
  signOut: authNotConfigured,
61
+ signUpEmail: authNotConfigured,
62
+ suppressSignOutReload: authNotConfigured,
63
+ twoFactorDisable: authNotConfigured,
64
+ twoFactorEnable: authNotConfigured,
65
+ twoFactorGenerateBackupCodes: authNotConfigured,
66
+ twoFactorVerifyBackupCode: authNotConfigured,
67
+ twoFactorVerifyTotp: authNotConfigured,
68
+ updatePasskey: authNotConfigured,
69
+ updateResolvedUser: authNotConfigured,
28
70
  };
29
71
 
30
72
  return children(auth);