@lowdefy/server-dev 0.0.0-experimental-20260909201552 → 0.0.0-experimental-20260911113654

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/client/App.jsx +2 -3
  2. package/client/Inspector.jsx +1 -1
  3. package/client/Page.jsx +0 -13
  4. package/lib/client/ErrorBar.jsx +13 -17
  5. package/lib/client/auth/Auth.jsx +6 -11
  6. package/lib/client/auth/AuthConfigured.jsx +37 -200
  7. package/lib/client/auth/AuthNotConfigured.js +4 -46
  8. package/lib/client/utils/request.js +1 -8
  9. package/lib/client/utils/usePageConfig.js +0 -22
  10. package/lib/docs/clientErrorStore.js +0 -3
  11. package/lib/docs/createDocsMcpServer.js +13 -149
  12. package/lib/docs/createDocsMcpServer.test.mjs +2 -225
  13. package/lib/docs/docs.test.mjs +1 -72
  14. package/lib/docs/evalOperatorHeadless.js +1 -7
  15. package/lib/docs/findConfig.js +39 -84
  16. package/lib/docs/getBrowser.js +39 -51
  17. package/lib/docs/getBrowser.test.mjs +3 -77
  18. package/lib/docs/getBuildStatus.js +1 -4
  19. package/lib/docs/getCoreDoc.js +1 -9
  20. package/lib/docs/getOverview.js +5 -15
  21. package/lib/docs/getSchema.js +1 -5
  22. package/lib/docs/inspectStateHeadless.js +1 -7
  23. package/lib/docs/loadState.js +3 -15
  24. package/lib/docs/runRequest.js +23 -18
  25. package/lib/docs/screenshotPage.js +1 -6
  26. package/lib/docs/setupTestFixtures.mjs +6 -82
  27. package/lib/server/auth/{getStrategies.js → getAuthConfig.js} +10 -12
  28. package/lib/server/auth/getDevSession.js +70 -0
  29. package/lib/server/auth/getDevSession.test.mjs +138 -0
  30. package/lib/server/auth/getHeadlessUser.js +8 -7
  31. package/lib/server/auth/getMockUser.js +14 -18
  32. package/lib/server/auth/headlessUser.js +2 -3
  33. package/lib/server/auth/resolveHeadlessUser.test.mjs +4 -0
  34. package/lib/server/auth/session.js +39 -0
  35. package/lib/server/createLowdefyContext.js +45 -91
  36. package/lib/server/jitPageBuilder.js +3 -0
  37. package/lib/server/log/createHandleError.js +0 -26
  38. package/lib/server/log/createHandleError.test.mjs +2 -74
  39. package/lib/server/log/logRequest.js +3 -1
  40. package/manager/processes/startServer.mjs +2 -0
  41. package/manager/processes/startWatchers.mjs +0 -4
  42. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  43. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  44. package/manager/run.mjs +0 -14
  45. package/manager/utils/createCustomPluginTypesMap.mjs +2 -2
  46. package/manager/utils/setupWatcher.mjs +1 -1
  47. package/manager/watchers/serverArtifactWatcher.mjs +2 -0
  48. package/package.json +34 -38
  49. package/package.original.json +34 -38
  50. package/src/app.js +15 -60
  51. package/src/html/renderDevPage.js +1 -4
  52. package/src/middleware/errorHandler.js +1 -36
  53. package/src/middleware/errorHandler.test.mjs +3 -122
  54. package/src/routes/agent.js +1 -3
  55. package/src/routes/apiPage.js +37 -0
  56. package/src/routes/auth.js +21 -15
  57. package/src/routes/auth.test.mjs +84 -0
  58. package/src/routes/clientError.js +11 -9
  59. package/src/routes/cron.js +1 -3
  60. package/src/routes/cronForward.js +1 -3
  61. package/src/routes/detached.js +7 -13
  62. package/src/routes/devInspect.js +1 -3
  63. package/src/routes/docs/find.js +1 -9
  64. package/src/routes/docs/mcp.js +2 -28
  65. package/src/routes/docs/runRequest.js +8 -23
  66. package/src/routes/endpoints.js +10 -17
  67. package/src/routes/feedback.js +12 -7
  68. package/src/routes/jitPage.js +2 -42
  69. package/src/routes/mcp.js +5 -71
  70. package/src/routes/request.js +1 -3
  71. package/src/routes/request.test.mjs +5 -8
  72. package/src/routes/usage.js +1 -3
  73. package/lib/client/RedirectingPage.jsx +0 -56
  74. package/lib/client/auth/authNotConfiguredParity.test.mjs +0 -61
  75. package/lib/client/auth/authTemplateSync.test.mjs +0 -40
  76. package/lib/client/auth/betterAuthSessionContract.test.mjs +0 -181
  77. package/lib/client/utils/formatErrorsForCopy.js +0 -30
  78. package/lib/client/utils/formatErrorsForCopy.test.mjs +0 -39
  79. package/lib/client/utils/getErrorBarColor.js +0 -25
  80. package/lib/client/utils/getErrorBarColor.test.mjs +0 -34
  81. package/lib/docs/clientErrorStore.test.mjs +0 -42
  82. package/lib/docs/devEventBus.js +0 -136
  83. package/lib/docs/devEventBus.test.mjs +0 -225
  84. package/lib/docs/getHazards.js +0 -115
  85. package/lib/docs/getHazards.test.mjs +0 -175
  86. package/lib/docs/getStaleStatus.js +0 -36
  87. package/lib/docs/getStaleStatus.test.mjs +0 -74
  88. package/lib/docs/isPageReady.js +0 -53
  89. package/lib/docs/isPageReady.test.mjs +0 -129
  90. package/lib/docs/loadFrameworkHazards.js +0 -40
  91. package/lib/docs/loadFrameworkHazards.test.mjs +0 -43
  92. package/lib/docs/requestRestart.js +0 -30
  93. package/lib/docs/requestRestart.test.mjs +0 -49
  94. package/lib/docs/runEndpoint.js +0 -106
  95. package/lib/docs/runEndpoint.test.mjs +0 -244
  96. package/lib/docs/runJourney.js +0 -674
  97. package/lib/docs/runJourney.test.mjs +0 -952
  98. package/lib/docs/runRequest.test.mjs +0 -127
  99. package/lib/docs/serverErrorStore.js +0 -40
  100. package/lib/docs/serverErrorStore.test.mjs +0 -45
  101. package/lib/docs/truncateResponse.js +0 -39
  102. package/lib/docs/truncateResponse.test.mjs +0 -49
  103. package/lib/docs/unsettledPageNote.js +0 -24
  104. package/lib/docs/validateJourneySteps.js +0 -244
  105. package/lib/docs/validateJourneySteps.test.mjs +0 -183
  106. package/lib/server/auth/createSystemContext.js +0 -70
  107. package/lib/server/auth/getAuth.js +0 -46
  108. package/lib/server/loadDynamicJsMap.js +0 -47
  109. package/manager/utils/acquireManagerLock.mjs +0 -63
  110. package/manager/utils/acquireManagerLock.test.mjs +0 -79
  111. package/manager/watchers/pluginSourceWatcher.mjs +0 -57
  112. package/manager/watchers/pluginSourceWatcher.test.mjs +0 -103
  113. package/manager/watchers/restartRequestWatcher.mjs +0 -62
  114. package/manager/watchers/restartRequestWatcher.test.mjs +0 -76
  115. package/manager/watchers/selectWatchedPluginPackages.mjs +0 -74
  116. package/manager/watchers/selectWatchedPluginPackages.test.mjs +0 -112
  117. package/src/middleware/createSameOriginGuard.js +0 -68
  118. package/src/middleware/createSameOriginGuard.test.mjs +0 -102
  119. package/src/middleware/staleFlag.js +0 -76
  120. package/src/middleware/staleFlag.test.mjs +0 -127
  121. package/src/routes/docs/events.js +0 -50
  122. package/src/routes/docs/events.test.mjs +0 -117
  123. package/src/routes/docs/journey.js +0 -66
  124. package/src/routes/docs/journey.test.mjs +0 -147
  125. package/src/routes/docs/mcp.test.mjs +0 -130
  126. package/src/routes/docs/restart.js +0 -35
  127. package/src/routes/docs/restart.test.mjs +0 -64
  128. package/src/routes/docs/runEndpoint.js +0 -51
  129. package/src/routes/docs/runEndpoint.test.mjs +0 -127
  130. package/src/routes/docs/runRequest.test.mjs +0 -82
  131. package/src/routes/jitPage.test.mjs +0 -107
  132. package/src/routes/mcp.test.mjs +0 -199
  133. package/src/routes/mcpProtectedResourceMetadata.js +0 -34
  134. package/src/routes/mcpProtectedResourceMetadata.test.mjs +0 -65
  135. package/src/routes/mountOauthDiscovery.js +0 -44
  136. package/src/routes/mountOauthDiscovery.test.mjs +0 -79
  137. package/src/routes/normalizeMcpProtocolVersionHeader.js +0 -46
  138. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +0 -62
  139. package/src/routes/user.js +0 -27
  140. package/src/routes/wellKnownFallback.js +0 -30
  141. package/src/routes/wellKnownFallback.test.mjs +0 -32
package/client/App.jsx CHANGED
@@ -40,7 +40,7 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
40
40
  return children;
41
41
  }
42
42
 
43
- function App({ config, router }) {
43
+ function App({ router }) {
44
44
  const lowdefyRef = useRef({});
45
45
  const [runtimeErrors, setRuntimeErrors] = useState([]);
46
46
  // Subscribe to rootConfig SWR cache — deduplicates with Routing's fetch.
@@ -88,7 +88,6 @@ function App({ config, router }) {
88
88
  message: error.message,
89
89
  source: error.source,
90
90
  stack: error.stack,
91
- prodError: error.prodError === true,
92
91
  },
93
92
  ]);
94
93
  }, []);
@@ -144,7 +143,7 @@ function App({ config, router }) {
144
143
  />
145
144
  }
146
145
  >
147
- <Auth user={config?.user}>
146
+ <Auth>
148
147
  {(auth) => {
149
148
  return <Routing auth={auth} lowdefy={lowdefyRef.current} router={router} />;
150
149
  }}
@@ -123,7 +123,7 @@ async function bootstrapFromCheckpoint({ basePath, pageId, checkpointName, isCan
123
123
  context._internal.update();
124
124
  } catch (error) {
125
125
  // eslint-disable-next-line no-console
126
- console.error(`Failed to load checkpoint "${checkpointName}":`, error);
126
+ console.error('Failed to load checkpoint "%s":', checkpointName, error);
127
127
  }
128
128
  }
129
129
 
package/client/Page.jsx CHANGED
@@ -20,7 +20,6 @@ import Client from '@lowdefy/client';
20
20
 
21
21
  import BuildErrorPage from '../lib/client/BuildErrorPage.jsx';
22
22
  import InstallingPluginsPage from '../lib/client/InstallingPluginsPage.jsx';
23
- import RedirectingPage from '../lib/client/RedirectingPage.jsx';
24
23
  import usePageConfig from '../lib/client/utils/usePageConfig.js';
25
24
 
26
25
  const Page = ({
@@ -47,22 +46,10 @@ const Page = ({
47
46
  }
48
47
  }, [pageConfig?._warnings, lowdefy]);
49
48
 
50
- // Full load to the sign-in page so it can return here after sign-in — an
51
- // effect, not a fetcher side effect, so the redirect re-fires if the same
52
- // cached result renders again.
53
- useEffect(() => {
54
- if (pageConfig?.authRedirect) {
55
- window.location.assign(pageConfig.authRedirect);
56
- }
57
- }, [pageConfig?.authRedirect]);
58
-
59
49
  if (!pageConfig) {
60
50
  router.replace({ pathname: '/404' });
61
51
  return '';
62
52
  }
63
- if (pageConfig.authRedirect) {
64
- return <RedirectingPage redirect={pageConfig.authRedirect} />;
65
- }
66
53
  if (pageConfig.buildError) {
67
54
  return (
68
55
  <BuildErrorPage
@@ -16,17 +16,15 @@
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
-
22
19
  function getBarStyle(errors) {
20
+ const hasError = errors.some((e) => e.type !== 'ConfigWarning');
23
21
  return {
24
22
  position: 'fixed',
25
23
  bottom: 0,
26
24
  left: 0,
27
25
  right: 0,
28
26
  height: 28,
29
- backgroundColor: getErrorBarColor(errors),
27
+ backgroundColor: hasError ? '#cf1322' : '#d48806',
30
28
  color: '#fff',
31
29
  display: 'flex',
32
30
  alignItems: 'center',
@@ -40,6 +38,17 @@ function getBarStyle(errors) {
40
38
  };
41
39
  }
42
40
 
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
+
43
52
  function CopyIcon() {
44
53
  return (
45
54
  <svg
@@ -103,19 +112,6 @@ const ErrorBar = ({ errors }) => {
103
112
  }}
104
113
  >
105
114
  <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
- )}
119
115
  <span>{latest.message}</span>
120
116
  {latest.source && <span style={{ opacity: 0.7, marginLeft: 8 }}>{latest.source}</span>}
121
117
  </div>
@@ -14,6 +14,8 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ /* eslint-disable react/jsx-props-no-spreading */
18
+
17
19
  import React from 'react';
18
20
  import AuthConfigured from './AuthConfigured.jsx';
19
21
  import AuthNotConfigured from './AuthNotConfigured.js';
@@ -23,27 +25,20 @@ import { serializer } from '@lowdefy/helpers';
23
25
  // Client code imports the build JSON directly — Vite handles JSON imports;
24
26
  // the lib/build/*.js wrappers are server-only (they read from disk).
25
27
  // Deserialize to restore arrays from their ~arr build markers (providers
26
- // must be a real array for provider lookups in createAuthMethods).
28
+ // must be a real array for single-provider inference in createAuthMethods).
27
29
  import rawAuthConfig from '../../../build/auth.json';
28
30
 
29
31
  const authConfig = serializer.deserialize(rawAuthConfig);
30
32
 
31
- function Auth({ children, user }) {
33
+ function Auth({ children, session }) {
32
34
  if (authConfig.configured === true) {
33
35
  return (
34
- <AuthConfigured serverUser={user} authConfig={authConfig}>
36
+ <AuthConfigured serverSession={session} authConfig={authConfig}>
35
37
  {(auth) => children(auth)}
36
38
  </AuthConfigured>
37
39
  );
38
40
  }
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
- );
41
+ return <AuthNotConfigured authConfig={authConfig}>{(auth) => children(auth)}</AuthNotConfigured>;
47
42
  }
48
43
 
49
44
  export default Auth;
@@ -14,223 +14,60 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import React, { useEffect, useRef, useState } from 'react';
18
- import { createAuthClient } from 'better-auth/react';
17
+ /* eslint-disable react/jsx-props-no-spreading */
18
+
19
+ import React, { useEffect, useRef } from 'react';
19
20
  import {
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';
21
+ authConfigManager,
22
+ getSession,
23
+ SessionProvider,
24
+ signIn,
25
+ signOut,
26
+ useSession,
27
+ } from '@hono/auth-js/react';
29
28
 
30
- import { normalizeCaller, serializer } from '@lowdefy/helpers';
29
+ import { serializer } from '@lowdefy/helpers';
31
30
 
32
31
  import rawLowdefyConfig from '../../../build/config.json';
33
32
 
34
33
  const lowdefyConfig = serializer.deserialize(rawLowdefyConfig);
35
34
 
36
- // GenericOAuth providers have no client plugin here. BetterAuth 1.7.0 dropped
37
- // genericOAuthClient from better-auth/client/plugins, and nothing replaces it:
38
- // through 1.6.x it was a marker plugin carrying only an id, a version, an empty
39
- // $InferServerPlugin for TypeScript inference, and the generic-oauth error
40
- // codes - no actions, atoms, fetch plugins or path methods. signIn.oauth2 is
41
- // resolved by the client's dynamic path proxy from the call itself
42
- // (signIn.oauth2 -> POST /sign-in/oauth2, POST because providerId is always
43
- // sent), so the sign-in a Lowdefy GenericOAuth provider makes is unaffected.
44
- const authClient = createAuthClient({
45
- baseURL: `${window.location.origin}${lowdefyConfig.basePath ?? ''}/api/auth`,
46
- plugins: [
47
- adminClient(),
48
- emailOTPClient(),
49
- magicLinkClient(),
50
- oauthProviderClient(),
51
- organizationClient(),
52
- passkeyClient(),
53
- phoneNumberClient(),
54
- twoFactorClient(),
55
- ],
56
- });
57
-
58
- // UpdateSession owns session freshness - suppress BetterAuth's own
59
- // signal-driven refetch on the calls that would otherwise fire one, so
60
- // nothing competes with (and aborts) the refetch UpdateSession awaits.
61
- // Only calls that mutate an existing session are suppressed - calls that
62
- // can establish one (the sign-ins, and the two-factor, phone-number,
63
- // email-otp and passkey verifies) keep their signal: its refetch is what makes the store
64
- // notice a login when no navigation follows.
65
- // disableSignal silences all atomListeners for the call, including the
66
- // organization plugin's org atoms - none are exposed to app config, so a
67
- // future design exposing one must refresh it through its own action.
68
- const sessionScoped = (params) => ({
69
- ...params,
70
- fetchOptions: { ...params?.fetchOptions, disableSignal: true },
71
- });
35
+ // @hono/auth-js/react configures its fetch paths through a module-level
36
+ // manager instead of SessionProvider props.
37
+ if (lowdefyConfig.basePath) {
38
+ authConfigManager.setConfig({ basePath: `${lowdefyConfig.basePath}/api/auth` });
39
+ }
72
40
 
73
- // The server resolves the caller per request and embeds it in the page
74
- // config, so the first render never flashes unauthenticated. The BetterAuth
75
- // client store takes over once its session fetch settles.
76
- //
77
- // Roles and merged attributes resolve server-side from the active member row
78
- // - the base session carries neither. The last server-resolved caller is
79
- // held in state: while the session user is unchanged it stays authoritative
80
- // for the fields the session does not carry, and UpdateSession refreshes it
81
- // from /api/user after a change (e.g. SetActiveOrganization). Holding it in
82
- // state, not a ref, re-renders Session when it changes so _user recomputes -
83
- // the same reactive footing the session half already stands on.
84
- //
85
- // Only the session half is passed through normalizeCaller. BetterAuth's store
86
- // returns camelCase keys whatever the columns are named, so untransformed it
87
- // would sit emailVerified beside the resolved caller's email_verified on one
88
- // object; anything arriving from the server is already snake_case.
89
- function Session({ children, reloadSuppressedRef, serverUser }) {
90
- const { data: session, isPending } = authClient.useSession();
91
- const [resolvedUser, setResolvedUser] = useState(serverUser);
92
- const wasAuthenticated = useRef(Boolean(serverUser));
41
+ function Session({ children }) {
42
+ const wasAuthenticated = useRef(false);
43
+ const { data: session, status } = useSession();
44
+ wasAuthenticated.current = wasAuthenticated.current || status === 'authenticated';
93
45
 
94
46
  useEffect(() => {
95
- if (session) {
96
- wasAuthenticated.current = true;
97
- }
98
- // Reload after sign-out (or session revocation) so the server can apply
99
- // the page auth fork - a protected page redirects to the login page.
100
- // A logout with a callbackUrl suppresses the reload - its own navigation
101
- // would otherwise race this one.
102
- if (wasAuthenticated.current && !isPending && !session && !reloadSuppressedRef.current) {
47
+ if (wasAuthenticated.current && status === 'unauthenticated') {
103
48
  window.location.reload();
104
49
  }
105
- }, [session, isPending]);
50
+ }, [status]);
106
51
 
107
- if (isPending) {
108
- return children(resolvedUser, setResolvedUser);
109
- }
110
- if (!session?.user) {
111
- return children(null, setResolvedUser);
112
- }
113
- const resolved = resolvedUser;
114
- if (!resolved) {
115
- return children({ roles: [], ...normalizeCaller(session.user) }, setResolvedUser);
116
- }
117
- if (resolved.id !== session.user.id) {
118
- // The ref has not caught up with a changed session user - a sign-in as a
119
- // different person lands between the session refetch and the /api/user
120
- // result. Render the last resolved caller whole rather than mixing the new
121
- // session user with the previous person's resolved fields, or falling back
122
- // to a caller with no roles - blocks re-evaluate operators on render, so a
123
- // role-guarded page would act on an empty role list. UpdateSession closes
124
- // the window when it lands the new caller; a flow that changes the session
125
- // user without chaining it leaves the previous caller rendered until the
126
- // next page load.
127
- return children(resolved, setResolvedUser);
52
+ // If session is passed to SessionProvider from the server-rendered config
53
+ // we won't have a loading state here, but unauthenticated first loads do.
54
+ if (status === 'loading') {
55
+ return '';
128
56
  }
129
- // _user is one object with one meaning on both sides: the resolved caller is
130
- // spread whole, so every field resolveAuthentication emits is readable in app
131
- // config without this file naming it. session.user is the floor - the client
132
- // store may hold a fresher one than the resolved state, and the resolved
133
- // object wins wherever both carry a key.
134
- const user = { ...normalizeCaller(session.user), ...resolved };
135
- return children(user, setResolvedUser);
57
+ return children(session);
136
58
  }
137
59
 
138
- function AuthConfigured({ authConfig, children, serverUser }) {
139
- const reloadSuppressedRef = useRef(false);
140
- const auth = {
141
- authConfig,
142
- getSession: ({ disableCookieCache } = {}) =>
143
- authClient.getSession(disableCookieCache ? { query: { disableCookieCache: true } } : {}),
144
- // Refetches the session through the session atom - the store's only
145
- // writer - and resolves only after the store is written. fetchSession
146
- // returns undefined and never rejects (failures are written into the
147
- // atom), so the atom's own { data, error } is read back and returned,
148
- // keeping unwrap working and surfacing a failed refresh to the action.
149
- refreshSession: async ({ disableCookieCache } = {}) => {
150
- const sessionAtom = authClient.$store.atoms.session;
151
- await sessionAtom
152
- .get()
153
- .refetch(disableCookieCache ? { query: { disableCookieCache: true } } : undefined);
154
- const { data, error } = sessionAtom.get();
155
- return { data, error };
156
- },
157
- // The server-resolved caller - roles from the active member row and the
158
- // merged attributes bag - for re-syncing after session changes.
159
- getResolvedUser: async () => {
160
- const response = await fetch(`${lowdefyConfig.basePath ?? ''}/api/user`, {
161
- credentials: 'same-origin',
162
- });
163
- if (!response.ok) {
164
- // /api/user itself always answers 200 { user } - user: null when no
165
- // caller is admitted, which updateSession branches on. A non-OK
166
- // response is a transport failure (gateway error page, 5xx) with no
167
- // parsable body.
168
- throw new Error(`Failed to fetch the resolved user (HTTP ${response.status}).`);
169
- }
170
- return response.json();
171
- },
172
- suppressSignOutReload: () => {
173
- reloadSuppressedRef.current = true;
174
- },
175
- acceptInvitation: (params) => authClient.organization.acceptInvitation(sessionScoped(params)),
176
- // addPasskey runs the WebAuthn browser ceremony itself - options fetch,
177
- // authenticator prompt, verification.
178
- addPasskey: (params) => authClient.passkey.addPasskey(params),
179
- changePassword: (params) => authClient.changePassword(sessionScoped(params)),
180
- deletePasskey: (params) => authClient.passkey.deletePasskey(params),
181
- updatePasskey: (params) => authClient.passkey.updatePasskey(params),
182
- leaveOrganization: (params) => authClient.organization.leave(sessionScoped(params)),
183
- // The caller's memberships from the org plugin, keyed on the session
184
- // alone - not the active organization. sessionScoped so the list read
185
- // does not also fire the org-list atom's own refetch of the same route.
186
- listOrganizations: () => authClient.organization.list(sessionScoped()),
187
- // POST /oauth2/consent. The signed authorization query is not passed
188
- // here - the oauthProviderClient fetch plugin stamps it onto the body as
189
- // oauth_query from window.location.search, so the call must run while
190
- // the page still holds the query the oauth-provider redirect arrived
191
- // with.
192
- oauth2Consent: (params) => authClient.oauth2.consent(params),
193
- // POST /oauth2/continue - resumes the authorization after the post-login
194
- // organization choice (the page runs SetActiveOrganization first). Same
195
- // oauth_query contract as oauth2Consent: the signed query rides from
196
- // window.location.search, so the call must run before any navigation.
197
- oauth2Continue: (params) => authClient.oauth2.continue(params),
198
- // The one-time email code: send, then sign in with it.
199
- emailOtpSend: (params) => authClient.emailOtp.sendVerificationOtp(params),
200
- emailOtpVerify: (params) => authClient.signIn.emailOtp(params),
201
- phoneNumberRequestPasswordReset: (params) =>
202
- authClient.phoneNumber.requestPasswordReset(params),
203
- phoneNumberResetPassword: (params) => authClient.phoneNumber.resetPassword(params),
204
- phoneNumberSendOtp: (params) => authClient.phoneNumber.sendOtp(params),
205
- phoneNumberVerify: (params) => authClient.phoneNumber.verify(params),
206
- requestPasswordReset: (params) => authClient.requestPasswordReset(params),
207
- resetPassword: (params) => authClient.resetPassword(params),
208
- revokeOtherSessions: () => authClient.revokeOtherSessions(sessionScoped()),
209
- sendVerificationEmail: (params) => authClient.sendVerificationEmail(params),
210
- setActiveOrganization: (params) => authClient.organization.setActive(sessionScoped(params)),
211
- signInEmail: (params) => authClient.signIn.email(params),
212
- signInMagicLink: (params) => authClient.signIn.magicLink(params),
213
- signInOauth2: (params) => authClient.signIn.oauth2(params),
214
- signInPasskey: (params) => authClient.signIn.passkey(params),
215
- signInPhoneNumber: (params) => authClient.signIn.phoneNumber(params),
216
- signInSocial: (params) => authClient.signIn.social(params),
217
- signOut: () => authClient.signOut(),
218
- signUpEmail: (params) => authClient.signUp.email(params),
219
- twoFactorDisable: (params) => authClient.twoFactor.disable(sessionScoped(params)),
220
- twoFactorEnable: (params) => authClient.twoFactor.enable(sessionScoped(params)),
221
- twoFactorGenerateBackupCodes: (params) =>
222
- authClient.twoFactor.generateBackupCodes(sessionScoped(params)),
223
- twoFactorVerifyBackupCode: (params) => authClient.twoFactor.verifyBackupCode(params),
224
- twoFactorVerifyTotp: (params) => authClient.twoFactor.verifyTotp(params),
225
- };
60
+ function AuthConfigured({ authConfig, children, serverSession }) {
61
+ const auth = { authConfig, getSession, signIn, signOut };
226
62
  return (
227
- <Session reloadSuppressedRef={reloadSuppressedRef} serverUser={serverUser}>
228
- {(user, setResolvedUser) => {
229
- auth.user = user;
230
- auth.updateResolvedUser = (resolved) => setResolvedUser(resolved);
231
- return children(auth);
232
- }}
233
- </Session>
63
+ <SessionProvider session={serverSession}>
64
+ <Session>
65
+ {(session) => {
66
+ auth.session = session;
67
+ return children(auth);
68
+ }}
69
+ </Session>
70
+ </SessionProvider>
234
71
  );
235
72
  }
236
73
 
@@ -13,60 +13,18 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
-
17
- import { ConfigError } from '@lowdefy/errors';
16
+ /* eslint-disable react/jsx-props-no-spreading */
18
17
 
19
18
  function authNotConfigured() {
20
- throw new ConfigError(
21
- 'Auth is not configured. Add an "auth" section to lowdefy.yaml to use auth actions.'
22
- );
19
+ throw new Error('Auth not configured.');
23
20
  }
24
21
 
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 }) {
22
+ function AuthNotConfigured({ authConfig, children }) {
29
23
  const auth = {
30
24
  authConfig,
31
- user,
32
- acceptInvitation: authNotConfigured,
33
- addPasskey: authNotConfigured,
34
- changePassword: authNotConfigured,
35
- deletePasskey: authNotConfigured,
36
- getResolvedUser: authNotConfigured,
37
25
  getSession: 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,
26
+ signIn: authNotConfigured,
60
27
  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,
70
28
  };
71
29
 
72
30
  return children(auth);
@@ -14,8 +14,6 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { serializer } from '@lowdefy/helpers';
18
-
19
17
  async function request({ url, method = 'GET', body }) {
20
18
  const res = await fetch(url, {
21
19
  method,
@@ -29,12 +27,7 @@ async function request({ url, method = 'GET', body }) {
29
27
  }
30
28
  if (!res.ok) {
31
29
  const body = await res.json();
32
- // A Lowdefy error envelope is revived rather than flattened to its message,
33
- // so the class, configKey and source survive to the error handler.
34
- if (body?.['~e']) {
35
- throw serializer.deserialize(body);
36
- }
37
- throw new Error(body?.message ?? 'Request error');
30
+ throw new Error(body?.['~e']?.message ?? body?.message ?? 'Request error');
38
31
  }
39
32
  return res.json();
40
33
  }
@@ -13,8 +13,6 @@
13
13
 
14
14
  import useSWR from 'swr';
15
15
 
16
- import { serializer } from '@lowdefy/helpers';
17
-
18
16
  import { getNavVersion, getReloadVersion } from './useMutateCache.js';
19
17
 
20
18
  // URLs whose config is server-resolved per request — learned from the fetched
@@ -30,7 +28,6 @@ function parseJsModule(text) {
30
28
  }
31
29
 
32
30
  export async function fetchPageConfig(url) {
33
- const basePath = url.replace(/\/api\/page\/.*$/, '');
34
31
  // A stalled request (server restart mid-request, exhausted sockets) must
35
32
  // become a visible error, never an eternal Suspense fallback — the reload
36
33
  // recovery path cannot fire while the page tree is suspended.
@@ -51,20 +48,6 @@ export async function fetchPageConfig(url) {
51
48
  if (res.status === 404) {
52
49
  return null;
53
50
  }
54
- if (res.status === 401 || res.status === 403) {
55
- // 401: logged-out navigation to a protected page. 403: authorised but second
56
- // factor not yet enrolled. Page renders a redirect screen and full-loads to
57
- // the destination so it can return here afterwards. Returning a settled value
58
- // (never a parked promise) keeps the SWR key healthy: if the navigation is
59
- // dropped, the tab still recovers on the next reload event or via the manual
60
- // link on the redirect screen.
61
- const { redirect } = await res.json();
62
- const authRedirect = redirect ?? `${basePath}/404`;
63
- console.warn(
64
- `Lowdefy dev: "${url}" returned ${res.status} - redirecting to "${authRedirect}".`
65
- );
66
- return { authRedirect };
67
- }
68
51
  const data = await res.json();
69
52
  if (data?.buildError) {
70
53
  return data;
@@ -73,11 +56,6 @@ export async function fetchPageConfig(url) {
73
56
  return data;
74
57
  }
75
58
  if (!res.ok) {
76
- // A Lowdefy error envelope is revived rather than flattened to its message,
77
- // so the class, configKey and source survive to the error handler.
78
- if (data?.['~e']) {
79
- throw serializer.deserialize(data);
80
- }
81
59
  throw new Error(data.message || 'Request error');
82
60
  }
83
61
 
@@ -14,8 +14,6 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { publish } from './devEventBus.js';
18
-
19
17
  // Module-level ring buffer of recent client-reported errors — feeds the
20
18
  // getBuildStatus feedback endpoint so agents can see browser errors without
21
19
  // tailing server logs. Deliberately in-memory only: entries are lost on
@@ -29,7 +27,6 @@ function push(entry) {
29
27
  if (entries.length > MAX_ENTRIES) {
30
28
  entries.shift();
31
29
  }
32
- publish({ type: 'client_error', ...entry });
33
30
  }
34
31
 
35
32
  function list() {